Files
Bram 016e0cee0c
Build and Push Docker Images / build-and-push (push) Successful in 25s
im breaking it
2026-02-11 23:06:58 +01:00

106 lines
3.6 KiB
Python

#!/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://<host>:{port}/playlist.m3u)")
server.serve_forever()
if __name__ == "__main__":
main()