Architektura lokálního editoru IPTV playlistů: jak FastAPI a React 19 řeší úkoly synchronizace a přehrávání
Editace IPTV playlistů s tisíci kanály vyžaduje složitou synchronizaci stavu a obcházení technických omezení. Podívejme se na architektonická řešení open-source projektu m3u Studio, realizovaného na FastAPI a React 19.
Problém správy velkých playlistů
Standardní editory m3u souborů nezvládají dynamické změny poskytovatelů. Při aktualizaci seznamu kanálů se ztrácí kurace: pořadí, seskupení, oblíbené kanály. Klíčové obtíže:
- URL adresy streamů se mění při výměně poskytovatele, což ničí identifikátory
- Chybějící CORS v IPTV zdrojích blokuje přímé přehrávání v prohlížeči
- Nekompatibilita audiokodeků (AC-3/E-AC-3) v Chrome/Safari
- Synchronizace dvou panelů bez tlačítka uložení
m3u Studio tyto problémy řeší prostřednictvím lokálního webového rozhraní s drag-and-drop, vestavěným HLS přehrávačem a EPG. Stack zahrnuje FastAPI + Pydantic v2 na backendu, React 19 + TypeScript + Tailwind CSS na frontendu.
Uložení stavu podle názvů, ne ID
Tradiční přístup — hašování URL pro generování stabilních identifikátorů:
def _stable_id(url: str) -> str:
digest = hashlib.sha1(url.encode("utf-8"), usedforsecurity=False).hexdigest()
return digest[:12]
Tato metoda selhává při výměně poskytovatele: URL se mění úplně, identifikátory se přepočítávají, kurace se ztrácí. Řešení — ukládat pořadí podle názvů kanálů, které zůstávají stabilní mezi dodavateli. Vnitřní mechanismus převádí název ↔ ID prostřednictvím dynamického slovníku:
@dataclass(frozen=True, slots=True)
class MainState:
main_names: tuple[str, ...]
class StateStore:
def current_ids(self) -> list[str]:
"""Stored names → current playlist ids."""
with self._lock:
name_to_id = self._name_to_id_map()
return [name_to_id[n] for n in self._state.main_names if n in name_to_id]
API nadále pracuje s ID, ale při načtení nového playlistu se kurace automaticky aplikuje na kanály s shodnými názvy. To zajišťuje kontinuitu uživatelského zážitku při výměně zdroje dat.
Synchronizace Main a Source bez tlačítka uložení
Kurátorský seznam „Main“ a zdrojová skupina „hlavní“ musí být aktualizovány atomicky. Při přetažení kanálu do Main je potřeba:
- Aktualizovat stav kurace
- Přepsat m3u8 soubor s novým pořadím
- Uložit seznam názvů pro budoucí importy
Realizováno prostřednictvím centralizovaného pomocníka _sync_main_to_source, volaného po každé mutaci:
def _sync_main_to_source() -> None:
main_ids = _state.store.current_ids()
text = build_with_main_group(
header=_state.playlist.header,
all_channels=_state.playlist.channels,
main_ids=main_ids,
group_name=MAIN_GROUP_NAME,
)
PLAYLIST_PATH.write_text(text, encoding="utf-8")
_state.playlist = parse_playlist(PLAYLIST_PATH)
_state.store.bind_playlist(_state.playlist)
current_names = _state.store.state.main_names
if current_names:
DEFAULT_NAMES_PATH.write_text("\n".join(current_names), encoding="utf-8")
_state.store.set_default_names(current_names)
Kritický moment: přímé přebíndání prostřednictvím bind_playlist() místo přečtení state.json zabraňuje race conditions. Na frontendu React Query neplatní cache zdroje po mutaci:
onSettled: (server) => {
if (server) client.setQueryData(KEY_MAIN, server)
client.invalidateQueries({ queryKey: KEY_SOURCE })
}
Výsledek — okamžitá synchronizace panelů bez explicitního uložení.
HLS proxy pro obcházení CORS
IPTV poskytovatelé zřídka posílají CORS hlavičky, což znemožňuje přímé přehrávání v prohlížeči. Proxy server přepisuje nejen hlavní manifest, ale i všechny vnořené odkazy na segmenty:
async def proxy_stream(upstream_url: str) -> Response:
async with httpx.AsyncClient() as client:
resp = await client.get(upstream_url, follow_redirects=True)
content_type = resp.headers.get("content-type", "")
if "mpegurl" in content_type.lower() or upstream_url.endswith(".m3u8"):
base = urljoin(upstream_url, ".")
rewritten = []
for line in resp.text.splitlines():
if line.startswith("#") or not line.strip():
rewritten.append(line)
else:
absolute = urljoin(base, line)
rewritten.append(f"/api/proxy?u={quote(absolute)}")
return Response("\n".join(rewritten), media_type=content_type)
return Response(resp.content, media_type=content_type)
Tento 40řádkový handler rekurzivně nahrazuje všechny relativní cesty v m3u8 souborech za proxy URL, což zajišťuje správné načítání segmentů přes aplikační server.
Transkodování audia za letu
Pro kanály s AC-3/E-AC-3 audiom, které prohlížeče nepodporují, je implementován fallback mechanismus. Při aktivaci tlačítka „Opravit audio“ se spustí ffmpeg transkodování:
proc = await asyncio.create_subprocess_exec(
FFMPEG_BIN,
"-i", upstream_url,
"-c:v", "copy",
"-c:a", "aac",
"-f", "hls",
"-hls_time", "4",
"-hls_list_size", "6",
"-hls_flags", "delete_segments",
str(output_dir / "index.m3u8"),
)
Přehrávač se přepne na /api/transcode/{channel_id}/index.m3u8, čímž obnoví zvuk během 3–4 sekund (latence HLS segmentu). Procesy ffmpeg spravuje úloha na čištění na pozadí pro prevenci úniků zdrojů.
Implementace světlého tématu na dark-only bázi
Přepsání tisíců řádků kódu pro podporu světlého tématu se ukázalo jako nepraktické. Místo toho byly v index.css přidány přepsání utility tříd:
[data-theme="light"] .text-white { color: var(--color-fog-300); }
[data-theme="light"] .bg-white\/5 { background-color: var(--tint-bg-sm); }
[data-theme="light"] .bg-white\/10 { background-color: var(--tint-bg-md); }
[data-theme="light"] .border-white\/10 { border-color: var(--tint-border-sm); }
[data-theme="light"] .hover\:bg-white\/5:hover { background-color: var(--tint-bg-sm); }
Semantické proměnné --tint-bg-sm a --tint-border-sm tvoří hierarchii elevace. Specificita selektorů [data-theme="light"] .class (0,2,0) překrývá standardní Tailwind třídy (0,1,0), což umožňuje vyhnout se změnám komponent.
Co je důležité
- Stabilita kurace: Uložení pořadí podle názvů kanálů, ne ID, zachovává nastavení při výměně poskytovatele
- Atomová synchronizace: Sloučení aktualizace stavu, m3u8 souboru a seznamu názvů do jedné operace
- Proxy transformace: Rekurzivní přepsání všech URL v HLS manifestech pro obcházení CORS
- Audio fallback: On-demand transkodování AC-3 do AAC přes ffmpeg se správou procesů
- Thematizace bez refaktoringu: Přepsání utility tříd přes data atributy s kontrolou specificity
Další komponenty projektu zahrnují parser m3u s podporou EXTGRP a tvg atributů, resolver loga (lokální override → iptv-org/database → CDN), detektor duplicitat přes normalizaci názvů, XMLTV EPG načítání s cachováním a skokem do archivu. Drag-and-drop je realizován na @dnd-kit s custom collision detection.
Spouštění projektu je triviální:
git clone https://github.com/stepanovandrey89/m3ustudio.git
cd m3ustudio
docker compose up -d
Po otevření http://127.0.0.1:8000 uživatel nahraje svůj m3u8 soubor a začne editovat. Projekt je otevřený pro PR a diskuzi o architektonických řešeních.
— Editorial Team
Zatím žádné komentáře.