Beides mit derselben Rechnung, nur in einem anderen Fenster: vorn die ersten fünfzehn Minuten, hinten die letzten zehn. Was `Tonabgleich` findet, hängt allein daran, welchen Ausschnitt man ihm gibt — eine zweite Erkennung für den Abspann wäre dieselbe Suche ein zweites Mal. Je Staffel verglichen, nicht je Serie: Serien wechseln ihren Vorspann zwischen Staffeln, und über alle hinweg fände man nichts oder das Falsche. Gefundenes über fünf Minuten wird verworfen — zwei Folgen, die dieselbe Datei tragen, teilen vierzig, und darüber stünde sonst ein Überspringen-Knopf. Der Lauf geht auf Zuruf, nicht bei jedem Scan: er liest von jeder Folge ein Stück Ton und dauert Minuten. Der Knopf dafür steht auf der Serienseite, bei den Verwalterknöpfen. Die Marken gehen im Folgenvertrag mit — als „intro" und „credits", die Wörter, die die Apps von Jellyfin her ohnehin kennen. In der App zeichnet sie der vorhandene Überspringen-Knopf, der bisher nie etwas zu tun hatte; im Web ist er neu, rechtsbündig über der Zeitleiste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
256 lines
7.5 KiB
Swift
256 lines
7.5 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?
|
|
/// Vorspann und Abspann, sofern der Server sie gefunden hat.
|
|
///
|
|
/// Fehlt bei älteren Servern ganz — deshalb optional und nicht eine
|
|
/// leere Liste: ein `[]` im Vertrag zwänge jeden Server, das Feld zu
|
|
/// schicken.
|
|
let markers: [Marker]?
|
|
}
|
|
|
|
/// Ein überspringbarer Abschnitt.
|
|
struct Marker: Decodable, Sendable {
|
|
/// „intro" oder „credits" — dieselben Wörter wie bei Jellyfin.
|
|
let kind: String
|
|
let startSeconds: Double
|
|
let endSeconds: Double
|
|
}
|
|
|
|
/// 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? }
|
|
}
|