Live-TV: die Sender waren da, zwei eigene Fehler haben sie verschluckt

Der Abzug vom Server zeigt beides schwarz auf weiß.

Erstens: /media/grabbers/devices/1/channels antwortet mit 278 Sendern —
unter dem Sammelnamen `DeviceChannel`. Die Hülle kannte nur `Channel` und
hat die vollständige Liste als leer verworfen.

Zweitens: das Programmraster antwortet, aber nicht auf `type=1`. Die 1
steht bei Plex für Film, und im Programm laufen auch Folgen, Sport und
Nachrichten — der Filter hat das Raster geleert. Ohne ihn kommen die
Sendungen samt Sendername, Nummer, Logo und Kennung.

Das Raster hat jetzt Vorrang vor der Tunerliste, auch wenn beide
antworten: nur dort gehören Sender und Programm zusammen, es enthält
ohnehin nur die eingeschalteten Sender, und die Tunerliste führt alle 278.

Einschalten läuft über die Adresse der laufenden Sendung beim
Programmanbieter — Plex behandelt eine laufende Sendung wie einen Film.
Die Adresse wird einmal entpackt, bevor sie in die Anfrage geht: sonst
kodiert URLComponents ein zweites Mal, und aus %3A wird %253A.

Die Tests stehen jetzt auf den echten Antworten aus dem Abzug, nicht auf
erfundenen: DeviceChannel mit `name` und `key`, Rastereinträge mit
channelVcn, channelID und einer Folge neben einem Film.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-12 13:08:15 +02:00
co-authored by Claude Opus 5
parent 7e36f37506
commit 2362865ba9
9 changed files with 2726 additions and 2578 deletions
@@ -44,7 +44,9 @@ struct LiveTVGuideTests {
let guide = await server.liveTVGuide(dvr: dvr)
#expect(guide.channels.count == 2)
#expect(guide.channels.map(\.title) == ["SRF 1 HD", "SRF2"])
// Nach Sendernummer, und ohne die Nummer noch einmal im Namen:
// Plex schreibt sie dort mit hinein.
#expect(guide.channels.map(\.title) == ["SRF2", "ZDFGERH (ZDF HD)"])
}
@Test("Fehlt der Sendername, trägt das Kürzel den Sender")
@@ -54,12 +56,10 @@ struct LiveTVGuideTests {
])
let guide = await server.liveTVGuide(dvr: dvr)
let second = try #require(guide.channels.last)
let callSignOnly = try #require(guide.channels.first)
#expect(second.title == "SRF2")
// Die Sendernummer kommt mal als Zahl, mal als Zeichenkette — beides
// muss lesbar sein, sonst fehlt sie bei der Hälfte der Sender.
#expect(second.channelIdentifier == "2")
#expect(callSignOnly.title == "SRF2")
#expect(callSignOnly.channelIdentifier == "002")
}
@Test("Sendungen landen beim richtigen Sender, nach Anfangszeit sortiert")
@@ -69,10 +69,12 @@ struct LiveTVGuideTests {
])
let guide = await server.liveTVGuide(dvr: dvr)
let first = try #require(guide.channels.first)
let programs = try #require(guide.programs[first.id])
let zdf = try #require(guide.channels.last)
let programs = try #require(guide.programs[zdf.id])
#expect(programs.map(\.title) == ["Tagesschau", "Meteo"])
// Auch die Folge ist dabei: mit `type=1` kam nur „Film" durch, und
// das Raster blieb leer.
#expect(programs.map(\.title) == ["Endlich Witwer", "heute journal"])
#expect(guide.programs.count == 2)
}
@@ -148,14 +150,47 @@ struct LiveTVGuideTests {
#expect(lookup.trace.contains { $0.path == "/livetv/dvrs/2/channels" && $0.result.contains("Channel: 2") })
}
@Test("Die Einschaltadresse reist mit dem Sender mit")
func keepsTuneKey() async throws {
@Test("Die Adresse der laufenden Sendung führt zum Strom")
func tunesThroughTheProgram() async throws {
// Das Raster nennt keine Teil-Adresse. Der Weg führt über die
// Sendung selbst: Plex behandelt eine laufende Sendung wie einen Film.
let server = server(routes: [
"/tv.plex.providers.epg.cloud:2/grid": try fixture("livetv-grid"),
])
let guide = await server.liveTVGuide(dvr: dvr)
let zdf = try #require(guide.channels.last)
let program = try #require(guide.programs[zdf.id]?.first)
let url = try #require(await server.tune(channel: zdf, program: program, maximumBitrate: nil))
let path = try #require(URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?.first { $0.name == "path" }?.value)
#expect(guide.channels.first?.key == "/livetv/channels/11")
#expect(path == "/tv.plex.providers.epg.cloud:2/metadata/plex://movie/6254592919e23c64f6c5605d")
// Doppelt kodiert fände der Server nichts — aus %3A würde %253A.
#expect(!url.absoluteString.contains("%253A"))
}
@Test("Die Sender des Tuners stehen unter DeviceChannel")
func readsDeviceChannelKey() async throws {
// Der Server lieferte 278 Sender, die Hülle verwarf sie: sie kannte
// nur `Channel`. Der Tuner schreibt `DeviceChannel`.
let json = """
{"MediaContainer":{"size":2,"DeviceChannel":[
{"drm":false,"favorite":false,"hd":true,"identifier":"300","key":"300","name":"ZDF HD"},
{"hd":true,"identifier":"301","key":"301","name":"SRF 1 HD"}
]}}
"""
let server = server(routes: ["/media/grabbers/devices/1/channels": json])
let withTuner = PlexDVR(
key: "2",
devices: [PlexTunerDevice(key: "1")],
epgIdentifier: "tv.plex.providers.epg.cloud:2"
)
let lookup = await server.channels(inDVR: withTuner)
#expect(lookup.channels.map(\.title) == ["ZDF HD", "SRF 1 HD"])
// Die Nummer steht beim Tuner schlicht im Schlüssel.
#expect(lookup.channels.first?.channelIdentifier == "300")
}
}