Unbundle covers: fetch psppt-covers.zip from repo on demand with user prompt
This commit is contained in:
+73
-33
@@ -53,8 +53,20 @@ _TMDB_SEARCH = "https://api.themoviedb.org/3/search/movie"
|
||||
_TMDB_IMAGE = "https://image.tmdb.org/t/p/"
|
||||
_GOOGLE_CSE_API = "https://www.googleapis.com/customsearch/v1"
|
||||
_MANUAL_DIR = config.USER_DATA_DIR / "manual_covers"
|
||||
_OVGDB_ZIP = config.DATA_DIR / "covers" / "openvgdb-covers.zip"
|
||||
_LIBRETRO_ZIP = config.DATA_DIR / "covers" / "libretro-covers.zip"
|
||||
# Offline cover bundles live in the writable cover dir (downloaded on demand),
|
||||
# not the read-only app bundle. A source checkout keeps them in data/covers/.
|
||||
_OVGDB_ZIP = config.COVERS_DIR / "openvgdb-covers.zip"
|
||||
_LIBRETRO_ZIP = config.COVERS_DIR / "libretro-covers.zip"
|
||||
_BUNDLE_ZIP = config.COVERS_DIR / "psppt-covers.zip"
|
||||
|
||||
|
||||
def _bundle_zip_path(name):
|
||||
"""Resolve a cover bundle zip: writable dir first, legacy checkout second."""
|
||||
for base in (config.COVERS_DIR, config.LEGACY_COVERS_DIR):
|
||||
p = base / name
|
||||
if p.exists():
|
||||
return p
|
||||
return config.COVERS_DIR / name
|
||||
|
||||
_WIKI_SEARCH = "https://en.wikipedia.org/w/api.php"
|
||||
_WIKI_SUMMARY = "https://en.wikipedia.org/api/rest_v1/page/summary/"
|
||||
@@ -158,48 +170,74 @@ def clear_cover_cache():
|
||||
pass
|
||||
|
||||
|
||||
def _download_file(url, dest, timeout=600):
|
||||
"""Stream `url` to `dest` (atomic); returns True on success."""
|
||||
def _download_file(url, dest, timeout=600, progress_cb=None, cancel_event=None):
|
||||
"""Stream `url` to `dest` (atomic); returns True on success.
|
||||
|
||||
``progress_cb(done, total)`` is called with bytes so far (total is 0 when
|
||||
the server doesn't report a content length). ``cancel_event`` aborts the
|
||||
download when set.
|
||||
"""
|
||||
req = urllib.request.Request(url, headers=_UA)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
if resp.status != 200:
|
||||
return False
|
||||
total = int(resp.headers.get("Content-Length") or 0)
|
||||
tmp = dest.with_name(dest.name + ".tmp")
|
||||
done = 0
|
||||
with open(tmp, "wb") as f:
|
||||
while True:
|
||||
if cancel_event is not None and cancel_event.is_set():
|
||||
raise InterruptedError("download cancelled")
|
||||
chunk = resp.read(1 << 20)
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
done += len(chunk)
|
||||
if progress_cb is not None:
|
||||
progress_cb(done, total)
|
||||
os.replace(tmp, dest)
|
||||
return True
|
||||
|
||||
|
||||
def ensure_cover_bundles(server_url=""):
|
||||
"""Download missing offline cover bundle zips from the store server.
|
||||
def covers_bundle_present():
|
||||
"""True when the offline cover bundle is installed (both inner zips)."""
|
||||
return (_bundle_zip_path("libretro-covers.zip").exists()
|
||||
and _bundle_zip_path("openvgdb-covers.zip").exists())
|
||||
|
||||
``server_url`` is the base URL (e.g. ``http://192.168.10.100:8000``); the
|
||||
zips are fetched from ``<base>/api/covers/<name>.zip`` unless a direct URL
|
||||
is set in config. Returns the names of bundles downloaded.
|
||||
|
||||
def download_covers_bundle(url=None, progress_cb=None, cancel_event=None):
|
||||
"""Download and extract the combined cover bundle into the cover dir.
|
||||
|
||||
``url`` defaults to ``config.COVERS_BUNDLE_URL``. Raises on failure; the
|
||||
caller decides whether to surface the error.
|
||||
"""
|
||||
base = (server_url or config.STORE_SERVER_URL or "").rstrip("/")
|
||||
downloaded = []
|
||||
for name, path, direct in (
|
||||
("libretro-covers.zip", _LIBRETRO_ZIP, config.LIBRETRO_COVERS_URL),
|
||||
("openvgdb-covers.zip", _OVGDB_ZIP, config.OVGDB_COVERS_URL),
|
||||
):
|
||||
if path.exists():
|
||||
continue
|
||||
url = direct or (f"{base}/api/covers/{name}" if base else "")
|
||||
if not url:
|
||||
continue
|
||||
try:
|
||||
if _download_file(url, path):
|
||||
downloaded.append(name)
|
||||
log.info("downloaded offline cover bundle %s", name)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.warning("could not download cover bundle %s: %s", name, exc)
|
||||
return downloaded
|
||||
url = (url or config.COVERS_BUNDLE_URL or "").strip()
|
||||
if not url:
|
||||
raise ValueError("no cover bundle URL configured")
|
||||
config.COVERS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
_download_file(url, _BUNDLE_ZIP, timeout=3600,
|
||||
progress_cb=progress_cb, cancel_event=cancel_event)
|
||||
inner = ("libretro-covers.zip", "openvgdb-covers.zip")
|
||||
with zipfile.ZipFile(_BUNDLE_ZIP) as z:
|
||||
missing = [n for n in inner if n not in z.namelist()]
|
||||
if missing:
|
||||
raise ValueError(f"cover bundle is missing: {', '.join(missing)}")
|
||||
with zipfile.ZipFile(_BUNDLE_ZIP) as z:
|
||||
for name in inner:
|
||||
out = config.COVERS_DIR / name
|
||||
tmp = out.with_name(out.name + ".tmp")
|
||||
with z.open(name) as src, open(tmp, "wb") as dst:
|
||||
while True:
|
||||
chunk = src.read(1 << 20)
|
||||
if not chunk:
|
||||
break
|
||||
dst.write(chunk)
|
||||
os.replace(tmp, out)
|
||||
# Invalidate cached namelists so lookups pick up the freshly extracted zips.
|
||||
global _libretro_names, _ovgdb_names
|
||||
_libretro_names = None
|
||||
_ovgdb_names = None
|
||||
return config.COVERS_DIR
|
||||
|
||||
|
||||
def cover_url(name, kind="Named_Boxarts", system="games"):
|
||||
@@ -482,9 +520,10 @@ def _ovgdb_name_list():
|
||||
global _ovgdb_names
|
||||
if _ovgdb_names is None:
|
||||
_ovgdb_names = set()
|
||||
if _OVGDB_ZIP.exists():
|
||||
path = _bundle_zip_path("openvgdb-covers.zip")
|
||||
if path.exists():
|
||||
try:
|
||||
with zipfile.ZipFile(_OVGDB_ZIP) as z:
|
||||
with zipfile.ZipFile(path) as z:
|
||||
_ovgdb_names = {n[:-4] for n in z.namelist() if n.endswith(".jpg")}
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
@@ -535,7 +574,7 @@ def fetch_openvgdb_local(name):
|
||||
if entry is None:
|
||||
return None
|
||||
try:
|
||||
with zipfile.ZipFile(_OVGDB_ZIP) as z:
|
||||
with zipfile.ZipFile(_bundle_zip_path("openvgdb-covers.zip")) as z:
|
||||
data = z.read(entry)
|
||||
return data if _is_image(data) else None
|
||||
except Exception: # noqa: BLE001
|
||||
@@ -559,7 +598,7 @@ def fetch_libretro_local(name):
|
||||
if entry is None:
|
||||
return None
|
||||
try:
|
||||
with zipfile.ZipFile(_LIBRETRO_ZIP) as z:
|
||||
with zipfile.ZipFile(_bundle_zip_path("libretro-covers.zip")) as z:
|
||||
data = z.read(entry)
|
||||
return data if _is_image(data) else None
|
||||
except Exception: # noqa: BLE001
|
||||
@@ -570,9 +609,10 @@ def _libretro_name_list():
|
||||
global _libretro_names
|
||||
if _libretro_names is None:
|
||||
_libretro_names = set()
|
||||
if _LIBRETRO_ZIP.exists():
|
||||
path = _bundle_zip_path("libretro-covers.zip")
|
||||
if path.exists():
|
||||
try:
|
||||
with zipfile.ZipFile(_LIBRETRO_ZIP) as z:
|
||||
with zipfile.ZipFile(path) as z:
|
||||
_libretro_names = {n[:-4] for n in z.namelist() if n.endswith(".png")}
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user