Files
ScarriffleandClaude Opus 5 a7cf6c5def 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>
2026-10-03 19:38:27 +02:00

264 lines
9.9 KiB
Swift

import Testing
import Foundation
import MediaKit
@testable import FlaroPlayback
@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")
}
}