# 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) ```swift 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 ```swift 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.