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/<uuid> 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 <noreply@anthropic.com>
515 lines
20 KiB
Swift
515 lines
20 KiB
Swift
import Foundation
|
|
|
|
/// Ein Fernsehempfänger am Server.
|
|
///
|
|
/// **Dieser ganze Bereich ist an keinem echten Server geprüft.** Die
|
|
/// Live-TV-Endpunkte sind der am schlechtesten dokumentierte Teil der
|
|
/// Plex-API — es gibt keine offizielle Beschreibung, nur das, was sich aus den
|
|
/// Anfragen der offiziellen Apps ablesen lässt. Die Modelle sind deshalb
|
|
/// durchgehend nachsichtig: fehlende Felder führen zu leeren Listen, nicht zu
|
|
/// Fehlern.
|
|
public struct PlexDVR: Sendable, Equatable, Decodable, Identifiable {
|
|
public let key: String
|
|
public let uuid: String?
|
|
public let language: String?
|
|
public let lineupTitle: String?
|
|
public let lineup: String?
|
|
/// Die Empfänger, jeweils mit ihrer Senderzuordnung.
|
|
public let devices: [PlexTunerDevice]
|
|
/// Die Kennung des Programmanbieters, etwa `tv.plex.providers.epg.cloud:2`.
|
|
///
|
|
/// Steht direkt am Empfänger — verlässlicher, als sie aus der
|
|
/// Anbieterliste zu suchen.
|
|
public let epgIdentifier: String?
|
|
/// Die Senderlisten, aus denen ausgewählt wurde.
|
|
public let lineups: [PlexLineup]
|
|
|
|
public var id: String { key }
|
|
|
|
/// Welche Sender eingeschaltet sind, als Schlüssel.
|
|
///
|
|
/// Die Zuordnung enthält **keine Namen** — nur, welche Sender der
|
|
/// Senderliste aktiv sind. Die Namen stehen beim Programmanbieter. Das
|
|
/// war der Grund, warum die Senderansicht leer blieb: die Zuordnung sah
|
|
/// nach einer Senderliste aus, war aber nur ein Verweis darauf.
|
|
public var enabledChannelKeys: Set<String> {
|
|
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"
|
|
case lineups = "Lineup"
|
|
}
|
|
|
|
public init(from decoder: any Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
// Als Zahl oder Zeichenkette — je nach Serverfassung.
|
|
if let text = try? values.decode(String.self, forKey: .key) {
|
|
self.key = text
|
|
} else {
|
|
self.key = String(try values.decode(Int.self, forKey: .key))
|
|
}
|
|
self.uuid = try values.decodeIfPresent(String.self, forKey: .uuid)
|
|
self.language = try values.decodeIfPresent(String.self, forKey: .language)
|
|
self.lineupTitle = try values.decodeIfPresent(String.self, forKey: .lineupTitle)
|
|
self.lineup = try values.decodeIfPresent(String.self, forKey: .lineup)
|
|
self.devices = try values.decodeIfPresent([PlexTunerDevice].self, forKey: .devices) ?? []
|
|
self.epgIdentifier = try values.decodeIfPresent(String.self, forKey: .epgIdentifier)
|
|
self.lineups = try values.decodeIfPresent([PlexLineup].self, forKey: .lineups) ?? []
|
|
}
|
|
|
|
public init(
|
|
key: String,
|
|
uuid: String? = nil,
|
|
lineupTitle: String? = nil,
|
|
lineup: String? = nil,
|
|
language: String? = nil,
|
|
devices: [PlexTunerDevice] = [],
|
|
epgIdentifier: String? = nil,
|
|
lineups: [PlexLineup] = []
|
|
) {
|
|
self.key = key
|
|
self.uuid = uuid
|
|
self.lineupTitle = lineupTitle
|
|
self.lineup = lineup
|
|
self.language = language
|
|
self.devices = devices
|
|
self.epgIdentifier = epgIdentifier
|
|
self.lineups = lineups
|
|
}
|
|
}
|
|
|
|
/// Eine Senderliste des Anbieters — bei Kabel oder IPTV oft mehrere hundert.
|
|
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 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) {
|
|
self.id = id
|
|
self.title = title
|
|
}
|
|
}
|
|
|
|
/// Ein Eintrag der Senderzuordnung.
|
|
///
|
|
/// 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?
|
|
|
|
/// Plex schreibt hier `"1"` oder `"0"` als Zeichenkette, nicht als
|
|
/// Wahrheitswert.
|
|
public var isEnabled: Bool { enabled != "0" }
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
/// Ein Empfänger — eine Fernsehkarte oder ein Netzwerk-Tuner.
|
|
public struct PlexTunerDevice: Sendable, Equatable, Decodable, Identifiable {
|
|
public let key: String?
|
|
public let uuid: String?
|
|
public let title: String?
|
|
/// Die Gerätekennung — unter ihr führt der Server die gefundenen Sender.
|
|
public let deviceIdentifier: String?
|
|
/// Welche Sender der Senderliste hier eingeschaltet sind.
|
|
public let channelMappings: [PlexChannelMapping]
|
|
|
|
public var id: String { uuid ?? key ?? title ?? "tuner" }
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case key, uuid, title, deviceId, deviceIdentifier
|
|
case channelMapping = "ChannelMapping"
|
|
}
|
|
|
|
public init(from decoder: any Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.key = try? values.decodeIfPresent(String.self, forKey: .key)
|
|
self.uuid = try values.decodeIfPresent(String.self, forKey: .uuid)
|
|
self.title = try values.decodeIfPresent(String.self, forKey: .title)
|
|
self.deviceIdentifier = (try? values.decodeIfPresent(String.self, forKey: .deviceId))
|
|
?? (try? values.decodeIfPresent(String.self, forKey: .deviceIdentifier))
|
|
?? nil
|
|
self.channelMappings = try values
|
|
.decodeIfPresent([PlexChannelMapping].self, forKey: .channelMapping) ?? []
|
|
}
|
|
|
|
public init(
|
|
key: String? = nil,
|
|
uuid: String? = nil,
|
|
title: String? = nil,
|
|
deviceIdentifier: String? = nil,
|
|
channelMappings: [PlexChannelMapping] = []
|
|
) {
|
|
self.key = key
|
|
self.uuid = uuid
|
|
self.title = title
|
|
self.deviceIdentifier = deviceIdentifier
|
|
self.channelMappings = channelMappings
|
|
}
|
|
}
|
|
|
|
/// Der Medien-Block einer Sendung im Raster.
|
|
///
|
|
/// Er trägt die Angaben zum Sender mit: das Raster ist bei Plex nicht nur
|
|
/// eine Liste von Sendungen, sondern die einzige Antwort, in der Sendername,
|
|
/// Nummer, Logo und Einschaltweg zusammen stehen. Drei geratene
|
|
/// Senderlisten-Adressen ergaben 404 — diese hier liefert die
|
|
/// Weboberfläche selbst aus.
|
|
struct PlexGridMedia: Decodable {
|
|
let channelIdentifier: String?
|
|
let channelTitle: String?
|
|
let channelCallSign: String?
|
|
let channelThumb: String?
|
|
let channelVcn: String?
|
|
/// Die laufende Nummer des Senders am Server.
|
|
let channelID: String?
|
|
/// Der Schlüssel des Senders im Raster — ein Kandidat fürs Einschalten.
|
|
let gridKey: String?
|
|
/// Anfang und Ende der Sendung.
|
|
///
|
|
/// Sie stehen hier und **nicht** am Eintrag darüber. Genau daran ist das
|
|
/// Raster gescheitert: oben gesucht, nicht gefunden, jede Sendung
|
|
/// verworfen — und übrig blieb „Kein Programm verfügbar" auf jedem Sender.
|
|
let beginsAt: Int?
|
|
let endsAt: Int?
|
|
let parts: [PlexGridPart]
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case channelIdentifier, channelTitle, channelCallSign, channelThumb, channelVcn, channelID
|
|
case gridKey, beginsAt, endsAt
|
|
case part = "Part"
|
|
}
|
|
|
|
init(from decoder: any Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.channelIdentifier = try? values.decodeIfPresent(String.self, forKey: .channelIdentifier)
|
|
self.channelTitle = try values.decodeIfPresent(String.self, forKey: .channelTitle)
|
|
self.channelCallSign = try values.decodeIfPresent(String.self, forKey: .channelCallSign)
|
|
self.channelThumb = try values.decodeIfPresent(String.self, forKey: .channelThumb)
|
|
// Die Sendernummer kommt mal als Zahl, mal als Zeichenkette.
|
|
if let text = try? values.decodeIfPresent(String.self, forKey: .channelVcn) {
|
|
self.channelVcn = text
|
|
} else if let number = try? values.decodeIfPresent(Int.self, forKey: .channelVcn) {
|
|
self.channelVcn = String(number)
|
|
} else {
|
|
self.channelVcn = nil
|
|
}
|
|
if let text = try? values.decodeIfPresent(String.self, forKey: .channelID) {
|
|
self.channelID = text
|
|
} else if let number = try? values.decodeIfPresent(Int.self, forKey: .channelID) {
|
|
self.channelID = String(number)
|
|
} else {
|
|
self.channelID = nil
|
|
}
|
|
self.gridKey = try? values.decodeIfPresent(String.self, forKey: .gridKey)
|
|
self.beginsAt = try? values.decodeIfPresent(Int.self, forKey: .beginsAt)
|
|
self.endsAt = try? values.decodeIfPresent(Int.self, forKey: .endsAt)
|
|
self.parts = try values.decodeIfPresent([PlexGridPart].self, forKey: .part) ?? []
|
|
}
|
|
|
|
/// Der Sender, sofern dieser Block einen benennt.
|
|
///
|
|
/// Ohne Namen kein Sender: eine Zeile, die nur „—" hieße, ist für die
|
|
/// Senderliste wertlos und stünde bei jedem Eintrag doppelt.
|
|
var channel: PlexChannel? {
|
|
guard let name = channelTitle ?? channelCallSign else { return nil }
|
|
let identity = channelIdentifier ?? channelID ?? parts.first?.key ?? name
|
|
return PlexChannel(
|
|
id: identity,
|
|
title: Self.withoutLeadingNumber(name, number: channelVcn),
|
|
channelIdentifier: channelVcn,
|
|
thumb: channelThumb,
|
|
key: parts.first?.key ?? channelID,
|
|
gridKey: gridKey
|
|
)
|
|
}
|
|
|
|
/// Entfernt die Sendernummer aus dem Namen.
|
|
///
|
|
/// Plex schreibt sie dort mit hinein — „006 ZDFGERH (ZDF HD)". Da die
|
|
/// Nummer in der Senderspalte ohnehin eigens steht, stünde sie sonst
|
|
/// zweimal nebeneinander.
|
|
static func withoutLeadingNumber(_ name: String, number: String?) -> String {
|
|
guard let number, !number.isEmpty else { return name }
|
|
let prefix = number + " "
|
|
guard name.hasPrefix(prefix) else { return name }
|
|
return String(name.dropFirst(prefix.count))
|
|
}
|
|
}
|
|
|
|
/// Der Strom hinter einer Rastersendung.
|
|
struct PlexGridPart: Decodable {
|
|
let key: String?
|
|
}
|
|
|
|
/// Ein Sender.
|
|
public struct PlexChannel: Sendable, Equatable, Decodable, Identifiable {
|
|
/// Die Nummer, unter der der Sender läuft — etwa `"3.1"`.
|
|
public let channelIdentifier: String?
|
|
public let title: String
|
|
public let thumb: String?
|
|
/// Der Schlüssel, über den sich der Sender einschalten lässt.
|
|
public let key: String?
|
|
/// Der Schlüssel des Senders im Programmraster.
|
|
public let gridKey: String?
|
|
public let id: String
|
|
|
|
/// Ob dieser Sender einen Namen trägt.
|
|
///
|
|
/// Eine Liste aus dreißig Strichen sieht aus wie eine Senderliste, ist
|
|
/// aber keine — dieser Irrtum hat hier schon einmal eine falsche Quelle
|
|
/// als die richtige durchgehen lassen.
|
|
public var hasName: Bool { title != Self.placeholder }
|
|
|
|
static let placeholder = "—"
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case channelIdentifier, title, thumb, key, gridKey
|
|
case identifier = "id"
|
|
case uuid, name, callSign, vcn, number, channelTitle, channelCallSign
|
|
}
|
|
|
|
public init(from decoder: any Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
// Vier Schreibweisen für denselben Namen, je nach Tunertyp und
|
|
// Anbieter. Nur `title` zu lesen hieße, die halbe Senderliste als
|
|
// namenlos zu verwerfen.
|
|
self.title = [CodingKeys.title, .name, .channelTitle, .callSign, .channelCallSign]
|
|
.lazy
|
|
.compactMap { try? values.decodeIfPresent(String.self, forKey: $0) }
|
|
.first { !$0.isEmpty } ?? Self.placeholder
|
|
|
|
// Beim Tuner heißt die Nummer schlicht `key` — „300". Ohne diesen
|
|
// Rückfall stünde die halbe Senderliste ohne Nummer da.
|
|
let number = Self.text(in: values, forKeys: [.channelIdentifier, .vcn, .number])
|
|
let keyValue = Self.text(in: values, forKeys: [.key])
|
|
self.channelIdentifier = number
|
|
?? keyValue.flatMap { $0.allSatisfy { character in character.isNumber || character == "." } ? $0 : nil }
|
|
self.thumb = try values.decodeIfPresent(String.self, forKey: .thumb)
|
|
self.key = Self.text(in: values, forKeys: [.key])
|
|
self.gridKey = try? values.decodeIfPresent(String.self, forKey: .gridKey)
|
|
|
|
// Plex liefert die Kennung mal als Zahl, mal als Zeichenkette, mal als
|
|
// `uuid`. Ein einzelner erwarteter Typ hätte hier bei manchen
|
|
// Serverfassungen die ganze Senderliste unlesbar gemacht.
|
|
if let text = try? values.decodeIfPresent(String.self, forKey: .identifier) {
|
|
self.id = text
|
|
} else if let number = try? values.decodeIfPresent(Int.self, forKey: .identifier) {
|
|
self.id = String(number)
|
|
} else if let uuid = try values.decodeIfPresent(String.self, forKey: .uuid) {
|
|
self.id = uuid
|
|
} else {
|
|
self.id = channelIdentifier ?? title
|
|
}
|
|
}
|
|
|
|
/// Ob dieser Sender in der Zuordnung des Empfängers steht.
|
|
///
|
|
/// Nachsichtig verglichen: die Zuordnung führt einen `channelKey`, und
|
|
/// je nach Anbieter entspricht der der Kennung, dem Pfad oder der
|
|
/// Sendernummer. Ein strenger Vergleich fände in zwei von drei Fällen
|
|
/// nichts — und „keiner passte zusammen" sieht aus wie „keine Sender".
|
|
public func matches(anyOf keys: Set<String>) -> Bool {
|
|
if keys.contains(id) { return true }
|
|
if let channelIdentifier, keys.contains(channelIdentifier) { return true }
|
|
if let key {
|
|
if keys.contains(key) { return true }
|
|
if let last = key.split(separator: "/").last, keys.contains(String(last)) {
|
|
return true
|
|
}
|
|
}
|
|
// Umgekehrt: manche Zuordnungen führen den vollen Pfad, der Sender
|
|
// aber nur die Kennung.
|
|
return keys.contains { $0.split(separator: "/").last.map(String.init) == id }
|
|
}
|
|
|
|
/// Ein Wert, der als Zahl oder als Zeichenkette kommen kann.
|
|
private static func text(
|
|
in values: KeyedDecodingContainer<CodingKeys>,
|
|
forKeys keys: [CodingKeys]
|
|
) -> String? {
|
|
for key in keys {
|
|
if let text = try? values.decodeIfPresent(String.self, forKey: key), !text.isEmpty {
|
|
return text
|
|
}
|
|
if let number = try? values.decodeIfPresent(Int.self, forKey: key) {
|
|
return String(number)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public init(
|
|
id: String,
|
|
title: String,
|
|
channelIdentifier: String? = nil,
|
|
thumb: String? = nil,
|
|
key: String? = nil,
|
|
gridKey: String? = nil
|
|
) {
|
|
self.id = id
|
|
self.title = title
|
|
self.channelIdentifier = channelIdentifier
|
|
self.thumb = thumb
|
|
self.key = key
|
|
self.gridKey = gridKey
|
|
}
|
|
}
|
|
|
|
/// Eine Sendung im Programm.
|
|
public struct PlexProgram: Sendable, Equatable, Identifiable {
|
|
public let id: String
|
|
public let title: String
|
|
public let subtitle: String?
|
|
public let summary: String?
|
|
public let startsAt: Date
|
|
public let endsAt: Date
|
|
public let channelID: String?
|
|
public let thumb: String?
|
|
/// Die Adresse dieser Sendung beim Programmanbieter.
|
|
public let metadataKey: String?
|
|
|
|
public init(
|
|
id: String,
|
|
title: String,
|
|
subtitle: String? = nil,
|
|
summary: String? = nil,
|
|
startsAt: Date,
|
|
endsAt: Date,
|
|
channelID: String? = nil,
|
|
thumb: String? = nil,
|
|
metadataKey: String? = nil
|
|
) {
|
|
self.id = id
|
|
self.title = title
|
|
self.subtitle = subtitle
|
|
self.summary = summary
|
|
self.startsAt = startsAt
|
|
self.endsAt = endsAt
|
|
self.channelID = channelID
|
|
self.thumb = thumb
|
|
self.metadataKey = metadataKey
|
|
}
|
|
|
|
public var duration: Duration {
|
|
.seconds(endsAt.timeIntervalSince(startsAt))
|
|
}
|
|
|
|
public func isRunning(at moment: Date = Date()) -> Bool {
|
|
moment >= startsAt && moment < endsAt
|
|
}
|
|
|
|
/// Wie weit die Sendung fortgeschritten ist, 0…1.
|
|
public func progress(at moment: Date = Date()) -> Double {
|
|
let total = endsAt.timeIntervalSince(startsAt)
|
|
guard total > 0 else { return 0 }
|
|
return min(max(moment.timeIntervalSince(startsAt) / total, 0), 1)
|
|
}
|
|
}
|
|
|
|
/// Ein Eintrag aus dem Programmraster.
|
|
///
|
|
/// Plex liefert das Raster als gewöhnliche Metadaten mit den Zeitangaben in
|
|
/// Sekunden seit 1970. Die Übersetzung in `PlexProgram` passiert hier, damit
|
|
/// die Oberfläche keine Zeitstempel rechnen muss.
|
|
struct PlexGridEntry: Decodable {
|
|
let ratingKey: String?
|
|
/// Die Adresse der Sendung beim Programmanbieter — über sie läuft das
|
|
/// Einschalten, denn Plex behandelt eine laufende Sendung wie einen Film.
|
|
let key: String?
|
|
let title: String?
|
|
let summary: String?
|
|
let grandparentTitle: String?
|
|
let thumb: String?
|
|
let beginsAt: Int?
|
|
let endsAt: Int?
|
|
|
|
/// Die Sendermedien; darin steht, zu welchem Sender die Sendung gehört.
|
|
let media: [PlexGridMedia]?
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case ratingKey, key, title, summary, grandparentTitle, thumb, beginsAt, endsAt
|
|
case media = "Media"
|
|
}
|
|
|
|
/// Der Sender, auf dem diese Sendung läuft.
|
|
///
|
|
/// Hier steht der Sender vollständig: Name, Nummer, Logo, Einschaltweg.
|
|
/// Das ist der Grund, warum das Raster die Senderliste ersetzen kann.
|
|
var channel: PlexChannel? { media?.compactMap(\.channel).first }
|
|
|
|
func program(channelID: String?) -> PlexProgram? {
|
|
// Die Zeiten stehen beim Programmanbieter im Medien-Block, nicht am
|
|
// Eintrag. Beide Stellen lesen, damit es auf beiden Serverfassungen
|
|
// trägt.
|
|
let beginsAt = self.beginsAt ?? media?.first?.beginsAt
|
|
let endsAt = self.endsAt ?? media?.first?.endsAt
|
|
guard let beginsAt, let endsAt, let title else { return nil }
|
|
return PlexProgram(
|
|
id: ratingKey ?? "\(channelID ?? "")-\(beginsAt)",
|
|
title: title,
|
|
subtitle: grandparentTitle,
|
|
summary: summary,
|
|
startsAt: Date(timeIntervalSince1970: TimeInterval(beginsAt)),
|
|
endsAt: Date(timeIntervalSince1970: TimeInterval(endsAt)),
|
|
channelID: channelID,
|
|
thumb: thumb,
|
|
metadataKey: key
|
|
)
|
|
}
|
|
}
|