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

138 lines
10 KiB
Markdown

# 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.