Live-TV-Sender gefunden, Film-Schriftzüge ergänzt

Senderliste
  Plex liefert die Sender verschachtelt in der Antwort auf /livetv/dvrs
  mit — jeder Empfänger bringt seine eigenen unter `Device` → `Channel`.
  Mein getrennter Endpunkt /livetv/dvrs/{id}/channels war eine Vermutung
  und lieferte nichts. Bei einem Server mit über dreißig eingerichteten
  Sendern stand deshalb „Keine Sender".

  Die verschachtelte Liste hat jetzt Vorrang, der Endpunkt bleibt als
  Rückfalllinie. `ChannelMapping` wird als ältere Schreibweise ebenfalls
  gelesen, und der Empfänger-Schlüssel kommt je nach Serverfassung als
  Zahl oder als Zeichenkette.

  Bleibt die Liste leer, sagt die Ansicht jetzt welcher Weg was ergeben
  hat. In einem Bereich ohne offizielle Beschreibung ist das die Angabe,
  mit der sich der nächste Fehler finden lässt.

Schriftzüge
  Plex liefert bei vielen Filmen ein `clearLogo` — den Schriftzug in
  seiner eigenen Type, freigestellt. Der tritt jetzt im Aufmacher an die
  Stelle des gesetzten Titels.

  Dabei ein Fallstrick: /photo/:/transcode liefert JPEG, und JPEG kennt
  keine Transparenz. Über den Bild-Transcoder geladen wäre aus dem
  freigestellten Logo ein schwarzer Kasten geworden. Es wird deshalb
  unverändert geladen. Fehlt der Schriftzug oder scheitert das Laden,
  bleibt es beim Titel.

161 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-09-12 09:28:21 +02:00
co-authored by Claude Opus 5
parent 7cfc6e0667
commit ed84b5bf22
11 changed files with 381 additions and 17 deletions
@@ -69,6 +69,8 @@ public struct PlexItem: Sendable, Equatable, Decodable, Identifiable {
/// Die Besetzung, mit Porträt und Rollennamen.
public let roles: [PlexTag]
public let markers: [PlexMarker]
/// Plakat, Hintergrund und — wenn vorhanden — der Schriftzug des Films.
public let images: [PlexImage]
private enum CodingKeys: String, CodingKey {
case ratingKey, key, guid, type, title, titleSort, summary, year, duration
@@ -84,6 +86,7 @@ public struct PlexItem: Sendable, Equatable, Decodable, Identifiable {
case writers = "Writer"
case roles = "Role"
case markers = "Marker"
case images = "Image"
}
/// Von Hand geschrieben statt erzeugt, weil der erzeugte Decoder einen
@@ -134,6 +137,7 @@ public struct PlexItem: Sendable, Equatable, Decodable, Identifiable {
self.writers = try values.decodeIfPresent([PlexTag].self, forKey: .writers) ?? []
self.roles = try values.decodeIfPresent([PlexTag].self, forKey: .roles) ?? []
self.markers = try values.decodeIfPresent([PlexMarker].self, forKey: .markers) ?? []
self.images = try values.decodeIfPresent([PlexImage].self, forKey: .images) ?? []
}
/// `ratingKey` ist serverweit eindeutig und eignet sich deshalb direkt
@@ -162,6 +166,15 @@ public struct PlexItem: Sendable, Equatable, Decodable, Identifiable {
/// Ob die Detailseite eine Versionsauswahl anbieten muss.
public var hasMultipleVersions: Bool { media.count > 1 }
/// Der Schriftzug des Titels, falls der Server einen hat.
///
/// Nicht jeder Film hat einen — dann bleibt es beim gesetzten Titel. Bei
/// Folgen liefert Plex in der Regel den Schriftzug der Serie mit, weil
/// eine einzelne Folge keinen eigenen hat.
public var clearLogoPath: String? {
images.first(where: \.isClearLogo)?.url
}
}
/// Die Hülle, in die Plex jede Antwort packt.
@@ -14,15 +14,96 @@ public struct PlexDVR: Sendable, Equatable, Decodable, Identifiable {
public let language: String?
public let lineupTitle: String?
public let lineup: String?
/// Die Empfänger am Server, jeweils mit ihren Sendern.
///
/// Plex liefert sie verschachtelt in der Antwort auf `/livetv/dvrs` mit.
/// Der getrennte Endpunkt `/livetv/dvrs/{id}/channels` war die erste
/// Vermutung und lieferte nichts — daran hing „Keine Sender" bei einem
/// Server, auf dem über dreißig Sender eingerichtet sind.
public let devices: [PlexTunerDevice]
public var id: String { key }
public init(key: String, uuid: String? = nil, lineupTitle: String? = nil, lineup: String? = nil, language: String? = nil) {
/// Alle Sender aller Empfänger dieses DVR.
public var channels: [PlexChannel] {
devices.flatMap(\.channels)
}
private enum CodingKeys: String, CodingKey {
case key, uuid, language, lineupTitle, lineup
case devices = "Device"
}
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) ?? []
}
public init(
key: String,
uuid: String? = nil,
lineupTitle: String? = nil,
lineup: String? = nil,
language: String? = nil,
devices: [PlexTunerDevice] = []
) {
self.key = key
self.uuid = uuid
self.lineupTitle = lineupTitle
self.lineup = lineup
self.language = language
self.devices = devices
}
}
/// 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?
public let channels: [PlexChannel]
public var id: String { uuid ?? key ?? title ?? "tuner" }
/// Plex nennt die Senderliste je nach Fassung `Channel` oder
/// `ChannelMapping`. Beide werden gelesen — eine falsche Annahme darüber
/// ergibt eine leere Liste, und die sieht aus wie „kein Sender vorhanden".
private enum CodingKeys: String, CodingKey {
case key, uuid, title
case channel = "Channel"
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)
if values.contains(.channel) {
self.channels = try values.decode([PlexChannel].self, forKey: .channel)
} else if values.contains(.channelMapping) {
self.channels = try values.decode([PlexChannel].self, forKey: .channelMapping)
} else {
self.channels = []
}
}
public init(key: String? = nil, uuid: String? = nil, title: String? = nil, channels: [PlexChannel] = []) {
self.key = key
self.uuid = uuid
self.title = title
self.channels = channels
}
}
@@ -61,3 +61,24 @@ public struct PlexMarker: Sendable, Equatable, Decodable, Identifiable {
self.endTimeOffset = endTimeOffset
}
}
/// Ein Bild zu einem Titel.
///
/// Plex liefert neben Plakat und Hintergrund noch das `clearLogo`: den
/// Schriftzug des Films als freigestelltes PNG — „Harry Potter" in seiner
/// eigenen Schrift statt in der der App. Genau dafür ist dieser Typ da.
public struct PlexImage: Sendable, Equatable, Decodable, Identifiable {
public let alt: String?
public let type: String
public let url: String
public var id: String { url }
public var isClearLogo: Bool { type == "clearLogo" }
public init(alt: String? = nil, type: String, url: String) {
self.alt = alt
self.type = type
self.url = url
}
}