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>
136 lines
4.7 KiB
Swift
136 lines
4.7 KiB
Swift
import Foundation
|
||
|
||
/// Die Auflösung als Stufe, nicht als Zahl.
|
||
///
|
||
/// **Warum es das gibt.** Bisher stand auf der Detailseite schlicht `1080` —
|
||
/// die Zeichenkette, die Plex liefert, in Großbuchstaben. Weder eine Zahl mit
|
||
/// Bedeutung noch ein Name, den jemand benutzt.
|
||
///
|
||
/// **Warum die Breite zählt und nicht die Höhe.** Ein Kinofilm liegt in 2,39:1
|
||
/// vor und misst dann 1920 × 804. Nach der Höhe wäre das „720p"; nach der
|
||
/// Breite ist es, was es ist. Genau so sahen die Fassungen aus, bei denen die
|
||
/// alte Einstufung danebenlag — und zwar ausgerechnet bei den großen Filmen.
|
||
/// Die Höhe bleibt als Notnagel für Server, die keine Breite melden.
|
||
public enum VideoResolution: Sendable, Equatable, Comparable, CaseIterable {
|
||
case sd
|
||
case hd
|
||
case fullHD
|
||
case qhd
|
||
case uhd4K
|
||
case uhd8K
|
||
|
||
/// Wie der Name in der Oberfläche steht.
|
||
///
|
||
/// Nicht übersetzt: „Full HD" und „4K" heißen in jeder Sprache so, und
|
||
/// eine Übersetzungstabelle für vier feste Wörter wäre eine Fehlerquelle
|
||
/// ohne Nutzen.
|
||
public var name: String {
|
||
switch self {
|
||
case .sd: "SD"
|
||
case .hd: "HD"
|
||
case .fullHD: "Full HD"
|
||
case .qhd: "QHD"
|
||
case .uhd4K: "4K"
|
||
case .uhd8K: "8K"
|
||
}
|
||
}
|
||
|
||
/// Die technische Schreibweise — für Stufen, die man wählt, statt für
|
||
/// Material, das man beschreibt.
|
||
public var technicalName: String {
|
||
switch self {
|
||
case .sd: "480p"
|
||
case .hd: "720p"
|
||
case .fullHD: "1080p"
|
||
case .qhd: "1440p"
|
||
case .uhd4K: "2160p"
|
||
case .uhd8K: "4320p"
|
||
}
|
||
}
|
||
|
||
/// Die Bildhöhe, die diese Stufe üblicherweise hat.
|
||
///
|
||
/// Für Entscheidungen, die eine Zahl brauchen — etwa welche
|
||
/// Download-Stufen bei diesem Material überhaupt Sinn ergeben.
|
||
public var nominalHeight: Int {
|
||
switch self {
|
||
case .sd: 480
|
||
case .hd: 720
|
||
case .fullHD: 1_080
|
||
case .qhd: 1_440
|
||
case .uhd4K: 2_160
|
||
case .uhd8K: 4_320
|
||
}
|
||
}
|
||
|
||
/// Stuft eine Fassung ein.
|
||
///
|
||
/// In dieser Reihenfolge: Breite, dann Höhe, dann der Name des Servers.
|
||
/// Gibt `nil` zurück, wenn nichts davon etwas hergibt — dann ist eine
|
||
/// erfundene Stufe schlechter als gar keine Angabe.
|
||
public init?(width: Int?, height: Int?, serverLabel: String? = nil) {
|
||
if let width, width > 0 {
|
||
self = Self.fromWidth(width)
|
||
} else if let height, height > 0 {
|
||
self = Self.fromHeight(height)
|
||
} else if let stufe = Self.fromServerLabel(serverLabel) {
|
||
self = stufe
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
/// Der verlässliche Weg. Die Breite ist vom Bildformat unabhängig.
|
||
private static func fromWidth(_ width: Int) -> VideoResolution {
|
||
switch width {
|
||
case 7_000...: .uhd8K
|
||
case 3_000..<7_000: .uhd4K // 3840 und 4096
|
||
case 2_000..<3_000: .qhd // 2560
|
||
case 1_700..<2_000: .fullHD // 1920
|
||
case 1_100..<1_700: .hd // 1280
|
||
default: .sd
|
||
}
|
||
}
|
||
|
||
/// Der Notnagel.
|
||
///
|
||
/// Aus der Höhe allein ist ein breitwandiges 1080p (804 Zeilen) nicht von
|
||
/// einem vollformatigen 720p zu unterscheiden. Das ist keine Nachlässigkeit
|
||
/// in den Grenzen, sondern liegt an der Angabe: die Information fehlt
|
||
/// schlicht. Deshalb kommt die Breite zuerst.
|
||
private static func fromHeight(_ height: Int) -> VideoResolution {
|
||
switch height {
|
||
case 2_800...: .uhd8K
|
||
case 1_500..<2_800: .uhd4K
|
||
case 1_250..<1_500: .qhd
|
||
case 850..<1_250: .fullHD
|
||
case 650..<850: .hd
|
||
default: .sd
|
||
}
|
||
}
|
||
|
||
/// Was Plex hinschreibt: „4k", „1080", „sd".
|
||
private static func fromServerLabel(_ raw: String?) -> VideoResolution? {
|
||
guard let raw = raw?.lowercased().trimmingCharacters(in: .whitespaces),
|
||
!raw.isEmpty
|
||
else { return nil }
|
||
|
||
switch raw {
|
||
case "8k", "4320", "4320p": return .uhd8K
|
||
case "4k", "uhd", "2160", "2160p": return .uhd4K
|
||
case "qhd", "1440", "1440p": return .qhd
|
||
case "fhd", "1080", "1080p": return .fullHD
|
||
case "hd", "720", "720p": return .hd
|
||
case "sd", "576", "576p", "480", "480p", "360", "360p", "240", "240p": return .sd
|
||
default: break
|
||
}
|
||
|
||
// „1080i" und Ähnliches: die Ziffern vorne deuten, den Rest verwerfen.
|
||
let ziffern = raw.prefix { $0.isNumber }
|
||
guard let zahl = Int(ziffern), zahl > 0 else { return nil }
|
||
// Über tausend ist es eine Breite, darunter eine Höhe — andersherum
|
||
// gibt es keine sinnvolle Lesart.
|
||
return zahl >= 2_000 ? fromWidth(zahl) : fromHeight(zahl)
|
||
}
|
||
}
|