MediaBackend: der Vertrag, den Plex und Jellyfin beide erfüllen
Ein Protokoll für das, was jeder Server können muss — blättern, Bilder liefern, Zustand melden. Bewusst schmal: alles, was nur einer kann, steht nicht darin, sondern als abfragbare Fähigkeit. Ein gemeinsamer Nenner, der alles umfasst, wäre keiner mehr. Deshalb `BackendCapability`. 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. Plex meldet hier ausdrücklich keine Favoriten und wirft, wenn jemand es trotzdem versucht; stillschweigend nichts zu tun wäre die schlechtere Antwort. `MediaQuery` ersetzt `LibraryQuery` nach oben hin. Deren Sortierwerte waren Plex' Drahtformat („titleSort:asc"), was hieß: entweder lernt Jellyfin Plex' Schreibweise, oder jede Ansicht muss wissen, mit wem sie spricht. `PlexBackend` ist eine Hülle, keine Umschreibung — die Endpunkte bleiben, wo sie sind. Der Fehlerübersetzer nimmt den ungetypten Wurf, weil Swift getippte Fehler durch eine Closure hindurch nicht ableiten kann; der zweite `catch` ist dabei keine Zierde, sondern verhindert, dass ein unerwarteter Fehler durchfällt und die Signatur zur Lüge macht. Die App benutzt noch nichts davon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ba6be4ac79
commit
effdcd4532
@@ -0,0 +1,91 @@
|
||||
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<BackendCapability> { 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: - 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
|
||||
}
|
||||
|
||||
/// Eine Seite aus einer Mediathek.
|
||||
public struct MediaPage: Sendable, Equatable {
|
||||
public let items: [MediaItem]
|
||||
/// Wie viele es insgesamt gibt — Grundlage fürs Nachladen.
|
||||
public let totalCount: Int?
|
||||
|
||||
public init(items: [MediaItem], totalCount: Int? = nil) {
|
||||
self.items = items
|
||||
self.totalCount = totalCount
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user