Files
Flaro/Packages/HaloKit/Sources/MediaKit/VideoResolution.swift
T
ScarriffleandClaude Opus 5 4b9a1e7b67 Flixy heißt ab hier Halo
Reine Umbenennung, kein Verhalten geändert — bewusst als eigener Commit,
damit „umbenannt" und „umgebaut" im Nachhinein auseinanderzuhalten sind.

712 mal `Flixy`, 778 mal `flixy`, 7 mal `FLIXY` in 169 Dateien, dazu 25
Pfade. Ziele: `Halo`, `HaloTV`, `HaloTopShelf`, `HaloUITests`, `HaloKit` mit
`HaloDesign`, `HaloDownloads`, `HaloPlayback` und `HaloProbe`. `MediaKit`,
`JellyfinKit` und `DownloadWidget` tragen den Namen nicht und bleiben.

`docs/superpowers/specs/` ist ausgenommen. Datierte Entwurfsdokumente sind
ein Protokoll getroffener Entscheidungen; sie nachträglich umzuschreiben
hiesse, die Vergangenheit zu glätten.

**Die Bundle-ID wandert mit** — ausdrücklich so gewollt, mit bekanntem Preis:
`com.scarriffleservices.halo` ist für iOS eine *andere* App. Die installierte
Fassung bleibt daneben stehen, mit Downloads und Anmeldung in ihrem Container;
Halo startet leer. Einmal neu anmelden, heruntergeladene Folgen einmal neu
laden. Dasselbe gilt für die App-Gruppe, die sich TV-App und Top Shelf teilen.

**Eine Folge davon, die beim Entscheiden noch nicht auf dem Tisch lag:** der
Aufräumschritt aus dem vorigen Commit kommt an die alten Plex-Token nicht mehr
heran. Der Schlüsselbund grenzt nach Team und Bundle-Kennung ab — was unter
`…flixy` liegt, ist für `…halo` unerreichbar. Erledigt wird es trotzdem, nur
anders: **die alte App löschen.** iOS räumt deren Schlüsselbund-Einträge beim
Deinstallieren mit ab. Der Code bleibt stehen, weil er für jeden Stand vor
dieser Umbenennung richtig ist und im Jahr einen Startvergleich kostet.

Die Projektdatei ist aus `project.yml` neu erzeugt, der alte Ordner gelöscht
statt umbenannt — sie steht ohnehin nicht im Repo.

Das Verzeichnis des Repos selbst heißt weiter `Flixy`; das umzubenennen ist
ein Handgriff ausserhalb des Projekts.

368 Tests grün. iOS und tvOS bauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 11:12:32 +02:00

136 lines
4.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}