import Foundation import Testing @testable import PlexKit @Suite("Transcode-Anfrage") struct TranscodeRequestTests { private func value(_ request: TranscodeRequest, _ name: String) -> String? { request.queryItems.first { $0.name == name }?.value } @Test("Der Pfad zeigt auf den Titel, nicht auf die Datei") func pathPointsAtMetadata() { // Plex erwartet hier die Metadaten-Adresse. Gibt man den Dateipfad an, // antwortet der Transcoder mit 404. let request = TranscodeRequest(ratingKey: "45231", isLocal: true) #expect(value(request, "path") == "/library/metadata/45231") } @Test("Umpacken ist erlaubt, DirectPlay ausdrücklich nicht") func allowsDirectStreamButNotDirectPlay() { // directStream=1 lässt Plex nur den Container wechseln, wenn der // Bildcodec passt — das spart die Neukodierung. directPlay=0, weil // diese Frage der PlaybackDecider bereits entschieden hat. let request = TranscodeRequest(ratingKey: "1", isLocal: true) #expect(value(request, "directStream") == "1") #expect(value(request, "directPlay") == "0") } @Test("Ohne fastSeek müsste der Transcoder beim Springen von vorn beginnen") func enablesFastSeek() { #expect(value(TranscodeRequest(ratingKey: "1", isLocal: true), "fastSeek") == "1") } @Test("Die Bitratengrenze bringt eine passende Qualitätsstufe mit") func bitrateCarriesQuality() { // Ohne videoQuality ignoriert Plex die Grenze bei manchen Fassungen. let request = TranscodeRequest(ratingKey: "1", maximumBitrate: 4_000, isLocal: false) #expect(value(request, "maxVideoBitrate") == "4000") #expect(value(request, "videoQuality") != nil) } @Test("Höhere Bitraten ergeben höhere Qualitätsstufen") func qualityRisesWithBitrate() { func quality(_ bitrate: Int) -> Int { Int(value(TranscodeRequest(ratingKey: "1", maximumBitrate: bitrate, isLocal: true), "videoQuality") ?? "0")! } #expect(quality(2_000) < quality(8_000)) #expect(quality(8_000) < quality(20_000)) #expect(quality(20_000) <= 100) } @Test("Ohne Grenze steht keine Bitrate in der Anfrage") func noBitrateWhenUnlimited() { // Ein leerer Wert ist für Plex nicht dasselbe wie ein fehlender — er // würde als Grenze von null gelesen. let request = TranscodeRequest(ratingKey: "1", isLocal: true) #expect(value(request, "maxVideoBitrate") == nil) #expect(value(request, "videoQuality") == nil) } @Test("Lokal und außer Haus werden unterschieden") func distinguishesLocation() { #expect(value(TranscodeRequest(ratingKey: "1", isLocal: true), "location") == "lan") #expect(value(TranscodeRequest(ratingKey: "1", isLocal: false), "location") == "wan") } @Test("Die Sitzungskennung steht in beiden Feldern") func sessionAppearsTwice() { // Plex erwartet sie sowohl als `session` als auch als // `X-Plex-Session-Identifier`. Fehlt eines, lässt sich der Transcoder // später nicht gezielt anhalten — und er rechnet minutenlang weiter. let request = TranscodeRequest(ratingKey: "1", isLocal: true, sessionIdentifier: "sitzung-1") #expect(value(request, "session") == "sitzung-1") #expect(value(request, "X-Plex-Session-Identifier") == "sitzung-1") } @Test("Untertitel werden nur eingebrannt, wenn eine Spur gewählt ist") func subtitlesOnlyWhenRequested() { let without = TranscodeRequest(ratingKey: "1", isLocal: true) #expect(value(without, "subtitles") == nil) let with = TranscodeRequest(ratingKey: "1", isLocal: true, subtitleStreamID: 310_024) #expect(value(with, "subtitles") == "burn") #expect(value(with, "subtitleStreamID") == "310024") } } @Suite("Dauer in Plex-Einheiten") struct DurationConversionTests { @Test("Millisekunden werden ganzzahlig gerechnet") func millisecondsAreWholeNumbers() { #expect(Duration.seconds(90).milliseconds == 90_000) #expect(Duration.milliseconds(4_892_000).milliseconds == 4_892_000) #expect(Duration.zero.milliseconds == 0) } @Test("Sekunden als Gleitkommazahl") func secondsAsDouble() { #expect(abs(Duration.milliseconds(1_500).seconds - 1.5) < 0.0001) } } /// Was der Server mit seinen Codes meint. @Suite("Entscheidung des Servers") struct TranscodeDecisionTests { private func decision(code: Int, text: String) throws -> TranscodeDecision { let json = """ {"MediaContainer":{"generalDecisionCode":\(code),"generalDecisionText":"\(text)"}} """ return try PlexResponse.decode(TranscodeDecision.self, from: Data(json.utf8)) } @Test("Umwandlung erlaubt ist eine Zusage, keine Absage") func treatsConversionAsAllowed() throws { // Der Fehler, an dem nichts mehr abspielte: die App fordert eine // Umrechnung an, der Server sagt „direkte Wiedergabe nicht verfügbar; // Umwandlung ok — und die App las das als Nein. #expect(try decision(code: 1_001, text: "Direct play not available").isAllowed) #expect(try decision(code: 2_000, text: "Conversion OK").isAllowed) } @Test("Direkte Wiedergabe bleibt eine Zusage") func treatsDirectPlayAsAllowed() throws { #expect(try decision(code: 1_000, text: "Direct play OK").isAllowed) } @Test("Ab dreitausend ist es eine Absage") func treatsHighCodesAsRefusal() throws { #expect(try decision(code: 3_000, text: "Not allowed").isAllowed == false) #expect(try decision(code: 6_000, text: "Transcoder busy").isAllowed == false) } @Test("Ohne Code keine Absage") func allowsWhenSilent() throws { let json = #"{"MediaContainer":{}}"# let decision = try PlexResponse.decode(TranscodeDecision.self, from: Data(json.utf8)) #expect(decision.isAllowed) } }