import Foundation /// Eine Mediathek auf dem Server. public struct PlexLibrary: Sendable, Equatable, Decodable, Identifiable { public enum Kind: String, Sendable, Codable { case movie, show, artist, photo /// Live-TV und Aufnahmen. case livetv } /// Die Nummer des Abschnitts, als Zeichenkette. Steckt in fast jedem /// weiteren Pfad. public let key: String public let title: String public let type: String public let uuid: String? public let art: String? public let thumb: String? public let updatedAt: Int? public var id: String { key } public var kind: Kind? { Kind(rawValue: type) } /// Ob Flixy diese Mediathek darstellen kann. Musik und Fotos sind laut /// Plan nicht vorgesehen — sie erscheinen deshalb gar nicht erst in der /// Seitenleiste, statt dort auf einen leeren Bildschirm zu führen. public var isSupported: Bool { kind == .movie || kind == .show } public init( key: String, title: String, type: String, uuid: String? = nil, art: String? = nil, thumb: String? = nil, updatedAt: Int? = nil ) { self.key = key self.title = title self.type = type self.uuid = uuid self.art = art self.thumb = thumb self.updatedAt = updatedAt } } /// Eine Reihe auf der Startseite. /// /// Plex nennt sie „Hub" und liefert sie fertig zusammengestellt — Weiterschauen, /// Zuletzt hinzugefügt, Sammlungen. Die App muss diese Reihen also nicht selbst /// bauen, sondern nur darstellen. public struct PlexHub: Sendable, Equatable, Decodable, Identifiable { /// Stabiler Bezeichner wie `home.continue` oder `movie.recentlyadded`. /// Danach richtet sich die Sortierung in den Einstellungen. public let hubIdentifier: String public let title: String public let type: String? public let key: String? public let size: Int? /// Ob es mehr Einträge gibt, als die Reihe zeigt — Grundlage für ein /// „Alle anzeigen" am Ende. public let more: Bool /// Aus welcher Mediathek diese Reihe stammt. /// /// `nil` bei den übergreifenden Reihen — „Fortsetzen" geht quer durch /// alle, und Plex nennt dort keine Nummer. /// /// **Warum es überhaupt gelesen wird.** Ohne diese Angabe kann die App /// eine Reihe keiner Mediathek zuordnen, und die Auswahl „welche /// Mediatheken speisen den Aufmacher" greift ins Leere: sie wirft dann /// jede Reihe weg, fällt auf „alles" zurück und sieht aus wie ein /// Schalter, der nichts tut. Genau so war es. public let librarySectionID: String? public let items: [PlexItem] public var id: String { hubIdentifier } /// Ob diese Reihe Angefangenes zeigt. /// /// Entscheidet über die Form der Kacheln: Weiterschauen lebt vom /// Szenenbild — man erkennt die Stelle wieder, an der man aufgehört hat. /// Überall sonst ist das Plakat richtig, denn danach sucht man einen Film /// aus. /// /// Geprüft wird die Kennung, nicht der Titel: der ist übersetzt und hieße /// auf Deutsch „Weiterschauen", auf Englisch „Continue Watching". public var isContinueWatching: Bool { let identifier = hubIdentifier.lowercased() // `ondeck` ist die ältere Bezeichnung; Server unterschiedlicher // Fassungen liefern die eine oder die andere. return identifier.contains("continue") || identifier.contains("ondeck") } private enum CodingKeys: String, CodingKey { case hubIdentifier, title, type, key, size, more, librarySectionID case items = "Metadata" } public init(from decoder: any Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.title = try values.decode(String.self, forKey: .title) // Ältere Serverfassungen liefern keinen `hubIdentifier`. Der Titel ist // dann der einzige verfügbare Schlüssel — schlechter, aber besser als // eine Antwort, die gar nicht lesbar ist. self.hubIdentifier = try values.decodeIfPresent(String.self, forKey: .hubIdentifier) ?? title self.type = try values.decodeIfPresent(String.self, forKey: .type) self.key = try values.decodeIfPresent(String.self, forKey: .key) self.size = try values.decodeIfPresent(Int.self, forKey: .size) self.more = try values.decodeIfPresent(Bool.self, forKey: .more) ?? false // **Zahl oder Zeichenkette.** Plex liefert die Abschnittsnummer je // nach Endpunkt mal als `3`, mal als `"3"`. Nur eines davon zu lesen // hiesse, dass es auf manchen Servern geht und auf anderen nicht — // und zwar lautlos, denn eine fehlende Nummer ist ein gültiger Fall. if let zahl = try? values.decodeIfPresent(Int.self, forKey: .librarySectionID) { self.librarySectionID = String(zahl) } else { self.librarySectionID = try values.decodeIfPresent(String.self, forKey: .librarySectionID) } self.items = try values.decodeIfPresent([PlexItem].self, forKey: .items) ?? [] } public init( hubIdentifier: String, title: String, type: String? = nil, key: String? = nil, size: Int? = nil, more: Bool = false, librarySectionID: String? = nil, items: [PlexItem] = [] ) { self.hubIdentifier = hubIdentifier self.librarySectionID = librarySectionID self.title = title self.type = type self.key = key self.size = size self.more = more self.items = items } }