Files
Flaro/Packages/HaloKit/Tests/JellyfinKitTests/AdaptiveURLTests.swift
T
ScarriffleandClaude Opus 5 4b9a1e7b67 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>
2026-10-03 11:12:32 +02:00

77 lines
3.2 KiB
Swift

import Testing
import Foundation
import MediaKit
@testable import JellyfinKit
@Suite("Leiter bestellen")
struct AdaptiveURLTests {
private func url(_ text: String) -> URL { URL(string: text)! }
private func werte(_ url: URL, _ name: String) -> [String] {
(URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? [])
.filter { $0.name.lowercased() == name.lowercased() }
.compactMap(\.value)
}
/// `main.m3u8` ist die Liste der Schnipsel **einer** Stufe. Auf ihr kann
/// der Parameter nichts bewirken — es gibt dort keine Varianten.
@Test("Aus der Schnipselliste wird die Stufenliste")
func mainBecomesMaster() {
let ergebnis = JellyfinBackend.adaptive(url("http://h/videos/abc/main.m3u8?Tag=1"))
#expect(ergebnis.lastPathComponent == "master.m3u8")
// Die übrige Abfrage bleibt unangetastet.
#expect(werte(ergebnis, "Tag") == ["1"])
}
@Test("Eine Stufenliste bleibt, wie sie ist")
func masterStays() {
let ergebnis = JellyfinBackend.adaptive(url("http://h/videos/abc/master.m3u8"))
#expect(ergebnis.lastPathComponent == "master.m3u8")
}
/// Die Falle, an der `maxHeight` schon einmal hängen geblieben ist: bei
/// zwei gleichnamigen Werten gilt beim Server der erste. Ein
/// danebengestellter zweiter bewirkt nichts.
@Test("Der Parameter steht genau einmal da")
func parameterAppearsOnce() {
let ergebnis = JellyfinBackend.adaptive(
url("http://h/videos/abc/master.m3u8?enableAdaptiveBitrateStreaming=false&Tag=1")
)
#expect(werte(ergebnis, "enableAdaptiveBitrateStreaming") == ["true"])
#expect(werte(ergebnis, "Tag") == ["1"])
}
@Test("Ohne vorhandenen Wert kommt er dazu")
func parameterIsAdded() {
let ergebnis = JellyfinBackend.adaptive(url("http://h/videos/abc/master.m3u8?Tag=1"))
#expect(werte(ergebnis, "enableAdaptiveBitrateStreaming") == ["true"])
}
/// Groß- und Kleinschreibung ist beim Server gleichgültig — beim
/// Herausfiltern darf sie es nicht sein, sonst steht er doch zweimal da.
@Test("Auch anders geschrieben wird er ersetzt")
func parameterIsReplacedRegardlessOfCase() {
let ergebnis = JellyfinBackend.adaptive(
url("http://h/videos/abc/master.m3u8?EnableAdaptiveBitrateStreaming=false")
)
let alle = (URLComponents(url: ergebnis, resolvingAgainstBaseURL: false)?.queryItems ?? [])
.filter { $0.name.lowercased() == "enableadaptivebitratestreaming" }
#expect(alle.count == 1)
#expect(alle.first?.value == "true")
}
/// Die Höhe gehört weiterhin dran: die Leiter ist die Abstufung, die
/// Obergrenze bleibt unsere.
@Test("Höhenbegrenzung und Leiter vertragen sich")
func heightAndLadderCoexist() {
let begrenzt = JellyfinBackend.limiting(
height: "1280x720",
in: url("http://h/videos/abc/main.m3u8")
)
let ergebnis = JellyfinBackend.adaptive(begrenzt)
#expect(werte(ergebnis, "maxHeight") == ["720"])
#expect(werte(ergebnis, "enableAdaptiveBitrateStreaming") == ["true"])
#expect(ergebnis.lastPathComponent == "master.m3u8")
}
}