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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
81e22c4f97
commit
9df58ed5a6
@@ -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.
|
||||
Reference in New Issue
Block a user