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(FlixySession.self) private var session
|
||||||
@Environment(\.flixyAccent) private var accent
|
@Environment(\.flixyAccent) private var accent
|
||||||
@Environment(PlaybackDiagnostics.self) private var diagnostics
|
@Environment(PlaybackDiagnostics.self) private var diagnostics
|
||||||
|
@State private var liveTVStructure: String?
|
||||||
|
@State private var isInspectingLiveTV = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
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") {
|
Section("settings.app") {
|
||||||
LabeledContent("settings.version", value: Branding.version)
|
LabeledContent("settings.version", value: Branding.version)
|
||||||
LabeledContent("settings.build", value: Branding.buildStamp)
|
LabeledContent("settings.build", value: Branding.buildStamp)
|
||||||
@@ -89,6 +121,14 @@ struct DiagnosticsView: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.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.
|
/// Der Bericht als Text zum Weitergeben.
|
||||||
///
|
///
|
||||||
/// Enthält bewusst keinen Token: die Quelladresse ist schon ohne
|
/// 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.
|
/// Woher die Senderliste kam.
|
||||||
public struct ChannelLookup: Sendable {
|
public struct ChannelLookup: Sendable {
|
||||||
public enum Source: Sendable, Equatable {
|
public enum Source: Sendable, Equatable {
|
||||||
|
|||||||
Reference in New Issue
Block a user