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.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.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.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//") }) } }