**`LanguageCode`** bringt Kürzel auf eine vergleichbare Form. Der Anlass ist ein stiller Fehler: verglichen wurden die ersten zwei Zeichen, also `ge` gegen `de` — und `ger` ist die ISO-639-2/B-Form von Deutsch, die ffmpeg in viele Dateien schreibt. Bei solchen Titeln wirkte die Spracheinstellung überhaupt nicht. `gsw` bleibt `gsw`. Schweizerdeutsch ist keine Schreibweise von Deutsch, und es gleichzusetzen hiesse, jemandem Schweizerdeutsch unterzuschieben, der Hochdeutsch wollte. Es gehört in die Rangliste, nicht in eine Gleichsetzung. **`AudioTrackChooser`** wägt ab, statt zu filtern: jede Spur bekommt Punkte, die höchste gewinnt. Damit gibt es immer eine Antwort, und Audiodeskription wird gemieden, ohne unmöglich zu werden — gibt es eine Sprache nur mit Audiodeskription, ist sie besser als Stille. Gemerkt wird nicht eine Nummer, sondern eine Beschreibung der Spur: Folgen derselben Serie haben unterschiedlich sortierte Spuren, und „Spur 2" bedeutet in der nächsten Folge etwas anderes. Siebzehn Tests, darunter der ZDF-Fall wörtlich — deutsche Spur mit Audiodeskription als Standard, englisch betitelte deutsche Spur daneben — und zwar in beide Richtungen: mit Meiden gewinnt die richtige, ohne Meiden gewinnt das Kürzel. Sonst stünde der Schalter nur dabei. Dazu trägt `MediaStream` jetzt `title`, `isDefault`, `isHearingImpaired` und `isOriginal`. Der Name aus der Datei ist das zweite Signal neben dem Kürzel und manchmal das einzige richtige. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
203 lines
7.1 KiB
Swift
203 lines
7.1 KiB
Swift
import Testing
|
|
import Foundation
|
|
import MediaKit
|
|
@testable import FlixyPlayback
|
|
|
|
@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")
|
|
}
|
|
}
|