Files
Flaro/Packages/FlaroKit/Sources/FlaroServerKit/FlaroContracts.swift
T
ScarriffleandClaude Opus 5 7673c73757 Vier Sekunden, die ffprobe zu früh aufhörte zu lesen
Die Untertitel kamen vier Sekunden zu spät. Nicht an ihnen lag es: der
Server meldete dem Spieler einen Anfang, der vor dem lag, an dem der
Strom wirklich begann, und genau diese Differenz landet eins zu eins im
Versatz.

Die Schlüsselbildsuche las mit `-read_intervals` bis eine halbe Sekunde
hinter den Wunsch. ffprobe bricht am Ende eines Fensters auf Paketebene
ab — das letzte Schlüsselbild darin kam regelmässig gar nicht mehr
heraus, und die Suche gab das davor zurück. Bei Bildgruppen von vier
Sekunden sind das vier Sekunden.

Nachgemessen gegen die Stelle, an der ffmpeg tatsächlich anfängt, an
einer Datei mit Schlüsselbildern alle vier Sekunden:

    Wunsch   Suche (0,5 s)   ffmpeg    Abweichung
      52,0            48      52,0         −4,0
      73,3            68      72,0         −4,0
     100,2            96     100,0         −4,0

Mit zehn Sekunden Nachlauf lag dieselbe Messung bei allen sieben Werten
auf 0,000. Was über den Wunsch hinausgelesen wird, verwirft die
Filterung ohnehin.

Die Prüfung vergleicht nicht gegen eine erwartete Zahl, sondern gegen
ffmpeg selbst — mit dem alten Wert fällt sie um.

Dazu der Filmteil der Abspannerkennung: `Abspannkante` als reine
Rechnung mit elf Prüfungen, `Bildabtaster` über ffmpeg, Marken auch an
Filmen, Knöpfe zum Suchen und Verwerfen auf der Filmseite. An einer
erzeugten Datei trifft die Kante auf vier Sekunden genau.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-13 06:42:16 +02:00

264 lines
7.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?
/// Das Bild, das jetzt im Aufmacher gilt — vom Server gewählt.
let heroImage: String?
let files: [File]
let state: ItemState?
/// Ab wann abspielbar. In der Zukunft heisst: noch nicht.
let publishAt: Date?
/// Der Abspann, sofern er im Bild gefunden wurde.
let markers: [Marker]?
}
struct Series: Decodable, Sendable {
let id: String
let title: String
let year: Int?
let overview: String?
let posterImage: String?
let backdropImage: String?
/// Das Bild, das jetzt im Aufmacher gilt — vom Server gewählt.
let heroImage: 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?
/// Das Bild, das jetzt im Aufmacher gilt — vom Server gewählt.
let heroImage: 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? }
}