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>
156 lines
5.8 KiB
Swift
156 lines
5.8 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import PlexKit
|
|
|
|
@Suite("Schriftzüge und Bildliste")
|
|
struct ClearLogoTests {
|
|
private func item(from json: String) throws -> PlexItem {
|
|
let data = Data(#"{"MediaContainer":{"Metadata":[\#(json)]}}"#.utf8)
|
|
return try #require(
|
|
try PlexResponse.decode(PlexMediaContainer<PlexItem>.self, from: data).content.first
|
|
)
|
|
}
|
|
|
|
@Test("Der Schriftzug wird aus der Bildliste gelesen")
|
|
func findsClearLogo() throws {
|
|
let decoded = try item(from: """
|
|
{
|
|
"ratingKey": "1", "type": "movie", "title": "Harry Potter",
|
|
"Image": [
|
|
{"alt": "Harry Potter", "type": "coverPoster", "url": "/library/metadata/1/thumb/1"},
|
|
{"alt": "Harry Potter", "type": "background", "url": "/library/metadata/1/art/1"},
|
|
{"alt": "Harry Potter", "type": "clearLogo", "url": "/library/metadata/1/clearLogo/1"}
|
|
]
|
|
}
|
|
""")
|
|
|
|
#expect(decoded.clearLogoPath == "/library/metadata/1/clearLogo/1")
|
|
}
|
|
|
|
@Test("Ohne Schriftzug bleibt es beim Titel")
|
|
func withoutClearLogo() throws {
|
|
// Der Normalfall bei selbst zusammengestellten Mediatheken.
|
|
let decoded = try item(from: """
|
|
{
|
|
"ratingKey": "1", "type": "movie", "title": "Ein Film",
|
|
"Image": [{"type": "coverPoster", "url": "/library/metadata/1/thumb/1"}]
|
|
}
|
|
""")
|
|
#expect(decoded.clearLogoPath == nil)
|
|
}
|
|
|
|
@Test("Eine fehlende Bildliste ist kein Fehler")
|
|
func missingImageList() throws {
|
|
// Listen liefern keine Bildliste mit — nur die Einzelabfrage tut das.
|
|
let decoded = try item(from: #"{"ratingKey": "1", "type": "movie", "title": "Ein Film"}"#)
|
|
#expect(decoded.images.isEmpty)
|
|
#expect(decoded.clearLogoPath == nil)
|
|
}
|
|
}
|
|
|
|
@Suite("Empfänger, Zuordnung und Senderliste")
|
|
struct TunerDecodingTests {
|
|
|
|
/// Die Antwort, wie sie ein echter Server liefert.
|
|
///
|
|
/// Nachgebildet aus dem Strukturabzug eines Servers mit Swisscom-Liste:
|
|
/// 28 zugeordnete Sender von 521 in der Liste. Entscheidend daran ist,
|
|
/// was NICHT drinsteht — die Zuordnung führt keine Namen.
|
|
private let dvrJSON = """
|
|
{"MediaContainer":{"size":1,"Dvr":[{
|
|
"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": [{
|
|
"key": "2", "uuid": "tuner-1", "title": "Swisscom Blue TV",
|
|
"ChannelMapping": [
|
|
{"channelKey": "srf1", "lineupIdentifier": "swisscom-de", "enabled": "1"},
|
|
{"channelKey": "srf2", "lineupIdentifier": "swisscom-de", "enabled": "1"},
|
|
{"channelKey": "abgeschaltet", "lineupIdentifier": "swisscom-de", "enabled": "0"}
|
|
]
|
|
}]
|
|
}]}}
|
|
"""
|
|
|
|
private func dvr() throws -> PlexDVR {
|
|
let container = try PlexResponse.decode(
|
|
PlexMediaContainer<PlexDVR>.self,
|
|
from: Data(dvrJSON.utf8)
|
|
)
|
|
return try #require(container.content.first)
|
|
}
|
|
|
|
@Test("Die Kennung des Programmanbieters steht am Empfänger")
|
|
func readsEPGIdentifier() throws {
|
|
// Verlässlicher, als sie aus der Anbieterliste zu suchen.
|
|
#expect(try dvr().epgIdentifier == "tv.plex.providers.epg.cloud:2")
|
|
}
|
|
|
|
@Test("Die Senderliste wird gelesen")
|
|
func readsLineup() throws {
|
|
#expect(try dvr().lineups.first?.id == "swisscom-de")
|
|
}
|
|
|
|
@Test("Nur eingeschaltete Sender zählen zur Zuordnung")
|
|
func onlyEnabledMappings() throws {
|
|
// `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")
|
|
func numericDVRKey() throws {
|
|
// Je nach Serverfassung kommt er als Zahl oder als Zeichenkette.
|
|
let json = #"{"MediaContainer":{"Dvr":[{"key":42}]}}"#
|
|
let container = try PlexResponse.decode(
|
|
PlexMediaContainer<PlexDVR>.self,
|
|
from: Data(json.utf8)
|
|
)
|
|
#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"]))
|
|
}
|
|
}
|