feat: transformation en application Radyobòkaz Player (WIP sécurisé)

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).
This commit is contained in:
sucupira
2026-07-21 17:54:36 -04:00
parent b1165a817e
commit 72f4d2e160
82 changed files with 1226 additions and 2330 deletions
+93
View File
@@ -0,0 +1,93 @@
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),
}));
}
}