Der Server antwortet auf eine angeforderte Umrechnung mit „direkte Wiedergabe nicht verfügbar; Umwandlung ok" — Code 1001. Meine Prüfung ließ nur 1000 durch, also ausschließlich DirectPlay. Damit blockierte die App ausgerechnet den Fall, den sie selbst angefordert hatte: wer im Player eine Stufe wählt, will umgerechnet bekommen, und die einzige richtige Antwort darauf kam als Fehler an. Es spielte gar nichts mehr. 1000 heißt „direkt abspielbar", 1001 „direkt nicht, umrechnen schon", 2000 „Umwandlung in Ordnung" — alles Zusagen. Abgelehnt wird erst ab 3000, etwa bei fehlender Berechtigung oder überlastetem Transcoder. Vier Tests halten die Codes fest, damit die Grenze nicht wieder zur Auslegungssache wird. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
142 lines
5.9 KiB
Swift
142 lines
5.9 KiB
Swift
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)
|
|
}
|
|
}
|