Der Aufmacher zeigte monatelang denselben Film

Er war nun einmal der erste in "Weiterschauen", und genommen wurden
schlicht die ersten fünf der Liste. Jetzt wird gezogen — aber nicht bei
jedem Blick: der Zufall hängt an einem Zeitfenster von sechs Stunden.
Wer die App dreimal am Abend öffnet, findet sich zurecht; wer am
nächsten Morgen wiederkommt, sieht etwas anderes.

Titel ohne Hintergrundbild sind dabei nicht ausgeschlossen, sondern
ziehen ein schlechteres Los. Sie ganz auszusperren war zu hart — in
einer Mediathek, in der wenige Titel eines tragen, blieben immer
dieselben drei übrig.

Dieselbe Regel im Web, damit beide nicht auseinanderlaufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-14 16:34:09 +02:00
co-authored by Claude Opus 5
parent daa836fd8c
commit c3f9afdb07
2 changed files with 106 additions and 6 deletions
+81 -5
View File
@@ -148,11 +148,87 @@ struct HomeView: View {
var seen = Set<String>()
let unique = candidates.filter { seen.insert($0.id).inserted }
// Ohne Hintergrundbild ist der Aufmacher eine graue Fläche mit Text.
// Lieber einen Titel überspringen als eine leere Bühne zeigen — es sei
// denn, keiner hätte eines.
let withArtwork = unique.filter { $0.art != nil || $0.seriesArt != nil }
return Array((withArtwork.isEmpty ? unique : withArtwork).prefix(layout.heroCount))
// **Gemischt, aber nicht bei jedem Blick.**
//
// Vorher standen hier schlicht die ersten fünf der Liste. Das ist
// deterministisch, und deshalb zeigte der Aufmacher monatelang
// denselben Film — er war nun einmal der erste in „Weiterschauen".
//
// Bei jedem Laden zu würfeln wäre das andere Extrem: man tippt auf
// etwas, geht zurück, und es ist weg. Deshalb ein Zufall mit
// Zeitfenster — innerhalb von sechs Stunden dieselbe Auswahl, danach
// eine neue. Wer die App dreimal am Abend öffnet, findet sich zurecht;
// wer sie am nächsten Morgen öffnet, sieht etwas anderes.
var generator = TimeWindowGenerator(hours: 6)
switch layout.heroSource {
case .continueFirst:
// Innerhalb der Gruppen ziehen, nicht über sie hinweg:
// Angefangenes bleibt vorn, wechselt aber untereinander.
let continuing = unique.filter { $0.watchProgress != nil }
let rest = unique.filter { $0.watchProgress == nil }
let ordered = Self.drawn(continuing, using: &generator)
+ Self.drawn(rest, using: &generator)
return Array(ordered.prefix(layout.heroCount))
case .latestOnly:
return Array(Self.drawn(unique, using: &generator).prefix(layout.heroCount))
}
}
/// Zieht die Titel in eine Reihenfolge — mit ungleichen Losen.
///
/// Ohne Hintergrundbild ist der Aufmacher eine graue Fläche mit Text, und
/// ein Titel ohne eines gehört dort eigentlich nicht hin. Ihn ganz
/// auszuschließen war aber zu hart: bei einer Mediathek, in der wenige
/// Titel ein Hintergrundbild tragen, blieben immer dieselben drei übrig.
///
/// Also bekommt jeder ein Los, und wer kein Hintergrundbild hat, ein
/// schlechteres — sein Wert liegt nur im untersten Fünftel. Solange es
/// genug Titel mit Bild gibt, kommt er praktisch nie nach vorn; gibt es
/// wenige, hat er seine Chance. Kein Sonderfall, keine Schwelle, ab der
/// die Regel kippt.
private static func drawn(
_ items: [MediaItem],
using generator: inout TimeWindowGenerator
) -> [MediaItem] {
var keyed: [(item: MediaItem, key: Double)] = []
keyed.reserveCapacity(items.count)
for item in items {
let hasBackdrop = item.art != nil || item.seriesArt != nil
let weight = hasBackdrop ? 1.0 : 0.2
keyed.append((item, Double.random(in: 0..<1, using: &generator) * weight))
}
return keyed.sorted { $0.key > $1.key }.map(\.item)
}
/// Ein Zufall, der sich innerhalb eines Zeitfensters gleich verhält.
///
/// Derselbe Startwert für alle Aufrufe im selben Fenster — damit ist die
/// Auswahl stabil, solange man die App benutzt, und trotzdem eine andere,
/// wenn man später wiederkommt. Ohne diesen Umweg müsste die gewählte
/// Reihe irgendwo gespeichert und wieder eingelesen werden, nur damit sie
/// beim Zurückblättern nicht springt.
///
/// SplitMix64: kurz, ohne Abhängigkeit, und für das Mischen von fünf
/// Kacheln mehr als gut genug. Für irgendetwas Sicherheitsrelevantes wäre
/// er es nicht — das ist hier auch nicht die Frage.
private struct TimeWindowGenerator: RandomNumberGenerator {
private var state: UInt64
init(hours: Double, now: Date = .now) {
let window = UInt64(max(0, now.timeIntervalSince1970 / (hours * 3_600)))
state = window &* 0x9E37_79B9_7F4A_7C15
}
mutating func next() -> UInt64 {
state &+= 0x9E37_79B9_7F4A_7C15
var z = state
z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9
z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB
return z ^ (z >> 31)
}
}
private func load() async {