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,102 @@
|
||||
import Foundation
|
||||
|
||||
/// Wie schnell ein Download gerade läuft — und wie lange er noch braucht.
|
||||
///
|
||||
/// **Ein Wertetyp mit Uhr von außen.** Die Rückmeldungen der Übertragung
|
||||
/// kommen mehrmals je Sekunde; was daraus eine Zahl macht, ist reine Rechnung
|
||||
/// und gehört nicht in den Koordinator. Die Zeitpunkte kommen als Argument
|
||||
/// herein, damit sich eine Übertragung ohne echte Uhr nachstellen lässt — eine
|
||||
/// Rate, die man nur im Betrieb beurteilen kann, ist keine prüfbare Zahl.
|
||||
///
|
||||
/// **Warum geglättet.** Zwischen zwei Rückmeldungen schwankt die rohe
|
||||
/// Differenz um ein Vielfaches: ein Puffer läuft voll, der Funk hat ein Loch,
|
||||
/// das System hält die Hintergrundsitzung kurz an. Ungeglättet stünde dort
|
||||
/// eine Zahl, die zehnmal je Sekunde springt — und eine springende Zahl ist
|
||||
/// keine Auskunft, sondern Unruhe.
|
||||
public struct DownloadRate: Sendable, Equatable {
|
||||
/// Wie stark ein neuer Messwert gegenüber dem bisherigen zählt.
|
||||
///
|
||||
/// Ein Drittel: schnell genug, dass ein echter Einbruch binnen weniger
|
||||
/// Sekunden ankommt, träge genug, dass einzelne Ausreißer untergehen.
|
||||
private static let weight = 1.0 / 3.0
|
||||
|
||||
/// Kürzere Abstände werden nicht gemessen.
|
||||
///
|
||||
/// Über fünfzig Millisekunden gerechnet entscheidet der Zufall, wann das
|
||||
/// System den Puffer leert — nicht die Leitung.
|
||||
static let minimumInterval: Duration = .milliseconds(500)
|
||||
|
||||
/// Ab dieser Lücke beginnt die Messung von vorn.
|
||||
///
|
||||
/// So lange ohne eine einzige Rückmeldung heißt nicht „langsam", sondern
|
||||
/// „stand still": die App war angehalten, das Gerät gesperrt, das Netz
|
||||
/// weg. Diese Pause in die Rate einzurechnen ergäbe eine Zahl nahe null
|
||||
/// und eine Restzeit von Stunden.
|
||||
static let maximumInterval: Duration = .seconds(10)
|
||||
|
||||
/// Ein Messpunkt: wie viel war wann da.
|
||||
///
|
||||
/// Als Typ und nicht als Tupel — ein Tupel kann `Equatable` nicht erben,
|
||||
/// und ohne das wäre der ganze Wertetyp nicht vergleichbar.
|
||||
private struct Anchor: Equatable {
|
||||
var bytes: Int64
|
||||
var at: ContinuousClock.Instant
|
||||
}
|
||||
|
||||
/// Der letzte Messpunkt, gegen den gerechnet wird.
|
||||
private var anchor: Anchor?
|
||||
private var smoothed: Double?
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Bytes je Sekunde — `nil`, solange nichts gemessen wurde.
|
||||
///
|
||||
/// **Nicht null.** Null wäre eine Auskunft („es geht nichts durch"), und
|
||||
/// die stimmt in der ersten halben Sekunde nie.
|
||||
public var bytesPerSecond: Double? { smoothed }
|
||||
|
||||
/// Nimmt den Stand der Übertragung entgegen.
|
||||
public mutating func record(bytes: Int64, at now: ContinuousClock.Instant) {
|
||||
guard let anchor else {
|
||||
self.anchor = Anchor(bytes: bytes, at: now)
|
||||
return
|
||||
}
|
||||
|
||||
let elapsed = now - anchor.at
|
||||
guard elapsed >= Self.minimumInterval else { return }
|
||||
defer { self.anchor = Anchor(bytes: bytes, at: now) }
|
||||
|
||||
// Rückwärts heißt: eine neue Aufgabe fängt von vorn an. Dann ist jede
|
||||
// bisherige Messung ungültig — und eine negative Rate wäre Unsinn.
|
||||
guard bytes >= anchor.bytes, elapsed <= Self.maximumInterval else {
|
||||
smoothed = nil
|
||||
return
|
||||
}
|
||||
|
||||
let seconds = elapsed.inSeconds
|
||||
guard seconds > 0 else { return }
|
||||
|
||||
let gemessen = Double(bytes - anchor.bytes) / seconds
|
||||
smoothed = smoothed.map { $0 + (gemessen - $0) * Self.weight } ?? gemessen
|
||||
}
|
||||
|
||||
/// Wie lange es bei dieser Rate noch dauert.
|
||||
///
|
||||
/// `nil`, solange keine Rate feststeht oder die Gesamtgröße unbekannt ist.
|
||||
/// Eine Restzeit ohne Rate wäre geraten, und geraten sieht man ihr nicht
|
||||
/// an.
|
||||
public func remaining(received: Int64, total: Int64) -> Duration? {
|
||||
guard let smoothed, smoothed > 0, total > received else { return nil }
|
||||
return .seconds(Double(total - received) / smoothed)
|
||||
}
|
||||
}
|
||||
|
||||
extension Duration {
|
||||
/// Die Dauer als Sekunden.
|
||||
///
|
||||
/// `components` liefert ganze Sekunden und Attosekunden getrennt; für eine
|
||||
/// Division braucht es beides in einer Zahl.
|
||||
var inSeconds: Double {
|
||||
Double(components.seconds) + Double(components.attoseconds) / 1e18
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user