fdf
This commit is contained in:
810
MPV/LUA/main.lua
810
MPV/LUA/main.lua
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ This helper is intentionally minimal: one request at a time, last-write-wins.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
MEDEIA_MPV_HELPER_VERSION = "2026-03-22.6"
|
||||
MEDEIA_MPV_HELPER_VERSION = "2026-03-23.1"
|
||||
|
||||
import argparse
|
||||
import json
|
||||
@@ -1197,6 +1197,17 @@ def _acquire_ipc_lock(ipc_path: str) -> Optional[Any]:
|
||||
lock_path = _get_ipc_lock_path(ipc_path)
|
||||
fh = open(lock_path, "a+", encoding="utf-8", errors="replace")
|
||||
|
||||
# On Windows, locking a zero-length file can fail even when no process
|
||||
# actually owns the lock anymore. Prime the file with a single byte so
|
||||
# stale empty lock files do not wedge future helper startups.
|
||||
try:
|
||||
fh.seek(0, os.SEEK_END)
|
||||
if fh.tell() < 1:
|
||||
fh.write("\n")
|
||||
fh.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if os.name == "nt":
|
||||
try:
|
||||
import msvcrt # type: ignore
|
||||
|
||||
1
MPV/portable_config/script-opts/medeia-store-cache.json
Normal file
1
MPV/portable_config/script-opts/medeia-store-cache.json
Normal file
@@ -0,0 +1 @@
|
||||
{"choices":["local","rpi"]}
|
||||
@@ -1,2 +1,2 @@
|
||||
# Medeia MPV script options
|
||||
store=rpi
|
||||
store=local
|
||||
|
||||
@@ -361,6 +361,44 @@ def is_download_file_url(url: str) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _archive_item_access(identifier: str) -> Dict[str, Any]:
|
||||
ident = str(identifier or "").strip()
|
||||
if not ident:
|
||||
return {"mediatype": "", "lendable": False, "collection": []}
|
||||
|
||||
session = requests.Session()
|
||||
try:
|
||||
response = session.get(f"https://archive.org/metadata/{ident}", timeout=8)
|
||||
response.raise_for_status()
|
||||
data = response.json() if response is not None else {}
|
||||
except Exception:
|
||||
return {"mediatype": "", "lendable": False, "collection": []}
|
||||
finally:
|
||||
try:
|
||||
session.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
meta = data.get("metadata", {}) if isinstance(data, dict) else {}
|
||||
if not isinstance(meta, dict):
|
||||
meta = {}
|
||||
|
||||
mediatype = str(meta.get("mediatype") or "").strip().lower()
|
||||
collection = meta.get("collection")
|
||||
values: List[str] = []
|
||||
if isinstance(collection, list):
|
||||
values = [str(x).strip().lower() for x in collection if str(x).strip()]
|
||||
elif isinstance(collection, str) and collection.strip():
|
||||
values = [collection.strip().lower()]
|
||||
|
||||
lendable = any(v in {"inlibrary", "lendinglibrary"} for v in values)
|
||||
return {
|
||||
"mediatype": mediatype,
|
||||
"lendable": lendable,
|
||||
"collection": values,
|
||||
}
|
||||
|
||||
|
||||
def list_download_files(identifier: str) -> List[Dict[str, Any]]:
|
||||
"""Return a sorted list of downloadable files for an IA identifier.
|
||||
|
||||
@@ -620,6 +658,11 @@ class InternetArchive(Provider):
|
||||
quiet_mode: bool,
|
||||
) -> Optional[int]:
|
||||
"""Generic hook for download-file to show a selection table for IA items."""
|
||||
try:
|
||||
if self._should_delegate_borrow(str(url or "")):
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
from SYS.field_access import get_field as sh_get_field
|
||||
return maybe_show_formats_table(
|
||||
raw_urls=[url] if url else [],
|
||||
@@ -638,6 +681,72 @@ class InternetArchive(Provider):
|
||||
self._collection = conf.get("collection") or conf.get("default_collection")
|
||||
self._mediatype = conf.get("mediatype") or conf.get("default_mediatype")
|
||||
|
||||
@staticmethod
|
||||
def _should_delegate_borrow(url: str) -> bool:
|
||||
raw = str(url or "").strip()
|
||||
if not is_details_url(raw):
|
||||
return False
|
||||
identifier = extract_identifier(raw)
|
||||
if not identifier:
|
||||
return False
|
||||
access = _archive_item_access(identifier)
|
||||
return bool(access.get("lendable")) and str(access.get("mediatype") or "") == "texts"
|
||||
|
||||
def _download_via_openlibrary(self, url: str, output_dir: Path) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
from Provider.openlibrary import OpenLibrary
|
||||
except Exception as exc:
|
||||
log(f"[internetarchive] OpenLibrary borrow helper unavailable: {exc}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
provider = OpenLibrary(self.config)
|
||||
try:
|
||||
result = provider.download_url(url, output_dir)
|
||||
finally:
|
||||
try:
|
||||
session = getattr(provider, "_session", None)
|
||||
if session is not None:
|
||||
session.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not isinstance(result, dict):
|
||||
return result
|
||||
|
||||
search_result = result.get("search_result")
|
||||
metadata: Dict[str, Any] = {}
|
||||
title = None
|
||||
tags: List[str] = []
|
||||
if search_result is not None:
|
||||
try:
|
||||
title = str(getattr(search_result, "title", "") or "").strip() or None
|
||||
except Exception:
|
||||
title = None
|
||||
try:
|
||||
metadata = dict(getattr(search_result, "full_metadata", {}) or {})
|
||||
except Exception:
|
||||
metadata = {}
|
||||
try:
|
||||
tags_val = getattr(search_result, "tag", None)
|
||||
if isinstance(tags_val, set):
|
||||
tags = [str(t) for t in sorted(tags_val) if t]
|
||||
elif isinstance(tags_val, list):
|
||||
tags = [str(t) for t in tags_val if t]
|
||||
except Exception:
|
||||
tags = []
|
||||
|
||||
normalized: Dict[str, Any] = {"path": result.get("path")}
|
||||
if metadata:
|
||||
normalized["metadata"] = metadata
|
||||
normalized["full_metadata"] = metadata
|
||||
if title:
|
||||
normalized["title"] = title
|
||||
if tags:
|
||||
normalized["tags"] = tags
|
||||
normalized["media_kind"] = "book"
|
||||
normalized["provider_action"] = "borrow"
|
||||
return normalized
|
||||
|
||||
def validate(self) -> bool:
|
||||
try:
|
||||
_ia()
|
||||
@@ -824,13 +933,18 @@ class InternetArchive(Provider):
|
||||
|
||||
return out
|
||||
|
||||
def download_url(self, url: str, output_dir: Path) -> Optional[Path]:
|
||||
def download_url(self, url: str, output_dir: Path) -> Optional[Any]:
|
||||
"""Download an Internet Archive URL.
|
||||
|
||||
Supports:
|
||||
- https://archive.org/details/<identifier>
|
||||
- https://archive.org/download/<identifier>/<filename>
|
||||
"""
|
||||
if self._should_delegate_borrow(url):
|
||||
delegated = self._download_via_openlibrary(url, output_dir)
|
||||
if delegated is not None:
|
||||
return delegated
|
||||
|
||||
sr = SearchResult(
|
||||
table="internetarchive",
|
||||
title=str(url),
|
||||
@@ -842,6 +956,15 @@ class InternetArchive(Provider):
|
||||
def download(self, result: SearchResult, output_dir: Path) -> Optional[Path]:
|
||||
raw_path = str(getattr(result, "path", "") or "").strip()
|
||||
|
||||
if self._should_delegate_borrow(raw_path):
|
||||
delegated = self._download_via_openlibrary(raw_path, output_dir)
|
||||
if isinstance(delegated, dict):
|
||||
delegated_path = delegated.get("path")
|
||||
if delegated_path:
|
||||
return Path(str(delegated_path))
|
||||
if isinstance(delegated, (str, Path)):
|
||||
return Path(str(delegated))
|
||||
|
||||
# Fast path for explicit IA file URLs.
|
||||
# This uses the shared direct downloader, which already integrates with
|
||||
# pipeline transfer progress bars.
|
||||
|
||||
@@ -210,6 +210,135 @@ def _resolve_archive_id(
|
||||
return ""
|
||||
|
||||
|
||||
def _fetch_openlibrary_edition_metadata(
|
||||
session: requests.Session,
|
||||
edition_id: str,
|
||||
) -> Dict[str, Any]:
|
||||
if not edition_id:
|
||||
return {}
|
||||
|
||||
try:
|
||||
resp = session.get(
|
||||
f"https://openlibrary.org/books/{edition_id}.json",
|
||||
timeout=6,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json() or {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
|
||||
identifiers = data.get("identifiers")
|
||||
if not isinstance(identifiers, dict):
|
||||
identifiers = {}
|
||||
|
||||
def _first_clean(value: Any) -> str:
|
||||
raw = _first_str(value)
|
||||
return str(raw or "").strip()
|
||||
|
||||
isbn_10 = _first_clean(identifiers.get("isbn_10"))
|
||||
isbn_13 = _first_clean(identifiers.get("isbn_13"))
|
||||
archive_id = str(data.get("ocaid") or "").strip()
|
||||
if not archive_id:
|
||||
archive_id = _first_clean(identifiers.get("internet_archive"))
|
||||
|
||||
out: Dict[str, Any] = {
|
||||
"openlibrary_id": str(edition_id).strip(),
|
||||
"openlibrary": str(edition_id).strip(),
|
||||
}
|
||||
if isbn_10:
|
||||
out["isbn_10"] = isbn_10
|
||||
if isbn_13:
|
||||
out["isbn_13"] = isbn_13
|
||||
if archive_id:
|
||||
out["archive_id"] = archive_id
|
||||
return out
|
||||
|
||||
|
||||
def _select_preferred_isbns(values: Any) -> Tuple[str, str]:
|
||||
items: List[Any]
|
||||
if isinstance(values, list):
|
||||
items = values
|
||||
elif values in (None, ""):
|
||||
items = []
|
||||
else:
|
||||
items = [values]
|
||||
|
||||
isbn_10 = ""
|
||||
isbn_13 = ""
|
||||
for raw in items:
|
||||
token = re.sub(r"[^0-9Xx]", "", str(raw or "")).upper().strip()
|
||||
if not token:
|
||||
continue
|
||||
if len(token) == 13 and not isbn_13:
|
||||
isbn_13 = token
|
||||
elif len(token) == 10 and not isbn_10:
|
||||
isbn_10 = token
|
||||
return isbn_10, isbn_13
|
||||
|
||||
|
||||
def _build_pipeline_progress_callback(
|
||||
progress: Any,
|
||||
title: str,
|
||||
) -> Callable[[str, int, Optional[int], str], None]:
|
||||
transfer_label = str(title or "book").strip() or "book"
|
||||
state = {"active": False, "finished": False}
|
||||
|
||||
def _ensure_started(total: Optional[int]) -> None:
|
||||
if state["active"]:
|
||||
return
|
||||
try:
|
||||
progress.begin_transfer(label=transfer_label, total=total)
|
||||
state["active"] = True
|
||||
state["finished"] = False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _finish() -> None:
|
||||
if not state["active"] or state["finished"]:
|
||||
return
|
||||
try:
|
||||
progress.finish_transfer(label=transfer_label)
|
||||
except Exception:
|
||||
pass
|
||||
state["finished"] = True
|
||||
state["active"] = False
|
||||
|
||||
def _callback(kind: str, completed: int, total: Optional[int], label: str) -> None:
|
||||
text = str(label or kind or "download").strip() or "download"
|
||||
try:
|
||||
progress.set_status(f"openlibrary: {text}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if kind == "step":
|
||||
if text != "download pages":
|
||||
_finish()
|
||||
return
|
||||
|
||||
if kind in {"pages", "bytes"}:
|
||||
_ensure_started(total)
|
||||
try:
|
||||
progress.update_transfer(
|
||||
label=transfer_label,
|
||||
completed=int(completed) if completed is not None else None,
|
||||
total=int(total) if total is not None else None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
if total is not None:
|
||||
try:
|
||||
if int(completed) >= int(total):
|
||||
_finish()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
setattr(_callback, "_finish_transfer", _finish)
|
||||
return _callback
|
||||
|
||||
|
||||
def _archive_id_from_url(url: str) -> str:
|
||||
"""Best-effort extraction of an Archive.org item identifier from a URL."""
|
||||
|
||||
@@ -1082,6 +1211,12 @@ class OpenLibrary(Provider):
|
||||
|
||||
meta = result.full_metadata or {}
|
||||
edition_id = str(meta.get("openlibrary_id") or "").strip()
|
||||
edition_meta = _fetch_openlibrary_edition_metadata(self._session, edition_id)
|
||||
if edition_meta and isinstance(meta, dict):
|
||||
for key, value in edition_meta.items():
|
||||
if value and not meta.get(key):
|
||||
meta[key] = value
|
||||
result.full_metadata = meta
|
||||
|
||||
# Accept direct Archive.org URLs too (details/borrow/download) even when no OL edition id is known.
|
||||
archive_id = str(meta.get("archive_id") or "").strip()
|
||||
@@ -1097,7 +1232,9 @@ class OpenLibrary(Provider):
|
||||
archive_id = _first_str(ia_candidates) or ""
|
||||
|
||||
if not archive_id and edition_id:
|
||||
archive_id = _resolve_archive_id(self._session, edition_id, ia_candidates)
|
||||
archive_id = str(edition_meta.get("archive_id") or "").strip()
|
||||
if not archive_id:
|
||||
archive_id = _resolve_archive_id(self._session, edition_id, ia_candidates)
|
||||
|
||||
if not archive_id:
|
||||
# Try to extract identifier from the SearchResult path (URL).
|
||||
@@ -1114,17 +1251,49 @@ class OpenLibrary(Provider):
|
||||
try:
|
||||
archive_meta = fetch_archive_item_metadata(archive_id)
|
||||
tags = archive_item_metadata_to_tags(archive_id, archive_meta)
|
||||
if edition_id:
|
||||
tags.append(f"openlibrary:{edition_id}")
|
||||
if tags:
|
||||
try:
|
||||
result.tag.update(tags)
|
||||
except Exception:
|
||||
# Fallback for callers that pass plain dicts.
|
||||
pass
|
||||
|
||||
isbn_10 = str(meta.get("isbn_10") or edition_meta.get("isbn_10") or "").strip()
|
||||
isbn_13 = str(meta.get("isbn_13") or edition_meta.get("isbn_13") or "").strip()
|
||||
if not isbn_10 and not isbn_13:
|
||||
isbn_10, isbn_13 = _select_preferred_isbns(archive_meta.get("isbn"))
|
||||
|
||||
if isinstance(meta, dict):
|
||||
meta["archive_id"] = archive_id
|
||||
if archive_meta:
|
||||
meta["archive_metadata"] = archive_meta
|
||||
if edition_id:
|
||||
meta.setdefault("openlibrary_id", edition_id)
|
||||
meta.setdefault("openlibrary", edition_id)
|
||||
if isbn_10:
|
||||
meta.setdefault("isbn_10", isbn_10)
|
||||
if isbn_13:
|
||||
meta.setdefault("isbn_13", isbn_13)
|
||||
if not meta.get("isbn"):
|
||||
meta["isbn"] = isbn_13 or isbn_10
|
||||
result.full_metadata = meta
|
||||
|
||||
extra_identifier_tags: List[str] = []
|
||||
if edition_id:
|
||||
extra_identifier_tags.append(f"openlibrary:{edition_id}")
|
||||
if isbn_13:
|
||||
extra_identifier_tags.append(f"isbn_13:{isbn_13}")
|
||||
extra_identifier_tags.append(f"isbn:{isbn_13}")
|
||||
elif isbn_10:
|
||||
extra_identifier_tags.append(f"isbn_10:{isbn_10}")
|
||||
extra_identifier_tags.append(f"isbn:{isbn_10}")
|
||||
if extra_identifier_tags:
|
||||
try:
|
||||
result.tag.update(extra_identifier_tags)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
# Never block downloads on metadata fetch.
|
||||
pass
|
||||
@@ -1133,6 +1302,13 @@ class OpenLibrary(Provider):
|
||||
if not safe_title or "http" in safe_title.lower():
|
||||
safe_title = sanitize_filename(archive_id) or "archive"
|
||||
|
||||
internal_progress_finish = None
|
||||
if progress_callback is None and isinstance(self.config, dict):
|
||||
pipeline_progress = self.config.get("_pipeline_progress")
|
||||
if pipeline_progress is not None:
|
||||
progress_callback = _build_pipeline_progress_callback(pipeline_progress, safe_title)
|
||||
internal_progress_finish = getattr(progress_callback, "_finish_transfer", None)
|
||||
|
||||
# 1) Direct download if available.
|
||||
try:
|
||||
can_direct, pdf_url = self._archive_check_direct_download(archive_id)
|
||||
@@ -1318,6 +1494,12 @@ class OpenLibrary(Provider):
|
||||
except Exception as exc:
|
||||
log(f"[openlibrary] Borrow workflow error: {exc}", file=sys.stderr)
|
||||
return None
|
||||
finally:
|
||||
if callable(internal_progress_finish):
|
||||
try:
|
||||
internal_progress_finish()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def validate(self) -> bool:
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user