Files
Flaro/Packages/FlixyKit/Tests/FlixyPlaybackTests/AudioTrackChooserTests.swift
T
ScarriffleandClaude Opus 5 983015b9ad Die Sprache lässt sich jetzt auch je Serie und je Mediathek sagen
Das Regelwerk stand schon und wurde abgefragt — es gab nur keinen Ort, an
dem man etwas hineinschreiben konnte. Jetzt steht die Wahl auf der
Serienseite und im Filter-Sheet der Mediathek, beide über denselben
Auswahlknopf: zwei mit demselben Inhalt liefen unweigerlich auseinander.

Bei der Mediathek in einem eigenen Abschnitt, nicht zwischen den Filtern.
Ein Filter schränkt ein, was zu sehen ist; das hier bestimmt, was zu hören
ist — dazwischengestellt läse es sich als „nur Titel mit deutscher Tonspur
zeigen".

Die Rangfolge Serie → Mediathek → allgemein ist dabei von den Einstellungen
zu `AudioTrackChooser.resolve` gewandert. Sie ist eine Regel, keine Ablage,
und im App-Ziel gibt es kein Unit-Test-Ziel — dort liesse sie sich nicht
durchspielen. Sechs Tests decken sie jetzt ab, darunter der Fall, an dem
man sonst lange sucht: eine leere Zeichenkette steht in den Einstellungen
für „soll der Server entscheiden" und darf nicht als Sprache gelten, sonst
schaltet sie die Ebene darüber stillschweigend ab.

Nebenbei bekommt das Filter-Sheet die Mediathek selbst statt nur ihrer Art.
Die Genres wurden vorher über irgendeine Mediathek dieser Art nachgeschlagen
— bei zwei Filmmediatheken also über die falsche.

Und der Puffer steht auf einer Minute statt auf 25 Sekunden. Es ist ein
Mindestwunsch, keine Obergrenze; der harte Deckel liegt ohnehin woanders,
nämlich darin, wie weit der Transcoder schon produziert hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 17:12:17 +02:00

264 lines
9.9 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")
}
}
@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")
}
}