Live-TV: die Sendung nachschlagen, statt ihre Adresse blind weiterzureichen
Die gebaute Adresse ist sauber — nachgerechnet und ausgedruckt. Also lehnt Plex selbst ab, und zwar mit Recht: eine Sendung beim Cloud-Programmführer hat keine Datei, die sich umrechnen ließe. Die nackte 400 war die Antwort auf eine unsinnige Bitte. Deshalb wird die Sendung jetzt zuerst nachgeschlagen. Das Raster nennt keine abspielbare Adresse; die Einzelabfrage beim Anbieter nennt womöglich eine. Was dabei zurückkam, steht in jedem Fall in der Spur — auch wenn nichts Abspielbares dabei war. Dafür musste die Anfrage einen bereits kodierten Pfad durchreichen können: `appending(path:)` kodiert das Prozent ein zweites Mal, und aus `plex%3A%2F%2F` wird `plex%253A%252F%252F`. Und der angebotene Endpunkt-Zettel des Programmanbieters ist eindeutig: content, metadata, grid, timeline — nichts zum Einschalten. Der Empfänger muss es also tun, und den kannte `tune` bisher gar nicht. Seine Adressen stehen jetzt mit im Suchraum. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1803dd670f
commit
62a913f2cd
@@ -376,15 +376,41 @@ extension PlexServer {
|
||||
public func tune(
|
||||
channel: PlexChannel,
|
||||
program: PlexProgram? = nil,
|
||||
dvr: PlexDVR? = nil,
|
||||
maximumBitrate: Int?
|
||||
) async -> LiveTuneResult {
|
||||
let session = UUID().uuidString
|
||||
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.
|
||||
// Die Sendung erst nachschlagen. Das Raster nennt keine abspielbare
|
||||
// Adresse — die Einzelabfrage beim Programmanbieter womöglich schon.
|
||||
// Ihre Adresse blind an den Transcoder zu geben, hat dreimal eine
|
||||
// nackte 400 ergeben: eine Sendung beim Cloud-Programmführer hat
|
||||
// keine Datei, die sich umrechnen ließe.
|
||||
if let metadataKey = program?.metadataKey {
|
||||
let lookup = await programMedia(atKey: metadataKey)
|
||||
trace.append(ChannelProbe(path: "\(metadataKey) (nachgeschlagen)", result: lookup.summary))
|
||||
|
||||
for part in lookup.parts {
|
||||
guard attempts < 14 else { break }
|
||||
attempts += 1
|
||||
|
||||
let outcome = await attemptStream(
|
||||
path: part,
|
||||
ratingKey: lookup.ratingKey ?? channel.id,
|
||||
mediaIndex: 0,
|
||||
includeProfile: true,
|
||||
maximumBitrate: maximumBitrate,
|
||||
session: session
|
||||
)
|
||||
trace.append(outcome.probe)
|
||||
if let url = outcome.url {
|
||||
return LiveTuneResult(url: url, sessionIdentifier: session, trace: trace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let metadataKey = program?.metadataKey {
|
||||
// Ohne Profil als dritter Versuch: die Profilangaben stecken
|
||||
// voller `&` und `=`, die in einem Abfragewert mehrdeutig sind.
|
||||
@@ -421,10 +447,13 @@ extension PlexServer {
|
||||
))
|
||||
}
|
||||
|
||||
for key in Self.sessionKeys(channel: channel, program: program) {
|
||||
let sessionPath = "/livetv/sessions/\(session)"
|
||||
let openPath = "\(sessionPath)/\(key)"
|
||||
|
||||
let sessionPath = "/livetv/sessions/\(session)"
|
||||
for openPath in Self.openPaths(
|
||||
channel: channel,
|
||||
program: program,
|
||||
dvr: dvr,
|
||||
sessionPath: sessionPath
|
||||
) {
|
||||
let item: PlexItem?
|
||||
do {
|
||||
item = try await client.send(
|
||||
@@ -447,7 +476,7 @@ extension PlexServer {
|
||||
trace.append(ChannelProbe(path: openPath, result: "Sitzung offen"))
|
||||
|
||||
for candidate in Self.streamCandidates(item: item, sessionPath: sessionPath) {
|
||||
guard attempts < 10 else { break }
|
||||
guard attempts < 14 else { break }
|
||||
attempts += 1
|
||||
|
||||
let outcome = await attemptStream(
|
||||
@@ -471,6 +500,39 @@ extension PlexServer {
|
||||
return LiveTuneResult(url: nil, sessionIdentifier: session, trace: trace)
|
||||
}
|
||||
|
||||
/// Schlägt eine Sendung beim Programmanbieter nach.
|
||||
///
|
||||
/// Liefert die abspielbaren Teil-Adressen und eine Kurzbeschreibung
|
||||
/// dessen, was zurückkam. Die Beschreibung steht auch dann in der Spur,
|
||||
/// wenn nichts Abspielbares dabei war — sonst bliebe nach einem
|
||||
/// Fehlschlag wieder nur Raten, wo die Adresse steckt.
|
||||
private func programMedia(atKey key: String) async -> (parts: [String], ratingKey: String?, summary: String) {
|
||||
let data: Data
|
||||
do {
|
||||
data = try await client.send(PlexRequest(
|
||||
path: key,
|
||||
query: [URLQueryItem(name: "includeChildren", value: "1")],
|
||||
timeout: .seconds(20),
|
||||
isPathEncoded: true
|
||||
))
|
||||
} catch {
|
||||
return ([], nil, Self.shortDescription(of: error))
|
||||
}
|
||||
|
||||
guard let item = (try? PlexResponse.decode(PlexMediaContainer<PlexItem>.self, from: data))?
|
||||
.content.first
|
||||
else {
|
||||
return ([], nil, Self.containerSummary(data))
|
||||
}
|
||||
|
||||
let parts = item.media.flatMap(\.parts).compactMap(\.key)
|
||||
let summary = parts.isEmpty
|
||||
? "\(item.media.count) Fassungen, keine Teil-Adresse — \(Self.containerSummary(data))"
|
||||
: "\(parts.count) Teil-Adressen"
|
||||
|
||||
return (parts, item.ratingKey, summary)
|
||||
}
|
||||
|
||||
/// Baut eine Stromadresse und ruft sie einmal ab.
|
||||
///
|
||||
/// Getrennte Methode statt einer Hilfsfunktion in `tune`: eine solche
|
||||
@@ -549,6 +611,37 @@ extension PlexServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wo sich ein Tuner belegen lässt.
|
||||
///
|
||||
/// Der Programmanbieter gibt unter `/media/providers` keinen einzigen
|
||||
/// Endpunkt zum Einschalten an — er kann Inhalt, Metadaten und
|
||||
/// Programmraster, mehr nicht. Der Empfänger muss also über den DVR
|
||||
/// belegt werden, und dessen Adressen stehen hier neben den
|
||||
/// Sitzungsformen, weil dieser Teil der Schnittstelle nirgends
|
||||
/// beschrieben ist.
|
||||
static func openPaths(
|
||||
channel: PlexChannel,
|
||||
program: PlexProgram?,
|
||||
dvr: PlexDVR?,
|
||||
sessionPath: String
|
||||
) -> [String] {
|
||||
var paths: [String] = []
|
||||
|
||||
if let dvr {
|
||||
for key in [channel.gridKey, channel.key, channel.channelIdentifier, channel.id]
|
||||
.compactMap({ $0 })
|
||||
.map({ $0.split(separator: "/").last.map(String.init) ?? $0 }) {
|
||||
paths.append("/livetv/dvrs/\(dvr.key)/channels/\(key)/tune")
|
||||
paths.append("/livetv/dvrs/\(dvr.key)/channels/\(key)")
|
||||
}
|
||||
}
|
||||
|
||||
paths += sessionKeys(channel: channel, program: program).map { "\(sessionPath)/\($0)" }
|
||||
|
||||
var seen = Set<String>()
|
||||
return paths.filter { seen.insert($0).inserted }
|
||||
}
|
||||
|
||||
/// Unter welchen Schlüsseln sich eine Live-Sitzung eröffnen lässt.
|
||||
///
|
||||
/// Der Raster-Schlüssel zuerst: er bezeichnet den Sender genau so, wie
|
||||
|
||||
Reference in New Issue
Block a user