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:
+2374
-2373
File diff suppressed because it is too large
Load Diff
@@ -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:
|
/// Zwei Quellen, die zusammengeführt werden müssen — das war der
|
||||||
/// Plex liefert die Sender bereits verschachtelt in der Antwort auf
|
/// eigentliche Stolperstein:
|
||||||
/// `/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.
|
|
||||||
///
|
///
|
||||||
/// Die Auskunft über die Quelle steht in der Diagnose. Bei einem Bereich
|
/// - Der Empfänger kennt nur die **Zuordnung**: welche Sender einer
|
||||||
/// ohne offizielle Beschreibung ist „welcher Weg hat gegriffen" die
|
/// Senderliste eingeschaltet sind, als nackte Schlüssel. Bei einer
|
||||||
/// Angabe, mit der sich der nächste Fehler finden lässt.
|
/// 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 {
|
public func channels(inDVR dvr: PlexDVR) async -> ChannelLookup {
|
||||||
if !dvr.channels.isEmpty {
|
let enabled = dvr.enabledChannelKeys
|
||||||
return ChannelLookup(channels: dvr.channels, source: .embeddedInDVR)
|
|
||||||
|
// `??` 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 {
|
do {
|
||||||
let container = try await client.send(
|
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
|
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(
|
return ChannelLookup(
|
||||||
channels: container.content,
|
channels: channels,
|
||||||
source: container.content.isEmpty ? .nothingFound : .separateEndpoint
|
source: channels.isEmpty
|
||||||
|
? .lineupWithoutMatches(available: container.content.count, enabled: enabled.count)
|
||||||
|
: .lineup(available: container.content.count)
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return ChannelLookup(channels: [], source: .failed(String(describing: error)))
|
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
|
/// Werte werden nicht gezeigt, nur Struktur und Anzahl — das genügt, um
|
||||||
/// den richtigen Schlüssel zu finden, und nichts davon ist vertraulich.
|
/// den richtigen Schlüssel zu finden, und nichts davon ist vertraulich.
|
||||||
public func liveTVStructure() async -> String {
|
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
|
let data: Data
|
||||||
do {
|
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 {
|
} catch {
|
||||||
return "Abfrage gescheitert: \(error)"
|
return "Abfrage gescheitert: \(error)"
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let object = try? JSONSerialization.jsonObject(with: data) else {
|
guard let object = try? JSONSerialization.jsonObject(with: data) else {
|
||||||
return "Antwort ist kein JSON (\(data.count) Bytes)"
|
return "Antwort ist kein JSON (\(data.count) Bytes)"
|
||||||
}
|
}
|
||||||
@@ -200,12 +246,12 @@ extension PlexServer {
|
|||||||
/// Woher die Senderliste kam.
|
/// Woher die Senderliste kam.
|
||||||
public struct ChannelLookup: Sendable {
|
public struct ChannelLookup: Sendable {
|
||||||
public enum Source: Sendable, Equatable {
|
public enum Source: Sendable, Equatable {
|
||||||
/// Aus der Antwort auf `/livetv/dvrs` — der übliche Weg.
|
/// Aus der Senderliste des Programmanbieters.
|
||||||
case embeddedInDVR
|
case lineup(available: Int)
|
||||||
/// Über `/livetv/dvrs/{id}/channels`.
|
/// Die Senderliste kam an, aber keiner passte zur Zuordnung.
|
||||||
case separateEndpoint
|
case lineupWithoutMatches(available: Int, enabled: Int)
|
||||||
/// Beide Wege lieferten nichts.
|
/// Am Empfänger hängt keine Senderliste oder kein Programmanbieter.
|
||||||
case nothingFound
|
case noLineup
|
||||||
case failed(String)
|
case failed(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,10 +266,14 @@ public struct ChannelLookup: Sendable {
|
|||||||
/// Kurzbeschreibung für die Diagnose.
|
/// Kurzbeschreibung für die Diagnose.
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch source {
|
switch source {
|
||||||
case .embeddedInDVR: "\(channels.count) Sender, aus der Empfängerliste"
|
case .lineup(let available):
|
||||||
case .separateEndpoint: "\(channels.count) Sender, über /livetv/dvrs/…/channels"
|
"\(channels.count) von \(available) Sendern eingeschaltet"
|
||||||
case .nothingFound: "Kein Weg lieferte Sender"
|
case .lineupWithoutMatches(let available, let enabled):
|
||||||
case .failed(let reason): "Abfrage gescheitert: \(reason)"
|
"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)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,24 +14,32 @@ public struct PlexDVR: Sendable, Equatable, Decodable, Identifiable {
|
|||||||
public let language: String?
|
public let language: String?
|
||||||
public let lineupTitle: String?
|
public let lineupTitle: String?
|
||||||
public let lineup: String?
|
public let lineup: String?
|
||||||
/// Die Empfänger am Server, jeweils mit ihren Sendern.
|
/// Die Empfänger, jeweils mit ihrer Senderzuordnung.
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
public let devices: [PlexTunerDevice]
|
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 }
|
public var id: String { key }
|
||||||
|
|
||||||
/// Alle Sender aller Empfänger dieses DVR.
|
/// Welche Sender eingeschaltet sind, als Schlüssel.
|
||||||
public var channels: [PlexChannel] {
|
///
|
||||||
devices.flatMap(\.channels)
|
/// 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 {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case key, uuid, language, lineupTitle, lineup
|
case key, uuid, language, lineupTitle, lineup, epgIdentifier
|
||||||
case devices = "Device"
|
case devices = "Device"
|
||||||
|
case lineups = "Lineup"
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: any Decoder) throws {
|
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.lineupTitle = try values.decodeIfPresent(String.self, forKey: .lineupTitle)
|
||||||
self.lineup = try values.decodeIfPresent(String.self, forKey: .lineup)
|
self.lineup = try values.decodeIfPresent(String.self, forKey: .lineup)
|
||||||
self.devices = try values.decodeIfPresent([PlexTunerDevice].self, forKey: .devices) ?? []
|
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(
|
public init(
|
||||||
@@ -55,7 +65,9 @@ public struct PlexDVR: Sendable, Equatable, Decodable, Identifiable {
|
|||||||
lineupTitle: String? = nil,
|
lineupTitle: String? = nil,
|
||||||
lineup: String? = nil,
|
lineup: String? = nil,
|
||||||
language: String? = nil,
|
language: String? = nil,
|
||||||
devices: [PlexTunerDevice] = []
|
devices: [PlexTunerDevice] = [],
|
||||||
|
epgIdentifier: String? = nil,
|
||||||
|
lineups: [PlexLineup] = []
|
||||||
) {
|
) {
|
||||||
self.key = key
|
self.key = key
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
@@ -63,6 +75,39 @@ public struct PlexDVR: Sendable, Equatable, Decodable, Identifiable {
|
|||||||
self.lineup = lineup
|
self.lineup = lineup
|
||||||
self.language = language
|
self.language = language
|
||||||
self.devices = devices
|
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 key: String?
|
||||||
public let uuid: String?
|
public let uuid: String?
|
||||||
public let title: 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" }
|
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 {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case key, uuid, title
|
case key, uuid, title
|
||||||
case channel = "Channel"
|
|
||||||
case channelMapping = "ChannelMapping"
|
case channelMapping = "ChannelMapping"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,21 +131,20 @@ public struct PlexTunerDevice: Sendable, Equatable, Decodable, Identifiable {
|
|||||||
self.key = try? values.decodeIfPresent(String.self, forKey: .key)
|
self.key = try? values.decodeIfPresent(String.self, forKey: .key)
|
||||||
self.uuid = try values.decodeIfPresent(String.self, forKey: .uuid)
|
self.uuid = try values.decodeIfPresent(String.self, forKey: .uuid)
|
||||||
self.title = try values.decodeIfPresent(String.self, forKey: .title)
|
self.title = try values.decodeIfPresent(String.self, forKey: .title)
|
||||||
|
self.channelMappings = try values
|
||||||
if values.contains(.channel) {
|
.decodeIfPresent([PlexChannelMapping].self, forKey: .channelMapping) ?? []
|
||||||
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 = []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.key = key
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.title = title
|
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) {
|
public init(id: String, title: String, channelIdentifier: String? = nil, thumb: String? = nil, key: String? = nil) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
|
|||||||
@@ -48,67 +48,64 @@ struct ClearLogoTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suite("Empfänger und Sender")
|
@Suite("Empfänger, Zuordnung und Senderliste")
|
||||||
struct TunerDecodingTests {
|
struct TunerDecodingTests {
|
||||||
|
|
||||||
@Test("Sender werden aus dem Empfänger gelesen")
|
/// Die Antwort, wie sie ein echter Server liefert.
|
||||||
func readsChannelsFromDevice() throws {
|
///
|
||||||
// Der Fall, an dem „Keine Sender" hing: Plex liefert sie verschachtelt
|
/// Nachgebildet aus dem Strukturabzug eines Servers mit Swisscom-Liste:
|
||||||
// in der Antwort auf /livetv/dvrs mit, nicht über einen eigenen
|
/// 28 zugeordnete Sender von 521 in der Liste. Entscheidend daran ist,
|
||||||
// Endpunkt.
|
/// was NICHT drinsteht — die Zuordnung führt keine Namen.
|
||||||
let json = """
|
private let dvrJSON = """
|
||||||
{"MediaContainer":{"Dvr":[{
|
{"MediaContainer":{"size":1,"Dvr":[{
|
||||||
"key": "1", "lineupTitle": "Kabel",
|
"key": "1",
|
||||||
|
"uuid": "dvr-uuid",
|
||||||
|
"epgIdentifier": "tv.plex.providers.epg.cloud:2",
|
||||||
|
"lineupTitle": "Swisscom Blue TV (Deutschschweiz)",
|
||||||
|
"lineup": "lineup://tv.plex.providers.epg.cloud/swisscom",
|
||||||
|
"Lineup": [{"id": "swisscom-de", "title": "Swisscom Blue TV (Deutschschweiz)"}],
|
||||||
"Device": [{
|
"Device": [{
|
||||||
"key": "2", "uuid": "tuner-1", "title": "HDHomeRun",
|
"key": "2", "uuid": "tuner-1", "title": "Swisscom Blue TV",
|
||||||
"Channel": [
|
"ChannelMapping": [
|
||||||
{"id": 10, "channelIdentifier": "1.1", "title": "SRF 1 HD", "key": "/livetv/channel/1"},
|
{"channelKey": "srf1", "lineupIdentifier": "swisscom-de", "enabled": "1"},
|
||||||
{"id": 11, "channelIdentifier": "1.2", "title": "SRF zwei"}
|
{"channelKey": "srf2", "lineupIdentifier": "swisscom-de", "enabled": "1"},
|
||||||
|
{"channelKey": "abgeschaltet", "lineupIdentifier": "swisscom-de", "enabled": "0"}
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
}]}}
|
}]}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
private func dvr() throws -> PlexDVR {
|
||||||
let container = try PlexResponse.decode(
|
let container = try PlexResponse.decode(
|
||||||
PlexMediaContainer<PlexDVR>.self,
|
PlexMediaContainer<PlexDVR>.self,
|
||||||
from: Data(json.utf8)
|
from: Data(dvrJSON.utf8)
|
||||||
)
|
)
|
||||||
let dvr = try #require(container.content.first)
|
return try #require(container.content.first)
|
||||||
|
|
||||||
#expect(dvr.devices.count == 1)
|
|
||||||
#expect(dvr.channels.count == 2)
|
|
||||||
#expect(dvr.channels.first?.title == "SRF 1 HD")
|
|
||||||
#expect(dvr.channels.first?.channelIdentifier == "1.1")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Auch die ältere Schreibweise ChannelMapping wird gelesen")
|
@Test("Die Kennung des Programmanbieters steht am Empfänger")
|
||||||
func readsChannelMapping() throws {
|
func readsEPGIdentifier() throws {
|
||||||
let json = """
|
// Verlässlicher, als sie aus der Anbieterliste zu suchen.
|
||||||
{"MediaContainer":{"Dvr":[{
|
#expect(try dvr().epgIdentifier == "tv.plex.providers.epg.cloud:2")
|
||||||
"key": "1",
|
|
||||||
"Device": [{"uuid": "t", "ChannelMapping": [{"id": "a", "title": "3sat"}]}]
|
|
||||||
}]}}
|
|
||||||
"""
|
|
||||||
let container = try PlexResponse.decode(
|
|
||||||
PlexMediaContainer<PlexDVR>.self,
|
|
||||||
from: Data(json.utf8)
|
|
||||||
)
|
|
||||||
#expect(container.content.first?.channels.first?.title == "3sat")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Ein Empfänger ohne Sender ergibt eine leere Liste, keinen Fehler")
|
@Test("Die Senderliste wird gelesen")
|
||||||
func deviceWithoutChannels() throws {
|
func readsLineup() throws {
|
||||||
let json = #"{"MediaContainer":{"Dvr":[{"key":"1","Device":[{"uuid":"t"}]}]}}"#
|
#expect(try dvr().lineups.first?.id == "swisscom-de")
|
||||||
let container = try PlexResponse.decode(
|
}
|
||||||
PlexMediaContainer<PlexDVR>.self,
|
|
||||||
from: Data(json.utf8)
|
@Test("Nur eingeschaltete Sender zählen zur Zuordnung")
|
||||||
)
|
func onlyEnabledMappings() throws {
|
||||||
#expect(container.content.first?.channels.isEmpty == true)
|
// `enabled` kommt als Zeichenkette „1" oder „0", nicht als
|
||||||
|
// Wahrheitswert.
|
||||||
|
let keys = try dvr().enabledChannelKeys
|
||||||
|
#expect(keys == ["srf1", "srf2"])
|
||||||
|
#expect(!keys.contains("abgeschaltet"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Ein Empfänger-Schlüssel als Zahl wird gelesen")
|
@Test("Ein Empfänger-Schlüssel als Zahl wird gelesen")
|
||||||
func numericDVRKey() throws {
|
func numericDVRKey() throws {
|
||||||
// Je nach Serverfassung kommt er als Zahl oder als Zeichenkette. Ein
|
// Je nach Serverfassung kommt er als Zahl oder als Zeichenkette.
|
||||||
// fester Typ hätte die ganze Empfängerliste unlesbar gemacht.
|
|
||||||
let json = #"{"MediaContainer":{"Dvr":[{"key":42}]}}"#
|
let json = #"{"MediaContainer":{"Dvr":[{"key":42}]}}"#
|
||||||
let container = try PlexResponse.decode(
|
let container = try PlexResponse.decode(
|
||||||
PlexMediaContainer<PlexDVR>.self,
|
PlexMediaContainer<PlexDVR>.self,
|
||||||
@@ -117,3 +114,42 @@ struct TunerDecodingTests {
|
|||||||
#expect(container.content.first?.key == "42")
|
#expect(container.content.first?.key == "42")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suite("Abgleich von Zuordnung und Senderliste")
|
||||||
|
struct ChannelMatchingTests {
|
||||||
|
|
||||||
|
@Test("Übereinstimmung über die Kennung")
|
||||||
|
func matchesByIdentifier() {
|
||||||
|
let channel = PlexChannel(id: "srf1", title: "SRF 1")
|
||||||
|
#expect(channel.matches(anyOf: ["srf1", "srf2"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Übereinstimmung über die Sendernummer")
|
||||||
|
func matchesByChannelNumber() {
|
||||||
|
let channel = PlexChannel(id: "x", title: "SRF 1", channelIdentifier: "1.1")
|
||||||
|
#expect(channel.matches(anyOf: ["1.1"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Übereinstimmung über den Pfad")
|
||||||
|
func matchesByPath() {
|
||||||
|
// Je nach Anbieter führt die Zuordnung den vollen Pfad, der Sender
|
||||||
|
// aber nur die Kennung — oder umgekehrt.
|
||||||
|
let channel = PlexChannel(id: "x", title: "SRF 1", key: "/livetv/channels/srf1")
|
||||||
|
#expect(channel.matches(anyOf: ["srf1"]))
|
||||||
|
#expect(channel.matches(anyOf: ["/livetv/channels/srf1"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Umgekehrt: Zuordnung führt den Pfad, Sender die Kennung")
|
||||||
|
func matchesReversed() {
|
||||||
|
let channel = PlexChannel(id: "srf1", title: "SRF 1")
|
||||||
|
#expect(channel.matches(anyOf: ["/lineups/swisscom/channels/srf1"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Nicht zugeordnete Sender fallen heraus")
|
||||||
|
func nonMatchingIsExcluded() {
|
||||||
|
// Der Punkt der ganzen Übung: von 521 Sendern der Liste sollen die 28
|
||||||
|
// übrig bleiben, die eingeschaltet sind.
|
||||||
|
let channel = PlexChannel(id: "orf1", title: "ORF 1")
|
||||||
|
#expect(!channel.matches(anyOf: ["srf1", "srf2"]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user