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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2fcf6821c8
commit
4b9a1e7b67
@@ -0,0 +1,132 @@
|
||||
import Foundation
|
||||
|
||||
/// Wann der **teure** Weg gegangen werden darf.
|
||||
///
|
||||
/// Der billige Hebel ist `preferredPeakBitRate`: er wirkt ohne Neuaufbau.
|
||||
/// Wenn er nichts ausrichtet — weil die Playlist nur eine Stufe hat oder weil
|
||||
/// schon die unterste läuft —, bleibt nur der Neuaufbau mit kleinerer Bitrate:
|
||||
/// Transcoder anhalten, Engine abreißen, neu aushandeln, zurückspringen. Das
|
||||
/// sind Sekunden mit schwarzem Bild.
|
||||
///
|
||||
/// **Ein Notnagel, keine Regelschleife.** Zu oft ausgelöst erzeugt er genau
|
||||
/// das Ruckeln, das er beheben soll. Deshalb steht hier eine Liste von
|
||||
/// Bedingungen, die **alle** gelten müssen — und nicht eine Schwelle.
|
||||
///
|
||||
/// Eigener Typ ohne Uhr und ohne Netz, damit sich jede einzelne Bedingung
|
||||
/// einzeln hinschreiben und prüfen lässt. Eine Regel, die nur im laufenden
|
||||
/// Film zu beobachten ist, ist keine Regel.
|
||||
public enum DowngradeRule {
|
||||
/// Wie lange nach dem Start nichts ausgelöst wird.
|
||||
///
|
||||
/// Das anfängliche Füllen des Puffers sieht aus wie eine Notlage und ist
|
||||
/// keine.
|
||||
public static let settleIn: Duration = .seconds(10)
|
||||
/// Wie lange nach einem Sprung nichts ausgelöst wird.
|
||||
///
|
||||
/// Ein Sprung leert den Puffer **immer**. Ihn als Beleg für eine schlechte
|
||||
/// Leitung zu nehmen hieße, sich für das eigene Tun zu bestrafen.
|
||||
public static let afterSeek: Duration = .seconds(10)
|
||||
/// Sperrfrist zwischen zwei Absenkungen.
|
||||
public static let cooldown: Duration = .seconds(180)
|
||||
/// Das Fenster, in dem Rückler gezählt werden.
|
||||
public static let window: Duration = .seconds(90)
|
||||
/// Ab wie vielen Rücklern im Fenster es genügt.
|
||||
public static let stallsNeeded = 3
|
||||
/// Ab welcher Länge ein **einzelner** Rückler genügt.
|
||||
///
|
||||
/// Fünfzehn Sekunden Stillstand sind keine Schwankung mehr. Ihn
|
||||
/// mitzuzählen, bis drei zusammenkommen, hieße viereinhalb Minuten
|
||||
/// zusehen.
|
||||
public static let longStall: Duration = .seconds(15)
|
||||
|
||||
/// Was zum Zeitpunkt der Frage gilt.
|
||||
public struct Facts: Sendable, Equatable {
|
||||
/// Ob überhaupt eingegriffen werden darf. Eine ausdrücklich gewählte
|
||||
/// Stufe ist eine Ansage und wird nicht überstimmt.
|
||||
public let isAutomatic: Bool
|
||||
/// Ob die Playlist mehrere Stufen führt.
|
||||
public let hasLadder: Bool
|
||||
/// Ob schon die unterste davon läuft.
|
||||
public let isAtLowestRung: Bool
|
||||
public let condition: StreamCondition
|
||||
public let stallsInWindow: Int
|
||||
public let longestStallInWindow: Duration
|
||||
/// Wie lange schon gespielt wird.
|
||||
public let playedFor: Duration
|
||||
/// Wie lange der letzte Sprung her ist. `nil` heißt: keiner gewesen.
|
||||
public let sinceLastSeek: Duration?
|
||||
/// Wie lange die letzte Absenkung her ist. `nil` heißt: keine gewesen.
|
||||
public let sinceLastDowngrade: Duration?
|
||||
/// Wie viele es in dieser Wiedergabe schon gab.
|
||||
public let used: Int
|
||||
/// Wie viele es höchstens geben darf.
|
||||
public let budget: Int
|
||||
|
||||
public init(
|
||||
isAutomatic: Bool,
|
||||
hasLadder: Bool,
|
||||
isAtLowestRung: Bool,
|
||||
condition: StreamCondition,
|
||||
stallsInWindow: Int,
|
||||
longestStallInWindow: Duration,
|
||||
playedFor: Duration,
|
||||
sinceLastSeek: Duration? = nil,
|
||||
sinceLastDowngrade: Duration? = nil,
|
||||
used: Int = 0,
|
||||
budget: Int = 2
|
||||
) {
|
||||
self.isAutomatic = isAutomatic
|
||||
self.hasLadder = hasLadder
|
||||
self.isAtLowestRung = isAtLowestRung
|
||||
self.condition = condition
|
||||
self.stallsInWindow = stallsInWindow
|
||||
self.longestStallInWindow = longestStallInWindow
|
||||
self.playedFor = playedFor
|
||||
self.sinceLastSeek = sinceLastSeek
|
||||
self.sinceLastDowngrade = sinceLastDowngrade
|
||||
self.used = used
|
||||
self.budget = budget
|
||||
}
|
||||
}
|
||||
|
||||
/// Warum **nicht** abgesenkt wird.
|
||||
///
|
||||
/// Als Aufzählung statt als `Bool`, weil genau das im Protokoll stehen
|
||||
/// soll: „es hakt, aber wir tun nichts" ist ohne Grund von einem Fehler
|
||||
/// nicht zu unterscheiden.
|
||||
public enum Verdict: Sendable, Equatable {
|
||||
case downgrade
|
||||
/// Eine gewählte Stufe wird nicht überstimmt.
|
||||
case notAutomatic
|
||||
/// Der billige Hebel ist noch nicht ausgereizt.
|
||||
case cheaperPathRemains
|
||||
/// Es hakt nicht genug.
|
||||
case notBadEnough
|
||||
/// Zu früh nach Start oder Sprung.
|
||||
case tooEarly
|
||||
/// Sperrfrist läuft noch.
|
||||
case cooling
|
||||
/// Verbraucht. Ab hier gehört es dem Nutzer gesagt.
|
||||
case budgetSpent
|
||||
}
|
||||
|
||||
public static func verdict(_ facts: Facts) -> Verdict {
|
||||
guard facts.isAutomatic else { return .notAutomatic }
|
||||
|
||||
// Solange es noch eine Sprosse tiefer gibt, ist der billige Weg der
|
||||
// richtige — er kostet kein schwarzes Bild.
|
||||
if facts.hasLadder, !facts.isAtLowestRung { return .cheaperPathRemains }
|
||||
|
||||
let schlimmGenug = facts.stallsInWindow >= stallsNeeded
|
||||
|| facts.longestStallInWindow >= longStall
|
||||
guard schlimmGenug, facts.condition != .healthy else { return .notBadEnough }
|
||||
|
||||
guard facts.playedFor >= settleIn else { return .tooEarly }
|
||||
if let seit = facts.sinceLastSeek, seit < afterSeek { return .tooEarly }
|
||||
|
||||
if let seit = facts.sinceLastDowngrade, seit < cooldown { return .cooling }
|
||||
guard facts.used < facts.budget else { return .budgetSpent }
|
||||
|
||||
return .downgrade
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user