// Round ten: the lyrics tools (a store the sheet's face and a lyrics sheet // share: nudge, choose another candidate, forget and look again), You → // Clear play history, a record's Re-fetch the cover. Mocks, walks, doc. const fs = require('fs'); const root = 'F:/Coding/SoulStream/'; const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/'; const ins = (name) => fs.readFileSync(scratch + name, 'utf8'); let p, s; const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); }; const save = () => fs.writeFileSync(root + p, s); const once = (a) => { const i = s.indexOf(a); if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90)); if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90)); return i; }; const rep = (a, b) => { once(a); s = s.replace(a, () => b); }; const NL = String.fromCharCode(10); const afterLine = (anchor, text) => { const i = once(anchor); const nl = s.indexOf(NL, i); s = s.slice(0, nl + 1) + text + s.slice(nl + 1); }; const beforeLine = (anchor, text) => { const i = once(anchor); const ls = s.lastIndexOf(NL, i) + 1; s = s.slice(0, ls) + text + s.slice(ls); }; // ── the sheet's lyrics face reads the store ── load('src/mobile/Sheet.svelte'); rep(` /* ── lyrics: the app's own pipeline, per track ───────────────────── */ let lyr = $state({ key: null, status: "none", lines: [], plain: "", offset: 0 }); $effect(() => { if (face !== "lyrics" || !player.track) return; const t = player.track; const key = t.path; if (lyr.key === key) return; lyr = { key, status: "loading", lines: [], plain: "", offset: 0 }; api.lyricsGet({ title: t.title, artist: t.artist || player.album?.artist || "", album: player.album?.title ?? null, durationSecs: t.duration_secs ?? null, path: t.path, }).then( (p) => { if (lyr.key !== key) return; lyr = { key, status: p?.status || "none", lines: p?.synced || [], plain: p?.plain || "", offset: (p?.offset_ms || 0) + (p?.auto_offset_ms || 0), }; }, () => { if (lyr.key === key) lyr = { key, status: "none", lines: [], plain: "", offset: 0 }; }, ); });`, ` /* ── lyrics: the store (lyrics.svelte.js), asked for the playing track whenever the face is up ───────────────────────────────────────── */ $effect(() => { if (face !== "lyrics" || !player.track) return; void player.track.path; void loadLyrics(); });`); rep(`import { nav, closeSheet, morphSource, openHalf, openViewer, push } from "./nav.svelte.js";`, `import { nav, closeSheet, morphSource, openHalf, openViewer, push } from "./nav.svelte.js"; import { lyr, loadLyrics } from "./lyrics.svelte.js";`); rep(`
{#if lyr.status === "loading"}`, `
{ e.preventDefault(); openHalf("lyrics"); }} role="presentation"> {#if lyr.status === "loading"}`); rep(` {:else if lyr.plain}
{lyr.plain}
Plain lyrics — nothing timed for this recording.
`, ` {:else if lyr.plain}
{lyr.plain}
Plain lyrics — nothing timed for this recording.
`); rep(` {:else if lyr.status === "instrumental"}
Instrumental.
`, ` {:else if lyr.status === "instrumental"}
Instrumental.
`); save(); // the synced list gets its tools link after the lines: the {/each} that closes the lines { const a = ` {/each} {:else if lyr.plain}`; once(a); s = s.replace(a, () => ` {/each} {:else if lyr.plain}`); save(); } // ── the lyrics sheet ── load('src/mobile/HalfSheet.svelte'); rep(` import { lib, albumById, albumsBy, tracksOf, loadLibrary, refreshPlaylists } from "./lib.svelte.js";`, ` import { lib, albumById, albumsBy, tracksOf, loadLibrary, refreshPlaylists, refreshRecents } from "./lib.svelte.js"; import { lyr, nudge, searchCandidates, closeCandidates, chooseCandidate, forgetLyrics } from "./lyrics.svelte.js"; import { art, wantArt } from "./art.svelte.js";`); beforeLine(` {:else if kind === "librarysort"}`, ins('ins-halfsheet-lyrics.txt')); // the record's sheet: re-fetch the cover rep(` {@render row("users", "Go to artist", album.artist, act(() => push("artist", { name: album.artist })))}
Format{albumFmt(tracks) || "—"}
`, ` {@render row("users", "Go to artist", album.artist, act(() => push("artist", { name: album.artist })))} {#if album.id != null}
{@render row("image", "Re-fetch the cover", "the canonical high-resolution cover, into the folder and the tags", act(() => refetchCover(album)))} {/if}
Format{albumFmt(tracks) || "—"}
`); rep(` /* the prompt's field, seeded when the prompt opens */`, ` /* the desktop's per-record cover refresh; the thumbnail is asked for again */ async function refetchCover(album) { try { await api.refreshAlbumArt(album.id); if (album.art_path) { delete art[album.art_path]; wantArt(album.art_path); } note("Cover re-fetched for “" + album.title + "”"); } catch (e) { oops(e); } } /* the prompt's field, seeded when the prompt opens */`); save(); // ── You: clear play history ── load('src/mobile/screens/You.svelte'); rep(` import { lib, loadLibrary } from "../lib.svelte.js";`, ` import { lib, loadLibrary, refreshRecents } from "../lib.svelte.js";`); rep(` async function rescan() {`, ` function clearHistory() { openHalf("confirm", { title: "Clear the play history?", body: "Jump back in and the play counts start over. The library and the playlists stay.", ok: "Clear", danger: true, onconfirm: async () => { try { await api.clearPlayHistory(); await refreshRecents(); note("Play history cleared"); } catch (e) { player.error = String(e); } }, }); } async function rescan() {`); rep(`