import { useEffect, useRef } from 'react'; import { useAppDispatch, useAppSelector } from '../../hooks/useAppDispatch'; import { useRadioNextMutation } from '../../api/endpoints/radio'; import { appendToQueue } from '../../store/slices/queue'; import { pickToEntry } from '../../lib/radio'; // Prefetch more radio tracks once the current position is within this many of // the end of the queue. const LOOKAHEAD = 3; /** * Headless controller: while a radio drives the queue, top it up as it drains * (§6.5 / B6 infinite feed). Mounted once in AppShell. A failed prefetch just * stops the feed — playback of what's already queued is unaffected. */ export function RadioController() { const dispatch = useAppDispatch(); const radio = useAppSelector((s) => s.queue.radio); const currentIndex = useAppSelector((s) => s.queue.currentIndex); const entries = useAppSelector((s) => s.queue.entries); const [radioNext] = useRadioNextMutation(); const loadingRef = useRef(false); useEffect(() => { if (!radio) return; const nearEnd = currentIndex >= entries.length - LOOKAHEAD; if (!nearEnd || loadingRef.current) return; loadingRef.current = true; void (async () => { try { const res = await radioNext({ seedTrackId: radio.seedTrackId ?? undefined, fromLikes: radio.fromLikes, exploration: radio.exploration, count: 10, excludeIds: entries.map((e) => e.trackId), }).unwrap(); if (res.picks.length > 0) { dispatch(appendToQueue(res.picks.map(pickToEntry))); } } catch { // best-effort — leave the queue as-is } finally { loadingRef.current = false; } })(); }, [radio, currentIndex, entries, radioNext, dispatch]); return null; }