Files
Flaro/Packages/FlaroKit/Sources/FlaroServerKit/FlaroContracts.swift
T
ScarriffleandClaude Opus 5 8cf2118723 Vorspann und Abspann — erkannt, abgelegt, überspringbar
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>
2026-10-12 21:28:36 +02:00

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? }
}