🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m1s
Traceability Validation / Check Requirement Traces (push) Successful in 23s
Build & Release / Run Tests (push) Successful in 4m7s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 19m5s
Build & Release / Build Linux (push) Successful in 16m20s
Build & Release / Build Android (push) Successful in 19m12s
Build & Release / Create Release (push) Successful in 8s
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
// 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<T>(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<string>();
|
|
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<string>();
|
|
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);
|
|
}
|