From 08e6e14534e62089b0b62e2693cb48ccdb69ef17 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 12 Sep 2026 15:18:12 +0200 Subject: [PATCH] =?UTF-8?q?Live-TV:=20der=20Empf=C3=A4nger=20spricht=20ein?= =?UTF-8?q?e=20andere=20Sprache=20als=20der=20Programmf=C3=BChrer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aus der Antwort von /media/grabbers/devices/1: {"channelKey":"6228f7f2364e02fa5ec8e51c-5fc706063f45c6002eab58a3", "deviceIdentifier":"300", "lineupIdentifier":"002"} Der Programmführer kennt den Sender als `6228f7f2…-5fc706…`, der Tuner als `300`. Die Zuordnung im Empfänger übersetzt zwischen beidem — und genau diese Übersetzung habe ich nie benutzt. Jede Anfrage an den Empfänger ging in der Sprache des Programmführers hinaus, und darauf gibt es nur eine Antwort: 404. Die Nummer am Empfänger steht jetzt an erster Stelle im Suchraum und im Bericht. Der Empfänger ist ein HDHomeRun FLEX QUATRO mit vier Tunern; er kennt Sender ausschließlich unter dieser Nummer. Nebenbei aus derselben Ausgabe: /livetv/sessions antwortet mit size 0. Die Route gibt es also — sie ist eine Liste. /livetv/sessions/ gibt es nur, wenn die Sitzung vorher angelegt wurde, und deshalb war jeder Abruf dort ein 404 ohne Aussagekraft. Co-Authored-By: Claude Opus 5 --- .../PlexKit/Endpoints/PlexServer+LiveTV.swift | 36 +++++++++++++++++-- .../Sources/PlexKit/Models/PlexLiveTV.swift | 30 +++++++++++++++- .../Tests/PlexKitTests/LiveTVGuideTests.swift | 30 ++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index 1d9ed16..057ebb4 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -385,6 +385,11 @@ extension PlexServer { // 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. + // Die Sendernummer am Empfänger. Der Programmführer kennt den Sender + // als `6228f7f2…-5fc706…`, der Tuner als `300` — ohne diese + // Übersetzung antwortet der Empfänger auf alles mit 404. + let deviceChannel = dvr?.deviceChannel(forChannelKey: channel.id) + var trace: [ChannelProbe] = [ChannelProbe( path: "Sender-Schlüssel", result: [ @@ -392,9 +397,34 @@ extension PlexServer { "key=\(channel.key ?? "–")", "id=\(channel.id)", "nummer=\(channel.channelIdentifier ?? "–")", + "amEmpfänger=\(deviceChannel ?? "–")", ].joined(separator: ", ") )] + // Zuerst der Weg über den Empfänger, mit **seiner** Sendernummer. + // Alles bisher Probierte sprach den Empfänger in der Sprache des + // Programmführers an; das musste 404 geben. + if let deviceChannel, let dvr { + for path in [ + "/livetv/dvrs/\(dvr.key)/channels/\(deviceChannel)", + "/livetv/sessions/\(session)/\(deviceChannel)", + ] { + attempts += 1 + let outcome = await attemptStream( + path: path, + ratingKey: channel.id, + mediaIndex: -1, + includeProfile: true, + maximumBitrate: maximumBitrate, + session: session + ) + trace.append(outcome.probe) + if let url = outcome.url { + return LiveTuneResult(url: url, sessionIdentifier: session, trace: trace) + } + } + } + // 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. @@ -706,7 +736,9 @@ extension PlexServer { var paths: [String] = [] if let dvr { - let keys = [channel.gridKey, channel.key, channel.channelIdentifier, channel.id] + // Die Nummer am Empfänger zuerst: nur sie spricht seine Sprache. + let keys = ([dvr.deviceChannel(forChannelKey: channel.id)] + + [channel.gridKey, channel.key, channel.channelIdentifier, channel.id]) .compactMap { $0 } .map { $0.split(separator: "/").last.map(String.init) ?? $0 } @@ -721,7 +753,7 @@ extension PlexServer { 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) { + for key in keys.prefix(3) { paths.append("\(base)/channels/\(key)/tune") paths.append("\(base)/channels/\(key)") } diff --git a/Packages/FlixyKit/Sources/PlexKit/Models/PlexLiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Models/PlexLiveTV.swift index a5c4e4d..383c15f 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Models/PlexLiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Models/PlexLiveTV.swift @@ -36,6 +36,20 @@ public struct PlexDVR: Sendable, Equatable, Decodable, Identifiable { Set(devices.flatMap(\.channelMappings).filter(\.isEnabled).map(\.channelKey)) } + /// Die Sendernummer am Empfänger für eine Kennung des Programmführers. + /// + /// Nachsichtig verglichen, weil die Kennung je nach Quelle mit oder ohne + /// Vorsatz der Senderliste kommt. + public func deviceChannel(forChannelKey key: String) -> String? { + let tail = key.split(separator: "-").last.map(String.init) + for mapping in devices.flatMap(\.channelMappings) { + guard let identifier = mapping.deviceIdentifier else { continue } + if mapping.channelKey == key { return identifier } + if let tail, mapping.channelKey.hasSuffix(tail) { return identifier } + } + return nil + } + private enum CodingKeys: String, CodingKey { case key, uuid, language, lineupTitle, lineup, epgIdentifier case devices = "Device" @@ -115,7 +129,15 @@ public struct PlexLineup: Sendable, Equatable, Decodable, Identifiable { /// Sagt, welcher Sender der Senderliste am Empfänger eingeschaltet ist — /// mehr nicht. Namen, Nummern und Logos stehen beim Programmanbieter. public struct PlexChannelMapping: Sendable, Equatable, Decodable { + /// Die Kennung des Senders beim Programmführer. public let channelKey: String + /// Die Sendernummer **am Empfänger** — etwa `"300"`. + /// + /// Der Stein von Rosetta dieser Schnittstelle: der Programmführer kennt + /// den Sender als `6228f7f2…-5fc706…`, der Tuner als `300`. Ohne diese + /// Übersetzung redet man mit dem Empfänger in einer Sprache, die er nicht + /// spricht — und bekommt auf jede Anfrage 404. + public let deviceIdentifier: String? public let lineupIdentifier: String? private let enabled: String? @@ -123,8 +145,14 @@ public struct PlexChannelMapping: Sendable, Equatable, Decodable { /// Wahrheitswert. public var isEnabled: Bool { enabled != "0" } - public init(channelKey: String, lineupIdentifier: String? = nil, enabled: String? = "1") { + public init( + channelKey: String, + deviceIdentifier: String? = nil, + lineupIdentifier: String? = nil, + enabled: String? = "1" + ) { self.channelKey = channelKey + self.deviceIdentifier = deviceIdentifier self.lineupIdentifier = lineupIdentifier self.enabled = enabled } diff --git a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift index c9fde4f..1529d9c 100644 --- a/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift +++ b/Packages/FlixyKit/Tests/PlexKitTests/LiveTVGuideTests.swift @@ -295,6 +295,36 @@ struct LiveTVGuideTests { #expect(path == "/livetv/sessions/xyz/stream") } + @Test("Die Zuordnung übersetzt zwischen Programmführer und Empfänger") + func translatesChannelKeyToDeviceChannel() { + // Der Programmführer kennt den Sender als `6228f7f2…-5fc706…`, der + // Tuner als `300`. Ohne diese Übersetzung spricht man den Empfänger + // in einer Sprache an, die er nicht versteht — und bekommt 404 auf + // alles. Genau daran ist das Einschalten reihenweise gescheitert. + let dvr = PlexDVR( + key: "2", + devices: [PlexTunerDevice(key: "1", channelMappings: [ + PlexChannelMapping( + channelKey: "6228f7f2364e02fa5ec8e51c-5fc706063f45c6002eab58a3", + deviceIdentifier: "300", + lineupIdentifier: "002" + ), + PlexChannelMapping( + channelKey: "6228f7f2364e02fa5ec8e51c-5fc705fa6b022a002d77f212", + deviceIdentifier: "299", + lineupIdentifier: "001" + ), + ])] + ) + + #expect(dvr.deviceChannel(forChannelKey: "6228f7f2364e02fa5ec8e51c-5fc706063f45c6002eab58a3") == "300") + #expect(dvr.deviceChannel(forChannelKey: "6228f7f2364e02fa5ec8e51c-5fc705fa6b022a002d77f212") == "299") + // Auch ohne den Vorsatz der Senderliste, denn die Kennung kommt je + // nach Quelle mit oder ohne ihn. + #expect(dvr.deviceChannel(forChannelKey: "5fc706063f45c6002eab58a3") == "300") + #expect(dvr.deviceChannel(forChannelKey: "unbekannt") == nil) + } + @Test("Die Sender des Tuners stehen unter DeviceChannel") func readsDeviceChannelKey() async throws { // Der Server lieferte 278 Sender, die Hülle verwarf sie: sie kannte