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>
121 lines
5.6 KiB
Swift
121 lines
5.6 KiB
Swift
import Foundation
|
|
|
|
/// Welche Titel oben gross gezeigt werden — und in welcher Reihenfolge.
|
|
///
|
|
/// **Warum es das als eigenen Typ gibt.** Die Auswahl soll sich zufällig
|
|
/// anfühlen und trotzdem stabil sein: dreimal am Abend dieselbe, am nächsten
|
|
/// Morgen eine andere. Das ist genau die Art Regel, die man nicht im
|
|
/// laufenden Betrieb beurteilen kann — „es wechselt zu oft" ist kein Befund,
|
|
/// den man debuggen könnte. Mit einer Liste hinein und einer heraus schon.
|
|
///
|
|
/// **Der Fehler, der es hierher gebracht hat.** Vorher wurde ein Zufallszahl-
|
|
/// erzeuger einmal je Zeitfenster aufgesetzt und dann Titel für Titel
|
|
/// abgefragt. Das Los hing damit an der **Position** in der Liste: kam ein
|
|
/// Titel dazu, verschwand einer, oder änderte sich der Fortschritt, bekamen
|
|
/// alle dahinter ein anderes Los — und der Aufmacher war nach jedem Neustart
|
|
/// ein anderer, obwohl das Zeitfenster dasselbe war.
|
|
///
|
|
/// Jetzt hängt das Los an der **Kennung** des Titels. Wer dazukommt, stört
|
|
/// niemanden; wer geht, hinterlässt eine Lücke und sonst nichts.
|
|
public enum HeroSelection {
|
|
/// Wie lange dieselbe Auswahl gilt.
|
|
public static let windowHours: Double = 6
|
|
|
|
/// Welche Reihen den Aufmacher speisen dürfen.
|
|
///
|
|
/// **Leer heisst alle.** Eine Einstellung, bei der nichts angekreuzt ist,
|
|
/// soll nicht in einen leeren Aufmacher führen — „ich habe nichts
|
|
/// gewählt" heisst „mir ist es gleich", nicht „nimm nichts".
|
|
///
|
|
/// **Ist etwas gewählt, kommt nur daher etwas.** Reihen, die sich keiner
|
|
/// Mediathek zuordnen lassen — Weiterschauen, Als Nächstes, Favoriten —
|
|
/// bleiben dann draussen.
|
|
///
|
|
/// Das war zuerst andersherum, und es war die falsche Entscheidung. Die
|
|
/// Überlegung „Angefangenes soll man nicht verlieren" klingt freundlich,
|
|
/// führt aber dazu, dass im Aufmacher weiter eine Serie steht, obwohl
|
|
/// gerade „nur Filme" eingestellt wurde — und niemand bringt das mit der
|
|
/// Einstellung in Verbindung. Eine Einstellung muss das tun, was sie
|
|
/// sagt; wer Weiterschauen oben haben will, wählt keine Mediathek aus.
|
|
public static func rows(
|
|
_ rows: [MediaRowGroup],
|
|
limitedTo libraries: Set<String>
|
|
) -> [MediaRowGroup] {
|
|
guard !libraries.isEmpty else { return rows }
|
|
let übrig = rows.filter { row in
|
|
guard let libraryID = row.libraryID else { return false }
|
|
return libraries.contains(libraryID)
|
|
}
|
|
// **Bleibt nichts übrig, gilt die Auswahl als wirkungslos.**
|
|
//
|
|
// Ein Aufmacher, der lautlos verschwindet, ist der schlechtere
|
|
// Fehler — man sucht ihn überall, nur nicht in einer Einstellung
|
|
// zwei Bildschirme weiter. Und es gibt Fälle, in denen das kein
|
|
// Versehen ist, sondern die Datenlage: eine gespeicherte Kennung
|
|
// nach einem Serverwechsel, oder ein Server, dessen Reihen gar
|
|
// keine Mediathek nennen.
|
|
return übrig.isEmpty ? rows : übrig
|
|
}
|
|
|
|
/// Ordnet die Titel, ohne die Reihenfolge fest vorzugeben.
|
|
///
|
|
/// **Ungleiche Lose.** Ohne Hintergrundbild ist der Aufmacher eine graue
|
|
/// Fläche mit Text, und ein Titel ohne eines gehört dort eigentlich nicht
|
|
/// hin. Ihn auszuschliessen wäre aber zu hart: bei einer Mediathek, in
|
|
/// der wenige Titel eines tragen, blieben immer dieselben drei übrig.
|
|
/// Also bekommt er ein schlechteres Los statt gar keines.
|
|
public static func ordered(
|
|
_ items: [MediaItem],
|
|
at time: Date = .now,
|
|
windowHours: Double = windowHours
|
|
) -> [MediaItem] {
|
|
let fenster = UInt64(max(0, time.timeIntervalSince1970 / (windowHours * 3_600)))
|
|
|
|
var bewertet: [(item: MediaItem, los: Double)] = []
|
|
bewertet.reserveCapacity(items.count)
|
|
for item in items {
|
|
bewertet.append((item, lot(for: item, window: fenster)))
|
|
}
|
|
|
|
// Bei gleichem Los die Kennung: zweimal dieselbe Eingabe soll zweimal
|
|
// dasselbe ergeben, sonst ist ein Fehler nicht wiederholbar.
|
|
bewertet.sort { links, rechts in
|
|
links.los == rechts.los ? links.item.id < rechts.item.id : links.los > rechts.los
|
|
}
|
|
return bewertet.map(\.item)
|
|
}
|
|
|
|
/// Das Los eines Titels in diesem Zeitfenster — zwischen 0 und 1.
|
|
static func lot(for item: MediaItem, window: UInt64) -> Double {
|
|
let hatBild = item.art != nil || item.seriesArt != nil
|
|
let gewicht = hatBild ? 1.0 : 0.2
|
|
return unitRandom(seed: stableHash(item.id) ^ window) * gewicht
|
|
}
|
|
|
|
/// Ein Streuwert, der über Programmläufe hinweg gleich bleibt.
|
|
///
|
|
/// **Nicht `hashValue`.** Swift streut Hashes je Programmlauf zufällig;
|
|
/// damit wäre die Auswahl nach jedem Start eine andere — also genau der
|
|
/// Fehler, der hier behoben werden soll, nur an einer anderen Stelle.
|
|
static func stableHash(_ text: String) -> UInt64 {
|
|
// FNV-1a: vier Zeilen, keine Abhängigkeit, und für das Mischen von
|
|
// ein paar Kacheln mehr als gut genug.
|
|
var hash: UInt64 = 0xCBF2_9CE4_8422_2325
|
|
for byte in text.utf8 {
|
|
hash ^= UInt64(byte)
|
|
hash = hash &* 0x0000_0100_0000_01B3
|
|
}
|
|
return hash
|
|
}
|
|
|
|
/// SplitMix64, auf 0..<1 gebracht.
|
|
static func unitRandom(seed: UInt64) -> Double {
|
|
var z = seed &+ 0x9E37_79B9_7F4A_7C15
|
|
z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9
|
|
z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB
|
|
z = z ^ (z >> 31)
|
|
// Die oberen 53 Bit: genau die, die ein `Double` verlustfrei trägt.
|
|
return Double(z >> 11) / Double(1 << 53)
|
|
}
|
|
}
|