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>
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:
- Sender und Programme sind beide
BaseItemDto— derselbe Typ, denJellyfinItemschon abbildet. Es fehlen nur Felder:ChannelId,ChannelName,ChannelNumber,Number,StartDate,EndDate,CurrentProgram,IsLive,ProgramId,SeriesTimerIdsowie die KennzeichenIsMovie/IsSeries/IsNews/IsSports/IsKids. GET /LiveTv/Channels?addCurrentProgram=trueliefert 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 PlexKitfällt weg,PlexChannel/PlexProgram/PlexDVRwerden zuLiveChannel/LiveProgram.PlayerModel.liveTVwird vonPlexServer?zu(any LiveTVBackend)?. Die beiden Stellen, die ihn nutzen —refreshLiveProgrammeIfNeededund die Freigabe der Subskription — gehen über das Protokoll.FlixySession.supportsLiveTVfragtbackend?.capabilities.contains(.liveTV)ab stattserver != nil. Damit taucht Live-TV bei Jellyfin überhaupt erst in der Navigation auf.ChannelSelection(die gespeicherte Senderauswahl) arbeitet aufLiveChannel.
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
- Die Plex-Fassade. Der Rateweg liefert
PlexChannelmit 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. - 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. - 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 nochPlexKit. supportsLiveTVfragt die Fähigkeit ab, undJellyfinBackend.capabilitiessagt die Wahrheit.