Files
Flaro/docs/superpowers/specs/2026-09-14-livetv-fuer-jellyfin-design.md
T
ScarriffleandClaude Opus 5 9df58ed5a6 Live-TV soll nicht länger an Plex kleben
Spec für Jellyfin-Live-TV. Der eigentliche Befund ist nicht, dass
Jellyfin fehlt, sondern dass das Feature quer an der Abstraktion vorbei
gebaut ist: die Views importieren PlexKit unmittelbar und sprechen über
session.server statt über session.backend.

Die Jellyfin-Endpunkte stammen aus der OpenAPI-Spec des Servers, nicht
aus dem Gedächtnis — in genau dieser Ecke steht, was dreimaliges Raten
auf einem echten Server gekostet hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 06:05:09 +02:00

10 KiB

Live-TV für Jellyfin

Kontext

Live-TV gibt es nur bei Plex — und zwar nicht, weil Jellyfin es nicht könnte, sondern weil das Feature quer an der Backend-Abstraktion vorbeigebaut ist.

LiveTVView, ProgramGuide, ChannelSelectionSheet und PlayerModel importieren alle unmittelbar PlexKit und arbeiten mit PlexDVR, PlexChannel, PlexProgram. Sie sprechen über session.server (den Plex-Server), nicht über session.backend (das neutrale MediaBackend). In PlayerModel.swift:126 steht es ausdrücklich: „Bei Jellyfin ist er nil."

Dazu eine Unwahrheit im Bestand: JellyfinBackend.swift:20 meldet .liveTV als Fähigkeit, ohne eine Zeile dahinter. Folgenlos war das bisher nur, weil niemand die Fähigkeit abfragt — FlixySession.swift:167 sagt supportsLiveTV { server != nil }, und bei Jellyfin ist das nil.

Was Jellyfin kann — verifiziert, nicht geraten

Aus der OpenAPI-Spec des Servers (api.jellyfin.org/openapi/jellyfin-openapi-stable.json), nicht aus dem Gedächtnis. Der Grund steht im Plex-Gegenstück: dort endeten drei geratene Adressen auf einem echten Server alle mit 404.

Zweck Endpunkt Antwort
Verfügbarkeit GET /LiveTv/Info LiveTvInfo — IsEnabled, Services, EnabledUsers
Sender GET /LiveTv/Channels BaseItemDtoQueryResult
Programme GET /LiveTv/Programs BaseItemDtoQueryResult
Aufnahmen GET /LiveTv/Recordings BaseItemDtoQueryResult
Aufnahme löschen DELETE /LiveTv/Recordings/{recordingId} —
Timer GET/POST /LiveTv/Timers, DELETE /LiveTv/Timers/{timerId} TimerInfoDtoQueryResult
Serien-Timer GET/POST /LiveTv/SeriesTimers, DELETE /LiveTv/SeriesTimers/{timerId} SeriesTimerInfoDtoQueryResult
Timer-Vorlage GET /LiveTv/Timers/Defaults SeriesTimerInfoDto

Zwei Funde, die den Bau vereinfachen:

  1. Sender und Programme sind beide BaseItemDto — derselbe Typ, den JellyfinItem schon abbildet. Es fehlen nur Felder: ChannelId, ChannelName, ChannelNumber, Number, StartDate, EndDate, CurrentProgram, IsLive, ProgramId, SeriesTimerId sowie die Kennzeichen IsMovie/IsSeries/IsNews/IsSports/IsKids.
  2. GET /LiveTv/Channels?addCurrentProgram=true liefert die laufende Sendung mit. Die Senderliste braucht dafür keinen zweiten Aufruf.

Nützliche Parameter, ebenfalls aus der Spec: /LiveTv/Programs filtert über channelIds, minStartDate, maxEndDate, isAiring; /LiveTv/Channels über type, limit, startIndex, enableImages.

Die Wiedergabe braucht keinen eigenen Weg: ein Sender ist ein BaseItemDto und läuft über das vorhandene preparePlayback.

Entscheidungen (mit dem Nutzer abgestimmt)

Thema Entscheidung
Architektur Sauber abstrahieren. Neutrale Typen in MediaKit, Live-TV als eigene Fähigkeit hinter einem Protokoll.
Umfang Parität mit Plex plus Aufnahmen. Sender, Programmführer, Senderauswahl, Einschalten — dazu Jellyfins Aufnahmen, Timer und Serien-Timer.
Zuschnitt Ein Durchgang. Abstraktion, Parität und Aufnahmen zusammen. Empfohlen war eine Teilung in zwei Stücke; der Nutzer hat sich dagegen entschieden.
Aufnahmen in der Oberfläche Dritter Modus in Live-TV, neben Raster und Senderliste. Bei Plex fehlt der Schalter schlicht.

