perf: parallel lrclib requests + in-memory cache for lyrics
- Android + web: fire exact/artist-search/title-search in parallel so total wait = slowest single request instead of sum of all three - Android: HashMap cache keyed by artist|title, cleared on app restart - Web: Map cache (session lifetime), cache hit is instant re-open - Web: extract _showLyricsResult helper, safeJson wrapper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
d0df01a76d
commit
d7f8b301ab
2 files changed
+117
-116
No files matched your search
+52
-57
@@ -100,6 +100,7 @@ export function switchNpTab(tab) {
|
||||
let _lyricsLines = [];
|
||||
let _lyricsSongId = null;
|
||||
let _lyricsLoadToken = 0;
|
||||
const _lyricsCache = new Map(); // "artist|title" → result object (null = not found)
|
||||
|
||||
const LYRICS_LOOKAHEAD_MS = 200;
|
||||
|
||||
@@ -147,89 +148,83 @@ async function _loadNpLyrics(songId) {
|
||||
content.innerHTML = '<div style="text-align:center;padding:60px 0;color:rgba(255,255,255,.3);font-size:14px">Lade Lyrics…</div>';
|
||||
|
||||
const token = ++_lyricsLoadToken;
|
||||
const s = S.queue[S.qIdx];
|
||||
const s = S.queue[S.qIdx];
|
||||
const artist = (s?.artist || '').split(/[/,]/)[0].trim();
|
||||
const title = s?.title || '';
|
||||
const album = s?.album || '';
|
||||
const dur = s?.duration || 0;
|
||||
const cacheKey = `${artist}|${title}`;
|
||||
|
||||
const SIG = () => AbortSignal.timeout(5000);
|
||||
const SIG = () => AbortSignal.timeout(6000);
|
||||
const safeJson = async (promise) => { try { const r = await promise; return r.ok ? r.json() : null; } catch { return null; } };
|
||||
const bestMatch = (candidates, durSecs) => {
|
||||
if(!candidates?.length) return null;
|
||||
const synced = candidates.filter(c => c.syncedLyrics);
|
||||
const pool = synced.length ? synced : candidates;
|
||||
if(!durSecs) return pool[0];
|
||||
const pick = pool.sort((a, b) => Math.abs(a.duration - durSecs) - Math.abs(b.duration - durSecs))[0];
|
||||
const pick = [...pool].sort((a, b) => Math.abs(a.duration - durSecs) - Math.abs(b.duration - durSecs))[0];
|
||||
return Math.abs(pick.duration - durSecs) <= 5 ? pick : (synced[0] || candidates[0]);
|
||||
};
|
||||
|
||||
// 1. Exact match (fast path)
|
||||
let result = null;
|
||||
try {
|
||||
const p = new URLSearchParams({ artist_name: artist, track_name: title });
|
||||
if(album) p.set('album_name', album);
|
||||
if(dur) p.set('duration', dur);
|
||||
const r = await fetch(`https://lrclib.net/api/get?${p}`, { signal: SIG() });
|
||||
if(r.ok) result = await r.json();
|
||||
} catch {}
|
||||
|
||||
// 2. lrclib artist+title search
|
||||
if(token !== _lyricsLoadToken) return;
|
||||
if(!result?.syncedLyrics && !result?.plainLyrics) {
|
||||
try {
|
||||
const p = new URLSearchParams({ track_name: title });
|
||||
if(artist) p.set('artist_name', artist);
|
||||
const r = await fetch(`https://lrclib.net/api/search?${p}`, { signal: SIG() });
|
||||
if(r.ok) result = bestMatch(await r.json(), dur);
|
||||
} catch {}
|
||||
// Cache hit — instant
|
||||
if(_lyricsCache.has(cacheKey)) {
|
||||
const cached = _lyricsCache.get(cacheKey);
|
||||
content.innerHTML = '';
|
||||
_showLyricsResult(content, cached);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. lrclib title-only search — catches "Utada" vs "Hikaru Utada" type mismatches
|
||||
if(token !== _lyricsLoadToken) return;
|
||||
if(!result?.syncedLyrics && !result?.plainLyrics) {
|
||||
try {
|
||||
const p = new URLSearchParams({ track_name: title });
|
||||
const r = await fetch(`https://lrclib.net/api/search?${p}`, { signal: SIG() });
|
||||
if(r.ok) result = bestMatch(await r.json(), dur);
|
||||
} catch {}
|
||||
}
|
||||
// Fire all three lrclib requests in parallel — total wait = slowest single request
|
||||
const exactP = new URLSearchParams({ artist_name: artist, track_name: title });
|
||||
if(album) exactP.set('album_name', album);
|
||||
if(dur) exactP.set('duration', dur);
|
||||
const artistP = new URLSearchParams({ track_name: title });
|
||||
if(artist) artistP.set('artist_name', artist);
|
||||
const titleP = new URLSearchParams({ track_name: title });
|
||||
|
||||
const [exactData, artistData, titleData] = await Promise.all([
|
||||
safeJson(fetch(`https://lrclib.net/api/get?${exactP}`, { signal: SIG() })),
|
||||
safeJson(fetch(`https://lrclib.net/api/search?${artistP}`, { signal: SIG() })),
|
||||
safeJson(fetch(`https://lrclib.net/api/search?${titleP}`, { signal: SIG() })),
|
||||
]);
|
||||
|
||||
// 4. NetEase fallback — good coverage for Asian/Japanese music
|
||||
if(token !== _lyricsLoadToken) return;
|
||||
|
||||
let result = (exactData?.syncedLyrics || exactData?.plainLyrics ? exactData : null)
|
||||
?? bestMatch(artistData, dur)
|
||||
?? bestMatch(titleData, dur);
|
||||
|
||||
// NetEase fallback — good for Asian/Japanese music
|
||||
if(!result?.syncedLyrics && !result?.plainLyrics) {
|
||||
try {
|
||||
const q = encodeURIComponent(`${artist} ${title}`.trim());
|
||||
const sr = await fetch(`https://music.163.com/api/search/get?s=${q}&type=1&limit=5`, {
|
||||
headers: { Referer: 'https://music.163.com' },
|
||||
signal: SIG(),
|
||||
});
|
||||
if(sr.ok) {
|
||||
const sd = await sr.json();
|
||||
const songs = sd?.result?.songs || [];
|
||||
const durMs = dur * 1000;
|
||||
const song = dur
|
||||
? songs.sort((a, b) => Math.abs(a.duration - durMs) - Math.abs(b.duration - durMs))
|
||||
.find(s => Math.abs(s.duration - durMs) <= 5000)
|
||||
: songs[0];
|
||||
if(song) {
|
||||
if(token !== _lyricsLoadToken) return;
|
||||
const lr = await fetch(
|
||||
`https://music.163.com/api/song/lyric?id=${song.id}&lv=1&kv=1&tv=-1`,
|
||||
{ headers: { Referer: 'https://music.163.com' }, signal: SIG() },
|
||||
);
|
||||
if(lr.ok) {
|
||||
const ld = await lr.json();
|
||||
const lrc = ld?.lrc?.lyric;
|
||||
if(lrc) result = { syncedLyrics: lrc };
|
||||
}
|
||||
}
|
||||
const q = encodeURIComponent(`${artist} ${title}`.trim());
|
||||
const sd = await safeJson(fetch(`https://music.163.com/api/search/get?s=${q}&type=1&limit=5`, {
|
||||
headers: { Referer: 'https://music.163.com' }, signal: SIG(),
|
||||
}));
|
||||
const songs = sd?.result?.songs || [];
|
||||
const durMs = dur * 1000;
|
||||
const song = dur
|
||||
? [...songs].sort((a, b) => Math.abs(a.duration - durMs) - Math.abs(b.duration - durMs))
|
||||
.find(s => Math.abs(s.duration - durMs) <= 5000)
|
||||
: songs[0];
|
||||
if(song && token === _lyricsLoadToken) {
|
||||
const ld = await safeJson(fetch(
|
||||
`https://music.163.com/api/song/lyric?id=${song.id}&lv=1&kv=1&tv=-1`,
|
||||
{ headers: { Referer: 'https://music.163.com' }, signal: SIG() },
|
||||
));
|
||||
const lrc = ld?.lrc?.lyric;
|
||||
if(lrc) result = { syncedLyrics: lrc };
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if(token !== _lyricsLoadToken) return;
|
||||
_lyricsCache.set(cacheKey, result ?? null);
|
||||
content.innerHTML = '';
|
||||
_showLyricsResult(content, result);
|
||||
}
|
||||
|
||||
function _showLyricsResult(content, result) {
|
||||
if(result?.syncedLyrics) {
|
||||
const parsed = _parseLrc(result.syncedLyrics);
|
||||
if(parsed.length) { _renderSyncedLines(content, parsed); return; }
|
||||
|
||||
Reference in new issue
Block a user