Files
Flaro/Packages/FlixyKit/Sources/MediaKit/MediaVersion.swift
T
ScarriffleandClaude Opus 5 2dbe260901 Mediatheken stehen sofort da statt sich jedes Mal neu zu laden
Jeder Aufruf einer Mediathek war ein Serverabruf mit Ladekreis davor. Die
Plakate lagen längst auf der Platte; was fehlte, war die Liste, die sagt,
welche davon hierhergehören.

Jetzt liegt die erste Seite als Abzug daneben: beim Aufrufen steht sie
sofort da, der Server wird still nachgefragt, und man sieht in aller Regel
nichts davon. Antwortet er nicht, bleibt der Abzug stehen — eine Liste von
gestern ist im Zug brauchbarer als ein Fehlerbildschirm.

Bewusst nur die erste Seite. Stünden 240 gespeicherte Titel da und der
Server antwortet mit 60, müsste entweder die Liste vor den Augen schrumpfen
oder der Versatz fürs Nachladen wäre falsch.

Gespeichert wird ausserdem nur, was eine Kachel zeigt. Ein Titel aus einer
Listenabfrage bringt jede Spur jeder Datei mit — im Raster steht davon
nichts, und ausgepackt werden müsste es bei jedem Aufrufen, also genau da,
wo der Abzug Zeit sparen soll.

Die Mediathekenliste selbst wird nur als Rettungsanker gemerkt, nicht als
Beschleuniger: sie sofort zu zeigen hiesse, eine Sitzung für fertig zu
erklären, bevor feststeht, dass der Token noch gilt.

Abmelden räumt alles weg. Titel, Zusammenfassungen und Sehfortschritt sind
persönlich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 15:59:15 +02:00

223 lines
8.2 KiB
Swift

