// Hero banner mix builder, shared by the movies/TV/music landing stores. // // The hero used to show the first N items of fixed pools (continue watching, // recently played, latest), which made it identical on every visit. Instead we // sample a couple of items at random from each pool — pools are ordered most // personal first — and shuffle the rotation, so the banner is a fresh mix each // time while still leading with something personal. // TRACES: UR-034 | DR-038 import type { MediaItem } from "$lib/api/types"; /** Fisher-Yates shuffle; returns a new array, input is untouched. */ export function shuffle(items: T[]): T[] { const result = [...items]; for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; } return result; } /** * Build a varied hero rotation from ordered pools (most personal first). * * Takes up to `perPool` random items from each pool, de-duplicated by id and * filtered to items with usable artwork. The first pick from the first * non-empty pool leads the rotation (so a resumable/recent item greets the * user), the rest are shuffled. If that yields fewer than `count` items, the * remainder is backfilled from whatever is left across all pools. */ export function buildHeroMix( pools: MediaItem[][], hasArt: (item: MediaItem) => boolean, count = 6, perPool = 2, ): MediaItem[] { const seen = new Set(); const usable = pools.map((pool) => pool.filter((item) => { if (!hasArt(item) || seen.has(item.id)) return false; seen.add(item.id); return true; }), ); const picked = new Set(); const picks: MediaItem[] = []; for (const pool of usable) { for (const item of shuffle(pool).slice(0, perPool)) { picked.add(item.id); picks.push(item); } } if (picks.length === 0) return []; const [leader, ...rest] = picks; const leftovers = shuffle(usable.flat().filter((item) => !picked.has(item.id))); return [leader, ...shuffle(rest), ...leftovers].slice(0, count); }