Add a paged lyrics quality queue
This commit is contained in:
@@ -542,6 +542,21 @@
|
||||
<div id="repList"></div>
|
||||
</section>
|
||||
|
||||
<section id="p-quality" class="panel">
|
||||
<h1 class="page">Lyrics quality</h1>
|
||||
<p class="lead">Work the songs that need attention: missing, short, machine-drafted, untimed, or reported lyrics.</p>
|
||||
<div class="card">
|
||||
<form id="qualityFilterForm" class="row">
|
||||
<label>Issue<select class="field" id="qualityCategory"><option value="all">All issues</option><option value="missing">No lyrics</option><option value="short">Fewer than minimum lines</option><option value="automatic">Auto-transcribed, unreviewed</option><option value="untimed">Untimed lines</option><option value="reports">Open reports</option></select></label>
|
||||
<label>Minimum lines<input class="field" id="qualityMinLines" type="number" min="1" max="200" value="8" /></label>
|
||||
<button class="btn pri" type="submit">Update queue</button><span class="grow"></span><span id="qualityCount" class="muted" role="status" aria-live="polite"></span>
|
||||
</form>
|
||||
<p id="qualityStatus" class="msg" role="status" aria-live="polite"></p>
|
||||
<div class="scroll"><table class="tbl" id="qualityTable"></table></div>
|
||||
<div class="row"><button class="btn" id="qualityPrev" type="button">Previous</button><button class="btn" id="qualityNext" type="button">Next</button><span class="grow"></span><span class="muted">50 songs per page</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================= LIBRARY ================= -->
|
||||
<section id="p-library" class="panel">
|
||||
<h1 class="page">Library</h1>
|
||||
@@ -733,6 +748,7 @@
|
||||
const ICONS = {
|
||||
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"/>',
|
||||
library: '<path d="M9 18V6l10-2v12"/><circle cx="6.5" cy="18" r="2.5"/><circle cx="16.5" cy="16" r="2.5"/>',
|
||||
peers: '<circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="6" r="2.5"/><circle cx="18" cy="18" r="2.5"/><path d="M8.3 10.9l7.4-3.8M8.3 13.1l7.4 3.8"/>',
|
||||
history: '<path d="M3 12a9 9 0 109-9 9 9 0 00-6.4 2.6L3 8"/><path d="M3 3v5h5M12 7v5l3 2"/>',
|
||||
@@ -741,7 +757,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'], ['reports', 'Reports'], ['library', 'Library'], ['peers', 'P2P'], ['history', 'History'], ['analytics', 'Stats'], ['deep-search', 'Deep search'], ['audit', 'Audit log'], ['access', 'API']];
|
||||
const TABS = [['editor', 'Editor'], ['quality', 'Lyrics queue'], ['reports', 'Reports'], ['library', 'Library'], ['peers', 'P2P'], ['history', 'History'], ['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('');
|
||||
@@ -756,6 +772,7 @@
|
||||
if (push) history.replaceState(null, '', location.pathname + location.search + '#' + name);
|
||||
syncBars();
|
||||
if (name === 'reports') loadReports();
|
||||
if (name === 'quality') loadLyricsQuality();
|
||||
if (name === 'history') loadRevs();
|
||||
if (name === 'peers') loadP2p();
|
||||
if (name === 'library') loadUploads();
|
||||
@@ -945,6 +962,37 @@
|
||||
$('auditFilterForm').addEventListener('submit', event => { event.preventDefault(); auditOffset = 0; loadAudit(); });
|
||||
$('auditPrev').addEventListener('click', () => { auditOffset = Math.max(0, auditOffset - 50); loadAudit(); });
|
||||
$('auditNext').addEventListener('click', () => { auditOffset += 50; loadAudit(); });
|
||||
|
||||
/* ---------------- lyrics quality queue ---------------- */
|
||||
let qualityOffset = 0;
|
||||
async function loadLyricsQuality() {
|
||||
const table = $('qualityTable'); table.innerHTML = '<tr><td class="muted">Loading quality queue…</td></tr>';
|
||||
const category = $('qualityCategory').value, minLines = Number($('qualityMinLines').value) || 8;
|
||||
try {
|
||||
const j = await api(`/api/admin/lyrics/quality?category=${encodeURIComponent(category)}&minLines=${minLines}&limit=50&offset=${qualityOffset}`);
|
||||
if (!j.ok) throw Error(j.error || 'Could not load lyrics queue.');
|
||||
$('qualityCount').textContent = `${j.total.toLocaleString()} songs · ${j.songs.length ? qualityOffset + 1 : 0}–${qualityOffset + j.songs.length}`;
|
||||
$('qualityPrev').disabled = qualityOffset === 0; $('qualityNext').disabled = qualityOffset + j.songs.length >= j.total;
|
||||
table.innerHTML = j.songs.length ? `<tr><th>Song</th><th>Lyrics</th><th>Why here</th><th>Actions</th></tr>${j.songs.map(song => `<tr><td><b>${esc(song.title)}</b><br><small>${esc(song.channel || song.id)}</small></td><td>${song.lines} lines${song.source === 'auto' ? ' · auto draft' : ''}${song.untimed ? ' · untimed' : ''}</td><td>${esc(song.reasons.join(' · '))}${song.openReports ? ` · ${song.openReports} reports` : ''}</td><td><div class="row"><button class="btn sm pri" data-quality-fix="${esc(song.id)}">Fix</button><button class="btn sm" data-quality-pull="${esc(song.id)}">Pull LRCLIB</button><button class="btn sm" data-quality-whisper="${esc(song.id)}">Queue Whisper</button></div></td></tr>`).join('')}` : '<tr><td class="muted">No songs match this filter. Try All issues or reduce the minimum line count.</td></tr>';
|
||||
} catch (error) { table.innerHTML = `<tr><td class="err">${esc(error.message)}</td></tr>`; }
|
||||
}
|
||||
$('qualityFilterForm').addEventListener('submit', event => { event.preventDefault(); qualityOffset = 0; loadLyricsQuality(); });
|
||||
$('qualityPrev').addEventListener('click', () => { qualityOffset = Math.max(0, qualityOffset - 50); loadLyricsQuality(); });
|
||||
$('qualityNext').addEventListener('click', () => { qualityOffset += 50; loadLyricsQuality(); });
|
||||
$('qualityTable').addEventListener('click', async event => {
|
||||
const button = event.target.closest('button'); if (!button) return;
|
||||
const id = button.dataset.qualityFix || button.dataset.qualityPull || button.dataset.qualityWhisper;
|
||||
if (button.dataset.qualityFix) { leOpen(id); return; }
|
||||
$('qualityStatus').className = 'msg'; $('qualityStatus').textContent = button.dataset.qualityPull ? 'Looking up LRCLIB…' : 'Adding to the Whisper queue…';
|
||||
try {
|
||||
const result = button.dataset.qualityPull
|
||||
? await api(`/api/notes/${encodeURIComponent(id)}/lyrics/web`, { method: 'POST', body: {} })
|
||||
: await api(`/api/admin/transcriptions/${encodeURIComponent(id)}`, { method: 'POST', body: {} });
|
||||
if (!result.ok) throw Error(result.error || 'Action failed.');
|
||||
$('qualityStatus').className = 'msg ok'; $('qualityStatus').textContent = button.dataset.qualityPull ? `LRCLIB saved revision ${result.rev}.` : `Whisper ${result.job?.status || 'job queued'}.`;
|
||||
loadLyricsQuality();
|
||||
} catch (error) { $('qualityStatus').className = 'msg err'; $('qualityStatus').textContent = error.message; }
|
||||
});
|
||||
$('tokenForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const label = $('tokenLabel').value.trim();
|
||||
|
||||
Reference in New Issue
Block a user