diff --git a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift index b474584..fe3efc2 100644 --- a/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift +++ b/Packages/FlixyKit/Sources/PlexKit/Endpoints/PlexServer+LiveTV.swift @@ -380,9 +380,40 @@ extension PlexServer { maximumBitrate: Int? ) async -> LiveTuneResult { let session = UUID().uuidString - var trace: [ChannelProbe] = [] 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 // Adresse — die Einzelabfrage beim Programmanbieter womöglich schon. // 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 summary = parts.isEmpty - ? "\(item.media.count) Fassungen, keine Teil-Adresse — \(Self.containerSummary(data))" - : "\(parts.count) Teil-Adressen" + guard parts.isEmpty else { + return (parts, item.ratingKey, "\(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. @@ -628,12 +690,26 @@ extension PlexServer { 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 }) { + let keys = [channel.gridKey, channel.key, channel.channelIdentifier, channel.id] + .compactMap { $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)") } + + // 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)" }