Add searchable lyrics index
This commit is contained in:
@@ -558,6 +558,14 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="p-lyric-search" class="panel">
|
||||
<h1 class="page">Lyrics search</h1>
|
||||
<p class="lead">Search the saved lyric text, then open a matching song directly in the editor.</p>
|
||||
<div class="card"><form id="lyricsSearchForm" class="analytics-form"><label class="query">Words in lyrics<input class="field" id="lyricsSearchQuery" type="search" maxlength="120" placeholder="grace will lead me home" /></label><label>Lyrics<select class="field" id="lyricsSearchHasLyrics"><option value="yes">With lyrics</option><option value="no">Without lyrics · title search</option><option value="all">All</option></select></label><label>Chapters<select class="field" id="lyricsSearchHasChapters"><option value="all">Any</option><option value="yes">Has chapters</option><option value="no">No chapters</option></select></label><button class="btn pri" type="submit">Search</button><button class="btn" id="lyricsSearchRebuild" type="button">Rebuild text index</button></form>
|
||||
<p id="lyricsSearchStatus" class="msg" role="status" aria-live="polite"></p><div class="scroll"><table class="tbl" id="lyricsSearchTable"></table></div><div class="row"><button class="btn" id="lyricsSearchPrev" type="button">Previous</button><button class="btn" id="lyricsSearchNext" type="button">Next</button><span class="grow"></span><span class="muted" id="lyricsSearchCount"></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="p-cache" class="panel">
|
||||
<h1 class="page">Cache health</h1>
|
||||
<p class="lead">Review server copies by state and recency. Pinned copies stay out of automatic eviction.</p>
|
||||
@@ -778,6 +786,7 @@
|
||||
editor: '<path d="M4 20h4L19 9a2.8 2.8 0 00-4-4L4 16v4z"/><path d="M13.5 6.5l4 4"/>',
|
||||
reports: '<path d="M5 21V4M5 4h12l-2.5 4L17 12H5"/>',
|
||||
quality: '<path d="M4 5h16M4 12h16M4 19h10"/><path d="m17 18 2 2 4-5"/>',
|
||||
'lyric-search': '<circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.5 15.5 5 5M8 10.5h5M10.5 8v5"/>',
|
||||
cache: '<ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3"/>',
|
||||
index: '<path d="M4 19V5M4 19h16M8 15l3-4 3 2 5-7"/><circle cx="19" cy="6" r="1.5"/>',
|
||||
library: '<path d="M9 18V6l10-2v12"/><circle cx="6.5" cy="18" r="2.5"/><circle cx="16.5" cy="16" r="2.5"/>',
|
||||
@@ -789,7 +798,7 @@
|
||||
audit: '<path d="M5 4h14v16H5zM8 8h8M8 12h8M8 16h5"/>',
|
||||
access: '<circle cx="8" cy="15" r="4"/><path d="M11 12l9-9M16 7l3 3M14 9l2 2"/>',
|
||||
};
|
||||
const TABS = [['editor', 'Editor'], ['quality', 'Lyrics queue'], ['cache', 'Cache'], ['index', 'Search index'], ['reports', 'Reports'], ['library', 'Library'], ['peers', 'P2P'], ['history', 'History'], ['backups', 'Backups'], ['analytics', 'Stats'], ['deep-search', 'Deep search'], ['audit', 'Audit log'], ['access', 'API']];
|
||||
const TABS = [['editor', 'Editor'], ['quality', 'Lyrics queue'], ['lyric-search', 'Lyrics search'], ['cache', 'Cache'], ['index', 'Search index'], ['reports', 'Reports'], ['library', 'Library'], ['peers', 'P2P'], ['history', 'History'], ['backups', 'Backups'], ['analytics', 'Stats'], ['deep-search', 'Deep search'], ['audit', 'Audit log'], ['access', 'API']];
|
||||
let tab = 'editor';
|
||||
function buildTabs() {
|
||||
const html = TABS.map(([k, l]) => `<button class="tab" role="tab" data-tab="${k}" aria-selected="false"><svg viewBox="0 0 24 24">${ICONS[k]}</svg><span>${l}</span>${k === 'reports' ? '<span class="badge hidden" data-badge>0</span>' : ''}</button>`).join('');
|
||||
@@ -805,6 +814,7 @@
|
||||
syncBars();
|
||||
if (name === 'reports') loadReports();
|
||||
if (name === 'quality') loadLyricsQuality();
|
||||
if (name === 'lyric-search') loadLyricsSearch();
|
||||
if (name === 'cache') loadCacheHealth();
|
||||
if (name === 'index') loadIndexHealth();
|
||||
if (name === 'history') loadRevs();
|
||||
@@ -1066,6 +1076,34 @@
|
||||
} catch (error) { $('qualityStatus').className = 'msg err'; $('qualityStatus').textContent = error.message; }
|
||||
});
|
||||
|
||||
/* ---------------- full-text lyrics search ---------------- */
|
||||
let lyricsSearchOffset = 0;
|
||||
function lyricsSearchQuery() {
|
||||
const params = new URLSearchParams({ q: $('lyricsSearchQuery').value.trim(), hasLyrics: $('lyricsSearchHasLyrics').value, hasChapters: $('lyricsSearchHasChapters').value, limit: '30', offset: String(lyricsSearchOffset) });
|
||||
return params.toString();
|
||||
}
|
||||
const safeSnippet = value => esc(value || '').replaceAll('\u0001', '<mark>').replaceAll('\u0002', '</mark>');
|
||||
async function loadLyricsSearch() {
|
||||
const table = $('lyricsSearchTable'); table.innerHTML = '<tr><td class="muted">Searching saved lyrics…</td></tr>';
|
||||
try {
|
||||
const result = await api('/api/admin/lyrics/search?' + lyricsSearchQuery()); if (!result.ok) throw Error(result.error || 'Lyrics search failed.');
|
||||
table.innerHTML = result.results.length ? `<tr><th>Song</th><th>Match</th><th>Chapters</th><th></th></tr>${result.results.map(row => `<tr><td><b>${esc(row.title || row.videoId)}</b><br><small>${esc(row.channel || row.videoId)}</small></td><td>${safeSnippet(row.snippet)}</td><td>${row.hasChapters ? 'Yes' : 'No'}</td><td><button class="btn sm pri" data-lyrics-search-open="${esc(row.videoId)}">Open editor</button></td></tr>`).join('')}` : '<tr><td class="muted">No matching notes found.</td></tr>';
|
||||
$('lyricsSearchCount').textContent = `${result.total.toLocaleString()} matches · ${result.results.length ? lyricsSearchOffset + 1 : 0}–${lyricsSearchOffset + result.results.length}`;
|
||||
$('lyricsSearchPrev').disabled = lyricsSearchOffset === 0; $('lyricsSearchNext').disabled = lyricsSearchOffset + result.results.length >= result.total;
|
||||
$('lyricsSearchStatus').textContent = result.query ? `Searching “${result.query}” · ${result.hasLyrics} lyrics · ${result.hasChapters} chapters` : 'Showing saved lyrics. Enter words to search inside them.';
|
||||
} catch (error) { table.innerHTML = `<tr><td class="err">${esc(error.message)}</td></tr>`; }
|
||||
}
|
||||
$('lyricsSearchForm').addEventListener('submit', event => { event.preventDefault(); lyricsSearchOffset = 0; loadLyricsSearch(); });
|
||||
$('lyricsSearchPrev').addEventListener('click', () => { lyricsSearchOffset = Math.max(0, lyricsSearchOffset - 30); loadLyricsSearch(); });
|
||||
$('lyricsSearchNext').addEventListener('click', () => { lyricsSearchOffset += 30; loadLyricsSearch(); });
|
||||
$('lyricsSearchTable').addEventListener('click', event => { const button = event.target.closest('[data-lyrics-search-open]'); if (button) leOpen(button.dataset.lyricsSearchOpen); });
|
||||
$('lyricsSearchRebuild').addEventListener('click', async () => {
|
||||
$('lyricsSearchStatus').textContent = 'Rebuilding the lyric text index…';
|
||||
const result = await api('/api/admin/lyrics/search/rebuild', { method: 'POST', body: {} });
|
||||
$('lyricsSearchStatus').textContent = result.ok ? `Indexed ${result.indexed} saved lyric documents.` : result.error || 'Index rebuild failed.';
|
||||
if (result.ok) loadLyricsSearch();
|
||||
});
|
||||
|
||||
/* ---------------- cache health and pinning ---------------- */
|
||||
let cacheOffset = 0;
|
||||
async function loadCacheHealth() {
|
||||
|
||||
Reference in New Issue
Block a user