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:
co-authored by
Claude Opus 5
parent
2fcf6821c8
commit
4b9a1e7b67
@@ -0,0 +1,263 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
import MediaKit
|
||||
@testable import HaloPlayback
|
||||
|
||||
@Suite("Sprachkürzel")
|
||||
struct LanguageCodeTests {
|
||||
/// Der Fall, an dem die alte Prüfung lautlos scheiterte: `ger` ist die
|
||||
/// ISO-639-2/B-Form von Deutsch, und ffmpeg schreibt sie in viele Dateien.
|
||||
@Test("ger, deu und de-DE meinen alle Deutsch")
|
||||
func germanHasThreeSpellings() {
|
||||
#expect(LanguageCode.matches("ger", "de"))
|
||||
#expect(LanguageCode.matches("deu", "de"))
|
||||
#expect(LanguageCode.matches("de-DE", "de"))
|
||||
#expect(LanguageCode.matches("GER", "de"))
|
||||
}
|
||||
|
||||
/// Schweizerdeutsch ist keine Schreibweise von Deutsch. Es gleichzusetzen
|
||||
/// wäre bequem und falsch — wer Hochdeutsch will, bekäme sonst
|
||||
/// Schweizerdeutsch untergeschoben.
|
||||
@Test("Schweizerdeutsch ist nicht Deutsch")
|
||||
func swissGermanStandsAlone() {
|
||||
#expect(!LanguageCode.matches("gsw", "de"))
|
||||
#expect(LanguageCode.matches("gsw", "gsw"))
|
||||
}
|
||||
|
||||
@Test("Auch die anderen Doppelformen")
|
||||
func otherDoubleForms() {
|
||||
#expect(LanguageCode.matches("fre", "fra"))
|
||||
#expect(LanguageCode.matches("dut", "nl"))
|
||||
#expect(LanguageCode.matches("cze", "cs"))
|
||||
#expect(LanguageCode.matches("chi", "zh"))
|
||||
}
|
||||
|
||||
@Test("Ohne Angabe kein Treffer")
|
||||
func missingIsNoMatch() {
|
||||
#expect(!LanguageCode.matches(nil, "de"))
|
||||
#expect(!LanguageCode.matches("de", nil))
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Wahl der Tonspur")
|
||||
struct AudioTrackChooserTests {
|
||||
private func spur(
|
||||
_ index: Int,
|
||||
_ sprache: String?,
|
||||
title: String? = nil,
|
||||
channels: Int = 2,
|
||||
isDefault: Bool = false
|
||||
) -> MediaStream {
|
||||
MediaStream(
|
||||
id: String(index),
|
||||
kind: .audio,
|
||||
codec: "aac",
|
||||
language: sprache,
|
||||
languageCode: sprache,
|
||||
displayTitle: title,
|
||||
channels: channels,
|
||||
title: title,
|
||||
isDefault: isDefault
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Der ZDF-Fall
|
||||
|
||||
/// Wörtlich der Ärger, wegen dem es den Typ gibt: die als deutsch
|
||||
/// ausgezeichnete Spur ist die mit Audiodeskription, und die als englisch
|
||||
/// ausgezeichnete ist in Wahrheit die deutsche ohne.
|
||||
private var zdf: [MediaStream] {
|
||||
[
|
||||
spur(0, "ger", title: "Deutsch (Audiodeskription)", isDefault: true),
|
||||
spur(1, "eng", title: "Deutsch"),
|
||||
]
|
||||
}
|
||||
|
||||
@Test("Audiodeskription wird gemieden, auch wenn sie Standard ist")
|
||||
func avoidsAudioDescription() {
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: zdf,
|
||||
wish: .init(language: "de")
|
||||
)
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
/// Ohne das Meiden gewinnt die ausgezeichnete Sprache — der Beweis, dass
|
||||
/// der Schalter tatsächlich das Ruder herumreißt und nicht nur dabeisteht.
|
||||
@Test("Ohne das Meiden gewinnt das Kürzel")
|
||||
func withoutAvoidanceTheCodeWins() {
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: zdf,
|
||||
wish: .init(language: "de", avoidsAudioDescription: false)
|
||||
)
|
||||
#expect(gewählt?.id == "0")
|
||||
}
|
||||
|
||||
/// Und wenn der Nutzer einmal gewählt hat, gilt das — auch gegen die
|
||||
/// Sprache.
|
||||
@Test("Die gemerkte Spur schlägt den Sprachwunsch")
|
||||
func rememberedBeatsLanguage() {
|
||||
let gemerkt = AudioTrackChooser.TrackSignature(zdf[1])
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: zdf,
|
||||
wish: .init(language: "de", remembered: gemerkt, avoidsAudioDescription: false)
|
||||
)
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
// MARK: - Die Rangliste
|
||||
|
||||
@Test("Fehlt die Wunschsprache, greift die Rangliste")
|
||||
func fallbackListApplies() {
|
||||
let spuren = [spur(0, "fre"), spur(1, "eng"), spur(2, "ita")]
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: spuren,
|
||||
wish: .init(language: "de", fallbacks: ["gsw", "en", "fr"])
|
||||
)
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
@Test("Die Reihenfolge der Rangliste zählt")
|
||||
func fallbackOrderMatters() {
|
||||
let spuren = [spur(0, "fre"), spur(1, "eng")]
|
||||
let zuerstFranzösisch = AudioTrackChooser.choose(
|
||||
from: spuren,
|
||||
wish: .init(language: "de", fallbacks: ["fr", "en"])
|
||||
)
|
||||
#expect(zuerstFranzösisch?.id == "0")
|
||||
}
|
||||
|
||||
@Test("Schweizerdeutsch lässt sich in die Rangliste stellen")
|
||||
func swissGermanInTheList() {
|
||||
let spuren = [spur(0, "eng"), spur(1, "gsw")]
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: spuren,
|
||||
wish: .init(language: "de", fallbacks: ["gsw", "en"])
|
||||
)
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
// MARK: - Der Boden darunter
|
||||
|
||||
@Test("Ohne jede Übereinstimmung kommt trotzdem eine Spur")
|
||||
func alwaysAnswers() {
|
||||
let spuren = [spur(0, "jpn"), spur(1, "kor", isDefault: true)]
|
||||
let gewählt = AudioTrackChooser.choose(
|
||||
from: spuren,
|
||||
wish: .init(language: "de", fallbacks: ["en"])
|
||||
)
|
||||
// Die Standardspur — nachvollziehbar statt beliebig.
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
@Test("Gibt es die Sprache nur mit Audiodeskription, ist sie besser als nichts")
|
||||
func audioDescriptionBeatsSilence() {
|
||||
let spuren = [spur(0, "ger", title: "Deutsch (Audiodeskription)")]
|
||||
let gewählt = AudioTrackChooser.choose(from: spuren, wish: .init(language: "de"))
|
||||
#expect(gewählt?.id == "0")
|
||||
}
|
||||
|
||||
@Test("Ohne Tonspuren gibt es nichts zu wählen")
|
||||
func noAudioNoChoice() {
|
||||
let nurBild = [MediaStream(id: "0", kind: .video)]
|
||||
#expect(AudioTrackChooser.choose(from: nurBild, wish: .init()) == nil)
|
||||
}
|
||||
|
||||
@Test("Kommentarspuren werden gemieden")
|
||||
func avoidsCommentary() {
|
||||
let spuren = [
|
||||
spur(0, "ger", title: "Audiokommentar des Regisseurs", isDefault: true),
|
||||
spur(1, "ger", title: "Deutsch"),
|
||||
]
|
||||
let gewählt = AudioTrackChooser.choose(from: spuren, wish: .init(language: "de"))
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
/// „AD" als eigenes Wort, nicht als Teil von „Broadcast".
|
||||
@Test("AD erkennt nur das freistehende Wort")
|
||||
func adOnlyAsWholeWord() {
|
||||
let spuren = [
|
||||
spur(0, "ger", title: "Deutsch Broadcast Mix"),
|
||||
spur(1, "ger", title: "Deutsch AD"),
|
||||
]
|
||||
let gewählt = AudioTrackChooser.choose(from: spuren, wish: .init(language: "de"))
|
||||
#expect(gewählt?.id == "0")
|
||||
}
|
||||
|
||||
@Test("Mehr Kanäle gewinnen bei sonst gleichem Stand")
|
||||
func moreChannelsWin() {
|
||||
let spuren = [spur(0, "ger", title: "Deutsch", channels: 2),
|
||||
spur(1, "ger", title: "Deutsch", channels: 6)]
|
||||
let gewählt = AudioTrackChooser.choose(from: spuren, wish: .init(language: "de"))
|
||||
#expect(gewählt?.id == "1")
|
||||
}
|
||||
|
||||
/// Zweimal dieselbe Datei muss zweimal dasselbe ergeben — sonst ist ein
|
||||
/// Fehler nicht wiederholbar.
|
||||
@Test("Gleichstand entscheidet die Nummer")
|
||||
func tiesAreStable() {
|
||||
let spuren = [spur(1, "ger", title: "Deutsch"), spur(0, "ger", title: "Deutsch")]
|
||||
#expect(AudioTrackChooser.choose(from: spuren, wish: .init(language: "de"))?.id == "0")
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Rangfolge der drei Ebenen")
|
||||
struct WishResolutionTests {
|
||||
/// Serie schlägt Mediathek schlägt allgemein. Das ist der ganze Zweck der
|
||||
/// Abstufung — und der einzige Ort, an dem sie steht.
|
||||
@Test("Die Serie gewinnt")
|
||||
func seriesWins() {
|
||||
let wunsch = AudioTrackChooser.resolve(series: "en", library: "fr", general: "de")
|
||||
#expect(wunsch.language == "en")
|
||||
}
|
||||
|
||||
@Test("Ohne Serie gewinnt die Mediathek")
|
||||
func libraryWinsWithoutSeries() {
|
||||
#expect(AudioTrackChooser.resolve(series: nil, library: "fr", general: "de").language == "fr")
|
||||
}
|
||||
|
||||
@Test("Ohne beides gilt die allgemeine")
|
||||
func generalIsTheFloor() {
|
||||
#expect(AudioTrackChooser.resolve(series: nil, library: nil, general: "de").language == "de")
|
||||
}
|
||||
|
||||
/// In den Einstellungen steht eine leere Zeichenkette für „soll der Server
|
||||
/// entscheiden". Sie als Sprache zu behandeln hiesse, die Ebene darüber
|
||||
/// stillschweigend abzuschalten — genau der Fehler, den man erst bemerkt,
|
||||
/// wenn eine Einstellung scheinbar wirkungslos ist.
|
||||
@Test("Leer heisst nichts gesagt, nicht eine Sprache namens leer")
|
||||
func emptyIsNotAnAnswer() {
|
||||
#expect(AudioTrackChooser.resolve(series: "", library: "fr", general: "de").language == "fr")
|
||||
#expect(AudioTrackChooser.resolve(series: " ", library: "", general: "de").language == "de")
|
||||
#expect(AudioTrackChooser.resolve(series: nil, library: nil, general: "").language == nil)
|
||||
}
|
||||
|
||||
/// Die Rangliste ist keine vierte Ebene: sie gilt überall gleich und wird
|
||||
/// von keiner Ebene überstimmt.
|
||||
@Test("Rangliste und Schalter gehen unverändert durch")
|
||||
func theRestPassesThrough() {
|
||||
let wunsch = AudioTrackChooser.resolve(
|
||||
series: nil, library: nil, general: "de",
|
||||
fallbacks: ["gsw", "en"], avoidsAudioDescription: false
|
||||
)
|
||||
#expect(wunsch.fallbacks == ["gsw", "en"])
|
||||
#expect(wunsch.avoidsAudioDescription == false)
|
||||
}
|
||||
|
||||
/// Der ZDF-Fall durch die ganze Kette: die Mediathek sagt Deutsch, die
|
||||
/// gemerkte Spur sagt etwas anderes — und behält recht.
|
||||
@Test("Die gemerkte Spur überlebt die Rangfolge")
|
||||
func rememberedSurvives() {
|
||||
let spuren = [
|
||||
MediaStream(id: "0", kind: .audio, language: "ger", languageCode: "ger",
|
||||
channels: 2, title: "Deutsch (Audiodeskription)", isDefault: true),
|
||||
MediaStream(id: "1", kind: .audio, language: "eng", languageCode: "eng",
|
||||
channels: 2, title: "Deutsch"),
|
||||
]
|
||||
let wunsch = AudioTrackChooser.resolve(
|
||||
series: nil, library: "de", general: "en",
|
||||
remembered: AudioTrackChooser.TrackSignature(spuren[1])
|
||||
)
|
||||
#expect(AudioTrackChooser.choose(from: spuren, wish: wunsch)?.id == "1")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user