Diagnose zeigt den Aufbau der Live-TV-Antwort
Die Sender bleiben leer, und ich habe jetzt zweimal geraten, wo sie stehen könnten — beim ersten Mal ein eigener Endpunkt, beim zweiten die Verschachtelung unter Device. Beides war eine Vermutung, weil dieser Teil der Plex-API keine offizielle Beschreibung hat. Statt ein drittes Mal zu raten: Einstellungen → Diagnose → „Live-TV- Antwort ansehen" fragt den Server und zeigt den Aufbau der Antwort. Schlüssel, Anzahl und Sendernamen, keine Werte und keine Zugangsdaten — genug, um den richtigen Schlüssel zu finden. Von Listen wird nur der erste Eintrag ausgeklappt; bei dreißig Sendern wäre die vollständige Ausgabe unlesbar, und alle haben denselben Aufbau. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+2383
-2351
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,8 @@ struct DiagnosticsView: View {
|
||||
@Environment(FlixySession.self) private var session
|
||||
@Environment(\.flixyAccent) private var accent
|
||||
@Environment(PlaybackDiagnostics.self) private var diagnostics
|
||||
@State private var liveTVStructure: String?
|
||||
@State private var isInspectingLiveTV = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
@@ -71,6 +73,36 @@ struct DiagnosticsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
Task { await inspectLiveTV() }
|
||||
} label: {
|
||||
if isInspectingLiveTV {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Label("diagnostics.liveTV", systemImage: "antenna.radiowaves.left.and.right")
|
||||
}
|
||||
}
|
||||
.disabled(isInspectingLiveTV)
|
||||
|
||||
if let liveTVStructure {
|
||||
Text(liveTVStructure)
|
||||
.font(.system(.caption2, design: .monospaced))
|
||||
.foregroundStyle(Color.flixyTextSecondary)
|
||||
.textSelection(.enabled)
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = liveTVStructure
|
||||
} label: {
|
||||
Label("diagnostics.copy", systemImage: "doc.on.doc")
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("sidebar.liveTV")
|
||||
} footer: {
|
||||
Text("diagnostics.liveTVHint")
|
||||
}
|
||||
|
||||
Section("settings.app") {
|
||||
LabeledContent("settings.version", value: Branding.version)
|
||||
LabeledContent("settings.build", value: Branding.buildStamp)
|
||||
@@ -89,6 +121,14 @@ struct DiagnosticsView: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
/// Fragt den Server, wie seine Antwort auf `/livetv/dvrs` aufgebaut ist.
|
||||
private func inspectLiveTV() async {
|
||||
guard let server = session.server else { return }
|
||||
isInspectingLiveTV = true
|
||||
defer { isInspectingLiveTV = false }
|
||||
liveTVStructure = await server.liveTVStructure()
|
||||
}
|
||||
|
||||
/// Der Bericht als Text zum Weitergeben.
|
||||
///
|
||||
/// Enthält bewusst keinen Token: die Quelladresse ist schon ohne
|
||||
|
||||
@@ -137,6 +137,66 @@ extension PlexServer {
|
||||
}
|
||||
}
|
||||
|
||||
extension PlexServer {
|
||||
/// Der Aufbau der Antwort auf `/livetv/dvrs`, zum Ansehen.
|
||||
///
|
||||
/// Nicht zum Anzeigen im Alltag, sondern zum Nachsehen: die
|
||||
/// Live-TV-Endpunkte von Plex haben keine offizielle Beschreibung, und
|
||||
/// ohne die tatsächliche Antwort bleibt jede Vermutung darüber, wo die
|
||||
/// Sender stehen, eine Vermutung. Diese Ausgabe beendet das Raten.
|
||||
///
|
||||
/// Werte werden nicht gezeigt, nur Struktur und Anzahl — das genügt, um
|
||||
/// den richtigen Schlüssel zu finden, und nichts davon ist vertraulich.
|
||||
public func liveTVStructure() async -> String {
|
||||
let data: Data
|
||||
do {
|
||||
data = try await client.send(PlexRequest(path: "/livetv/dvrs", timeout: .seconds(10)))
|
||||
} catch {
|
||||
return "Abfrage gescheitert: \(error)"
|
||||
}
|
||||
|
||||
guard let object = try? JSONSerialization.jsonObject(with: data) else {
|
||||
return "Antwort ist kein JSON (\(data.count) Bytes)"
|
||||
}
|
||||
return Self.outline(object).joined(separator: "\n")
|
||||
}
|
||||
|
||||
/// Beschreibt den Aufbau eines JSON-Werts, ohne die Werte selbst.
|
||||
///
|
||||
/// Ausnahme sind Titel und Sendernummern: die stehen ohnehin im
|
||||
/// Programmführer und sind beim Suchen die nützlichste Angabe.
|
||||
private static func outline(_ value: Any, indent: Int = 0) -> [String] {
|
||||
let padding = String(repeating: " ", count: indent)
|
||||
|
||||
switch value {
|
||||
case let dictionary as [String: Any]:
|
||||
return dictionary.keys.sorted().flatMap { key -> [String] in
|
||||
let child = dictionary[key]!
|
||||
if ["title", "channelIdentifier", "lineupTitle"].contains(key),
|
||||
let text = child as? String {
|
||||
return ["\(padding)\(key): \(text)"]
|
||||
}
|
||||
switch child {
|
||||
case is [Any], is [String: Any]:
|
||||
return ["\(padding)\(key):"] + outline(child, indent: indent + 1)
|
||||
default:
|
||||
return ["\(padding)\(key): \(type(of: child))"]
|
||||
}
|
||||
}
|
||||
|
||||
case let array as [Any]:
|
||||
guard let first = array.first else { return ["\(padding)(leer)"] }
|
||||
// Nur das erste Element ausklappen — bei dreißig Sendern wäre die
|
||||
// vollständige Ausgabe unlesbar, und alle haben denselben Aufbau.
|
||||
return ["\(padding)[\(array.count) Einträge, erster:]"]
|
||||
+ outline(first, indent: indent + 1)
|
||||
|
||||
default:
|
||||
return ["\(padding)\(type(of: value))"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Woher die Senderliste kam.
|
||||
public struct ChannelLookup: Sendable {
|
||||
public enum Source: Sendable, Equatable {
|
||||
|
||||
Reference in New Issue
Block a user