Live-TV: die Adresse der Sendung wird genau einmal kodiert
Die nackte „400 Bad Request"-Seite war keine Plex-Meldung, sondern der Webserver: er hat die Anfrage selbst nicht verstanden. Grund war mein eigener Fix von vorhin — ich habe `%3A%2F%2F` entpackt, und damit stand `plex://episode/…` im Klartext in der Anfrage. Beide naheliegenden Wege liegen daneben, und ich habe beide durchgemacht: unverändert durchgereicht kodiert URLComponents ein zweites Mal, aus %3A wird %253A. Vorher entpackt bricht sich der Server die Adresse. Richtig ist genau einmal — dafür wird `path` von Hand eingesetzt, nachdem alles andere kodiert ist. Dazu aus der Spur gelernt: jede Form von /livetv/sessions/… antwortet auf diesem Server mit 404. Den Tuner belegt er offenbar selbst, sobald die Sendung umgerechnet werden soll. Deshalb steht die Adresse der Sendung jetzt an erster Stelle, und die Sitzungen kommen nur noch dahinter. Als dritte Form ein Versuch ohne das Geräteprofil: dessen Angaben stecken voller `&` und `=`, die in einem Abfragewert mehrdeutig sind. Trägt es ohne, liegt es daran, und das steht dann in der Spur. Ebenso, was der Programmanbieter überhaupt an Endpunkten angibt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a9a45e685d
commit
1803dd670f
@@ -382,6 +382,45 @@ extension PlexServer {
|
||||
var trace: [ChannelProbe] = []
|
||||
var attempts = 0
|
||||
|
||||
// Zuerst die Sendung selbst. Auf diesem Server antwortet jede Form von
|
||||
// `/livetv/sessions/…` mit 404 — den Tuner belegt er offenbar von
|
||||
// sich aus, sobald die Sendung umgerechnet werden soll.
|
||||
if let metadataKey = program?.metadataKey {
|
||||
// Ohne Profil als dritter Versuch: die Profilangaben stecken
|
||||
// voller `&` und `=`, die in einem Abfragewert mehrdeutig sind.
|
||||
// Trägt es ohne, liegt es daran — und das steht dann in der Spur.
|
||||
let variants: [(index: Int, profile: Bool)] = [(0, true), (-1, true), (0, false)]
|
||||
for variant in variants {
|
||||
attempts += 1
|
||||
let outcome = await attemptStream(
|
||||
path: metadataKey,
|
||||
ratingKey: channel.id,
|
||||
mediaIndex: variant.index,
|
||||
includeProfile: variant.profile,
|
||||
maximumBitrate: maximumBitrate,
|
||||
session: session
|
||||
)
|
||||
trace.append(outcome.probe)
|
||||
if let url = outcome.url {
|
||||
return LiveTuneResult(url: url, sessionIdentifier: session, trace: trace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Was der Programmanbieter an Endpunkten angibt — für den Fall, dass
|
||||
// dieser Server den Tuner doch selbst belegt haben will. Steht mit in
|
||||
// der Spur, damit auch ein Fehlschlag etwas verrät.
|
||||
if let epgIdentifier = program?.metadataKey?
|
||||
.split(separator: "/").first.map(String.init) {
|
||||
let features = await providerFeatures(for: epgIdentifier)
|
||||
trace.append(ChannelProbe(
|
||||
path: "Angebotene Endpunkte",
|
||||
result: features.isEmpty
|
||||
? "keine"
|
||||
: features.map { "\($0.type)=\($0.key ?? "–")" }.joined(separator: ", ")
|
||||
))
|
||||
}
|
||||
|
||||
for key in Self.sessionKeys(channel: channel, program: program) {
|
||||
let sessionPath = "/livetv/sessions/\(session)"
|
||||
let openPath = "\(sessionPath)/\(key)"
|
||||
@@ -408,47 +447,19 @@ extension PlexServer {
|
||||
trace.append(ChannelProbe(path: openPath, result: "Sitzung offen"))
|
||||
|
||||
for candidate in Self.streamCandidates(item: item, sessionPath: sessionPath) {
|
||||
guard attempts < 8 else { break }
|
||||
guard attempts < 10 else { break }
|
||||
attempts += 1
|
||||
|
||||
let request = TranscodeRequest(
|
||||
let outcome = await attemptStream(
|
||||
path: candidate.path,
|
||||
ratingKey: item.ratingKey,
|
||||
mediaIndex: candidate.mediaIndex,
|
||||
partIndex: candidate.mediaIndex,
|
||||
includeProfile: true,
|
||||
maximumBitrate: maximumBitrate,
|
||||
isLocal: connection.local,
|
||||
sessionIdentifier: session,
|
||||
pathOverride: candidate.path
|
||||
session: session
|
||||
)
|
||||
guard let url = transcodeURL(for: request) else { continue }
|
||||
|
||||
let outcome = await probeStream(url: url)
|
||||
trace.append(ChannelProbe(
|
||||
path: "\(candidate.path) (mediaIndex \(candidate.mediaIndex))",
|
||||
result: outcome
|
||||
))
|
||||
|
||||
if outcome.hasPrefix("HTTP 200") {
|
||||
return LiveTuneResult(url: url, sessionIdentifier: session, trace: trace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Letzter Versuch ohne eigene Sitzung: die Sendung unmittelbar
|
||||
// umrechnen lassen. Auf Servern, die den Tuner selbst belegen, genügt
|
||||
// das — und geprüft wird es genauso wie alles andere.
|
||||
if let metadataKey = program?.metadataKey, attempts < 8 {
|
||||
let request = TranscodeRequest(
|
||||
ratingKey: channel.id,
|
||||
maximumBitrate: maximumBitrate,
|
||||
isLocal: connection.local,
|
||||
sessionIdentifier: session,
|
||||
pathOverride: metadataKey
|
||||
)
|
||||
if let url = transcodeURL(for: request) {
|
||||
let outcome = await probeStream(url: url)
|
||||
trace.append(ChannelProbe(path: metadataKey, result: outcome))
|
||||
if outcome.hasPrefix("HTTP 200") {
|
||||
trace.append(outcome.probe)
|
||||
if let url = outcome.url {
|
||||
return LiveTuneResult(url: url, sessionIdentifier: session, trace: trace)
|
||||
}
|
||||
}
|
||||
@@ -460,6 +471,39 @@ extension PlexServer {
|
||||
return LiveTuneResult(url: nil, sessionIdentifier: session, trace: trace)
|
||||
}
|
||||
|
||||
/// Baut eine Stromadresse und ruft sie einmal ab.
|
||||
///
|
||||
/// Getrennte Methode statt einer Hilfsfunktion in `tune`: eine solche
|
||||
/// Funktion greift über jede Unterbrechung hinweg auf die veränderlichen
|
||||
/// Werte daneben zu, und genau das lässt der Nebenläufigkeitsprüfer von
|
||||
/// Swift 6 nicht durchgehen.
|
||||
private func attemptStream(
|
||||
path: String,
|
||||
ratingKey: String,
|
||||
mediaIndex: Int,
|
||||
includeProfile: Bool,
|
||||
maximumBitrate: Int?,
|
||||
session: String
|
||||
) async -> (url: URL?, probe: ChannelProbe) {
|
||||
let request = TranscodeRequest(
|
||||
ratingKey: ratingKey,
|
||||
mediaIndex: mediaIndex,
|
||||
partIndex: mediaIndex,
|
||||
maximumBitrate: maximumBitrate,
|
||||
isLocal: connection.local,
|
||||
sessionIdentifier: session,
|
||||
pathOverride: path
|
||||
)
|
||||
|
||||
guard let url = transcodeURL(for: request, includeProfile: includeProfile) else {
|
||||
return (nil, ChannelProbe(path: path, result: "Adresse nicht baubar"))
|
||||
}
|
||||
|
||||
let outcome = await probeStream(url: url)
|
||||
let label = "\(path) (mediaIndex \(mediaIndex)\(includeProfile ? "" : ", ohne Profil"))"
|
||||
return (outcome.hasPrefix("HTTP 200") ? url : nil, ChannelProbe(path: label, result: outcome))
|
||||
}
|
||||
|
||||
/// Welche Adresse mit welchem Index umzurechnen ist.
|
||||
///
|
||||
/// Die Adresse der Sitzung zuerst — der Transcoder sucht sich den Teil
|
||||
@@ -521,6 +565,15 @@ extension PlexServer {
|
||||
// Volle Adressen taugen hier nicht: an `/livetv/sessions/<uuid>/`
|
||||
// gehört ein Schlüssel, kein zweiter Pfad.
|
||||
keys = keys.map { $0.split(separator: "/").last.map(String.init) ?? $0 }
|
||||
// Und ein Schlüssel, der noch `%3A` enthält, ist als Pfadabschnitt
|
||||
// keiner — die entpackte Form gehört mit in die Auswahl. Aber nur,
|
||||
// solange dabei kein Schrägstrich herauskommt: `plex://movie/abc` ist
|
||||
// als ein Pfadabschnitt genauso wenig brauchbar.
|
||||
keys += keys.compactMap { key in
|
||||
guard key.contains("%"), let plain = key.removingPercentEncoding,
|
||||
!plain.contains("/") else { return nil }
|
||||
return plain
|
||||
}
|
||||
|
||||
var seen = Set<String>()
|
||||
return keys.filter { !$0.isEmpty && seen.insert($0).inserted }
|
||||
|
||||
Reference in New Issue
Block a user