Auf der Serienseite stand ein Menü für den Ton, gefüllt mit zwanzig Sprachen, von denen die Serie zwei hatte. Für Untertitel gab es keines, und auf der Filmseite überhaupt nichts. Jetzt beides an beiden Orten, und zwar nur mit dem, was es wirklich gibt: Bei einer **Serie** über den neuen Endpunkt — die Schnittmenge über alle Folgen. Was dadurch wegfällt, steht als Satz darunter, sonst sähe eine gekürzte Liste aus wie die ganze Wahrheit. Bei einem **Film** aus den Spuren der Datei. Kein Serverweg nötig: dort ist die Datei der Titel, und ihre Spuren stehen in der Antwort, die die Seite ohnehin geholt hat. Ton und Untertitel teilen sich einen Typ. Sie unterscheiden sich in genau zwei Punkten — woher der Wert kommt und ob es „Keine" gibt; ein zweiter Typ wäre dieselbe Datei mit zwei geänderten Zeilen. „Keine" gibt es nur beim Untertitel: ein Film ohne Tonspur ist kein Wunsch, sondern ein Fehler. Die Untertitelsprache liegt je Titel, in einem Topf für Serien und Filme — eine Filmkennung kollidiert mit keiner Serie, und zwei Wörterbücher für dieselbe Frage liefen auseinander. Der Spieler liest sie jetzt: vorher kannte er nur die allgemeine, und eine Serie im Original mit deutschen Untertiteln liess sich nicht einstellen, ohne sie überall einzuschalten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
242 lines
6.9 KiB
Swift
242 lines
6.9 KiB
Swift
import Foundation
|
|
|
|
/// Was Flaro über die Leitung schickt.
|
|
///
|
|
/// Ein Spiegel der `Contracts.cs` auf der Serverseite, und nur das: hier wird
|
|
/// nichts gerechnet und nichts gedeutet. Die Übersetzung in die neutralen
|
|
/// Typen steht in `FlaroBackend+Mapping`.
|
|
///
|
|
/// **Die Felder heissen wie dort.** Keine `CodingKeys`, keine Umbenennung:
|
|
/// ASP.NET Core schreibt `camelCase`, Swift liest `camelCase`, und jede
|
|
/// Abweichung wäre eine Stelle, an der beim nächsten Serverfeld jemand
|
|
/// nachziehen müsste.
|
|
enum Wire {
|
|
struct Page<T: Decodable & Sendable>: Decodable, Sendable {
|
|
let items: [T]
|
|
let total: Int
|
|
}
|
|
|
|
struct SystemInfo: Decodable, Sendable {
|
|
let version: String?
|
|
let serverName: String?
|
|
let needsSetup: Bool
|
|
let attributions: [Attribution]?
|
|
}
|
|
|
|
struct Attribution: Decodable, Sendable {
|
|
let text: String
|
|
let logoUrl: String
|
|
let url: String
|
|
}
|
|
|
|
struct LoginResponse: Decodable, Sendable {
|
|
let token: String
|
|
let user: User
|
|
}
|
|
|
|
struct User: Decodable, Sendable {
|
|
let id: String
|
|
let userName: String
|
|
let isAdministrator: Bool
|
|
/// Das Profilbild, als Schlüssel in die Bildablage des Servers.
|
|
let avatarImage: String?
|
|
}
|
|
|
|
struct Library: Decodable, Sendable {
|
|
let id: String
|
|
let name: String
|
|
let kind: String
|
|
let path: String
|
|
}
|
|
|
|
struct Stream: Decodable, Sendable {
|
|
let index: Int
|
|
let kind: String
|
|
let codec: String?
|
|
let language: String?
|
|
let title: String?
|
|
let isDefault: Bool
|
|
let isForced: Bool
|
|
let channels: Int?
|
|
let width: Int?
|
|
let height: Int?
|
|
}
|
|
|
|
struct File: Decodable, Sendable {
|
|
let id: String
|
|
let fileName: String
|
|
let sizeBytes: Int64
|
|
let container: String?
|
|
let durationSeconds: Double?
|
|
let streams: [Stream]
|
|
}
|
|
|
|
struct ItemState: Decodable, Sendable {
|
|
let positionSeconds: Double
|
|
let runtimeSeconds: Double?
|
|
let watched: Bool
|
|
let playCount: Int
|
|
let favourite: Bool
|
|
let rating: Int?
|
|
let dismissed: Bool
|
|
let lastPlayedAt: Date?
|
|
/// Ob der Server den Titel in „Weiterschauen" führt.
|
|
///
|
|
/// **Nicht dasselbe wie „Stelle über null".** Seit es eine
|
|
/// einstellbare Untergrenze gibt, sind das zwei Fragen: eine Sekunde
|
|
/// wird gemerkt, zählt aber nicht als angefangen.
|
|
let inProgress: Bool?
|
|
}
|
|
|
|
struct Movie: Decodable, Sendable {
|
|
let id: String
|
|
let title: String
|
|
let year: Int?
|
|
let overview: String?
|
|
let posterImage: String?
|
|
let backdropImage: String?
|
|
let files: [File]
|
|
let state: ItemState?
|
|
/// Ab wann abspielbar. In der Zukunft heisst: noch nicht.
|
|
let publishAt: Date?
|
|
}
|
|
|
|
struct Series: Decodable, Sendable {
|
|
let id: String
|
|
let title: String
|
|
let year: Int?
|
|
let overview: String?
|
|
let posterImage: String?
|
|
let backdropImage: String?
|
|
let episodeCount: Int
|
|
let seasons: [Int]
|
|
let unwatchedCount: Int
|
|
let publishAt: Date?
|
|
}
|
|
|
|
struct Episode: Decodable, Sendable {
|
|
let id: String
|
|
let season: Int
|
|
let episode: Int
|
|
let title: String?
|
|
let overview: String?
|
|
let stillImage: String?
|
|
let files: [File]
|
|
let state: ItemState?
|
|
let publishAt: Date?
|
|
/// Zu welcher Serie sie gehört — ohne das ist eine Folge für sich
|
|
/// nicht darstellbar: Titel und Bild der Serie fehlten.
|
|
let seriesId: String?
|
|
}
|
|
|
|
/// Eine Kachel aus „Weiterschauen" oder „Nächste Folge".
|
|
struct Started: Decodable, Sendable {
|
|
let id: String
|
|
let kind: String
|
|
let title: String
|
|
let subTitle: String?
|
|
let season: Int?
|
|
let episode: Int?
|
|
let year: Int?
|
|
let posterImage: String?
|
|
let backdropImage: String?
|
|
let state: ItemState
|
|
/// Zu welcher Serie die Folge gehört.
|
|
let seriesId: String?
|
|
/// Das breite Bild der Serie — für den Aufmacher, nicht für die
|
|
/// Kachel: die will das Standbild der Folge.
|
|
let seriesBackdropImage: String?
|
|
let publishAt: Date?
|
|
}
|
|
|
|
struct CodeResponse: Decodable, Sendable {
|
|
let code: String
|
|
let handle: String
|
|
let expiresAt: Date
|
|
}
|
|
|
|
/// `state` ist „pending", „approved" oder „expired".
|
|
struct CodeState: Decodable, Sendable {
|
|
let state: String
|
|
let login: LoginResponse?
|
|
}
|
|
|
|
struct CodeRequest: Encodable, Sendable {
|
|
let deviceName: String
|
|
let deviceId: String
|
|
}
|
|
|
|
struct Delivery: Decodable, Sendable {
|
|
let method: String
|
|
let reasons: [String]
|
|
let audioToEncode: [Int]
|
|
let subtitlesDropped: [Int]
|
|
let packageState: String
|
|
let progress: Double
|
|
let error: String?
|
|
}
|
|
|
|
// --- Was hinausgeht --------------------------------------------------
|
|
|
|
/// Was das Gerät kann und was gewünscht ist.
|
|
///
|
|
/// **Ohne Codec-Angaben nimmt der Server Apples Profil an**, und das ist
|
|
/// hier genau richtig: dieses Gerät *ist* Apple. Eine eigene Liste wäre
|
|
/// eine zweite Wahrheit neben der, die der Server ohnehin pflegt.
|
|
struct StreamRequest: Encodable, Sendable {
|
|
var height: Int?
|
|
var audioStream: Int?
|
|
var startSeconds: Double
|
|
var transcode: Bool
|
|
}
|
|
|
|
struct Stromspur: Decodable, Sendable {
|
|
let index: Int
|
|
let label: String
|
|
let language: String?
|
|
let isDefault: Bool
|
|
}
|
|
|
|
struct Stromauskunft: Decodable, Sendable {
|
|
let sessionId: String
|
|
let url: String
|
|
/// Dieselbe Wiedergabe mit Untertitelspuren — fehlt bei Direktwiedergabe.
|
|
let masterUrl: String?
|
|
/// „direct", „remux" oder „transcode".
|
|
let method: String
|
|
/// Ab welcher Sekunde des Films dieser Strom beginnt.
|
|
let startSeconds: Double
|
|
let height: Int?
|
|
let audioStream: Int?
|
|
let audioStreams: [Stromspur]
|
|
let subtitleStreams: [Stromspur]
|
|
}
|
|
|
|
struct Sprachangabe: Decodable, Sendable {
|
|
let code: String
|
|
let name: String
|
|
}
|
|
|
|
struct Sprachauswahl: Decodable, Sendable {
|
|
let audio: [Sprachangabe]
|
|
let subtitles: [Sprachangabe]
|
|
let partial: Int
|
|
}
|
|
|
|
struct LoginRequest: Encodable, Sendable {
|
|
let userName: String
|
|
let password: String
|
|
let deviceName: String
|
|
let deviceId: String
|
|
}
|
|
|
|
struct ProgressRequest: Encodable, Sendable {
|
|
let positionSeconds: Double
|
|
let runtimeSeconds: Double?
|
|
}
|
|
|
|
struct WatchedRequest: Encodable, Sendable { let watched: Bool }
|
|
struct FavouriteRequest: Encodable, Sendable { let favourite: Bool }
|
|
struct RatingRequest: Encodable, Sendable { let rating: Int? }
|
|
}
|