Files
jellytau/src/lib/stores/home.ts
T
dtourolle ad48d89dfe chore(format): run prettier over src/ and scripts/
Formatting was configured but never enforced: `bun run format:check`
reported 199 unformatted files and ran in no workflow and in no git hook,
so .prettierrc (printWidth 100, trailing commas) described an intention
rather than the tree.

This is the one-time sweep that makes the check gateable. Whitespace and
token-reflow only -- no behavioural change: `bun run check` reports 0
errors and all 1053 frontend tests pass before and after.

Kept out of every other commit on purpose. A 199-file diff mixed with
real changes is unreviewable, and the next commit turns format:check
into a hard CI gate so this cannot silently accumulate again.
2026-08-21 17:41:44 +02:00

140 lines
5.4 KiB
TypeScript

// Home screen data store - featured items, continue watching, recently added
// TRACES: UR-023, UR-024, UR-034, UR-059, UR-067 | DR-026, DR-027, DR-038, DR-039, DR-089, DR-118, DR-197
import { writable, derived } from "svelte/store";
import type { MediaItem } from "$lib/api/types";
import { auth } from "./auth";
import { filterSupersededResumeItems, filterInProgressNextUpItems } from "./continueWatchingFilter";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("HomeStore");
interface HomeState {
heroItems: MediaItem[];
resumeItems: MediaItem[];
nextUpItems: MediaItem[];
latestItems: MediaItem[];
recentlyPlayedAudio: MediaItem[];
resumeMovies: MediaItem[];
/** Favourites per scope. Empty rows are not rendered. TRACES: UR-067 | DR-118 */
favoriteMovies: MediaItem[];
favoriteShows: MediaItem[];
favoriteMusic: MediaItem[];
isLoading: boolean;
error: string | null;
}
function createHomeStore() {
const initialState: HomeState = {
heroItems: [],
resumeItems: [],
nextUpItems: [],
latestItems: [],
recentlyPlayedAudio: [],
resumeMovies: [],
favoriteMovies: [],
favoriteShows: [],
favoriteMusic: [],
isLoading: false,
error: null,
};
const { subscribe, set, update } = writable<HomeState>(initialState);
async function loadHomeSections() {
// Only show loading spinner when no data is available yet
update((s) => ({
...s,
isLoading: s.heroItems.length === 0 && s.latestItems.length === 0,
error: null,
}));
try {
const repo = auth.getRepository();
// Use allSettled so one failing section (e.g. Next Up is online-only and
// rejects offline) doesn't wipe out the whole homepage. Each section falls
// back to an empty list; cached sections (resume/latest/recent) still show.
const settled = await Promise.allSettled([
repo.getResumeItems(undefined, 12),
repo.getNextUpEpisodes(undefined, 12),
repo.getLatestItems("", 16),
repo.getRecentlyPlayedAudio(12), // Backend now handles intelligent grouping
repo.getResumeMovies(12),
// Favourites, one request per row. The scope is opaque — Rust decides
// which item types it covers. TRACES: UR-067 | DR-118
repo.getFavorites("movies", { limit: 20 }),
repo.getFavorites("tv", { limit: 20 }),
repo.getFavorites("music", { limit: 20 }),
]);
const valueOr = <T>(i: number, fallback: T): T =>
settled[i].status === "fulfilled"
? (settled[i] as PromiseFulfilledResult<T>).value
: fallback;
const rawResume = valueOr(0, [] as typeof initialState.resumeItems);
const rawNextUp = valueOr(1, [] as typeof initialState.nextUpItems);
// Drop episodes the user has already moved past (their series' Next Up
// points further ahead) so Continue Watching isn't cluttered with stale
// partial positions left behind by skipping. The frontier is read from the
// unfiltered Next Up list, before in-progress entries are removed from it.
const resume = filterSupersededResumeItems(rawResume, rawNextUp);
// ...and the other way round: an episode already under way is Continue
// Watching's, so Next Episode only offers unstarted ones.
const nextUp = filterInProgressNextUpItems(rawNextUp, rawResume);
const latest = valueOr(2, [] as typeof initialState.latestItems);
const recentAudio = valueOr(3, [] as typeof initialState.recentlyPlayedAudio);
const resumeMovies = valueOr(4, [] as typeof initialState.resumeMovies);
const emptyResult = { items: [] as MediaItem[], totalRecordCount: 0 };
const favoriteMovies = valueOr(5, emptyResult).items;
const favoriteShows = valueOr(6, emptyResult).items;
const favoriteMusic = valueOr(7, emptyResult).items;
// Use resume items or latest as hero items
const hero = resume.length >= 3 ? resume.slice(0, 5) : latest.slice(0, 5);
update((s) => ({
...s,
heroItems: hero,
resumeItems: resume,
nextUpItems: nextUp,
latestItems: latest,
recentlyPlayedAudio: recentAudio,
resumeMovies: resumeMovies,
favoriteMovies,
favoriteShows,
favoriteMusic,
isLoading: false,
}));
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to load home sections";
update((s) => ({ ...s, isLoading: false, error: message }));
log.error("Failed to load home sections:", error);
}
}
function reset() {
set(initialState);
}
return {
subscribe,
loadHomeSections,
reset,
};
}
export const home = createHomeStore();
// Derived stores for convenience
export const heroItems = derived(home, ($home) => $home.heroItems);
export const resumeItems = derived(home, ($home) => $home.resumeItems);
export const nextUpItems = derived(home, ($home) => $home.nextUpItems);
export const latestItems = derived(home, ($home) => $home.latestItems);
export const recentlyPlayedAudio = derived(home, ($home) => $home.recentlyPlayedAudio);
export const resumeMovies = derived(home, ($home) => $home.resumeMovies);
export const favoriteMovies = derived(home, ($home) => $home.favoriteMovies);
export const favoriteShows = derived(home, ($home) => $home.favoriteShows);
export const favoriteMusic = derived(home, ($home) => $home.favoriteMusic);
export const isHomeLoading = derived(home, ($home) => $home.isLoading);