im breaking it
Build and Push Docker Images / build-and-push (push) Successful in 25s

This commit is contained in:
2026-02-11 23:06:58 +01:00
parent bfc3f22c29
commit 016e0cee0c
3 changed files with 52 additions and 0 deletions
+2
View File
@@ -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_<channel>.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.
+27
View File
@@ -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:
+23
View File
@@ -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
)