72f4d2e160
Fork de la librairie headless svelte-podcast transformé en lecteur : radio Funkwhale mizik.o-k-i.net (~2000 titres), page démo RSS Syntax.fm, file de lecture, préférences et progression persistées. État avant refonte playbook OKI (bugs connus : volume inerte, bouton démo RSS cassé en navigation client, cibles tactiles mobiles).
94 lines
2.4 KiB
TypeScript
94 lines
2.4 KiB
TypeScript
import { get, writable } from 'svelte/store';
|
|
import {
|
|
fetch_radio_page,
|
|
RADIO_PAGE_SIZE,
|
|
type RadioTrack,
|
|
} from './funkwhale';
|
|
import { radio_player } from './radio-store';
|
|
|
|
/**
|
|
* State of the browsable library (all tracks of the radio).
|
|
*/
|
|
export interface LibraryState {
|
|
tracks: RadioTrack[];
|
|
/** Total number of tracks on the radio (known after the first page). */
|
|
total: number | null;
|
|
loaded_pages: number;
|
|
total_pages: number | null;
|
|
is_loading: boolean;
|
|
is_done: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
const initial_state: LibraryState = {
|
|
tracks: [],
|
|
total: null,
|
|
loaded_pages: 0,
|
|
total_pages: null,
|
|
is_loading: false,
|
|
is_done: false,
|
|
error: null,
|
|
};
|
|
|
|
export const library = writable<LibraryState>(initial_state);
|
|
|
|
/**
|
|
* Loads every page of the radio's track list, one after another.
|
|
* Idempotent: does nothing if already loading or fully loaded.
|
|
*/
|
|
export async function load_library(): Promise<void> {
|
|
const state = get(library);
|
|
if (state.is_loading || state.is_done) return;
|
|
|
|
library.set({ ...state, is_loading: true, error: null });
|
|
|
|
try {
|
|
// First page: tells us how many tracks / pages exist.
|
|
const first = await fetch_radio_page(1);
|
|
const total_pages = Math.ceil(first.count / RADIO_PAGE_SIZE);
|
|
library.set({
|
|
tracks: first.tracks,
|
|
total: first.count,
|
|
loaded_pages: 1,
|
|
total_pages,
|
|
is_loading: true,
|
|
is_done: total_pages <= 1,
|
|
error: null,
|
|
});
|
|
|
|
// Load remaining pages in small parallel batches.
|
|
const CONCURRENCY = 4;
|
|
for (let start = 2; start <= total_pages; start += CONCURRENCY) {
|
|
// Pause while playback is loading a track: streaming and
|
|
// playback-related requests have priority over the library.
|
|
while (get(radio_player).is_loading) {
|
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
}
|
|
|
|
const pages = await Promise.all(
|
|
Array.from(
|
|
{ length: Math.min(CONCURRENCY, total_pages - start + 1) },
|
|
(_, i) => fetch_radio_page(start + i),
|
|
),
|
|
);
|
|
library.update((prev) => ({
|
|
...prev,
|
|
tracks: [...prev.tracks, ...pages.flatMap((p) => p.tracks)],
|
|
loaded_pages: Math.min(start + CONCURRENCY - 1, total_pages),
|
|
}));
|
|
}
|
|
|
|
library.update((prev) => ({
|
|
...prev,
|
|
is_loading: false,
|
|
is_done: true,
|
|
}));
|
|
} catch (error) {
|
|
library.update((prev) => ({
|
|
...prev,
|
|
is_loading: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
}));
|
|
}
|
|
}
|