Neutrale Typen (MediaKit)

LiveChannel     // id, number, title, artworkPath, currentProgram: LiveProgram?
LiveProgram     // id, channelID, title, summary, start, end, kind-Kennzeichen, seriesTimerID?
LiveRecording   // id, title, channelName, start, end, status, artworkPath
LiveTimer       // id, programID?, channelID, title, start, end, prePadding, postPadding
LiveSeriesTimer // id, name, channelID?, recordsNewOnly, recordsAnyChannel

LiveTVAvailability wandert aus PlexKit hoch und legt dabei die Plex-Typen ab: heute lautet sie case available(dvrs: [PlexDVR]). Neutral bleibt case available — welcher Empfänger dahintersteht, ist Sache der Implementierung. Plex merkt sich seinen DVR intern; niemand sonst braucht ihn.

LiveTuneResult wird ebenfalls neutral: .success(PlaybackTarget, subscriptionKey: String?) oder .failure(message: String, trace: [String]). Der Spur-Anteil bleibt erhalten — er ist der Grund, warum das Einschalten bei Plex überhaupt zu erklären ist, wenn es scheitert.

Zwei Protokolle, nicht eines

public protocol LiveTVBackend: Sendable {
    func liveTVAvailability() async -> LiveTVAvailability
    func liveChannels() async throws(MediaBackendError) -> [LiveChannel]
    func livePrograms(channelIDs: [String], from: Date, to: Date)
        async throws(MediaBackendError) -> [String: [LiveProgram]]
    func tune(channel: LiveChannel, program: LiveProgram?, maximumBitrate: Int?) async -> LiveTuneResult
    func endLiveSession(subscriptionKey: String) async
}

public protocol LiveTVRecordingBackend: Sendable {
    func recordings() async throws(MediaBackendError) -> [LiveRecording]
    func deleteRecording(id: String) async throws(MediaBackendError)
    func timers() async throws(MediaBackendError) -> [LiveTimer]
    func scheduleTimer(for program: LiveProgram) async throws(MediaBackendError)
    func cancelTimer(id: String) async throws(MediaBackendError)
    func seriesTimers() async throws(MediaBackendError) -> [LiveSeriesTimer]
    func scheduleSeriesTimer(for program: LiveProgram) async throws(MediaBackendError)
    func cancelSeriesTimer(id: String) async throws(MediaBackendError)
}

Getrennt, weil Plex das eine kann und das andere nicht. Ein as? LiveTVRecordingBackend sagt die Wahrheit; eine Methode, die bei Plex nur wirft, verspricht etwas und bricht es dann. Dazu BackendCapability.liveTVRecordings, damit die Oberfläche den dritten Schalter gar nicht erst zeigt.

Beide bleiben außerhalb des MediaBackend-Vertrags — so, wie es dort schon steht: „Alles, was nur ein Server kann … steht nicht hier, sondern als eigene Fähigkeit."

Plex zieht um, ohne umgebaut zu werden

Der Rateweg in PlexServer+LiveTV — vier Strategien, um die Senderliste zu finden, mit Spur für den Fehlerfall — bleibt Zeile für Zeile, wo er ist. PlexBackend bekommt nur eine Konformität zu LiveTVBackend, die darauf zeigt und die Plex-Typen in die neutralen übersetzt.

Das ist der Punkt, an dem diese Änderung schiefgehen könnte: dieser Code hat gekostet, und er wird nicht angefasst, sondern eingepackt.

PlexBackend.capabilities behält .liveTV und bekommt kein .liveTVRecordings.

Jellyfin bekommt eine neue Implementierung

Neu: JellyfinBackend+LiveTV.swift und JellyfinBackend+Recordings.swift, im Muster von JellyfinBackend+Playback.swift. JellyfinItem bekommt die oben genannten fehlenden Felder; JellyfinItem+Media.swift die Übersetzung nach LiveChannel und LiveProgram. TimerInfoDto und SeriesTimerInfoDto brauchen eigene, kleine Modelle — sie sind keine BaseItemDto.

JellyfinBackend.capabilities bekommt .liveTVRecordings zusätzlich zum vorhandenen .liveTV, das damit erstmals stimmt.

