Files
SoulStream/src/mobile/lyrics.svelte.js
T
KeelrLeek 26928b706d android: the repair sheet, browse, every long-press sheet, You's last sections, the lyrics tools — and the walk's audio check made honest
The playlist repair (Find a local match: the library searched by the
row's name, a pick relinks it through playlist_relink_item; an unknown
row takes the track's identity). A peer's sheet on the raw results (play
or save the folder, browse their files, message, buddy) and the Browse
screen — the hub's Browse tab: a user's whole share with sizes and totals,
a folder played or saved whole; a share can hold thousands of folders, so
it filters and renders 80 at a time. "Look up a user" on Community →
Buddies. The sheets the phone lacked: a queue row's (Play now, Move to
next, Remove from queue), a catalog song's, a catalog card's and the
record page's own, a raw file's (in the results and in Browse), a room
name's, a buddy's, the now-playing Save (the desktop's Save menu for a
streamed track or record). The artwork viewer with pinch zoom, one layer
the back gesture peels. Search's recent searches and its spelling
suggestion from the names this phone knows. You → Streaming, Downloads,
Sharing, Lyrics, Album artwork, and Clear play history; a record's
Re-fetch the cover. The lyrics tools on the sheet's face: the offset
nudged and kept per track, another LRCLIB candidate chosen, the lookup
forgotten and done again (lyrics.svelte.js, shared by the face and the
lyrics sheet).

The harness: every CDP call times out and the reload step re-attaches
(a WebView that stopped answering hung a walk for ten minutes); the
audio check counts SoulStream's own tracks' underruns from the mixer dump
instead of the thread-wide counters that any app's notification moves —
the afternoon's "dropouts" were other apps' sounds, and the browse of a
49,544-file share measured at zero; __settle resets every tab to its root
through the nav; an adb step kind; the walks cover all of the above on
the mock and on the phone.
2026-09-13 19:56:20 -05:00

108 lines
3.9 KiB
JavaScript

/* Lyrics on the phone — the app's own pipeline, per track, with the
desktop's tools: the offset nudged half a second at a time and kept for
the track, another LRCLIB candidate chosen when the words are wrong, the
lookup forgotten and done again. One store, read by the player sheet's
lyrics face and by the lyrics sheet. */
import * as api from "../lib/api.js";
import { player } from "./player.svelte.js";
export const lyr = $state({
key: null, // the track's path — the identity the words belong to
status: "none", // loading | synced | plain | instrumental | none
source: "",
lines: [], // [{ time_ms, text, words? }]
plain: "",
offset: 0, // the user's offset plus the automatic correction, ms
userOffset: 0,
autoOffset: 0,
candidates: null, // LRCLIB candidates while choosing, else null
searching: false,
});
let gen = 0;
export function identity() {
const t = player.track;
if (!t) return null;
return {
title: t.title,
artist: t.artist || player.album?.artist || "",
album: player.album?.title ?? null,
durationSecs: t.duration_secs ?? null,
path: t.path,
key: t.path,
};
}
function apply(p) {
lyr.status = p?.status || "none";
lyr.source = p?.source || "";
lyr.lines = p?.synced || [];
lyr.plain = p?.plain || "";
lyr.userOffset = p?.offset_ms || 0;
lyr.autoOffset = p?.auto_offset_ms || 0;
lyr.offset = lyr.userOffset + lyr.autoOffset;
}
/** The words for the playing track; `force` asks the network again. */
export async function loadLyrics(force = false) {
const id = identity();
if (!id) return;
if (!force && lyr.key === id.key) return;
const g = ++gen;
Object.assign(lyr, { key: id.key, status: "loading", source: "", lines: [], plain: "", offset: 0, userOffset: 0, autoOffset: 0, candidates: null, searching: false });
try {
const p = await api.lyricsGet({ title: id.title, artist: id.artist, album: id.album, durationSecs: id.durationSecs, path: id.path, refresh: force ? true : null });
if (g !== gen) return;
apply(p);
} catch {
if (g === gen) Object.assign(lyr, { status: "none", lines: [], plain: "" });
}
}
/* the desktop's nudge: half a second at a time, kept for the track */
let persist = 0;
export function nudge(deltaMs) {
const id = identity();
if (!id || !lyr.lines.length) return;
lyr.userOffset = Math.max(-30_000, Math.min(30_000, lyr.userOffset + deltaMs));
lyr.offset = lyr.userOffset + lyr.autoOffset;
clearTimeout(persist);
const value = lyr.userOffset;
persist = setTimeout(() => { api.lyricsSetOffset(id.title, id.artist, id.durationSecs, value).catch(() => {}); }, 500);
}
/** Search LRCLIB by this track's name; the candidates wait on the store. */
export async function searchCandidates() {
const id = identity();
if (!id) return;
const g = gen;
lyr.searching = true;
lyr.candidates = [];
try {
const found = await api.lyricsSearch(id.title, id.artist);
if (g === gen) lyr.candidates = found || [];
} catch {
if (g === gen) lyr.candidates = [];
} finally {
if (g === gen) lyr.searching = false;
}
}
export function closeCandidates() { lyr.candidates = null; }
/** Pin a picked candidate as this track's lyrics. */
export async function chooseCandidate(candidateId) {
const id = identity();
if (!id) return;
const g = gen;
try {
const p = await api.lyricsSelect(id.title, id.artist, id.durationSecs, candidateId);
if (g !== gen) return;
apply(p);
lyr.candidates = null;
} catch { /* keep what was showing */ }
}
/** Forget the words and look again. */
export async function forgetLyrics() {
const id = identity();
if (!id) return;
try { await api.lyricsClear(id.title, id.artist, id.durationSecs); } catch { /* nothing kept */ }
await loadLyrics(true);
}