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
@@ -256,8 +256,9 @@ extension PlexServer {
PlexRequest(
path: "/\(providerIdentifier)/grid",
query: [
// 1 steht bei Plex für „Sendung".
URLQueryItem(name: "type", value: "1"),
// Ohne Typfilter. `type=1` steht bei Plex für Filme, und
// im Programm laufen auch Folgen, Sport und Nachrichten —
// der Filter hat das Raster leer zurückgegeben.
URLQueryItem(name: "beginsAt>", value: String(Int(start.timeIntervalSince1970))),
URLQueryItem(name: "endsAt<", value: String(Int(end.timeIntervalSince1970))),
],
@@ -314,6 +315,26 @@ extension PlexServer {
pairs.map(\.program).sorted { $0.startsAt < $1.startsAt }
}
// Aus dem Raster: jeden Sender einmal, nach Sendernummer sortiert.
var seen = Set<String>()
let fromGrid = entries
.compactMap(\.channel)
.filter { seen.insert($0.id).inserted }
.sorted { ($0.channelIdentifier ?? "") < ($1.channelIdentifier ?? "") }
// Das Raster hat Vorrang vor der Senderliste des Tuners, auch wenn
// beide antworten: nur dort gehören Sender und Programm zusammen,
// stehen Logos dabei, und es enthält ohnehin nur die eingeschalteten
// Sender. Die Tunerliste führt alle 278, die meisten davon aus.
if !fromGrid.isEmpty {
return LiveTVGuide(
channels: fromGrid,
programs: programs,
source: "Raster: \(fromGrid.count) Sender",
trace: lookup.trace
)
}
if !lookup.channels.isEmpty {
return LiveTVGuide(
channels: lookup.channels,
@@ -323,24 +344,10 @@ extension PlexServer {
)
}
// Aus dem Raster: jeden Sender einmal, in der Reihenfolge, in der das
// Raster ihn nennt. Plex sortiert dort bereits nach Sendernummer.
var seen = Set<String>()
let fromGrid = entries.compactMap(\.channel).filter { seen.insert($0.id).inserted }
let enabled = dvr.enabledChannelKeys
let filtered = enabled.isEmpty ? fromGrid : fromGrid.filter { $0.matches(anyOf: enabled) }
// Passt keiner zur Zuordnung, ist die ganze Liste besser als keine:
// die Zuordnung zu erzwingen hieße, bei abweichenden Schlüsseln alles
// wegzuwerfen, was der Server gerade geliefert hat.
let channels = filtered.isEmpty ? fromGrid : filtered
return LiveTVGuide(
channels: channels,
channels: [],
programs: programs,
source: channels.isEmpty
? "Raster: keine Sender (\(lookup.description))"
: "Raster: \(channels.count) von \(fromGrid.count) Sendern",
source: "Raster: keine Sender (\(lookup.description))",
trace: lookup.trace + [ChannelProbe(
path: "/\(epgIdentifier)/grid",
result: fromGrid.isEmpty
@@ -356,10 +363,31 @@ extension PlexServer {
/// Transportstrom, den kein iPhone direkt abspielt. Deshalb führt der Weg
/// hier zwangsläufig über den Transcoder — anders als bei Dateien, wo
/// DirectPlay der Normalfall ist.
public func tune(channel: PlexChannel, maximumBitrate: Int?) async throws(PlexError) -> URL? {
///
/// Zwei Wege, je nachdem, woher der Sender kam. Steht die laufende
/// Sendung im Programmraster, ist ihre Adresse beim Programmanbieter der
/// direkte Weg: Plex behandelt eine laufende Sendung wie einen Film.
/// Stammt der Sender dagegen aus der Liste des Tuners, bleibt der Umweg
/// über eine Live-Sitzung.
public func tune(
channel: PlexChannel,
program: PlexProgram? = nil,
maximumBitrate: Int?
) async throws(PlexError) -> URL? {
let session = UUID().uuidString
if let metadataKey = program?.metadataKey {
return transcodeURL(for: TranscodeRequest(
ratingKey: channel.id,
maximumBitrate: maximumBitrate,
isLocal: connection.local,
sessionIdentifier: session,
pathOverride: metadataKey
))
}
guard let key = channel.key else { return nil }
let session = UUID().uuidString
let container = try await client.send(
PlexRequest(
path: "/livetv/sessions/\(session)/\(key)",