Zurück zur Startseite

IPTV Editor Architektur auf FastAPI und React 19 | Technische Aufschlüsselung

Analyse der Architekturlösungen des Open-Source IPTV-Playlist-Editors m3u Studio. Methoden zur Zustandssynchronisation, CORS-Umgehung über HLS Proxy, Audio-Transkodierung on-demand und Themenimplementierung ohne Code-Refactoring werden behandelt.

Tiefgehende technische Aufschlüsselung: Wie FastAPI und React 19 IPTV-Playlists verwalten
Advertisement 728x90

# # Architektur eines lokalen IPTV-Playlist-Editors: Wie FastAPI und React 19 Synchronisations- und Wiedergabeherausforderungen meistern

Das Bearbeiten von IPTV-Playlists mit Tausenden von Kanälen erfordert komplexe Zustandssynchronisation und Workarounds für technische Einschränkungen. Schauen wir uns die architektonischen Lösungen im Open-Source-Projekt m3u Studio an, das mit FastAPI und React 19 aufgebaut ist.

Umgang mit großen Playlists

Standard-Editoren für m3u-Dateien können dynamische Änderungen der Anbieter nicht bewältigen. Beim Aktualisieren der Kanalliste gehen Kuratierungen verloren: Reihenfolge, Gruppierung, Favoriten. Wichtige Herausforderungen:

  • Stream-URLs ändern sich beim Wechsel des Anbieters und machen Identifikatoren kaputt
  • Fehlende CORS-Unterstützung in IPTV-Quellen blockiert direkte Wiedergabe im Browser
  • Inkompatibilität von Audio-Codecs (AC-3/E-AC-3) in Chrome/Safari
  • Synchronisation zweier Panels ohne Speichern-Button

m3u Studio löst diese Probleme über eine lokale Weboberfläche mit Drag-and-Drop, einem integrierten HLS-Player und EPG. Der Tech-Stack umfasst FastAPI + Pydantic v2 auf dem Backend sowie React 19 + TypeScript + Tailwind CSS auf dem Frontend.

Google AdInline article slot

Zustandsspeicherung nach Namen, nicht nach IDs

Der klassische Ansatz hasht URLs, um stabile Identifikatoren zu erzeugen:

def _stable_id(url: str) -> str:
    digest = hashlib.sha1(url.encode("utf-8"), usedforsecurity=False).hexdigest()
    return digest[:12]

Diese Methode scheitert beim Wechsel des Anbieters: URLs ändern sich komplett, Identifikatoren werden neu berechnet, und Kuratierungen gehen verloren. Die Lösung speichert die Reihenfolge nach Kanalnamen, die über Anbieter hinweg stabil bleiben. Ein interner Mechanismus wandelt Name ↔ ID über ein dynamisches Dictionary um:

@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]

Die API arbeitet weiterhin mit IDs, aber beim Laden einer neuen Playlist wird die Kuratierung automatisch auf Kanäle mit passenden Namen angewendet. So bleibt die Benutzererfahrung kontinuierlich, auch beim Wechsel der Datenquelle.

Google AdInline article slot

Synchronisation von Haupt- und Quellenliste ohne Speichern-Button

Die kuratierte „Main“-Liste und die Quellen-„main“-Gruppe müssen atomar aktualisiert werden. Beim Ziehen eines Kanals in Main ist Folgendes nötig:

  • Aktualisierung des Kuratierungs-Zustands
  • Umschreiben der m3u8-Datei mit neuer Reihenfolge
  • Speichern der Namensliste für zukünftige Imports

Umgesetzt über einen zentralen Helfer _sync_main_to_source, der nach jeder Mutation aufgerufen wird:

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)

Kritischer Punkt: Direkte Neubindung via bind_playlist() statt erneutes Einlesen von state.json verhindert Race Conditions. Auf dem Frontend invalidiert React Query den Quellen-Cache nach der Mutation:

Google AdInline article slot
onSettled: (server) => {
  if (server) client.setQueryData(KEY_MAIN, server)
  client.invalidateQueries({ queryKey: KEY_SOURCE })
}

Ergebnis: Sofortige Panelsynchronisation ohne explizites Speichern.

HLS-Proxy zur Umgehung von CORS

IPTV-Anbieter versenden selten CORS-Header, was direkte Browser-Wiedgabe unmöglich macht. Der Proxy-Server umschreibt nicht nur das Master-Manifest, sondern alle verschachtelten Segment-Links:

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)

Dieser 40-Zeilen-Handler ersetzt rekursiv alle relativen Pfade in m3u8-Dateien durch Proxy-URLs und sorgt für korrektes Segment-Laden über den App-Server.

Audio-Transcoding on the Fly

Für Kanäle mit AC-3/E-AC-3-Audio, das Browser nicht unterstützen, gibt es einen Fallback-Mechanismus. Beim Aktivieren des „Fix audio“-Buttons springt ffmpeg-Transcoding an:

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"),
)

Der Player wechselt zu /api/transcode/{channel_id}/index.m3u8 und stellt das Audio in 3-4 Sekunden wieder her (HLS-Segment-Latenz). ffmpeg-Prozesse werden von einer Hintergrund-Cleanup-Aufgabe verwaltet, um Ressourcenlecks zu vermeiden.

Helles Theme auf dunkler Basis umsetzen

Tausende Codezeilen für Light-Theme-Unterstützung umzuschreiben war unpraktikabel. Stattdessen fügt index.css Overrides für Utility-Klassen hinzu:

[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); }

Semantische Variablen wie --tint-bg-sm und --tint-border-sm erzeugen eine Hierarchie der Erhöhung. Die Selektor-Spezifität von [data-theme="light"] .class (0,2,0) überschreibt Standard-Tailwind-Klassen (0,1,0), ohne Komponenten zu ändern.

Wichtige Erkenntnisse

  • Kuratierungsstabilität: Reihenfolge nach Kanalnamen speichern, nicht nach IDs, erhält Einstellungen beim Anbieterwechsel
  • Atomare Synchronisation: Zustandsupdates, m3u8-Umschreibung und Namenslisten-Speicherung in einer Operation kombinieren
  • Proxy-Transformation: Rekursives Umschreiben aller URLs in HLS-Manifesten zur CORS-Umgehung
  • Audio-Fallback: Bedarfsweises AC-3-zu-AAC-Transcoding via ffmpeg mit Prozessverwaltung
  • Theming ohne Refactoring: Utility-Klassen via Data-Attribute mit Spezifitätssteuerung überschreiben

Weitere Projektkomponenten: m3u-Parser mit EXTGRP- und tvg-Attribut-Support, Logo-Resolver (lokale Overrides → iptv-org/database → CDN), Duplikat-Detektor via Namensnormalisierung, XMLTV-EPG-Loader mit Caching und Archiv-Sprung. Drag-and-Drop nutzt @dnd-kit mit custom Collision Detection.

Das Projekt starten ist einfach:

git clone https://github.com/stepanovandrey89/m3ustudio.git
cd m3ustudio
docker compose up -d

Nach dem Öffnen von http://127.0.0.1:8000 lädt der Nutzer seine m3u8-Datei hoch und beginnt mit dem Editieren. Das Projekt ist offen für PRs und architektonische Diskussionen.

— Editorial Team

Advertisement 728x90

Weiterlesen