139 lines
4.9 KiB
Python
139 lines
4.9 KiB
Python
"""
|
|
Background metadata prefetcher: periodically scans schedules for programmes
|
|
missing TMDB metadata and fetches them, respecting TMDB rate limits.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from .config import SERIES_ROOT, TMDB_PREFETCH_INTERVAL_SEC
|
|
from .epg_builder import get_programme_name_from_path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MetadataPrefetcher:
|
|
"""
|
|
Runs in a background thread, periodically fetching TMDB metadata for
|
|
series episodes that are not yet cached locally.
|
|
Respects TMDB rate limits via the shared TMDBClient.
|
|
"""
|
|
|
|
def __init__(self, schedule_builder, tmdb_client, channel_repo):
|
|
self._schedule = schedule_builder
|
|
self._tmdb = tmdb_client
|
|
self._channels = channel_repo
|
|
self._interval_sec = TMDB_PREFETCH_INTERVAL_SEC
|
|
self._stop = threading.Event()
|
|
self._thread: threading.Thread | None = None
|
|
|
|
def start(self) -> None:
|
|
"""Start the background prefetch thread."""
|
|
if not os.environ.get("TMDB_API_KEY", "").strip():
|
|
logger.debug("TMDB_PREFETCH: skipped (no TMDB_API_KEY)")
|
|
return
|
|
if self._thread is not None and self._thread.is_alive():
|
|
return
|
|
self._stop.clear()
|
|
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
|
self._thread.start()
|
|
logger.info("TMDB metadata prefetcher started (interval: %.1fh)", self._interval_sec / 3600)
|
|
|
|
def stop(self) -> None:
|
|
"""Signal the prefetcher to stop."""
|
|
self._stop.set()
|
|
|
|
def _run_loop(self) -> None:
|
|
"""Background loop: run first scan immediately, then wait and repeat."""
|
|
while not self._stop.is_set():
|
|
self._run_scan()
|
|
if self._stop.is_set():
|
|
break
|
|
self._stop.wait(timeout=min(60, self._interval_sec))
|
|
if self._stop.is_set():
|
|
break
|
|
self._stop.wait(timeout=max(0, self._interval_sec - 60))
|
|
|
|
def _run_scan(self, scheduled: set | None = None) -> None:
|
|
"""
|
|
Prefetch TMDB metadata for series episodes that are not already
|
|
scheduled (not in the current EPG) and not in the TMDB cache.
|
|
This builds the cache so the next EPG build will be faster.
|
|
TMDBClient rate limiting applies to API calls.
|
|
"""
|
|
channels = self._channels.get_all()
|
|
if not channels:
|
|
return
|
|
|
|
scheduled = scheduled or set()
|
|
seen: set[tuple[Path, str | None, str | None, int | None]] = set()
|
|
fetched = 0
|
|
|
|
for ch in channels:
|
|
cid = ch.get("id", ch.get("name", ""))
|
|
try:
|
|
schedule = self._schedule.get_or_build(cid)
|
|
except Exception as e:
|
|
logger.debug("Prefetch channel %s: %s", cid, e)
|
|
continue
|
|
if not schedule:
|
|
continue
|
|
paths, _, path_configs = schedule
|
|
|
|
for i, path in enumerate(paths):
|
|
if SERIES_ROOT not in path.parents:
|
|
continue
|
|
cfg = path_configs[i] if i < len(path_configs) else {}
|
|
key = (
|
|
path,
|
|
cfg.get("display_name"),
|
|
cfg.get("tmdb_search"),
|
|
cfg.get("tmdb_id"),
|
|
)
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
if key in scheduled:
|
|
continue
|
|
|
|
programme_name = get_programme_name_from_path(path)
|
|
if self._tmdb.has_episode_metadata_cached(
|
|
programme_name,
|
|
path,
|
|
tmdb_search=cfg.get("tmdb_search"),
|
|
tmdb_id=cfg.get("tmdb_id"),
|
|
series_root=SERIES_ROOT,
|
|
):
|
|
continue
|
|
|
|
self._tmdb.get_episode_metadata(
|
|
programme_name,
|
|
path,
|
|
display_name=cfg.get("display_name"),
|
|
tmdb_search=cfg.get("tmdb_search"),
|
|
tmdb_id=cfg.get("tmdb_id"),
|
|
series_root=SERIES_ROOT,
|
|
)
|
|
fetched += 1
|
|
|
|
if fetched:
|
|
logger.info(
|
|
"TMDB prefetch: fetched metadata for %s unscheduled episodes (of %s total)",
|
|
fetched,
|
|
len(seen),
|
|
)
|
|
|
|
def run_once(self, scheduled: set | None = None) -> None:
|
|
"""
|
|
Run a single prefetch scan immediately (e.g. after EPG build).
|
|
scheduled: set of (path, display_name, tmdb_search, tmdb_id) for
|
|
programmes already in the current EPG; only fetch for episodes NOT
|
|
in this set, so the next scheduling process will be faster.
|
|
"""
|
|
if not os.environ.get("TMDB_API_KEY", "").strip():
|
|
return
|
|
threading.Thread(target=self._run_scan, args=(scheduled or set(),), daemon=True).start()
|