This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
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: wait, then run a scan."""
|
||||
while not self._stop.is_set():
|
||||
self._stop.wait(timeout=min(60, self._interval_sec))
|
||||
if self._stop.is_set():
|
||||
break
|
||||
if self._stop.wait(timeout=max(0, self._interval_sec - 60)):
|
||||
break
|
||||
self._run_scan()
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user