Halo heisst ab hier Flaro — die Marke stand im Weg

Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine
eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt
so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner
Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging
daneben.

**Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den
deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche
Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in
Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store
der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden
frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler.

Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen
ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim
letzten Mal stattfand.

---

2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der
Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`,
`Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen
`FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und
das Image.

**Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen
Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes
Ersetzen hätte den sonst zerlegt.

`docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte
Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das
Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden
in die Irre führt.

**Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag:
`com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat
jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe.

308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS
bauen, das Image baut, und ein frischer Container läuft durch Einrichtung
und Anmeldung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-03 19:38:27 +02:00
co-authored by Claude Opus 5
parent f98b995050
commit a7cf6c5def
390 changed files with 2078 additions and 2069 deletions
@@ -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)
}
}