Die Oberfläche verliert ihre Plex-Abhängigkeit

  • LiveTVView, ProgramGuide, ChannelSelectionSheet: import PlexKit fällt weg, PlexChannel/PlexProgram/PlexDVR werden zu LiveChannel/LiveProgram.
  • PlayerModel.liveTV wird von PlexServer? zu (any LiveTVBackend)?. Die beiden Stellen, die ihn nutzen — refreshLiveProgrammeIfNeeded und die Freigabe der Subskription — gehen über das Protokoll.
  • FlixySession.supportsLiveTV fragt backend?.capabilities.contains(.liveTV) ab statt server != nil. Damit taucht Live-TV bei Jellyfin überhaupt erst in der Navigation auf.
  • ChannelSelection (die gespeicherte Senderauswahl) arbeitet auf LiveChannel.

Die Diagnose der Sendersuche

LiveTVView hält heute zwei Dinge, die es nur bei Plex gibt: channelSource (woher die Senderliste kam) und trace: [ChannelProbe] (jede abgeklopfte Adresse mit ihrer Antwort). Beide sind die Antwort auf Plex' Rateweg — Jellyfin hat einen festen Endpunkt und braucht nichts davon.

Sie fallen deshalb nicht weg, sondern werden optional: Die neutrale Form kennt channelSource: String? und probes: [LiveChannelProbe], Jellyfin liefert nil und eine leere Liste, und die Oberfläche zeigt den Diagnoseblock nur, wenn etwas da ist. Ihn ganz zu streichen wäre falsch — er ist bei Plex der einzige Weg zu sehen, warum keine Sender kommen.

Aufnahmen in der Oberfläche

Ein dritter Modus neben Raster und Senderliste, sichtbar nur bei .liveTVRecordings:

  • Aufnahmen — Liste mit Titel, Sender, Zeitraum; Antippen spielt ab, Wischen löscht (mit Rückfrage).
  • Geplant — die Timer und Serien-Timer, mit der Möglichkeit abzusagen.

Im Programmführer führt langes Drücken auf einen Sendungsblock zu einem Menü: „Ansehen", „Aufnehmen", „Serie aufnehmen". Bei Plex enthält dasselbe Menü nur „Ansehen" — dann bleibt es beim heutigen Antippen, ohne Menü.

Der gespeicherte Modus (@AppStorage("liveTV.showsGuide")) wird von einem Wahrheitswert zu einer Aufzählung aus drei Werten. Steht ein Server ohne Aufnahmen bereit und war zuletzt „Aufnahmen" gewählt, fällt es auf das Raster zurück.

Risiken

  1. Die Plex-Fassade. Der Rateweg liefert PlexChannel mit Feldern, die es bei Jellyfin nicht gibt, und umgekehrt. Wo die neutrale Form zu schmal geschnitten ist, fällt bei Plex etwas weg, das heute funktioniert — insbesondere die Sendernummer am Empfänger, an der das Einschalten hängt.
  2. Die Zeitachse des Programmführers rechnet heute mit PlexProgram-Zeiten. Jellyfin liefert ISO-8601-Zeitstempel statt Plex' Sekunden seit 1970; die Übersetzung muss die Zeitzone richtig treffen, sonst steht das Raster um Stunden verschoben.
  3. Kein Jellyfin-Server mit Empfänger zum Prüfen. Ob der Nutzer einen hat, ist offen. Ohne einen lässt sich der Jellyfin-Weg nicht an echten Daten belegen — dann bleibt es bei „kompiliert und folgt der Spec".

Fertig ist es, wenn

  • Bei einem Jellyfin-Server mit Empfänger steht Live-TV in der Navigation, zeigt Sender und Programmführer und schaltet ein.
  • Bei einem Jellyfin-Server ohne Empfänger erscheint dieselbe erklärende Ansicht wie bei Plex, kein Fehlerdialog.
  • Aufnahmen, Timer und Serien-Timer sind bei Jellyfin zu sehen, anzulegen und abzusagen.
  • Bei Plex funktioniert Live-TV genau wie vorher — Senderliste, Raster, Einschalten, Fehlerspur. Der dritte Schalter fehlt dort.
  • Keine Datei unter Flixy/Views/LiveTV/ importiert noch PlexKit.
  • supportsLiveTV fragt die Fähigkeit ab, und JellyfinBackend.capabilities sagt die Wahrheit.