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:
@@ -14,24 +14,32 @@ public struct PlexDVR: Sendable, Equatable, Decodable, Identifiable {
|
||||
public let language: String?
|
||||
public let lineupTitle: String?
|
||||
public let lineup: String?
|
||||
/// Die Empfänger am Server, jeweils mit ihren Sendern.
|
||||
///
|
||||
/// Plex liefert sie verschachtelt in der Antwort auf `/livetv/dvrs` mit.
|
||||
/// Der getrennte Endpunkt `/livetv/dvrs/{id}/channels` war die erste
|
||||
/// Vermutung und lieferte nichts — daran hing „Keine Sender" bei einem
|
||||
/// Server, auf dem über dreißig Sender eingerichtet sind.
|
||||
/// Die Empfänger, jeweils mit ihrer Senderzuordnung.
|
||||
public let devices: [PlexTunerDevice]
|
||||
/// Die Kennung des Programmanbieters, etwa `tv.plex.providers.epg.cloud:2`.
|
||||
///
|
||||
/// Steht direkt am Empfänger — verlässlicher, als sie aus der
|
||||
/// Anbieterliste zu suchen.
|
||||
public let epgIdentifier: String?
|
||||
/// Die Senderlisten, aus denen ausgewählt wurde.
|
||||
public let lineups: [PlexLineup]
|
||||
|
||||
public var id: String { key }
|
||||
|
||||
/// Alle Sender aller Empfänger dieses DVR.
|
||||
public var channels: [PlexChannel] {
|
||||
devices.flatMap(\.channels)
|
||||
/// Welche Sender eingeschaltet sind, als Schlüssel.
|
||||
///
|
||||
/// Die Zuordnung enthält **keine Namen** — nur, welche Sender der
|
||||
/// Senderliste aktiv sind. Die Namen stehen beim Programmanbieter. Das
|
||||
/// war der Grund, warum die Senderansicht leer blieb: die Zuordnung sah
|
||||
/// nach einer Senderliste aus, war aber nur ein Verweis darauf.
|
||||
public var enabledChannelKeys: Set<String> {
|
||||
Set(devices.flatMap(\.channelMappings).filter(\.isEnabled).map(\.channelKey))
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key, uuid, language, lineupTitle, lineup
|
||||
case key, uuid, language, lineupTitle, lineup, epgIdentifier
|
||||
case devices = "Device"
|
||||
case lineups = "Lineup"
|
||||
}
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
@@ -47,6 +55,8 @@ public struct PlexDVR: Sendable, Equatable, Decodable, Identifiable {
|
||||
self.lineupTitle = try values.decodeIfPresent(String.self, forKey: .lineupTitle)
|
||||
self.lineup = try values.decodeIfPresent(String.self, forKey: .lineup)
|
||||
self.devices = try values.decodeIfPresent([PlexTunerDevice].self, forKey: .devices) ?? []
|
||||
self.epgIdentifier = try values.decodeIfPresent(String.self, forKey: .epgIdentifier)
|
||||
self.lineups = try values.decodeIfPresent([PlexLineup].self, forKey: .lineups) ?? []
|
||||
}
|
||||
|
||||
public init(
|
||||
@@ -55,7 +65,9 @@ public struct PlexDVR: Sendable, Equatable, Decodable, Identifiable {
|
||||
lineupTitle: String? = nil,
|
||||
lineup: String? = nil,
|
||||
language: String? = nil,
|
||||
devices: [PlexTunerDevice] = []
|
||||
devices: [PlexTunerDevice] = [],
|
||||
epgIdentifier: String? = nil,
|
||||
lineups: [PlexLineup] = []
|
||||
) {
|
||||
self.key = key
|
||||
self.uuid = uuid
|
||||
@@ -63,6 +75,39 @@ public struct PlexDVR: Sendable, Equatable, Decodable, Identifiable {
|
||||
self.lineup = lineup
|
||||
self.language = language
|
||||
self.devices = devices
|
||||
self.epgIdentifier = epgIdentifier
|
||||
self.lineups = lineups
|
||||
}
|
||||
}
|
||||
|
||||
/// Eine Senderliste des Anbieters — bei Kabel oder IPTV oft mehrere hundert.
|
||||
public struct PlexLineup: Sendable, Equatable, Decodable, Identifiable {
|
||||
public let id: String
|
||||
public let title: String?
|
||||
|
||||
public init(id: String, title: String? = nil) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
}
|
||||
}
|
||||
|
||||
/// Ein Eintrag der Senderzuordnung.
|
||||
///
|
||||
/// Sagt, welcher Sender der Senderliste am Empfänger eingeschaltet ist —
|
||||
/// mehr nicht. Namen, Nummern und Logos stehen beim Programmanbieter.
|
||||
public struct PlexChannelMapping: Sendable, Equatable, Decodable {
|
||||
public let channelKey: String
|
||||
public let lineupIdentifier: String?
|
||||
private let enabled: String?
|
||||
|
||||
/// Plex schreibt hier `"1"` oder `"0"` als Zeichenkette, nicht als
|
||||
/// Wahrheitswert.
|
||||
public var isEnabled: Bool { enabled != "0" }
|
||||
|
||||
public init(channelKey: String, lineupIdentifier: String? = nil, enabled: String? = "1") {
|
||||
self.channelKey = channelKey
|
||||
self.lineupIdentifier = lineupIdentifier
|
||||
self.enabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,16 +116,13 @@ public struct PlexTunerDevice: Sendable, Equatable, Decodable, Identifiable {
|
||||
public let key: String?
|
||||
public let uuid: String?
|
||||
public let title: String?
|
||||
public let channels: [PlexChannel]
|
||||
/// Welche Sender der Senderliste hier eingeschaltet sind.
|
||||
public let channelMappings: [PlexChannelMapping]
|
||||
|
||||
public var id: String { uuid ?? key ?? title ?? "tuner" }
|
||||
|
||||
/// Plex nennt die Senderliste je nach Fassung `Channel` oder
|
||||
/// `ChannelMapping`. Beide werden gelesen — eine falsche Annahme darüber
|
||||
/// ergibt eine leere Liste, und die sieht aus wie „kein Sender vorhanden".
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key, uuid, title
|
||||
case channel = "Channel"
|
||||
case channelMapping = "ChannelMapping"
|
||||
}
|
||||
|
||||
@@ -89,21 +131,20 @@ public struct PlexTunerDevice: Sendable, Equatable, Decodable, Identifiable {
|
||||
self.key = try? values.decodeIfPresent(String.self, forKey: .key)
|
||||
self.uuid = try values.decodeIfPresent(String.self, forKey: .uuid)
|
||||
self.title = try values.decodeIfPresent(String.self, forKey: .title)
|
||||
|
||||
if values.contains(.channel) {
|
||||
self.channels = try values.decode([PlexChannel].self, forKey: .channel)
|
||||
} else if values.contains(.channelMapping) {
|
||||
self.channels = try values.decode([PlexChannel].self, forKey: .channelMapping)
|
||||
} else {
|
||||
self.channels = []
|
||||
}
|
||||
self.channelMappings = try values
|
||||
.decodeIfPresent([PlexChannelMapping].self, forKey: .channelMapping) ?? []
|
||||
}
|
||||
|
||||
public init(key: String? = nil, uuid: String? = nil, title: String? = nil, channels: [PlexChannel] = []) {
|
||||
public init(
|
||||
key: String? = nil,
|
||||
uuid: String? = nil,
|
||||
title: String? = nil,
|
||||
channelMappings: [PlexChannelMapping] = []
|
||||
) {
|
||||
self.key = key
|
||||
self.uuid = uuid
|
||||
self.title = title
|
||||
self.channels = channels
|
||||
self.channelMappings = channelMappings
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +185,26 @@ public struct PlexChannel: Sendable, Equatable, Decodable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ob dieser Sender in der Zuordnung des Empfängers steht.
|
||||
///
|
||||
/// Nachsichtig verglichen: die Zuordnung führt einen `channelKey`, und
|
||||
/// je nach Anbieter entspricht der 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".
|
||||
public func matches(anyOf keys: Set<String>) -> Bool {
|
||||
if keys.contains(id) { return true }
|
||||
if let channelIdentifier, keys.contains(channelIdentifier) { return true }
|
||||
if let key {
|
||||
if keys.contains(key) { return true }
|
||||
if let last = key.split(separator: "/").last, keys.contains(String(last)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Umgekehrt: manche Zuordnungen führen den vollen Pfad, der Sender
|
||||
// aber nur die Kennung.
|
||||
return keys.contains { $0.split(separator: "/").last.map(String.init) == id }
|
||||
}
|
||||
|
||||
public init(id: String, title: String, channelIdentifier: String? = nil, thumb: String? = nil, key: String? = nil) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
|
||||
Reference in New Issue
Block a user