Live-TV: Zuordnung und Senderliste sind zweierlei

Der Strukturabzug vom Server hat es entschieden. ChannelMapping enthält
genau vier Felder — channelKey, deviceIdentifier, enabled,
lineupIdentifier — und keinen einzigen Namen. Es ist keine Senderliste,
sondern ein Verweis darauf: welche 28 von 521 Sendern der Swisscom-Liste
eingeschaltet sind.

Zweimal habe ich die Zuordnung für die Senderliste gehalten. Beim ersten
Mal blieb sie leer, beim zweiten hätten alle 28 Sender „—" geheißen und
dieselbe Kennung getragen — SwiftUI hätte bei doppelten Kennungen
ohnehin nur einen gezeigt.

Jetzt werden beide Quellen zusammengeführt: die Namen, Nummern und Logos
kommen von der Senderliste beim Programmanbieter, die Auswahl aus der
Zuordnung. Die Kennung des Anbieters steht als `epgIdentifier` direkt am
Empfänger — verlässlicher, als sie aus der Anbieterliste zu suchen.

Der Abgleich ist nachsichtig: je nach Anbieter entspricht der channelKey
der Kennung, dem Pfad oder der Sendernummer. Ein strenger Vergleich fände
in zwei von drei Fällen nichts, und „keiner passte zusammen" sieht aus
wie „keine Sender". Genau das unterscheidet die Meldung jetzt auch.

Der Strukturabzug in der Diagnose deckt jetzt beide Endpunkte ab — nach
einem Fehlschlag bleibt keine weitere Runde Raten übrig.

