This commit is contained in:
@@ -26,7 +26,7 @@ docker run -d --name iptv -p 8080:8080 \
|
|||||||
-v /path/to/data:/data \
|
-v /path/to/data:/data \
|
||||||
self-hosted-iptv
|
self-hosted-iptv
|
||||||
```
|
```
|
||||||
When using TMDB metadata, mount `/data` read-write (as above) so the app can store and reuse metadata in `/data/tmdb_cache/`. Use `-v /path/to/data:/data:ro` only if you do not use TMDB.
|
Mount `/data` read-write (omit `:ro`) so the app can persist the schedule cache in `/data/cache/` — otherwise the schedule must be rebuilt on every restart. TMDB metadata also requires read-write access for `/data/tmdb_cache/`.
|
||||||
|
|
||||||
**EPG timespan:** Set `EPG_TIMESPAN` to control how far ahead the guide is generated (default `14d`). Examples: `2d`, `7d`, `48h`. The randomized schedule is refreshed after (timespan − 3 hours) so a new block of content is ready before the current one ends. Example: `-e EPG_TIMESPAN=2d` gives 2 days of EPG and a new schedule every 45 hours.
|
**EPG timespan:** Set `EPG_TIMESPAN` to control how far ahead the guide is generated (default `14d`). Examples: `2d`, `7d`, `48h`. The randomized schedule is refreshed after (timespan − 3 hours) so a new block of content is ready before the current one ends. Example: `-e EPG_TIMESPAN=2d` gives 2 days of EPG and a new schedule every 45 hours.
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
"tmdb_id": "97596-kika-bob"
|
"tmdb_id": "97596-kika-bob"
|
||||||
},
|
},
|
||||||
"series/Kim Possible",
|
"series/Kim Possible",
|
||||||
|
"series/As Told By Ginger",
|
||||||
|
"series/De grote boze wolf show",
|
||||||
{
|
{
|
||||||
"path": "series/Beugelbekkie",
|
"path": "series/Beugelbekkie",
|
||||||
"display_name": "Beugelbekkie",
|
"display_name": "Beugelbekkie",
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ class CacheManager:
|
|||||||
"""
|
"""
|
||||||
If channels.json has changed, clear all caches and return True.
|
If channels.json has changed, clear all caches and return True.
|
||||||
Otherwise return False.
|
Otherwise return False.
|
||||||
|
Only clear when we have a stored hash that explicitly differs; when
|
||||||
|
stored is None (hash file missing), do not delete cache files — they
|
||||||
|
may be valid and we don't want to destroy them on every restart.
|
||||||
"""
|
"""
|
||||||
current = get_channels_hash(self._channels_file)
|
current = get_channels_hash(self._channels_file)
|
||||||
stored = self._get_stored_hash()
|
stored = self._get_stored_hash()
|
||||||
@@ -82,7 +85,7 @@ class CacheManager:
|
|||||||
if current is None:
|
if current is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if stored != current:
|
if stored is not None and stored != current:
|
||||||
logger.info("Channels changed (hash %s -> %s), invalidating cache", stored, current)
|
logger.info("Channels changed (hash %s -> %s), invalidating cache", stored, current)
|
||||||
self._clear_cache()
|
self._clear_cache()
|
||||||
if self._schedule_builder and hasattr(self._schedule_builder, "invalidate_cache"):
|
if self._schedule_builder and hasattr(self._schedule_builder, "invalidate_cache"):
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ class IPTVHandler(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body.encode("utf-8"))
|
self.wfile.write(body.encode("utf-8"))
|
||||||
|
|
||||||
|
def _extinf_channel(self, cid: str, name: str) -> str:
|
||||||
|
"""Build EXTINF line with tvg-id for EPG matching and persistence."""
|
||||||
|
safe = lambda s: (s or "").replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
return f'#EXTINF:-1 tvg-id="{safe(cid)}" tvg-name="{safe(name)}",{name}'
|
||||||
|
|
||||||
def send_channels_m3u(self):
|
def send_channels_m3u(self):
|
||||||
"""Master M3U for Plex DVR: one live stream URL per channel."""
|
"""Master M3U for Plex DVR: one live stream URL per channel."""
|
||||||
base = get_base_url(self)
|
base = get_base_url(self)
|
||||||
@@ -82,7 +87,7 @@ class IPTVHandler(BaseHTTPRequestHandler):
|
|||||||
for ch in channels:
|
for ch in channels:
|
||||||
cid = ch.get("id", ch.get("name", ""))
|
cid = ch.get("id", ch.get("name", ""))
|
||||||
name = ch.get("name", cid)
|
name = ch.get("name", cid)
|
||||||
lines.append(f"#EXTINF:-1,{name}")
|
lines.append(self._extinf_channel(cid, name))
|
||||||
lines.append(f"{base}/live/{cid}")
|
lines.append(f"{base}/live/{cid}")
|
||||||
body = "\n".join(lines) + "\n"
|
body = "\n".join(lines) + "\n"
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
|
|||||||
@@ -50,9 +50,15 @@ def main():
|
|||||||
channel_repo = ChannelRepository()
|
channel_repo = ChannelRepository()
|
||||||
schedule_builder = ScheduleBuilder()
|
schedule_builder = ScheduleBuilder()
|
||||||
schedule_builder.set_channel_repo(channel_repo)
|
schedule_builder.set_channel_repo(channel_repo)
|
||||||
tmdb_client = TMDBClient()
|
|
||||||
cache_manager = CacheManager(schedule_builder=schedule_builder)
|
cache_manager = CacheManager(schedule_builder=schedule_builder)
|
||||||
schedule_builder.set_cache_manager(cache_manager)
|
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,
|
||||||
|
)
|
||||||
|
tmdb_client = TMDBClient()
|
||||||
prefetcher = MetadataPrefetcher(schedule_builder, tmdb_client, channel_repo)
|
prefetcher = MetadataPrefetcher(schedule_builder, tmdb_client, channel_repo)
|
||||||
prefetcher.start()
|
prefetcher.start()
|
||||||
epg_builder = EPGBuilder(
|
epg_builder = EPGBuilder(
|
||||||
|
|||||||
Reference in New Issue
Block a user