Files
projects/Dockers/self-hosted-iptv/iptv/metadata_prefetcher.py
T
Bram df941720e3
Build and Push Docker Images / build-and-push (push) Successful in 24s
prefetcher
2026-02-11 21:52:16 +01:00

138 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: 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()