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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e61cb0403d
commit
f47c6385e3
@@ -5,10 +5,6 @@
|
|||||||
"comment" : "A separator between different parts of the episode metadata.",
|
"comment" : "A separator between different parts of the episode metadata.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"%@ GB" : {
|
|
||||||
"comment" : "A picker option in the settings view that lets the user select the maximum amount of storage space that can be used for downloaded content. The value is given in gigabytes.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"%@ → %@" : {
|
"%@ → %@" : {
|
||||||
"comment" : "A line in a diagnostic trace that shows a network request path and its result. The first argument is the network request path. The second argument is the network response result.",
|
"comment" : "A line in a diagnostic trace that shows a network request path and its result. The first argument is the network request path. The second argument is the network response result.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -21,8 +17,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"1080p" : {
|
"%@ GB" : {
|
||||||
"comment" : "A label for a 1080p video.",
|
"comment" : "A picker option in the settings view that lets the user select the maximum amount of storage space that can be used for downloaded content. The value is given in gigabytes.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"4K" : {
|
"4K" : {
|
||||||
@@ -33,8 +29,8 @@
|
|||||||
"comment" : "A label for a 720p video.",
|
"comment" : "A label for a 720p video.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"SD" : {
|
"1080p" : {
|
||||||
"comment" : "A label for a 720p movie.",
|
"comment" : "A label for a 1080p video.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"account.hint" : {
|
"account.hint" : {
|
||||||
@@ -2313,6 +2309,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"library.allLoaded %@" : {
|
||||||
|
"comment" : "A footer in the library view that shows the number of items in the library.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"library.allLoaded %lld" : {
|
"library.allLoaded %lld" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2922,23 +2922,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"liveTV.recordSeries" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Serie aufnehmen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Record series"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"liveTV.recording.inProgress" : {
|
"liveTV.recording.inProgress" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3007,6 +2990,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"liveTV.recordSeries" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Serie aufnehmen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Record series"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"liveTV.runningNow" : {
|
"liveTV.runningNow" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3611,23 +3611,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player.subtitleTracks" : {
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Untertitel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Subtitles"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"player.subtitlesOff" : {
|
"player.subtitlesOff" : {
|
||||||
"comment" : "Eintrag, der die Untertitel abschaltet",
|
"comment" : "Eintrag, der die Untertitel abschaltet",
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
@@ -3646,6 +3629,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"player.subtitleTracks" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Untertitel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Subtitles"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"player.tracks" : {
|
"player.tracks" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3730,23 +3730,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"quality.autoShort" : {
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Auto"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Auto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"quality.automatic" : {
|
"quality.automatic" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3764,6 +3747,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"quality.autoShort" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"quality.current" : {
|
"quality.current" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -4190,6 +4190,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SD" : {
|
||||||
|
"comment" : "A label for a 720p movie.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"search.hint" : {
|
"search.hint" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -6442,4 +6446,4 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.1"
|
"version" : "1.1"
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ struct PlayableDetailView: View {
|
|||||||
if let year = item.year { pieces.append(String(year)) }
|
if let year = item.year { pieces.append(String(year)) }
|
||||||
if let duration = item.duration { pieces.append(Duration.milliseconds(duration).runtimeLabel) }
|
if let duration = item.duration { pieces.append(Duration.milliseconds(duration).runtimeLabel) }
|
||||||
if let rating = item.contentRating { pieces.append(rating) }
|
if let rating = item.contentRating { pieces.append(rating) }
|
||||||
if let resolution = currentMedia?.videoResolution { pieces.append(resolution.uppercased()) }
|
if let resolution = currentMedia?.resolutionLabel { pieces.append(resolution) }
|
||||||
return pieces
|
return pieces
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ struct PlayableDetailView: View {
|
|||||||
if let year = item.year { pieces.append(String(year)) }
|
if let year = item.year { pieces.append(String(year)) }
|
||||||
if let duration = item.duration { pieces.append(Duration.milliseconds(duration).runtimeLabel) }
|
if let duration = item.duration { pieces.append(Duration.milliseconds(duration).runtimeLabel) }
|
||||||
if let rating = item.contentRating { pieces.append(rating) }
|
if let rating = item.contentRating { pieces.append(rating) }
|
||||||
if let resolution = currentMedia?.videoResolution { pieces.append(resolution.uppercased()) }
|
if let resolution = currentMedia?.resolutionLabel { pieces.append(resolution) }
|
||||||
return pieces.isEmpty ? nil : pieces.joined(separator: " · ")
|
return pieces.isEmpty ? nil : pieces.joined(separator: " · ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ struct LibraryFilterSheet: View {
|
|||||||
Picker("filter.resolution", selection: $draft.resolution) {
|
Picker("filter.resolution", selection: $draft.resolution) {
|
||||||
Text("filter.any").tag(String?.none)
|
Text("filter.any").tag(String?.none)
|
||||||
Text("4K").tag(String?.some("4k"))
|
Text("4K").tag(String?.some("4k"))
|
||||||
Text("1080p").tag(String?.some("1080"))
|
Text("Full HD").tag(String?.some("1080"))
|
||||||
Text("720p").tag(String?.some("720"))
|
Text("HD").tag(String?.some("720"))
|
||||||
Text("SD").tag(String?.some("sd"))
|
Text("SD").tag(String?.some("sd"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,10 @@ public func isInLocalNetwork() async -> Bool {
|
|||||||
var total = page.totalRecordCount ?? mapped.count
|
var total = page.totalRecordCount ?? mapped.count
|
||||||
|
|
||||||
if query.resolution?.lowercased() == "4k" {
|
if query.resolution?.lowercased() == "4k" {
|
||||||
mapped = mapped.filter { ($0.preferredVersion?.pixelHeight ?? 0) >= 1_600 }
|
// Nach der Stufe, nicht nach der Zeilenzahl: ein breitwandiges
|
||||||
|
// 4K misst 3840 × 1608 und fiele durch jede Höhenschwelle,
|
||||||
|
// die 2160 erwartet.
|
||||||
|
mapped = mapped.filter { ($0.preferredVersion?.resolution ?? .sd) >= .uhd4K }
|
||||||
// Die Gesamtzahl stimmt dann nicht mehr — lieber die gefilterte
|
// Die Gesamtzahl stimmt dann nicht mehr — lieber die gefilterte
|
||||||
// Menge als eine Zahl, die zum Nachladen auffordert, obwohl nichts
|
// Menge als eine Zahl, die zum Nachladen auffordert, obwohl nichts
|
||||||
// mehr kommt.
|
// mehr kommt.
|
||||||
|
|||||||
@@ -51,32 +51,22 @@ public struct MediaVersion: Sendable, Equatable, Identifiable {
|
|||||||
/// Danach richtet sich, welche Qualitätsstufen überhaupt Sinn ergeben —
|
/// Danach richtet sich, welche Qualitätsstufen überhaupt Sinn ergeben —
|
||||||
/// eine 720p-Datei auf 1080p hochzurechnen bringt kein Bild dazu.
|
/// eine 720p-Datei auf 1080p hochzurechnen bringt kein Bild dazu.
|
||||||
public var pixelHeight: Int? {
|
public var pixelHeight: Int? {
|
||||||
if let height { return height }
|
if let height, height > 0 { return height }
|
||||||
switch videoResolution?.lowercased() {
|
return resolution?.nominalHeight
|
||||||
case "4k": return 2_160
|
|
||||||
case "1080": return 1_080
|
|
||||||
case "720": return 720
|
|
||||||
case "480": return 480
|
|
||||||
case "sd": return 480
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Die Auflösung als Wort — „4K", „1080p".
|
/// Die Stufe, in der diese Fassung vorliegt.
|
||||||
///
|
public var resolution: VideoResolution? {
|
||||||
/// Aus der Bildhöhe, nicht aus dem Namen des Servers: Plex sagt „4k",
|
VideoResolution(width: width, height: height, serverLabel: videoResolution)
|
||||||
/// Jellyfin gibt Zahlen. Gemeint ist dasselbe.
|
|
||||||
public var resolutionLabel: String? {
|
|
||||||
guard let pixelHeight else { return videoResolution?.uppercased() }
|
|
||||||
switch pixelHeight {
|
|
||||||
case 1_600...: return "4K"
|
|
||||||
case 1_000..<1_600: return "1080p"
|
|
||||||
case 700..<1_000: return "720p"
|
|
||||||
case 1..<700: return "SD"
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
/// Eine kurze Beschreibung für die Versionsauswahl, etwa
|
||||||
/// „4K · HEVC · 38 Mbit/s".
|
/// „4K · HEVC · 38 Mbit/s".
|
||||||
public var qualityLabel: String {
|
public var qualityLabel: String {
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import MediaKit
|
||||||
|
|
||||||
/// Eine Datei auf der Platte des Servers.
|
/// Eine Datei auf der Platte des Servers.
|
||||||
///
|
///
|
||||||
@@ -144,23 +145,19 @@ public struct PlexMedia: Sendable, Equatable, Codable, Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Die Auflösung in der Schreibweise, die Menschen erwarten.
|
/// Die Stufe, in der diese Fassung vorliegt.
|
||||||
public var resolutionLabel: String? {
|
public var resolution: VideoResolution? {
|
||||||
guard let pixelHeight else { return videoResolution?.uppercased() }
|
VideoResolution(width: width, height: height, serverLabel: videoResolution)
|
||||||
switch pixelHeight {
|
|
||||||
case 1_600...: return "4K"
|
|
||||||
case 1_000..<1_600: return "1080p"
|
|
||||||
case 700..<1_000: return "720p"
|
|
||||||
case 1..<700: return "SD"
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Die Auflösung in der Schreibweise, die Menschen erwarten.
|
||||||
|
public var resolutionLabel: String? { resolution?.name }
|
||||||
|
|
||||||
/// Eine kurze Beschreibung für die Versionsauswahl auf der Detailseite,
|
/// Eine kurze Beschreibung für die Versionsauswahl auf der Detailseite,
|
||||||
/// etwa „4K · HEVC · 38,2 Mbit/s".
|
/// etwa „4K · HEVC · 38,2 Mbit/s".
|
||||||
public var qualityLabel: String {
|
public var qualityLabel: String {
|
||||||
var pieces: [String] = []
|
var pieces: [String] = []
|
||||||
if let videoResolution { pieces.append(videoResolution.uppercased()) }
|
if let label = resolutionLabel { pieces.append(label) }
|
||||||
if let videoCodec { pieces.append(videoCodec.uppercased()) }
|
if let videoCodec { pieces.append(videoCodec.uppercased()) }
|
||||||
if let bitrate { pieces.append("\(bitrate / 1000) Mbit/s") }
|
if let bitrate { pieces.append("\(bitrate / 1000) Mbit/s") }
|
||||||
return pieces.joined(separator: " · ")
|
return pieces.joined(separator: " · ")
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import Testing
|
||||||
|
@testable import MediaKit
|
||||||
|
|
||||||
|
@Suite("Auflösung einstufen")
|
||||||
|
struct VideoResolutionTests {
|
||||||
|
|
||||||
|
// MARK: - Nach der Breite
|
||||||
|
|
||||||
|
@Test("Die üblichen Breiten")
|
||||||
|
func commonWidths() {
|
||||||
|
#expect(VideoResolution(width: 7_680, height: 4_320) == .uhd8K)
|
||||||
|
#expect(VideoResolution(width: 3_840, height: 2_160) == .uhd4K)
|
||||||
|
#expect(VideoResolution(width: 4_096, height: 2_160) == .uhd4K)
|
||||||
|
#expect(VideoResolution(width: 2_560, height: 1_440) == .qhd)
|
||||||
|
#expect(VideoResolution(width: 1_920, height: 1_080) == .fullHD)
|
||||||
|
#expect(VideoResolution(width: 1_280, height: 720) == .hd)
|
||||||
|
#expect(VideoResolution(width: 720, height: 576) == .sd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Der Fall, wegen dem die Breite zuerst kommt: ein Kinofilm in 2,39:1 hat
|
||||||
|
/// 804 Zeilen. Nach der Höhe wäre das „HD" — und das stand bisher an
|
||||||
|
/// ausgerechnet den größten Filmen.
|
||||||
|
@Test("Breitwand bleibt Full HD, nicht HD")
|
||||||
|
func letterboxedStaysFullHD() {
|
||||||
|
#expect(VideoResolution(width: 1_920, height: 804) == .fullHD)
|
||||||
|
#expect(VideoResolution(width: 1_920, height: 800) == .fullHD)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Breitwandiges 4K bleibt 4K")
|
||||||
|
func letterboxed4K() {
|
||||||
|
#expect(VideoResolution(width: 3_840, height: 1_608) == .uhd4K)
|
||||||
|
#expect(VideoResolution(width: 3_840, height: 1_392) == .uhd4K)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Nach der Höhe, wenn es die Breite nicht gibt
|
||||||
|
|
||||||
|
@Test("Ohne Breite entscheidet die Höhe")
|
||||||
|
func fallsBackToHeight() {
|
||||||
|
#expect(VideoResolution(width: nil, height: 2_160) == .uhd4K)
|
||||||
|
#expect(VideoResolution(width: nil, height: 1_080) == .fullHD)
|
||||||
|
#expect(VideoResolution(width: nil, height: 720) == .hd)
|
||||||
|
#expect(VideoResolution(width: nil, height: 480) == .sd)
|
||||||
|
#expect(VideoResolution(width: 0, height: 1_080) == .fullHD)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Nach dem Namen des Servers
|
||||||
|
|
||||||
|
@Test("Was Plex hinschreibt")
|
||||||
|
func plexLabels() {
|
||||||
|
#expect(VideoResolution(width: nil, height: nil, serverLabel: "4k") == .uhd4K)
|
||||||
|
#expect(VideoResolution(width: nil, height: nil, serverLabel: "1080") == .fullHD)
|
||||||
|
#expect(VideoResolution(width: nil, height: nil, serverLabel: "720") == .hd)
|
||||||
|
#expect(VideoResolution(width: nil, height: nil, serverLabel: "sd") == .sd)
|
||||||
|
#expect(VideoResolution(width: nil, height: nil, serverLabel: "576") == .sd)
|
||||||
|
#expect(VideoResolution(width: nil, height: nil, serverLabel: "4K") == .uhd4K)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Zeilensprung wird als Zahl gelesen")
|
||||||
|
func interlacedLabel() {
|
||||||
|
#expect(VideoResolution(width: nil, height: nil, serverLabel: "1080i") == .fullHD)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Ohne jede Angabe keine erfundene Stufe")
|
||||||
|
func nothingIsNothing() {
|
||||||
|
#expect(VideoResolution(width: nil, height: nil) == nil)
|
||||||
|
#expect(VideoResolution(width: nil, height: nil, serverLabel: "") == nil)
|
||||||
|
#expect(VideoResolution(width: nil, height: nil, serverLabel: "irgendwas") == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Die Namen
|
||||||
|
|
||||||
|
@Test("Die Namen, die dastehen sollen")
|
||||||
|
func names() {
|
||||||
|
#expect(VideoResolution.uhd4K.name == "4K")
|
||||||
|
#expect(VideoResolution.fullHD.name == "Full HD")
|
||||||
|
#expect(VideoResolution.hd.name == "HD")
|
||||||
|
#expect(VideoResolution.sd.name == "SD")
|
||||||
|
#expect(VideoResolution.fullHD.technicalName == "1080p")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gebraucht für „größer als 4K" in Filtern.
|
||||||
|
@Test("Die Stufen lassen sich vergleichen")
|
||||||
|
func ordered() {
|
||||||
|
#expect(VideoResolution.sd < .hd)
|
||||||
|
#expect(VideoResolution.fullHD < .uhd4K)
|
||||||
|
#expect(VideoResolution.uhd8K > .uhd4K)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - An der Fassung
|
||||||
|
|
||||||
|
@Test("Die Fassung sagt Full HD statt 1080")
|
||||||
|
func versionSpeaksPlainly() {
|
||||||
|
let fassung = MediaVersion(
|
||||||
|
id: "1", videoResolution: "1080", width: 1_920, height: 804
|
||||||
|
)
|
||||||
|
#expect(fassung.resolutionLabel == "Full HD")
|
||||||
|
#expect(fassung.qualityLabel.hasPrefix("Full HD"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ohne Zahlen bleibt die Angabe des Servers die einzige Quelle — sie darf
|
||||||
|
/// dann nicht verlorengehen.
|
||||||
|
@Test("Auch ohne Maße kommt ein Name heraus")
|
||||||
|
func labelWithoutNumbers() {
|
||||||
|
#expect(MediaVersion(id: "1", videoResolution: "4k").resolutionLabel == "4K")
|
||||||
|
#expect(MediaVersion(id: "1", videoResolution: "4k").pixelHeight == 2_160)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user