Wiedergabe: „Umwandlung ok" war eine Zusage und wurde als Absage gelesen

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>
This commit is contained in:
Scarriffle
2026-09-12 22:14:29 +02:00
co-authored by Claude Opus 5
parent f628819765
commit 0bafec4838
3 changed files with 2830 additions and 2777 deletions
+26 -25
View File
@@ -1,10 +1,6 @@
{ {
"sourceLanguage" : "en", "sourceLanguage" : "en",
"strings" : { "strings" : {
"%@ GB": {
"comment": "A picker option in the settings view that lets the user select the maximum amount of storage space that can be used for downloaded content. The value is given in gigabytes.",
"isCommentAutoGenerated": true
},
"%@ → %@" : { "%@ → %@" : {
"comment" : "A line in a diagnostic trace that shows a network request path and its result. The first argument is the network request path. The second argument is the network response result.", "comment" : "A line in a diagnostic trace that shows a network request path and its result. The first argument is the network request path. The second argument is the network response result.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -17,8 +13,8 @@
} }
} }
}, },
"1080p": { "%@ GB" : {
"comment": "A label for a 1080p video.", "comment" : "A picker option in the settings view that lets the user select the maximum amount of storage space that can be used for downloaded content. The value is given in gigabytes.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"4K" : { "4K" : {
@@ -29,8 +25,8 @@
"comment" : "A label for a 720p video.", "comment" : "A label for a 720p video.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"SD": { "1080p" : {
"comment": "A label for a 720p movie.", "comment" : "A label for a 1080p video.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"action.apply" : { "action.apply" : {
@@ -2599,23 +2595,6 @@
} }
} }
}, },
"player.subtitleTracks": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Untertitel"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Subtitles"
}
}
}
},
"player.subtitlesOff" : { "player.subtitlesOff" : {
"comment" : "Eintrag, der die Untertitel abschaltet", "comment" : "Eintrag, der die Untertitel abschaltet",
"extractionState" : "stale", "extractionState" : "stale",
@@ -2634,6 +2613,23 @@
} }
} }
}, },
"player.subtitleTracks" : {
"extractionState" : "stale",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Untertitel"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Subtitles"
}
}
}
},
"player.tracks" : { "player.tracks" : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -2973,6 +2969,10 @@
} }
} }
}, },
"SD" : {
"comment" : "A label for a 720p movie.",
"isCommentAutoGenerated" : true
},
"search.hint" : { "search.hint" : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -4139,6 +4139,7 @@
} }
}, },
"technical.reason.chosen" : { "technical.reason.chosen" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -149,12 +149,24 @@ public struct TranscodeDecision: Sendable, Decodable {
/// Ob der Server die Wiedergabe grundsätzlich zulässt. /// Ob der Server die Wiedergabe grundsätzlich zulässt.
/// ///
/// Code 1000 heißt „in Ordnung". Alles andere ist eine Absage — meist /// `1000` heißt „direkt abspielbar", `1001` „direkt nicht, umrechnen
/// fehlende Berechtigungen oder ein überlasteter Transcoder. /// schon", `2000` „Umwandlung in Ordnung". Das sind alles **Zusagen** —
/// erst ab `3000` wird abgelehnt, etwa bei fehlender Berechtigung oder
/// überlastetem Transcoder.
///
/// Vorher galt nur `1000` als Zusage. Damit wurde ausgerechnet der Fall
/// blockiert, den die App selbst angefordert hatte: wer im Player eine
/// Stufe wählt, will umgerechnet bekommen, und der Server antwortete
/// „direkte Wiedergabe nicht verfügbar; Umwandlung ok" — eine Zusage, die
/// hier als Absage ankam. Es spielte dann gar nichts.
public var isAllowed: Bool { public var isAllowed: Bool {
generalDecisionCode == nil || generalDecisionCode == 1000 guard let generalDecisionCode else { return true }
return generalDecisionCode < Self.firstRefusalCode
} }
/// Ab hier lehnt Plex ab.
static let firstRefusalCode = 3_000
public var explanation: String? { public var explanation: String? {
generalDecisionText ?? transcodeDecisionText generalDecisionText ?? transcodeDecisionText
} }
@@ -99,3 +99,43 @@ struct DurationConversionTests {
#expect(abs(Duration.milliseconds(1_500).seconds - 1.5) < 0.0001) #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)
}
}