import Foundation /// Was ein Server können muss, damit die App mit ihm arbeiten kann. /// /// Der Vertrag ist bewusst schmal. Alles, was nur ein Server kann — Plex' PIN- /// Anmeldung, Jellyfins Verwaltung, die Eigenheiten des Programmführers — /// steht **nicht** hier, sondern als eigene Fähigkeit. Ein gemeinsamer Nenner, /// der alles umfasst, wäre kein Nenner mehr. public protocol MediaBackend: Sendable { /// Wie der Server heißt, so wie er sich selbst nennt. nonisolated var serverName: String { get } /// Was dieser Server darüber hinaus beherrscht. nonisolated var capabilities: Set { get } // MARK: - Blättern func libraries() async throws(MediaBackendError) -> [MediaLibrary] func homeRows() async throws(MediaBackendError) -> [MediaRowGroup] func items( inLibrary library: MediaLibrary, query: MediaQuery, offset: Int, limit: Int ) async throws(MediaBackendError) -> MediaPage func item(id: String) async throws(MediaBackendError) -> MediaItem func children(of id: String) async throws(MediaBackendError) -> [MediaItem] func search(_ text: String, limit: Int) async throws(MediaBackendError) -> [MediaRowGroup] /// Die möglichen Werte eines Filters, etwa alle Genres einer Mediathek. func filterOptions( inLibrary library: MediaLibrary, field: MediaQuery.Field ) async throws(MediaBackendError) -> [MediaFilterOption] /// Die nächste Folge nach dieser, sofern es eine gibt. func nextEpisode(after item: MediaItem) async throws(MediaBackendError) -> MediaItem? // MARK: - Bilder /// Ein Bild in der gewünschten Größe. /// /// `nonisolated`, weil die Adresse ohne Netzzugriff entsteht und der /// Systemplayer sie ohne Kopfzeilen abruft — der Token steckt deshalb in /// der Adresse selbst. nonisolated func artworkURL(path: String?, width: Int, height: Int) -> URL? nonisolated func rawImageURL(path: String?) -> URL? // MARK: - Herunterladen /// Die Adresse, unter der eine Datei unverändert zu holen ist. nonisolated func downloadURL(for file: MediaFile) -> URL? /// Die Adresse für eine verkleinerte Fassung. /// /// `nil`, wenn der Server das nicht anbietet — dann bleibt nur das /// Original, und die Oberfläche soll die Wahl gar nicht erst zeigen. nonisolated func compressedDownloadURL( itemID: String, versionIndex: Int, maximumBitrate: Int, resolution: String? ) -> URL? // MARK: - Abspielen /// Lässt den Server eine Wiedergabe vorbereiten und nennt die Adresse. /// /// Wirft mit der Begründung des Servers, wenn er ablehnt. Genau daran hing /// beim Live-TV tagelang alles: der Server hatte im Klartext gesagt, warum /// er nicht mitspielt, und die Antwort wurde weggeworfen. func preparePlayback(_ intent: PlaybackIntent) async throws(MediaBackendError) -> PlaybackTarget /// Beendet, was der Server für diese Wiedergabe angelegt hat. /// /// Ohne dieses Signal rechnet ein Transcoder minutenlang weiter, obwohl /// niemand mehr zusieht. func endPlayback(sessionID: String) async /// Meldet, wo man gerade steht. /// /// Wirft nicht: eine verlorene Meldung kostet ein paar Sekunden /// Fortschritt, und dafür mitten im Film eine Fehlermeldung zu zeigen /// wäre die schlechtere Hälfte des Tauschs. func reportProgress(_ report: PlaybackReport) async // MARK: - Zustand melden func markWatched(id: String) async throws(MediaBackendError) func markUnwatched(id: String) async throws(MediaBackendError) func setFavourite(_ isFavourite: Bool, id: String) async throws(MediaBackendError) func rate(id: String, rating: Int) async throws(MediaBackendError) func refreshLibrary(_ library: MediaLibrary) async throws(MediaBackendError) } /// Was ein Server über das Nötigste hinaus kann. /// /// Abgefragt statt vorausgesetzt: Plex kennt keine Favoriten, Jellyfin keine /// Plex-Empfänger, und eine Oberfläche, die Knöpfe für Dinge zeigt, die der /// Server nicht kann, ist schlimmer als eine, die sie weglässt. public enum BackendCapability: String, Sendable, Hashable { case liveTV case favourites case ratings case libraryScan /// Die Verwaltung des Servers aus der App heraus. case administration case transcoding } /// Ein Ausschnitt aus einer Mediathek. /// /// „Seite" ist hier eine technische Portion, **keine Seite in der Oberfläche**. /// Die Mediathek ist eine einzige lange Liste, die nachlädt, sobald man sich /// dem Ende nähert — Blättern mit „weiter zu Seite 2" gehört nicht in eine /// Mediathek, in der man stöbert. public struct MediaPage: Sendable, Equatable { public let items: [MediaItem] /// Ab welcher Stelle dieser Ausschnitt beginnt. public let offset: Int /// Wie viele es insgesamt gibt — Grundlage fürs Nachladen. public let totalCount: Int public init(items: [MediaItem], offset: Int = 0, totalCount: Int) { self.items = items self.offset = offset self.totalCount = totalCount } public var hasMore: Bool { offset + items.count < totalCount } public var nextOffset: Int { offset + items.count } } /// Ein möglicher Wert für einen Filter. public struct MediaFilterOption: Sendable, Equatable, Identifiable { public let id: String public let title: String public init(id: String, title: String) { self.id = id self.title = title } }