From 9df58ed5a64ffd6af1f534c4af4367904b349be5 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 14 Sep 2026 06:05:09 +0200 Subject: [PATCH] =?UTF-8?q?Live-TV=20soll=20nicht=20l=C3=A4nger=20an=20Ple?= =?UTF-8?q?x=20kleben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../2026-09-14-livetv-fuer-jellyfin-design.md | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-14-livetv-fuer-jellyfin-design.md diff --git a/docs/superpowers/specs/2026-09-14-livetv-fuer-jellyfin-design.md b/docs/superpowers/specs/2026-09-14-livetv-fuer-jellyfin-design.md new file mode 100644 index 0000000..695ce61 --- /dev/null +++ b/docs/superpowers/specs/2026-09-14-livetv-fuer-jellyfin-design.md @@ -0,0 +1,137 @@ +# 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.