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>
This commit is contained in:
Scarriffle
2026-10-03 11:12:32 +02:00
co-authored by Claude Opus 5
parent 2fcf6821c8
commit 4b9a1e7b67
280 changed files with 1456 additions and 1456 deletions
@@ -0,0 +1,65 @@
import Foundation
/// Macht aus dem, was jemand eintippt, eine Adresse, die antwortet.
///
/// Jellyfin hat kein Verzeichnis wie plex.tv — die Adresse kommt vom Nutzer.
/// Und die tippt niemand vollständig: „192.168.1.50" ohne Schema, ohne Port,
/// oder „medien.example.com" ohne „https://". Dieser Typ rät nicht **eine**
/// Ergänzung, sondern nennt die plausiblen in der Reihenfolge, in der sie es
/// wahrscheinlich sind; der Aufrufer probiert sie durch und nimmt die erste,
/// die sich als Jellyfin-Server ausweist.
///
/// Die Reihenfolge ist der ganze Inhalt dieser Datei, und sie ist nicht
/// beliebig: eine IP-Adresse im eigenen Netz spricht fast nie HTTPS, ein
/// Domänenname fast immer.
public enum JellyfinAddress {
/// Der Standardport eines Jellyfin-Servers.
public static let defaultPort = 8096
public static func candidates(for input: String) -> [URL] {
var text = input.trimmingCharacters(in: .whitespacesAndNewlines)
while text.hasSuffix("/") { text.removeLast() }
guard !text.isEmpty else { return [] }
// Wer ein Schema hinschreibt, meint es auch so. Daran wird nichts
// ergänzt — sonst landet eine Anfrage unverschlüsselt, obwohl jemand
// ausdrücklich „https" getippt hat.
if text.contains("://") {
return URL(string: text).map { [$0] } ?? []
}
// IPv6 in eckigen Klammern: hier wird nicht geraten, weil die
// Doppelpunkte sonst als Port gelesen würden.
if text.hasPrefix("[") {
return ["http://\(text)", "https://\(text)"].compactMap(URL.init(string:))
}
let authority = String(text.split(separator: "/", maxSplits: 1).first ?? "")
let host = String(authority.split(separator: ":").first ?? "")
let hasExplicitPort = authority.contains(":")
&& Int(authority.split(separator: ":").last ?? "") != nil
if hasExplicitPort {
// Port genannt, Schema nicht: erst unverschlüsselt, weil ein
// eigener Port fast immer ein Server ohne Zertifikat ist.
return ["http://\(text)", "https://\(text)"].compactMap(URL.init(string:))
}
// Eine reine Zahlenadresse, ein Bonjour-Name oder ein Wort ohne Punkt
// — alles Dinge, die es nur im eigenen Netz gibt.
let isLocalLooking = host.allSatisfy { $0.isNumber || $0 == "." }
|| host.hasSuffix(".local")
|| !host.contains(".")
let forms = isLocalLooking
? ["http://\(withDefaultPort(text))", "http://\(text)", "https://\(text)"]
: ["https://\(text)", "http://\(text)", "http://\(withDefaultPort(text))"]
return forms.compactMap(URL.init(string:))
}
/// Hängt den Standardport an — vor einen etwaigen Pfad, nicht dahinter.
private static func withDefaultPort(_ text: String) -> String {
guard let slash = text.firstIndex(of: "/") else { return "\(text):\(defaultPort)" }
return "\(text[text.startIndex..<slash]):\(defaultPort)\(text[slash...])"
}
}