Wiedergabe: das Geräteprofil zerfiel in zwei Dutzend eigene Parameter
`URLComponents` kodiert `&` und `=` innerhalb eines Abfragewerts nicht.
Das Geräteprofil steckt voll davon:
X-Plex-Client-Profile-Extra=add-transcode-target(type=videoProfile&
context=streaming&protocol=hls&container=mpegts&…)
Der Server hat das nicht als ein Profil gelesen, sondern als zwei Dutzend
eigene Angaben — darunter ein zweites `protocol=`. Eine widersprüchliche
Anfrage, und die Antwort darauf ist eine nackte 400 ohne Hinweis.
Der Fehler steckte in jeder Transcoder-Adresse, seit es sie gibt. Bei
Dateien fiel er nicht auf, weil dort meist DirectPlay greift und der
Transcoder gar nicht gefragt wird.
Kodiert wird der **ursprüngliche** Wert, nicht der bereits von
`URLComponents` behandelte: den ein zweites Mal zu kodieren macht aus
`%3D` ein `%253D` — derselbe Fehler, nur eine Ebene tiefer. Der Test
prüft beides: dass das Profil ein einziger Parameter bleibt und dass
`protocol` genau einmal vorkommt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c24938590d
commit
3467f0f690
@@ -691,16 +691,18 @@ extension PlexServer {
|
||||
// angelegt hat, wird umgerechnet — nicht die Abspielliste darin und
|
||||
// nicht meine eigene Kennung. Ihr Schlüssel steht in der Antwort.
|
||||
if let sessionKey = Self.firstSessionKey(in: data) {
|
||||
let outcome = await attemptStream(
|
||||
path: sessionKey,
|
||||
ratingKey: channel.id,
|
||||
mediaIndex: 0,
|
||||
includeProfile: true,
|
||||
maximumBitrate: maximumBitrate,
|
||||
session: session
|
||||
)
|
||||
probes.append(outcome.probe)
|
||||
if let url = outcome.url { return (url, subscription, probes) }
|
||||
for withProfile in [true, false] {
|
||||
let outcome = await attemptStream(
|
||||
path: sessionKey,
|
||||
ratingKey: channel.id,
|
||||
mediaIndex: 0,
|
||||
includeProfile: withProfile,
|
||||
maximumBitrate: maximumBitrate,
|
||||
session: session
|
||||
)
|
||||
probes.append(outcome.probe)
|
||||
if let url = outcome.url { return (url, subscription, probes) }
|
||||
}
|
||||
}
|
||||
|
||||
// Und die Abspielliste unmittelbar. Sie antwortete mit 404 statt 400:
|
||||
|
||||
@@ -32,20 +32,38 @@ extension PlexServer {
|
||||
)
|
||||
else { return nil }
|
||||
|
||||
components.queryItems = request.queryItems + [
|
||||
let items = request.queryItems + [
|
||||
URLQueryItem(name: "X-Plex-Token", value: token),
|
||||
] + (includeProfile ? clientProfileItems : []) + identity.queryItems
|
||||
components.queryItems = items
|
||||
|
||||
// `path` von Hand einsetzen heißt: genau einmal kodiert. Das ist für
|
||||
// Dateien richtig. Für eine Sendung beim Programmführer muss die
|
||||
// Kennung ein zweites Mal kodiert werden — dann übernimmt das
|
||||
// `URLComponents` von selbst, und hier ist nichts zu tun.
|
||||
if keepPathEncoding {
|
||||
components.percentEncodedQueryItems = components.percentEncodedQueryItems?.map { item in
|
||||
item.name == "path"
|
||||
? URLQueryItem(name: "path", value: request.percentEncodedPath)
|
||||
: item
|
||||
components.percentEncodedQueryItems = components.percentEncodedQueryItems?.map { item in
|
||||
// `path` von Hand einsetzen heißt: genau einmal kodiert. Das ist
|
||||
// für Dateien richtig. Für eine Sendung beim Programmführer muss
|
||||
// die Kennung ein zweites Mal kodiert werden — dann übernimmt das
|
||||
// `URLComponents` von selbst, und hier ist nichts zu tun.
|
||||
if item.name == "path", keepPathEncoding {
|
||||
return URLQueryItem(name: "path", value: request.percentEncodedPath)
|
||||
}
|
||||
|
||||
// Das Geräteprofil ist ein Wert voller `&`, `=` und Klammern —
|
||||
// und `URLComponents` lässt genau die unangetastet. Der Server
|
||||
// liest sie dann als eigene Parameter: aus einem Profil werden
|
||||
// zwei Dutzend Angaben, darunter ein zweites `protocol=`. Das
|
||||
// steckte in **jeder** Transcoder-Adresse, seit es sie gibt.
|
||||
if item.name.hasSuffix("Profile-Extra"),
|
||||
// Der **ursprüngliche** Wert, nicht der bereits von
|
||||
// `URLComponents` behandelte: den ein zweites Mal zu kodieren
|
||||
// macht aus `%3D` ein `%253D`.
|
||||
let value = items.first(where: { $0.name == item.name })?.value {
|
||||
return URLQueryItem(
|
||||
name: item.name,
|
||||
value: value.addingPercentEncoding(withAllowedCharacters: .plexParameterAllowed)
|
||||
?? value
|
||||
)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
return components.url
|
||||
}
|
||||
@@ -298,3 +316,15 @@ extension PlexServer {
|
||||
return components.url
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension CharacterSet {
|
||||
/// Was in einem Abfragewert unangetastet bleiben darf.
|
||||
///
|
||||
/// Streng gehalten: alles andere wird kodiert. `&` und `=` sind hier die
|
||||
/// eigentlichen Übeltäter — sie trennen Parameter, und `URLComponents`
|
||||
/// kodiert sie in Werten nicht von selbst.
|
||||
static let plexParameterAllowed = CharacterSet(
|
||||
charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -350,6 +350,27 @@ struct LiveTVGuideTests {
|
||||
== "/livetv/sessions/1268be73-1598-4fb9-ac9e-35825eba3fdd")
|
||||
}
|
||||
|
||||
@Test("Das Geräteprofil zerfällt nicht in eigene Parameter")
|
||||
func encodesProfileExtra() async throws {
|
||||
// `URLComponents` lässt `&` und `=` in einem Wert unangetastet. Das
|
||||
// Geräteprofil steckt voll davon, und der Server las es als zwei
|
||||
// Dutzend eigene Angaben — darunter ein zweites `protocol=`. Der
|
||||
// Fehler steckte in jeder Transcoder-Adresse, seit es sie gibt.
|
||||
let server = server(routes: [:])
|
||||
let url = try #require(
|
||||
await server.transcodeURL(for: TranscodeRequest(ratingKey: "5", isLocal: true))
|
||||
)
|
||||
let text = url.absoluteString
|
||||
let profile = try #require(text.split(separator: "&")
|
||||
.first { $0.hasPrefix("X-Plex-Client-Profile-Extra=") })
|
||||
|
||||
// Das ganze Profil steht in genau einem Parameter.
|
||||
#expect(profile.contains("%26"))
|
||||
#expect(profile.contains("add-transcode-target%28"))
|
||||
// Und `protocol` kommt nur ein einziges Mal vor.
|
||||
#expect(text.split(separator: "&").filter { $0.hasPrefix("protocol=") }.count == 1)
|
||||
}
|
||||
|
||||
@Test("Jede Wiedergabe meldet, dass sie die Entscheidungsantwort versteht")
|
||||
func alwaysSendsHasMDE() {
|
||||
// Plex' eigener Client schickt `hasMDE=1` bei jeder Wiedergabe mit.
|
||||
|
||||
Reference in New Issue
Block a user