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>
77 lines
3.2 KiB
Swift
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")
|
|
}
|
|
}
|