From 016e0cee0ca8d80515f9f455eff08b83aa9a10af Mon Sep 17 00:00:00 2001 From: Bram Date: Wed, 11 Feb 2026 23:06:58 +0100 Subject: [PATCH] im breaking it --- Dockers/self-hosted-iptv/README.md | 2 ++ Dockers/self-hosted-iptv/iptv/cache.py | 27 ++++++++++++++++++++++++++ Dockers/self-hosted-iptv/main.py | 23 ++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/Dockers/self-hosted-iptv/README.md b/Dockers/self-hosted-iptv/README.md index 8332976..3b2f2f8 100644 --- a/Dockers/self-hosted-iptv/README.md +++ b/Dockers/self-hosted-iptv/README.md @@ -46,6 +46,8 @@ Series are matched by folder name (e.g. `series/W817` → search "W817"); episod - EPG: `/data/cache/epg.xml` — XMLTV guide; valid only while `channels.json` is unchanged. - Schedule: `/data/cache/schedule_.json` — per-channel video schedules. +On startup, schedules are preloaded in the background so the first stream starts quickly. If the hash file (`channels_hash.txt`) is missing but cache files exist and are recent, the hash is restored so existing cache can be used. + **Stream crashes when switching episodes?** If the live stream freezes or crashes when transitioning between videos (e.g. different episodes with different resolutions), enable normalization to re-encode everything to a uniform format: - `NORMALIZE_LIVE_STREAM=1` — enables re-encoding (uses more CPU, produces a stable continuous stream) - `NORMALIZE_TARGET_RES=1920x1080` — target resolution (default). Use `1280x720` for lower CPU usage. diff --git a/Dockers/self-hosted-iptv/iptv/cache.py b/Dockers/self-hosted-iptv/iptv/cache.py index ef8cffc..b758b0a 100644 --- a/Dockers/self-hosted-iptv/iptv/cache.py +++ b/Dockers/self-hosted-iptv/iptv/cache.py @@ -3,11 +3,14 @@ import hashlib import json import logging +import time from pathlib import Path from .config import CHANNELS_FILE, DATA_ROOT, MOVIES_ROOT, SERIES_ROOT CACHE_DIR = DATA_ROOT / "cache" +# When hash file is missing but cache exists, bootstrap if cache is younger than this (seconds) +CACHE_BOOTSTRAP_MAX_AGE_SEC = 24 * 3600 # 24 hours (covers "restarted within a day") logger = logging.getLogger(__name__) @@ -115,6 +118,24 @@ class CacheManager: if stored != current_hash: self._write_hash(current_hash) + def _bootstrap_hash_if_recent(self, cache_path: Path) -> bool: + """If hash is missing but cache file exists and is recent, write hash. Returns True if bootstrapped.""" + if self._get_stored_hash() is not None: + return False + if not cache_path.exists(): + return False + try: + age = time.time() - cache_path.stat().st_mtime + if age <= CACHE_BOOTSTRAP_MAX_AGE_SEC: + current = get_channels_hash(self._channels_file) + if current is not None and self._ensure_cache_dir(): + self._write_hash(current) + logger.info("Cache hash bootstrapped from %s (age %.0fm)", cache_path.name, age / 60) + return True + except OSError: + pass + return False + def get_epg(self) -> str | None: """Return cached EPG XML if valid, else None.""" self.invalidate_if_needed() @@ -127,6 +148,9 @@ class CacheManager: logger.debug("EPG cache miss: %s does not exist", epg_path) return None stored = self._get_stored_hash() + if stored is None: + self._bootstrap_hash_if_recent(epg_path) + stored = self._get_stored_hash() if stored is None: logger.debug("EPG cache miss: %s does not exist (hash required)", self._hash_file()) return None @@ -204,6 +228,9 @@ class CacheManager: if not path.exists(): return None stored = self._get_stored_hash() + if stored is None: + self._bootstrap_hash_if_recent(path) + stored = self._get_stored_hash() if stored != current: return None try: diff --git a/Dockers/self-hosted-iptv/main.py b/Dockers/self-hosted-iptv/main.py index b1c26c0..e8f2945 100644 --- a/Dockers/self-hosted-iptv/main.py +++ b/Dockers/self-hosted-iptv/main.py @@ -6,6 +6,7 @@ with randomized order so the same show never appears twice in a row. import logging import os +import threading from iptv.config import ( CHANNELS_FILE, @@ -58,9 +59,31 @@ def main(): "Ensure /data is mounted read-write (not :ro).", cache_manager._cache_dir, ) + else: + # Bootstrap hash from existing cache if hash file was lost (e.g. restart) + epg_path = cache_manager._epg_cache_file() + if not cache_manager._bootstrap_hash_if_recent(epg_path): + for ch in channel_repo.get_all(): + cid = ch.get("id", ch.get("name", "")) + sched_path = cache_manager._schedule_cache_path(cid) + if cache_manager._bootstrap_hash_if_recent(sched_path): + break tmdb_client = TMDBClient() prefetcher = MetadataPrefetcher(schedule_builder, tmdb_client, channel_repo) prefetcher.start() + + def _warmup_schedules(): + """Preload schedules so first stream starts quickly.""" + for ch in channel_repo.get_all(): + cid = ch.get("id", ch.get("name", "")) + try: + schedule_builder.get_or_build(cid) + except Exception as e: + logger.debug("Schedule warmup %s: %s", cid, e) + + threading.Thread(target=_warmup_schedules, daemon=True).start() + logger.info("Schedule warmup started in background") + epg_builder = EPGBuilder( schedule_builder, tmdb_client, channel_repo, cache_manager, prefetcher )