Halo heisst ab hier Flaro — die Marke stand im Weg
Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging daneben. **Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler. Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim letzten Mal stattfand. --- 2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`, `Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen `FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und das Image. **Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes Ersetzen hätte den sonst zerlegt. `docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden in die Irre führt. **Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag: `com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe. 308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS bauen, das Image baut, und ein frischer Container läuft durch Einrichtung und Anmeldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f98b995050
commit
a7cf6c5def
@@ -0,0 +1,122 @@
|
||||
import Foundation
|
||||
|
||||
/// Buch über das Stocken, mit gleitendem Fenster.
|
||||
///
|
||||
/// **Warum es das gibt.** Der Watchdog im Player kannte nur eine Frage: steht
|
||||
/// es seit vierzig Sekunden am Stück? Bei jedem kurzen Erholen setzte er sich
|
||||
/// zurück — zehn Rückler à dreißig Sekunden lösten nichts aus, obwohl die
|
||||
/// halbe Stunde unbrauchbar war. Genau dieser Fall ist der, über den geklagt
|
||||
/// wird: nicht der tote Stream, sondern der, der immer wieder hakt.
|
||||
///
|
||||
/// **Warum als eigener Typ.** Keine Uhr, kein AVFoundation, kein Netz — die
|
||||
/// Zeiten kommen von außen herein. Damit lässt sich eine Folge von Rücklern
|
||||
/// als Zahlenreihe hinschreiben und das Urteil prüfen. Eine Regel, die man
|
||||
/// nur im laufenden Film beobachten kann, ist keine Regel, sondern ein
|
||||
/// Eindruck.
|
||||
///
|
||||
/// Alle Zeitpunkte sind **seit Beginn dieser Wiedergabe** gemessen, nicht als
|
||||
/// Datum. Das macht die Sache gegen Zeitzonen, Sommerzeit und eine Systemuhr,
|
||||
/// die sich mitten im Film stellt, gleichgültig.
|
||||
public struct StallLedger: Sendable, Equatable {
|
||||
public struct Episode: Sendable, Equatable {
|
||||
/// Wann dieser Rückler vorbei war.
|
||||
public let endedAt: Duration
|
||||
public let length: Duration
|
||||
|
||||
public init(endedAt: Duration, length: Duration) {
|
||||
self.endedAt = endedAt
|
||||
self.length = length
|
||||
}
|
||||
}
|
||||
|
||||
/// Wie weit zurück gezählt wird.
|
||||
///
|
||||
/// Drei Minuten: lang genug, dass ein Muster erkennbar wird, kurz genug,
|
||||
/// dass eine überstandene Durststrecke nicht noch eine Viertelstunde
|
||||
/// später nachwirkt.
|
||||
public let window: Duration
|
||||
|
||||
private(set) var episodes: [Episode] = []
|
||||
|
||||
public init(window: Duration = .seconds(180)) {
|
||||
self.window = window
|
||||
}
|
||||
|
||||
/// Trägt einen überstandenen Rückler ein.
|
||||
public mutating func record(_ episode: Episode) {
|
||||
episodes.append(episode)
|
||||
prune(at: episode.endedAt)
|
||||
}
|
||||
|
||||
/// Wie viele Rückler im Fenster liegen.
|
||||
public func count(at now: Duration) -> Int {
|
||||
episodes.count { $0.endedAt >= now - window }
|
||||
}
|
||||
|
||||
/// Welcher Anteil des Fensters mit Warten verbracht wurde — 0 bis 1.
|
||||
///
|
||||
/// Der laufende Rückler zählt mit, sofern er übergeben wird: wer mitten
|
||||
/// im dritten Stocken steht, soll nicht erst nach dessen Ende erfahren,
|
||||
/// dass es zu viel war.
|
||||
public func share(at now: Duration, ongoing: Duration = .zero) -> Double {
|
||||
let horizon = now - window
|
||||
let waited = episodes
|
||||
.filter { $0.endedAt >= horizon }
|
||||
.reduce(Duration.zero) { $0 + $1.length } + ongoing
|
||||
|
||||
// Am Anfang ist das Fenster noch nicht voll. Dann gegen die
|
||||
// tatsächlich vergangene Zeit rechnen, sonst sähe eine Wiedergabe,
|
||||
// die nach zwanzig Sekunden fünf Sekunden hakte, mit 3 % harmlos aus
|
||||
// statt mit 25 %.
|
||||
let elapsed = min(now, window)
|
||||
guard elapsed > .zero else { return 0 }
|
||||
return min(1, waited / elapsed)
|
||||
}
|
||||
|
||||
/// Der längste einzelne Rückler im Fenster.
|
||||
///
|
||||
/// Ein einzelner langer ist etwas anderes als viele kurze: der eine sagt
|
||||
/// „die Leitung war weg", die vielen sagen „sie trägt es dauerhaft nicht".
|
||||
/// Beide sollen auslösen dürfen, aber nicht über dieselbe Schwelle.
|
||||
public func longest(at now: Duration) -> Duration {
|
||||
episodes
|
||||
.filter { $0.endedAt >= now - window }
|
||||
.map(\.length)
|
||||
.max() ?? .zero
|
||||
}
|
||||
|
||||
/// Wann der letzte Rückler zu Ende war.
|
||||
///
|
||||
/// `nil`, wenn es im Fenster keinen gab — und das ist etwas anderes als
|
||||
/// „vor langer Zeit": wer noch nie gestockt hat, wartet auf keine Frist.
|
||||
///
|
||||
/// Gebraucht fürs Hinaufschalten: wer gerade gestockt hat, hat keine
|
||||
/// Reserve, und ein Neuaufbau nach oben wäre die zweite Unterbrechung
|
||||
/// binnen einer Minute.
|
||||
public func lastStall(at now: Duration) -> Duration? {
|
||||
episodes
|
||||
.filter { $0.endedAt >= now - window }
|
||||
.map(\.endedAt)
|
||||
.max()
|
||||
}
|
||||
|
||||
/// Wirft heraus, was aus dem Fenster gefallen ist.
|
||||
public mutating func prune(at now: Duration) {
|
||||
let horizon = now - window
|
||||
episodes.removeAll { $0.endedAt < horizon }
|
||||
}
|
||||
|
||||
public mutating func removeAll() {
|
||||
episodes.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
private extension Duration {
|
||||
/// Anteil zweier Dauern. `Duration` kann sich nicht selbst teilen.
|
||||
static func / (lhs: Duration, rhs: Duration) -> Double {
|
||||
let left = Double(lhs.components.seconds) + Double(lhs.components.attoseconds) / 1e18
|
||||
let right = Double(rhs.components.seconds) + Double(rhs.components.attoseconds) / 1e18
|
||||
guard right != 0 else { return 0 }
|
||||
return left / right
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user