Live-TV: die Lineup-Kennung ist eine Adresse, keine Kennung
Der zweite Abzug zeigt den Pfad, der 404 ergab:
/tv.plex.providers.epg.cloud:2/lineups/
lineup://tv.plex.providers.epg.cloud/6228f7f2364e02fa5ec8e51c
#Swisscom%20Blue%20TV%20%28Deutschschweiz%29%20%28521%20channels%29
/channels
`Lineup.id` heißt zwar Kennung, ist aber eine vollständige Adresse: mit
Schema, Doppelschrägstrich, Prozentzeichen und einem Anhang hinter der
Raute. Die habe ich roh in den Pfad gespleißt.
Gebraucht wird der nackte Teil in der Mitte —
`6228f7f2364e02fa5ec8e51c`. `PlexLineup.pathIdentifier` packt ihn aus,
vier Tests halten die Fälle fest.
Da die Pfadform zwischen Serverfassungen schwankt und dieser Teil der
API keine Beschreibung hat, werden drei Adressen der Reihe nach
probiert. Welche gegriffen hat, steht in der Diagnose — damit aus dem
Durchprobieren beim nächsten Mal Wissen wird. Der Strukturabzug deckt
jetzt alle drei ab.
173 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,41 +48,68 @@ extension PlexServer {
|
|||||||
public func channels(inDVR dvr: PlexDVR) async -> ChannelLookup {
|
public func channels(inDVR dvr: PlexDVR) async -> ChannelLookup {
|
||||||
let enabled = dvr.enabledChannelKeys
|
let enabled = dvr.enabledChannelKeys
|
||||||
|
|
||||||
// `??` mit einem asynchronen Aufruf rechts geht nicht — die
|
|
||||||
// Kurzschlussauswertung ist nicht nebenläufigkeitsfähig.
|
|
||||||
var identifier = dvr.epgIdentifier
|
var identifier = dvr.epgIdentifier
|
||||||
if identifier == nil {
|
if identifier == nil {
|
||||||
identifier = await electronicProgramGuideIdentifier()
|
identifier = await electronicProgramGuideIdentifier()
|
||||||
}
|
}
|
||||||
|
guard let epgIdentifier = identifier else {
|
||||||
guard let epgIdentifier = identifier, let lineup = dvr.lineups.first else {
|
|
||||||
return ChannelLookup(channels: [], source: .noLineup)
|
return ChannelLookup(channels: [], source: .noLineup)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
var lastFailure: String?
|
||||||
let container = try await client.send(
|
for path in Self.channelPaths(dvr: dvr, epgIdentifier: epgIdentifier) {
|
||||||
PlexRequest(
|
do {
|
||||||
path: "/\(epgIdentifier)/lineups/\(lineup.id)/channels",
|
let container = try await client.send(
|
||||||
timeout: .seconds(30)
|
PlexRequest(path: path, timeout: .seconds(30)),
|
||||||
),
|
as: PlexMediaContainer<PlexChannel>.self
|
||||||
as: PlexMediaContainer<PlexChannel>.self
|
)
|
||||||
)
|
guard !container.content.isEmpty else { continue }
|
||||||
|
|
||||||
// Ohne Zuordnung alles zeigen: besser die vollständige Liste als
|
// Ohne Zuordnung alles zeigen: besser die vollständige Liste
|
||||||
// gar keine, wenn der Empfänger keine Auswahl meldet.
|
// als gar keine, wenn der Empfänger keine Auswahl meldet.
|
||||||
let channels = enabled.isEmpty
|
let channels = enabled.isEmpty
|
||||||
? container.content
|
? container.content
|
||||||
: container.content.filter { $0.matches(anyOf: enabled) }
|
: container.content.filter { $0.matches(anyOf: enabled) }
|
||||||
|
|
||||||
return ChannelLookup(
|
if channels.isEmpty {
|
||||||
channels: channels,
|
return ChannelLookup(
|
||||||
source: channels.isEmpty
|
channels: [],
|
||||||
? .lineupWithoutMatches(available: container.content.count, enabled: enabled.count)
|
source: .lineupWithoutMatches(
|
||||||
: .lineup(available: container.content.count)
|
available: container.content.count,
|
||||||
)
|
enabled: enabled.count
|
||||||
} catch {
|
)
|
||||||
return ChannelLookup(channels: [], source: .failed(String(describing: error)))
|
)
|
||||||
|
}
|
||||||
|
return ChannelLookup(
|
||||||
|
channels: channels,
|
||||||
|
source: .lineup(available: container.content.count, path: path)
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
lastFailure = "\(path): \(error)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ChannelLookup(channels: [], source: .failed(lastFailure ?? "kein Weg lieferte Sender"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Die Adressen, unter denen die Senderliste stehen könnte.
|
||||||
|
///
|
||||||
|
/// Mehrere, weil dieser Teil der Plex-API keine Beschreibung hat und sich
|
||||||
|
/// die Form zwischen Serverfassungen unterscheidet. Sie werden der Reihe
|
||||||
|
/// nach probiert; welche gegriffen hat, steht in der Diagnose — damit aus
|
||||||
|
/// dem Durchprobieren beim nächsten Mal Wissen wird.
|
||||||
|
///
|
||||||
|
/// Entscheidend ist `pathIdentifier`: die Kennung der Senderliste kommt
|
||||||
|
/// als vollständige Adresse mit Schema und Anhang. Roh eingesetzt ergibt
|
||||||
|
/// jeder dieser Pfade 404.
|
||||||
|
static func channelPaths(dvr: PlexDVR, epgIdentifier: String) -> [String] {
|
||||||
|
var paths: [String] = []
|
||||||
|
if let lineup = dvr.lineups.first {
|
||||||
|
paths.append("/\(epgIdentifier)/lineups/\(lineup.pathIdentifier)/channels")
|
||||||
|
}
|
||||||
|
paths.append("/\(epgIdentifier)/channels")
|
||||||
|
paths.append("/livetv/dvrs/\(dvr.key)/channels")
|
||||||
|
return paths
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Die Kennung des Programmanbieters.
|
/// Die Kennung des Programmanbieters.
|
||||||
@@ -182,13 +209,13 @@ extension PlexServer {
|
|||||||
PlexRequest(path: "/livetv/dvrs", timeout: .seconds(10)),
|
PlexRequest(path: "/livetv/dvrs", timeout: .seconds(10)),
|
||||||
as: PlexMediaContainer<PlexDVR>.self
|
as: PlexMediaContainer<PlexDVR>.self
|
||||||
).content.first,
|
).content.first,
|
||||||
let epgIdentifier = dvr.epgIdentifier,
|
let epgIdentifier = dvr.epgIdentifier {
|
||||||
let lineup = dvr.lineups.first {
|
for path in Self.channelPaths(dvr: dvr, epgIdentifier: epgIdentifier) {
|
||||||
let path = "/\(epgIdentifier)/lineups/\(lineup.id)/channels"
|
report.append("\n=== \(path) ===")
|
||||||
report.append("\n=== \(path) ===")
|
report.append(await structure(ofPath: path))
|
||||||
report.append(await structure(ofPath: path))
|
}
|
||||||
} else {
|
} else {
|
||||||
report.append("\n=== Senderliste ===\nKeine Kennung oder Senderliste am Empfänger")
|
report.append("\n=== Senderliste ===\nKeine Kennung des Programmanbieters am Empfänger")
|
||||||
}
|
}
|
||||||
|
|
||||||
return report.joined(separator: "\n")
|
return report.joined(separator: "\n")
|
||||||
@@ -246,8 +273,8 @@ extension PlexServer {
|
|||||||
/// 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 {
|
||||||
/// Aus der Senderliste des Programmanbieters.
|
/// Aus der Senderliste des Programmanbieters, über diesen Pfad.
|
||||||
case lineup(available: Int)
|
case lineup(available: Int, path: String)
|
||||||
/// Die Senderliste kam an, aber keiner passte zur Zuordnung.
|
/// Die Senderliste kam an, aber keiner passte zur Zuordnung.
|
||||||
case lineupWithoutMatches(available: Int, enabled: Int)
|
case lineupWithoutMatches(available: Int, enabled: Int)
|
||||||
/// Am Empfänger hängt keine Senderliste oder kein Programmanbieter.
|
/// Am Empfänger hängt keine Senderliste oder kein Programmanbieter.
|
||||||
@@ -266,8 +293,8 @@ public struct ChannelLookup: Sendable {
|
|||||||
/// Kurzbeschreibung für die Diagnose.
|
/// Kurzbeschreibung für die Diagnose.
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch source {
|
switch source {
|
||||||
case .lineup(let available):
|
case .lineup(let available, let path):
|
||||||
"\(channels.count) von \(available) Sendern eingeschaltet"
|
"\(channels.count) von \(available) Sendern · \(path)"
|
||||||
case .lineupWithoutMatches(let available, let enabled):
|
case .lineupWithoutMatches(let available, let enabled):
|
||||||
"Senderliste hat \(available) Sender, \(enabled) sind zugeordnet — keiner passte zusammen"
|
"Senderliste hat \(available) Sender, \(enabled) sind zugeordnet — keiner passte zusammen"
|
||||||
case .noLineup:
|
case .noLineup:
|
||||||
|
|||||||
@@ -82,9 +82,28 @@ public struct PlexDVR: Sendable, Equatable, Decodable, Identifiable {
|
|||||||
|
|
||||||
/// Eine Senderliste des Anbieters — bei Kabel oder IPTV oft mehrere hundert.
|
/// Eine Senderliste des Anbieters — bei Kabel oder IPTV oft mehrere hundert.
|
||||||
public struct PlexLineup: Sendable, Equatable, Decodable, Identifiable {
|
public struct PlexLineup: Sendable, Equatable, Decodable, Identifiable {
|
||||||
|
/// Die Kennung, wie Plex sie liefert.
|
||||||
|
///
|
||||||
|
/// Trotz des Namens keine Kennung, sondern eine vollständige Adresse:
|
||||||
|
///
|
||||||
|
/// lineup://tv.plex.providers.epg.cloud/6228f7f2364e02fa5ec8e51c#Swisscom%20Blue%20TV
|
||||||
|
///
|
||||||
|
/// Mit Schema, Doppelschrägstrich, Prozentzeichen und einem Anhang hinter
|
||||||
|
/// der Raute. Roh in einen Pfad gesetzt ergibt das zwangsläufig 404 —
|
||||||
|
/// genau daran scheiterte die Senderabfrage.
|
||||||
public let id: String
|
public let id: String
|
||||||
public let title: String?
|
public let title: String?
|
||||||
|
|
||||||
|
/// Der nackte Teil in der Mitte, als Pfadabschnitt verwendbar.
|
||||||
|
///
|
||||||
|
/// Aus der Adresse oben wird `6228f7f2364e02fa5ec8e51c`.
|
||||||
|
public var pathIdentifier: String {
|
||||||
|
// Der Anhang hinter der Raute ist der Anzeigename und gehört nicht in
|
||||||
|
// die Adresse.
|
||||||
|
let withoutFragment = id.split(separator: "#", maxSplits: 1).first.map(String.init) ?? id
|
||||||
|
return withoutFragment.split(separator: "/").last.map(String.init) ?? withoutFragment
|
||||||
|
}
|
||||||
|
|
||||||
public init(id: String, title: String? = nil) {
|
public init(id: String, title: String? = nil) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
|
|||||||
@@ -153,3 +153,76 @@ struct ChannelMatchingTests {
|
|||||||
#expect(!channel.matches(anyOf: ["srf1", "srf2"]))
|
#expect(!channel.matches(anyOf: ["srf1", "srf2"]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suite("Kennung der Senderliste")
|
||||||
|
struct LineupIdentifierTests {
|
||||||
|
|
||||||
|
@Test("Aus der vollen Adresse wird der nackte Teil")
|
||||||
|
func extractsBareIdentifier() {
|
||||||
|
// Genau der Wert von einem echten Server. Roh in den Pfad gesetzt
|
||||||
|
// ergab er 404: Schema, Doppelschrägstrich, Prozentzeichen und ein
|
||||||
|
// Anhang hinter der Raute.
|
||||||
|
let lineup = PlexLineup(
|
||||||
|
id: "lineup://tv.plex.providers.epg.cloud/6228f7f2364e02fa5ec8e51c#Swisscom%20Blue%20TV%20%28521%20channels%29"
|
||||||
|
)
|
||||||
|
#expect(lineup.pathIdentifier == "6228f7f2364e02fa5ec8e51c")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Eine Adresse ohne Anhang funktioniert auch")
|
||||||
|
func withoutFragment() {
|
||||||
|
let lineup = PlexLineup(id: "lineup://tv.plex.providers.epg.cloud/abc123")
|
||||||
|
#expect(lineup.pathIdentifier == "abc123")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Eine bereits nackte Kennung bleibt unverändert")
|
||||||
|
func alreadyBare() {
|
||||||
|
// Nicht jeder Anbieter liefert eine Adresse — manche geben die
|
||||||
|
// Kennung direkt.
|
||||||
|
#expect(PlexLineup(id: "abc123").pathIdentifier == "abc123")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Ein leerer Anhang stört nicht")
|
||||||
|
func emptyFragment() {
|
||||||
|
#expect(PlexLineup(id: "lineup://host/abc123#").pathIdentifier == "abc123")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suite("Adressen der Senderliste")
|
||||||
|
struct ChannelPathTests {
|
||||||
|
private let dvr = PlexDVR(
|
||||||
|
key: "1",
|
||||||
|
lineups: [PlexLineup(
|
||||||
|
id: "lineup://tv.plex.providers.epg.cloud/6228f7f2364e02fa5ec8e51c#Swisscom%20Blue%20TV"
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test("Die Kennung der Senderliste wird ausgepackt, nicht roh eingesetzt")
|
||||||
|
func usesBareIdentifier() {
|
||||||
|
// Der Fehler, der 404 ergab: die volle Adresse mit Schema, Anhang und
|
||||||
|
// Prozentzeichen landete unverändert im Pfad.
|
||||||
|
let paths = PlexServer.channelPaths(dvr: dvr, epgIdentifier: "epg:2")
|
||||||
|
let first = paths[0]
|
||||||
|
|
||||||
|
#expect(first == "/epg:2/lineups/6228f7f2364e02fa5ec8e51c/channels")
|
||||||
|
#expect(!first.contains("lineup://"))
|
||||||
|
#expect(!first.contains("#"))
|
||||||
|
#expect(!first.contains("%"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Es gibt Rückfalllinien")
|
||||||
|
func offersFallbacks() {
|
||||||
|
// Die Form unterscheidet sich zwischen Serverfassungen, und dieser
|
||||||
|
// Teil der API hat keine Beschreibung.
|
||||||
|
let paths = PlexServer.channelPaths(dvr: dvr, epgIdentifier: "epg:2")
|
||||||
|
#expect(paths.count >= 2)
|
||||||
|
#expect(paths.contains("/livetv/dvrs/1/channels"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Ohne Senderliste bleiben die übrigen Wege")
|
||||||
|
func withoutLineup() {
|
||||||
|
let bare = PlexDVR(key: "7")
|
||||||
|
let paths = PlexServer.channelPaths(dvr: bare, epgIdentifier: "epg:2")
|
||||||
|
#expect(!paths.isEmpty)
|
||||||
|
#expect(paths.allSatisfy { !$0.contains("lineups//") })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user