Files
Flaro/Packages/FlixyKit/Sources/MediaKit/MediaVersion.swift
T
ScarriffleandClaude Opus 5 f47c6385e3 Auflösungen heissen jetzt, wie Menschen sie nennen
Auf der Detailseite stand „1080" — die Zeichenkette des Servers in
Grossbuchstaben. Weder eine Zahl mit Bedeutung noch ein Name, den jemand
benutzt. Jetzt steht dort „Full HD", „4K", „HD", „SD".

Die Einstufung geht neu über die **Breite**, nicht über die Höhe. Ein
Kinofilm in 2,39:1 misst 1920 × 804; nach der Höhe war das bisher „720p",
und zwar ausgerechnet bei den grossen Filmen. Die Höhe bleibt als Notnagel
für Server, die keine Breite melden, und der Name des Servers als letzter.

Nebenbei fällt damit der 4K-Filter bei Jellyfin nicht mehr durch, wenn ein
breitwandiges 4K nur 1608 Zeilen hat, und die doppelte Einstufung in
PlexMedia verschwindet.

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

223 lines
8.1 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 {
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 {
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 {
public enum Kind: Sendable, Equatable { 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: " · ")
}
}