166 Tests, neun davon zu Zuordnung und Abgleich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-09-12 10:28:38 +02:00
co-authored by Claude Opus 5
parent afccb81792
commit 90d5d96485
4 changed files with 2622 additions and 2474 deletions
@@ -31,30 +31,54 @@ extension PlexServer {
}
}
/// Die Sender eines Empfängers, samt Auskunft darüber, woher sie kamen.
/// Die eingeschalteten Sender eines Empfängers, mit Namen und Logos.
///
/// Zwei Quellen, in dieser Reihenfolge. Die erste ist die verlässliche:
/// Plex liefert die Sender bereits verschachtelt in der Antwort auf
/// `/livetv/dvrs` mit. Der getrennte Endpunkt war meine erste Vermutung
/// und lieferte nichts — daran hing „Keine Sender" bei einem Server mit
/// über dreißig eingerichteten Sendern.
/// Zwei Quellen, die zusammengeführt werden müssen — das war der
/// eigentliche Stolperstein:
///
/// Die Auskunft über die Quelle steht in der Diagnose. Bei einem Bereich
/// ohne offizielle Beschreibung ist „welcher Weg hat gegriffen" die
/// Angabe, mit der sich der nächste Fehler finden lässt.
/// - Der Empfänger kennt nur die **Zuordnung**: welche Sender einer
/// Senderliste eingeschaltet sind, als nackte Schlüssel. Bei einer
/// Swisscom-Liste sind das 28 von 521. Namen stehen dort keine.
/// - Die **Namen, Nummern und Logos** stehen beim Programmanbieter, unter
/// der Senderliste.
///
/// Zweimal habe ich vorher die Zuordnung für die Senderliste gehalten.
/// Beim ersten Mal blieb sie leer, beim zweiten hätten alle Sender „—"
/// geheißen und dieselbe Kennung getragen.
public func channels(inDVR dvr: PlexDVR) async -> ChannelLookup {
if !dvr.channels.isEmpty {
return ChannelLookup(channels: dvr.channels, source: .embeddedInDVR)
let enabled = dvr.enabledChannelKeys
// `??` mit einem asynchronen Aufruf rechts geht nicht — die
// Kurzschlussauswertung ist nicht nebenläufigkeitsfähig.
var identifier = dvr.epgIdentifier
if identifier == nil {
identifier = await electronicProgramGuideIdentifier()
}
guard let epgIdentifier = identifier, let lineup = dvr.lineups.first else {
return ChannelLookup(channels: [], source: .noLineup)
}
do {
let container = try await client.send(
PlexRequest(path: "/livetv/dvrs/\(dvr.key)/channels"),
PlexRequest(
path: "/\(epgIdentifier)/lineups/\(lineup.id)/channels",
timeout: .seconds(30)
),
as: PlexMediaContainer<PlexChannel>.self
)
// Ohne Zuordnung alles zeigen: besser die vollständige Liste als
// gar keine, wenn der Empfänger keine Auswahl meldet.
let channels = enabled.isEmpty
? container.content
: container.content.filter { $0.matches(anyOf: enabled) }
return ChannelLookup(
channels: container.content,
source: container.content.isEmpty ? .nothingFound : .separateEndpoint
channels: channels,
source: channels.isEmpty
? .lineupWithoutMatches(available: container.content.count, enabled: enabled.count)
: .lineup(available: container.content.count)
)
} catch {
return ChannelLookup(channels: [], source: .failed(String(describing: error)))
@@ -148,13 +172,35 @@ extension PlexServer {
/// Werte werden nicht gezeigt, nur Struktur und Anzahl — das genügt, um
/// den richtigen Schlüssel zu finden, und nichts davon ist vertraulich.
public func liveTVStructure() async -> String {
var report = ["=== /livetv/dvrs ==="]
report.append(await structure(ofPath: "/livetv/dvrs"))
// Und die Senderliste beim Anbieter — dort stehen die Namen. Ohne sie
// im selben Abzug bliebe nach einem Fehlschlag wieder eine Runde
// Raten übrig.
if let dvr = try? await client.send(
PlexRequest(path: "/livetv/dvrs", timeout: .seconds(10)),
as: PlexMediaContainer<PlexDVR>.self
).content.first,
let epgIdentifier = dvr.epgIdentifier,
let lineup = dvr.lineups.first {
let path = "/\(epgIdentifier)/lineups/\(lineup.id)/channels"
report.append("\n=== \(path) ===")
report.append(await structure(ofPath: path))
} else {
report.append("\n=== Senderliste ===\nKeine Kennung oder Senderliste am Empfänger")
}
return report.joined(separator: "\n")
}
private func structure(ofPath path: String) async -> String {
let data: Data
do {
data = try await client.send(PlexRequest(path: "/livetv/dvrs", timeout: .seconds(10)))
data = try await client.send(PlexRequest(path: path, timeout: .seconds(30)))
} catch {
return "Abfrage gescheitert: \(error)"
}
guard let object = try? JSONSerialization.jsonObject(with: data) else {
return "Antwort ist kein JSON (\(data.count) Bytes)"
}
@@ -200,12 +246,12 @@ extension PlexServer {
/// Woher die Senderliste kam.
public struct ChannelLookup: Sendable {
public enum Source: Sendable, Equatable {
/// Aus der Antwort auf `/livetv/dvrs` — der übliche Weg.
case embeddedInDVR
/// Über `/livetv/dvrs/{id}/channels`.
case separateEndpoint
/// Beide Wege lieferten nichts.
case nothingFound
/// Aus der Senderliste des Programmanbieters.
case lineup(available: Int)
/// Die Senderliste kam an, aber keiner passte zur Zuordnung.
case lineupWithoutMatches(available: Int, enabled: Int)
/// Am Empfänger hängt keine Senderliste oder kein Programmanbieter.
case noLineup
case failed(String)
}
@@ -220,10 +266,14 @@ public struct ChannelLookup: Sendable {
/// Kurzbeschreibung für die Diagnose.
public var description: String {
switch source {
case .embeddedInDVR: "\(channels.count) Sender, aus der Empfängerliste"
case .separateEndpoint: "\(channels.count) Sender, über /livetv/dvrs/…/channels"
case .nothingFound: "Kein Weg lieferte Sender"
case .failed(let reason): "Abfrage gescheitert: \(reason)"
case .lineup(let available):
"\(channels.count) von \(available) Sendern eingeschaltet"
case .lineupWithoutMatches(let available, let enabled):
"Senderliste hat \(available) Sender, \(enabled) sind zugeordnet — keiner passte zusammen"
case .noLineup:
"Am Empfänger hängt keine Senderliste"
case .failed(let reason):
"Abfrage gescheitert: \(reason)"
}
}
}