#!/usr/bin/env python3 """ Self-hosted IPTV: serves M3U playlists that schedule video files from /movies and /series 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, EPG_TIMESPAN_DAYS, SCHEDULE_CACHE_TTL_SEC, NORMALIZE_LIVE_STREAM, NORMALIZE_TARGET_RES, init_epg_timespan, ) from iptv.cache import CacheManager from iptv.channel import ChannelRepository from iptv.schedule import ScheduleBuilder from iptv.tmdb_client import TMDBClient from iptv.metadata_prefetcher import MetadataPrefetcher from iptv.epg_builder import EPGBuilder from iptv.server import create_server logger = logging.getLogger(__name__) def main(): init_epg_timespan() log_level = getattr(logging, os.environ.get("LOG_LEVEL", "INFO").upper(), logging.INFO) logging.basicConfig( level=log_level, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) if not CHANNELS_FILE.exists(): print(f"Warning: {CHANNELS_FILE} not found. Create it from data/channels.json.example") logger.info( "EPG timespan: %s days, schedule refresh after: %.1f h", EPG_TIMESPAN_DAYS, SCHEDULE_CACHE_TTL_SEC / 3600, ) if NORMALIZE_LIVE_STREAM: logger.info( "Live stream normalization enabled (target: %s) — prevents crashes when switching episodes", NORMALIZE_TARGET_RES, ) channel_repo = ChannelRepository() schedule_builder = ScheduleBuilder() schedule_builder.set_channel_repo(channel_repo) cache_manager = CacheManager(schedule_builder=schedule_builder) schedule_builder.set_cache_manager(cache_manager) if not cache_manager._ensure_cache_dir(): logger.warning( "Cannot create cache dir %s — schedule will rebuild on every restart. " "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 ) port = 8080 server = create_server( channel_repo=channel_repo, schedule_builder=schedule_builder, epg_builder=epg_builder, host="0.0.0.0", port=port, cache_manager=cache_manager, ) print(f"Serving at http://0.0.0.0:{port}/ (playlist: http://:{port}/playlist.m3u)") server.serve_forever() if __name__ == "__main__": main()