Vier geratene Adressen, vier 404. Statt ein fünftes Mal zu raten, klopft die App jetzt in einem Durchgang und gleichzeitig ein Dutzend Adressen ab: was /media/providers an Endpunkten angibt, welche Unterverzeichnisse Programmanbieter und Empfänger in ihrer eigenen Antwort führen, die bekannten Formen anderer Serverfassungen, die Senderliste des Tuners selbst — und zuletzt das Programmraster. Dabei zwei Fehlerquellen mitbehoben, die jede einzeln wie „keine Sender" ausgesehen hätten: der Sendername steht je nach Tunertyp in title, name, channelTitle oder callSign, und eine Antwort mit dreißig namenlosen Einträgen sieht aus wie eine Senderliste, ist aber keine. Bleibt es leer, steht jetzt jede abgeklopfte Adresse mit ihrer Antwort in der Ansicht. Das beantwortet „liegt es am Server oder an der App" ohne eine weitere Runde Rückfragen. Dazu drei Sachen bei Serien: - Groß steht, worum es geht — bei einer Folge ihr Titel, bei einer Staffel der Name der Serie. „Staffel 10" als Überschrift sagte nicht, welche Serie gemeint ist. - Die Einordnung steht darunter: Serienname bei Folgen, Staffelnummer bei Staffeln. - Von der Folge führt ein Weg zur Staffel und zur Serie. Wer über „Weiterschauen" bei einer Folge landete, saß dort bisher fest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
229 lines
8.5 KiB
Swift
229 lines
8.5 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"]))
|
|
}
|
|
}
|
|
|
|
@Suite("Kennung der Senderliste")
|
|
struct LineupIdentifierTests {
|
|
|
|
@Test("Aus der vollen Adresse wird der nackte Teil")
|
|
func extractsBareIdentifier() {
|
|
// Genau der Wert von einem echten Server. Roh in den Pfad gesetzt
|
|
// ergab er 404: Schema, Doppelschrägstrich, Prozentzeichen und ein
|
|
// Anhang hinter der Raute.
|
|
let lineup = PlexLineup(
|
|
id: "lineup://tv.plex.providers.epg.cloud/6228f7f2364e02fa5ec8e51c#Swisscom%20Blue%20TV%20%28521%20channels%29"
|
|
)
|
|
#expect(lineup.pathIdentifier == "6228f7f2364e02fa5ec8e51c")
|
|
}
|
|
|
|
@Test("Eine Adresse ohne Anhang funktioniert auch")
|
|
func withoutFragment() {
|
|
let lineup = PlexLineup(id: "lineup://tv.plex.providers.epg.cloud/abc123")
|
|
#expect(lineup.pathIdentifier == "abc123")
|
|
}
|
|
|
|
@Test("Eine bereits nackte Kennung bleibt unverändert")
|
|
func alreadyBare() {
|
|
// Nicht jeder Anbieter liefert eine Adresse — manche geben die
|
|
// Kennung direkt.
|
|
#expect(PlexLineup(id: "abc123").pathIdentifier == "abc123")
|
|
}
|
|
|
|
@Test("Ein leerer Anhang stört nicht")
|
|
func emptyFragment() {
|
|
#expect(PlexLineup(id: "lineup://host/abc123#").pathIdentifier == "abc123")
|
|
}
|
|
}
|
|
|
|
@Suite("Adressen der Senderliste")
|
|
struct ChannelPathTests {
|
|
private let dvr = PlexDVR(
|
|
key: "1",
|
|
lineups: [PlexLineup(
|
|
id: "lineup://tv.plex.providers.epg.cloud/6228f7f2364e02fa5ec8e51c#Swisscom%20Blue%20TV"
|
|
)]
|
|
)
|
|
|
|
@Test("Die Kennung der Senderliste wird ausgepackt, nicht roh eingesetzt")
|
|
func usesBareIdentifier() throws {
|
|
// Der Fehler, der 404 ergab: die volle Adresse mit Schema, Anhang und
|
|
// Prozentzeichen landete unverändert im Pfad.
|
|
let paths = PlexServer.knownPaths(dvr: dvr, epgIdentifier: "epg:2")
|
|
let lineupPath = try #require(paths.first { $0.contains("/lineups/") })
|
|
|
|
#expect(lineupPath == "/epg:2/lineups/6228f7f2364e02fa5ec8e51c/channels")
|
|
#expect(!lineupPath.contains("lineup://"))
|
|
#expect(!lineupPath.contains("#"))
|
|
#expect(!lineupPath.contains("%"))
|
|
}
|
|
|
|
@Test("Es gibt Rückfalllinien")
|
|
func offersFallbacks() {
|
|
// Die Form unterscheidet sich zwischen Serverfassungen, und dieser
|
|
// Teil der API hat keine Beschreibung.
|
|
let paths = PlexServer.knownPaths(dvr: dvr, epgIdentifier: "epg:2")
|
|
#expect(paths.count >= 2)
|
|
#expect(paths.contains("/livetv/dvrs/1/channels"))
|
|
}
|
|
|
|
@Test("Ohne Senderliste bleiben die übrigen Wege")
|
|
func withoutLineup() {
|
|
let bare = PlexDVR(key: "7")
|
|
let paths = PlexServer.knownPaths(dvr: bare, epgIdentifier: "epg:2")
|
|
#expect(!paths.isEmpty)
|
|
#expect(paths.allSatisfy { !$0.contains("lineups//") })
|
|
}
|
|
}
|