Live-TV: POST, nicht GET — nachgelesen statt geraten
Aus dem Quelltext von Plex' eigenem Web-Client:
const o = `/livetv/dvrs/${t}/channels/${a}/tune`,
l = {method:"POST", headers:{"X-Plex-Session-Identifier": …}}
Genau diesen Pfad hatte ich seit mehreren Runden richtig. Jedes Mal per
GET abgefragt, jedes Mal 404 — und ein 404 sieht aus wie „gibt es nicht"
statt wie „nicht so". Also habe ich den Pfad verworfen und weitergesucht,
wieder und wieder, an einer Stelle, die von Anfang an stimmte.
Die Antwort auf das POST trägt den Vorgang mitsamt Metadaten-Eintrag, und
dort steht die abspielbare Teil-Adresse — nicht im Raster und nicht bei
der Sendung. Sie wird suchend gelesen statt an fester Stelle: der Vorgang
liegt je nach Serverfassung unterschiedlich tief, und ein falscher Weg
hinein sähe wieder aus wie „nichts da".
Die Kennung wird wie in `encodeURIComponent` kodiert — insbesondere
bleibt der Bindestrich stehen. `.alphanumerics` hätte aus
`6228f7f2-5fc705fa` ein `6228f7f2%2D5fc705fa` gemacht, und der Server
fände den Sender nicht.
Dass die Antwort in einer öffentlichen Quelle stand, während ich zwei
Tage lang Adressen erfunden habe, ist die eigentliche Lehre dieser Runde.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
08e6e14534
commit
562fd758d5
@@ -401,27 +401,27 @@ extension PlexServer {
|
||||
].joined(separator: ", ")
|
||||
)]
|
||||
|
||||
// Zuerst der Weg über den Empfänger, mit **seiner** Sendernummer.
|
||||
// Alles bisher Probierte sprach den Empfänger in der Sprache des
|
||||
// Programmführers an; das musste 404 geben.
|
||||
if let deviceChannel, let dvr {
|
||||
for path in [
|
||||
"/livetv/dvrs/\(dvr.key)/channels/\(deviceChannel)",
|
||||
"/livetv/sessions/\(session)/\(deviceChannel)",
|
||||
] {
|
||||
attempts += 1
|
||||
let outcome = await attemptStream(
|
||||
path: path,
|
||||
ratingKey: channel.id,
|
||||
mediaIndex: -1,
|
||||
includeProfile: true,
|
||||
maximumBitrate: maximumBitrate,
|
||||
session: session
|
||||
)
|
||||
trace.append(outcome.probe)
|
||||
if let url = outcome.url {
|
||||
return LiveTuneResult(url: url, sessionIdentifier: session, trace: trace)
|
||||
}
|
||||
// Der Weg, den Plex' eigener Client geht. Nachgelesen in seinem
|
||||
// Quelltext, nicht erraten:
|
||||
//
|
||||
// POST /livetv/dvrs/{dvr}/channels/{kennung}/tune
|
||||
// X-Plex-Session-Identifier: <sitzung>
|
||||
//
|
||||
// **POST**, nicht GET. Genau diesen Pfad hatte ich seit mehreren
|
||||
// Runden richtig — und jedes Mal mit der falschen Methode abgefragt.
|
||||
// Ein GET darauf antwortet mit 404, und das sah aus wie „gibt es
|
||||
// nicht" statt wie „nicht so".
|
||||
if let dvr {
|
||||
attempts += 1
|
||||
let outcome = await tuneThroughDVR(
|
||||
channel: channel,
|
||||
dvr: dvr,
|
||||
session: session,
|
||||
maximumBitrate: maximumBitrate
|
||||
)
|
||||
trace.append(contentsOf: outcome.probes)
|
||||
if let url = outcome.url {
|
||||
return LiveTuneResult(url: url, sessionIdentifier: session, trace: trace)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,6 +634,83 @@ extension PlexServer {
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
/// Belegt einen Tuner über den Empfänger und liefert den Strom.
|
||||
///
|
||||
/// Die Antwort auf das POST trägt den Vorgang mitsamt Metadaten-Eintrag,
|
||||
/// und **dort** steht die abspielbare Teil-Adresse — nicht im Raster und
|
||||
/// nicht bei der Sendung. Danach ist es gewöhnliches Umrechnen.
|
||||
private func tuneThroughDVR(
|
||||
channel: PlexChannel,
|
||||
dvr: PlexDVR,
|
||||
session: String,
|
||||
maximumBitrate: Int?
|
||||
) async -> (url: URL?, probes: [ChannelProbe]) {
|
||||
let identifier = channel.id.addingPercentEncoding(
|
||||
withAllowedCharacters: .uriComponentAllowed
|
||||
) ?? channel.id
|
||||
let path = "/livetv/dvrs/\(dvr.key)/channels/\(identifier)/tune"
|
||||
|
||||
let data: Data
|
||||
do {
|
||||
data = try await client.send(PlexRequest(
|
||||
method: .post,
|
||||
path: path,
|
||||
additionalHeaders: ["X-Plex-Session-Identifier": session],
|
||||
timeout: .seconds(30),
|
||||
isPathEncoded: true
|
||||
))
|
||||
} catch {
|
||||
return (nil, [ChannelProbe(path: "POST \(path)", result: Self.shortDescription(of: error))])
|
||||
}
|
||||
|
||||
guard let part = Self.firstPartKey(in: data) else {
|
||||
return (nil, [ChannelProbe(
|
||||
path: "POST \(path)",
|
||||
result: "belegt, aber ohne Teil-Adresse — \(Self.containerSummary(data))"
|
||||
)])
|
||||
}
|
||||
|
||||
var probes = [ChannelProbe(path: "POST \(path)", result: "Tuner belegt: \(part)")]
|
||||
let outcome = await attemptStream(
|
||||
path: part,
|
||||
ratingKey: channel.id,
|
||||
mediaIndex: -1,
|
||||
includeProfile: true,
|
||||
maximumBitrate: maximumBitrate,
|
||||
session: session
|
||||
)
|
||||
probes.append(outcome.probe)
|
||||
return (outcome.url, probes)
|
||||
}
|
||||
|
||||
/// Sucht die erste abspielbare Teil-Adresse irgendwo in einer Antwort.
|
||||
///
|
||||
/// Bewusst suchend statt an einer festen Stelle lesend: der Vorgang
|
||||
/// liegt je nach Serverfassung unterschiedlich tief verschachtelt, und
|
||||
/// ein falscher Pfad in die Antwort hinein sähe wieder aus wie „nichts da".
|
||||
static func firstPartKey(in data: Data) -> String? {
|
||||
guard let root = try? JSONSerialization.jsonObject(with: data) else { return nil }
|
||||
return firstPartKey(in: root)
|
||||
}
|
||||
|
||||
private static func firstPartKey(in value: Any) -> String? {
|
||||
if let dictionary = value as? [String: Any] {
|
||||
if let parts = dictionary["Part"] as? [[String: Any]],
|
||||
let key = parts.first?["key"] as? String {
|
||||
return key
|
||||
}
|
||||
for nested in dictionary.values {
|
||||
if let found = firstPartKey(in: nested) { return found }
|
||||
}
|
||||
}
|
||||
if let array = value as? [Any] {
|
||||
for nested in array {
|
||||
if let found = firstPartKey(in: nested) { return found }
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Baut eine Stromadresse und ruft sie einmal ab.
|
||||
///
|
||||
/// Getrennte Methode statt einer Hilfsfunktion in `tune`: eine solche
|
||||
@@ -1143,3 +1220,16 @@ extension PlexServer {
|
||||
text.replacingOccurrences(of: token, with: "«token»")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension CharacterSet {
|
||||
/// Was `encodeURIComponent` unangetastet lässt.
|
||||
///
|
||||
/// Nicht `.alphanumerics`: das kodiert auch den Bindestrich, und
|
||||
/// Senderkennungen bestehen aus zwei Hälften mit einem dazwischen. Aus
|
||||
/// `6228f7f2-5fc705fa` würde `6228f7f2%2D5fc705fa`, und der Server fände
|
||||
/// den Sender nicht.
|
||||
static let uriComponentAllowed = CharacterSet(
|
||||
charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()"
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user