1// The next track: what autoplay plays when a song ends, and what the ⏭ 2// button beside play plays at once (JevPicks.jsx). One path for both, so 3// they hear the same history and record the same plays. 4// 5// Jev picks from the listener's history (picker.mjs, pickNext): signed in, 6// the account's radio history across sessions and devices 7// (radioHistory.mjs), whose most recent songs are not offered at all, and 8// each pick is recorded there; signed out, the titles this tab has played. 9// When Jev cannot answer, the next song in the list plays. 10// 11// Takes the page's functions as arguments, so it runs in vitest. 12const HEARD = 8; 13 14// The cycle a song as written ends on, or null while not known. Songs end 15// cycles, not seconds, after they start: a song whose form Jev walks ends 16// after the section Jev ends it on; any other song after the bars its 17// play-once mask gives it (songs.mjs). A couple of cycles more let the 18// final chord ring. Autoplay moves on there; the lobby shows progress to it. 19const RING_OUT = 2; 20export function endCycle(song, booth) { 21 if (booth && booth.last !== null && (song.timing?.jev || Number.isFinite(booth.segments))) { 22 return (booth.last + 1) * booth.every + RING_OUT; 23 } 24 if (song.timing?.jev) return null; // Jev has not ended it yet 25 return song.timing ? song.timing.bars + RING_OUT : null; 26} 27 28export function nextTracker({ pick, play, loadRecent, record }) { 29 let heardHere = []; // titles this tab played, oldest first 30 let recent = null; // signed in: the account's song ids, newest first 31 let recentFor = null; // whose `recent` it is 32 let busy = false; 33 34 return { 35 // `current` is the site song playing as written, or null; `user` who is 36 // signed in, or null. Resolves to { song, p, error } once `song` plays, 37 // or null when a pick is already under way. 38 async next({ songs, current, user, editor }) { 39 if (busy) return null; 40 busy = true; 41 try { 42 if (!user) [recent, recentFor] = [null, null]; 43 else if (recentFor !== user.id) [recent, recentFor] = [await loadRecent(), user.id]; 44 if (current) heardHere = [...heardHere, current.title].slice(-HEARD); 45 const heard = recent 46 ? recent 47 .slice(0, HEARD) 48 .reverse() 49 .map((id) => songs.find((s) => s.id === id)?.title) 50 .filter(Boolean) 51 : heardHere; 52 let song; 53 let p; 54 let error = null; 55 try { 56 ({ song, p } = await pick(songs, current, heard, recent ? { recent } : {})); 57 } catch (e) { 58 const playable = songs.filter((s) => s.code); 59 song = playable[(playable.findIndex((s) => s.id === current?.id) + 1) % playable.length]; 60 error = e.message; 61 } 62 await play(editor, song); 63 if (recent) { 64 recent = [song.id, ...recent]; 65 record(song.id); 66 } 67 return { song, p, error }; 68 } finally { 69 busy = false; 70 } 71 }, 72 }; 73}