Live-TV: den Medien-Block ansehen, statt ihn zu zählen

„1 Fassung, keine Teil-Adresse" beendet nichts. Wenn dort kein
abspielbarer Pfad steht, zählt, was stattdessen dort steht — irgendwo muss
der Server sagen, wie diese Sendung abzuspielen ist. Der Bericht zeigt
den Medien-Block jetzt Feld für Feld.

Dazu zwei Dinge, die ich benutzt habe, ohne sie je gesehen zu haben: der
Raster-Schlüssel des Senders und seine übrigen Kennungen stehen jetzt im
Bericht. Und der Raster-Schlüssel wird als vollständige Adresse probiert
— bisher war immer nur sein letzter Abschnitt als Sitzungsschlüssel an
der Reihe.

Der Empfänger kommt ebenfalls dazu: /livetv/dvrs/2/channels antwortet auf
diesem Server gar nicht, aber /media/grabbers/devices/1/channels führt
278 Sender. Wenn der DVR keine kennt, kennt sie das Gerät.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-12 14:50:56 +02:00
co-authored by Claude Opus 5
parent 62a913f2cd
commit 162247e8dc
@@ -380,9 +380,40 @@ extension PlexServer {
maximumBitrate: Int? maximumBitrate: Int?
) async -> LiveTuneResult { ) async -> LiveTuneResult {
let session = UUID().uuidString let session = UUID().uuidString
var trace: [ChannelProbe] = []
var attempts = 0 var attempts = 0
// Womit überhaupt gearbeitet wird. Ich habe den Raster-Schlüssel
// benutzt, ohne ihn je gesehen zu haben — und im Suchraum nur seinen
// letzten Abschnitt verwendet.
var trace: [ChannelProbe] = [ChannelProbe(
path: "Sender-Schlüssel",
result: [
"gridKey=\(channel.gridKey ?? "–")",
"key=\(channel.key ?? "–")",
"id=\(channel.id)",
"nummer=\(channel.channelIdentifier ?? "–")",
].joined(separator: ", ")
)]
// Der Raster-Schlüssel als Adresse, unverkürzt. Bisher habe ich nur
// seinen letzten Abschnitt als Sitzungsschlüssel probiert — als
// vollständige Adresse war er nie an der Reihe.
if let gridKey = channel.gridKey, gridKey.hasPrefix("/") {
attempts += 1
let outcome = await attemptStream(
path: gridKey,
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)
}
}
// Die Sendung erst nachschlagen. Das Raster nennt keine abspielbare // Die Sendung erst nachschlagen. Das Raster nennt keine abspielbare
// Adresse — die Einzelabfrage beim Programmanbieter womöglich schon. // Adresse — die Einzelabfrage beim Programmanbieter womöglich schon.
// Ihre Adresse blind an den Transcoder zu geben, hat dreimal eine // Ihre Adresse blind an den Transcoder zu geben, hat dreimal eine
@@ -526,11 +557,42 @@ extension PlexServer {
} }
let parts = item.media.flatMap(\.parts).compactMap(\.key) let parts = item.media.flatMap(\.parts).compactMap(\.key)
let summary = parts.isEmpty guard parts.isEmpty else {
? "\(item.media.count) Fassungen, keine Teil-Adresse — \(Self.containerSummary(data))" return (parts, item.ratingKey, "\(parts.count) Teil-Adressen")
: "\(parts.count) Teil-Adressen" }
return (parts, item.ratingKey, summary) // Keine Teil-Adresse — dann zählt, was stattdessen dort steht.
// „1 Fassung, keine Teil-Adresse" beendet nichts; der Aufbau der
// Antwort schon, denn irgendwo muss der Server sagen, wie diese
// Sendung abzuspielen ist.
return ([], item.ratingKey, "ohne Teil-Adresse\n" + Self.mediaOutline(data))
}
/// Der Aufbau des Medien-Blocks einer Antwort, kurz gehalten.
static func mediaOutline(_ data: Data) -> String {
guard let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let container = object["MediaContainer"] as? [String: Any],
let first = (container["Metadata"] as? [[String: Any]])?.first
else { return Self.containerSummary(data) }
var lines: [String] = []
if let media = (first["Media"] as? [[String: Any]])?.first {
lines += media.keys.sorted().map { key in
let value = media[key]!
if let text = value as? String { return " Media.\(key) = \(text)" }
if let number = value as? NSNumber { return " Media.\(key) = \(number)" }
if let array = value as? [Any] { return " Media.\(key): \(array.count) Einträge" }
return " Media.\(key): \(type(of: value))"
}
} else {
lines.append(" kein Media-Block")
}
// Und die Kurzform der Sendung selbst — dort stehen `key` und `guid`.
for key in ["key", "guid", "ratingKey", "type", "live", "onAir"] where first[key] != nil {
lines.append(" \(key) = \(first[key]!)")
}
return lines.joined(separator: "\n")
} }
/// Baut eine Stromadresse und ruft sie einmal ab. /// Baut eine Stromadresse und ruft sie einmal ab.
@@ -628,12 +690,26 @@ extension PlexServer {
var paths: [String] = [] var paths: [String] = []
if let dvr { if let dvr {
for key in [channel.gridKey, channel.key, channel.channelIdentifier, channel.id] let keys = [channel.gridKey, channel.key, channel.channelIdentifier, channel.id]
.compactMap({ $0 }) .compactMap { $0 }
.map({ $0.split(separator: "/").last.map(String.init) ?? $0 }) { .map { $0.split(separator: "/").last.map(String.init) ?? $0 }
for key in keys {
paths.append("/livetv/dvrs/\(dvr.key)/channels/\(key)/tune") paths.append("/livetv/dvrs/\(dvr.key)/channels/\(key)/tune")
paths.append("/livetv/dvrs/\(dvr.key)/channels/\(key)") paths.append("/livetv/dvrs/\(dvr.key)/channels/\(key)")
} }
// Und über das Gerät. Es ist die einzige Stelle, die auf diesem
// Server überhaupt Sender führt — 278 Stück unter `DeviceChannel`.
// Wenn der DVR keine kennt, kennt sie das Gerät.
for device in dvr.devices {
guard let deviceKey = device.key else { continue }
let base = deviceKey.hasPrefix("/") ? deviceKey : "/media/grabbers/devices/\(deviceKey)"
for key in keys.prefix(2) {
paths.append("\(base)/channels/\(key)/tune")
paths.append("\(base)/channels/\(key)")
}
}
} }
paths += sessionKeys(channel: channel, program: program).map { "\(sessionPath)/\($0)" } paths += sessionKeys(channel: channel, program: program).map { "\(sessionPath)/\($0)" }