import Foundation
/// Eine Fassung eines Titels — eine bestimmte Datei in einer bestimmten Güte.
///
/// Ein Film kann mehrfach vorliegen: als 4K-Remux und als kleine Fassung fürs
/// Handy. Welche gespielt wird, entscheidet der Nutzer oder die Voreinstellung.
public struct MediaVersion: Sendable, Equatable, Identifiable, Codable {
public let id: String
public let container: String?
public let videoCodec: String?
public let audioCodec: String?
/// Die Auflösung, wie der Server sie nennt — „1080", „4k".
public let videoResolution: String?
/// Gesamtbitrate in Kilobit je Sekunde.
public let bitrate: Int?
public let width: Int?
public let height: Int?
public let audioChannels: Int?
public let duration: Int?
public let files: [MediaFile]
public init(
id: String,
container: String? = nil,
videoCodec: String? = nil,
audioCodec: String? = nil,
videoResolution: String? = nil,
bitrate: Int? = nil,
width: Int? = nil,
height: Int? = nil,
audioChannels: Int? = nil,
duration: Int? = nil,
files: [MediaFile] = []
) {
self.id = id
self.container = container
self.videoCodec = videoCodec
self.audioCodec = audioCodec
self.videoResolution = videoResolution
self.bitrate = bitrate
self.width = width
self.height = height
self.audioChannels = audioChannels
self.duration = duration
self.files = files
}
/// Die Höhe in Bildzeilen.
///
/// Nicht immer gemeldet: manche Server nennen nur „4k" und keine Zahl.
/// Danach richtet sich, welche Qualitätsstufen überhaupt Sinn ergeben —
/// eine 720p-Datei auf 1080p hochzurechnen bringt kein Bild dazu.
public var pixelHeight: Int? {
if let height, height > 0 { return height }
return resolution?.nominalHeight
}
/// Die Stufe, in der diese Fassung vorliegt.
public var resolution: VideoResolution? {
VideoResolution(width: width, height: height, serverLabel: videoResolution)
}
/// Die Auflösung als Wort — „4K", „Full HD".
///
/// Nicht die Zeichenkette des Servers: Plex sagt „4k" und „1080",
/// Jellyfin gibt Zahlen. Gemeint ist dasselbe, und keines davon ist ein
/// Name, den jemand benutzt.
public var resolutionLabel: String? { resolution?.name }
/// Eine kurze Beschreibung für die Versionsauswahl, etwa
/// „4K · HEVC · 38 Mbit/s".
public var qualityLabel: String {
var pieces: [String] = []
if let label = resolutionLabel { pieces.append(label) }
if let videoCodec { pieces.append(videoCodec.uppercased()) }
if let bitrate { pieces.append("\(bitrate / 1000) Mbit/s") }
return pieces.joined(separator: " · ")
}
/// Was die Wiedergabe-Entscheidung über diese Fassung wissen muss.
public func profile(file: MediaFile?) -> MediaProfile {
MediaProfile(
container: file?.container ?? container,
videoCodec: videoCodec ?? file?.videoStream?.codec,
// Die **gewählte** Tonspur, nicht irgendeine: eine Datei mit
// deutschem AAC und englischem DTS ist für AVPlayer in Ordnung,
// solange Deutsch klingt.
audioCodec: file?.selectedAudioStream?.codec ?? audioCodec,
bitrate: bitrate,
hasKnownStreams: !(file?.streams.isEmpty ?? true)
)
}
}
/// Eine Datei auf dem Server.
public struct MediaFile: Sendable, Equatable, Identifiable, Codable {
public let id: String
/// Der Pfad, über den die Datei abgerufen wird.
public let key: String?
/// Zu welchem Titel diese Datei gehört.
///
/// Plex adressiert Dateien unmittelbar und braucht das nicht; Jellyfin
/// adressiert über den Titel und nennt die Datei nur als Beiwerk. Ohne
/// diese Angabe traf die Adresse dort den falschen Weg, sobald ein Titel
/// in mehreren Fassungen vorliegt — und nur dann, was den Fehler zu einem
/// gemacht hätte, den man erst spät findet.
public let itemID: String?
public let container: String?
/// Größe in Bytes.
///
/// `Int64`, nicht `Int`: ein Remux überschreitet vier Gigabyte mühelos,
/// und auf einer 32-Bit-Plattform wäre die Zahl dann falsch.
public let size: Int64?
public let duration: Int?
/// Der Pfad im Dateisystem des Servers — nur zur Anzeige.
public let file: String?
public let streams: [MediaStream]
public init(
id: String,
key: String? = nil,
itemID: String? = nil,
container: String? = nil,
size: Int64? = nil,
duration: Int? = nil,
file: String? = nil,
streams: [MediaStream] = []
) {
self.id = id
self.key = key
self.itemID = itemID
self.container = container
self.size = size
self.duration = duration
self.file = file
self.streams = streams
}
public var videoStream: MediaStream? { streams.first { $0.kind == .video } }
public var audioStreams: [MediaStream] { streams.filter { $0.kind == .audio } }
public var subtitleStreams: [MediaStream] { streams.filter { $0.kind == .subtitle } }
/// Die Tonspur, die klingen soll.
///
/// Die ausdrücklich gewählte, sonst die erste. Daran hängt die
/// Entscheidung, ob AVPlayer die Datei öffnen kann.
public var selectedAudioStream: MediaStream? {
audioStreams.first { $0.isSelected } ?? audioStreams.first
}
}
/// Eine Spur in einer Datei.
public struct MediaStream: Sendable, Equatable, Identifiable, Codable {
public enum Kind: String, Sendable, Equatable, Codable { case video, audio, subtitle }
public let id: String
public let kind: Kind
public let codec: String?
public let language: String?
public let languageCode: String?
/// Wie der Server die Spur nennt — etwa „Deutsch (AC3 5.1)".
public let displayTitle: String?
public let channels: Int?
public let isSelected: Bool
/// Untertitel, die auch bei abgeschalteten Untertiteln erscheinen sollen.
public let isForced: Bool
/// Wie die Spur **in der Datei** heißt.
///
/// Das zweite Signal neben dem Sprachkürzel — und manchmal das einzige
/// richtige. Wo im Kürzel „eng" steht, kann hier „Deutsch" stehen; genau
/// so sehen die Dateien aus, bei denen die Sprachwahl bisher danebengriff.
public let title: String?
/// Die Spur, die der Server als Standard markiert.
public let isDefault: Bool
/// Für Hörgeschädigte. **Nicht** Audiodeskription — dafür gibt es bei
/// Jellyfin kein Kennzeichen, die lässt sich nur am Namen erkennen.
public let isHearingImpaired: Bool
/// Die Originaltonspur, sofern die Datei das vermerkt.
public let isOriginal: Bool
public init(
id: String,
kind: Kind,
codec: String? = nil,
language: String? = nil,
languageCode: String? = nil,
displayTitle: String? = nil,
channels: Int? = nil,
isSelected: Bool = false,
isForced: Bool = false,
title: String? = nil,
isDefault: Bool = false,
isHearingImpaired: Bool = false,
isOriginal: Bool = false
) {
self.id = id
self.kind = kind
self.codec = codec
self.language = language
self.languageCode = languageCode
self.displayTitle = displayTitle
self.channels = channels
self.isSelected = isSelected
self.isForced = isForced
self.title = title
self.isDefault = isDefault
self.isHearingImpaired = isHearingImpaired
self.isOriginal = isOriginal
}
/// Die Nummer, unter der der Server diese Spur führt.
public var index: Int? { Int(id) }
/// Ein brauchbarer Name, auch wenn der Server keinen liefert.
///
/// „Spur 3" hilft niemandem bei der Wahl; Sprache und Codec schon.
public var name: String {
if let displayTitle, !displayTitle.isEmpty { return displayTitle }
let parts = [language, codec?.uppercased()].compactMap(\.self)
return parts.isEmpty ? "—" : parts.joined(separator: " · ")
}
}