Add searchable lyrics index
This commit is contained in:
@@ -156,6 +156,52 @@ export function registerAdminToolsRoutes(app, { db, adminAuth, media = null, bac
|
||||
return c.json({ ok: true, minLines, category, offset, limit, total: number(count.rows[0]?.total), songs }, 200, { 'Cache-Control': 'no-store' });
|
||||
});
|
||||
|
||||
app.get('/api/admin/lyrics/search', adminAuth, async c => {
|
||||
const query = String(c.req.query('q') || '').trim().slice(0, 120);
|
||||
const hasLyrics = ['yes', 'no', 'all'].includes(c.req.query('hasLyrics')) ? c.req.query('hasLyrics') : 'yes';
|
||||
const hasChapters = ['yes', 'no', 'all'].includes(c.req.query('hasChapters')) ? c.req.query('hasChapters') : 'all';
|
||||
const limit = Math.max(1, Math.min(50, Number(c.req.query('limit')) || 30));
|
||||
const offset = Math.max(0, Math.min(1_000_000, Number(c.req.query('offset')) || 0));
|
||||
let hits = [], total = 0;
|
||||
if (query && hasLyrics !== 'no') {
|
||||
const chapterOptions = { hasChapters: hasChapters === 'yes', withoutChapters: hasChapters === 'no' };
|
||||
hits = await db.searchNoteLyrics(query, { limit, offset, ...chapterOptions });
|
||||
total = await db.countNoteLyrics(query, chapterOptions);
|
||||
}
|
||||
const terms = query.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().match(/[\p{L}\p{N}_-]+/gu) || [];
|
||||
if (!query || hasLyrics === 'no') {
|
||||
const parts = [];
|
||||
const args = [];
|
||||
if (hasLyrics === 'yes') parts.push("l.video_id IS NOT NULL AND l.data<>'{}'");
|
||||
if (hasLyrics === 'no') parts.push('l.video_id IS NULL');
|
||||
if (hasChapters === 'yes') parts.push("EXISTS(SELECT 1 FROM video_notes ch WHERE ch.video_id=m.id AND ch.kind='chapters')");
|
||||
if (hasChapters === 'no') parts.push("NOT EXISTS(SELECT 1 FROM video_notes ch WHERE ch.video_id=m.id AND ch.kind='chapters')");
|
||||
for (const term of terms) { parts.push('lower(m.hay) LIKE ?'); args.push(`%${term}%`); }
|
||||
const where = parts.length ? `WHERE ${parts.join(' AND ')}` : '';
|
||||
const count = await sql.execute({ sql: `SELECT COUNT(*) AS n FROM video_meta m LEFT JOIN video_notes l ON l.video_id=m.id AND l.kind='lyrics' ${where}`, args });
|
||||
const rows = await sql.execute({ sql: `SELECT m.id,m.card,l.data AS lyrics,l.rev,EXISTS(SELECT 1 FROM video_notes ch WHERE ch.video_id=m.id AND ch.kind='chapters') AS chapters FROM video_meta m LEFT JOIN video_notes l ON l.video_id=m.id AND l.kind='lyrics' ${where} ORDER BY m.updated_at DESC LIMIT ? OFFSET ?`, args: [...args, limit, offset] });
|
||||
total = number(count.rows[0]?.n);
|
||||
hits = rows.rows.map(row => ({ videoId: row.id, snippet: parse(row.lyrics).lines?.slice(0, 3).map(line => line.text).join(' … ') || parse(row.card).title || row.id, rev: number(row.rev), hasChapters: !!row.chapters }));
|
||||
}
|
||||
if (hits.length && query && hasLyrics !== 'no') {
|
||||
const ids = hits.map(item => item.videoId);
|
||||
const rows = await sql.execute({ sql: `SELECT n.video_id,n.data,n.rev,m.card,EXISTS(SELECT 1 FROM video_notes ch WHERE ch.video_id=n.video_id AND ch.kind='chapters') AS chapters FROM video_notes n LEFT JOIN video_meta m ON m.id=n.video_id WHERE n.kind='lyrics' AND n.video_id IN (${ids.map(() => '?').join(',')})`, args: ids });
|
||||
const byId = new Map(rows.rows.map(row => [row.video_id, row]));
|
||||
hits = hits.map(hit => {
|
||||
const row = byId.get(hit.videoId), card = parse(row?.card);
|
||||
return { ...hit, title: card.title || hit.videoId, channel: card.channel || '', rev: number(row?.rev), hasChapters: !!row?.chapters };
|
||||
});
|
||||
}
|
||||
return c.json({ ok: true, query, hasLyrics, hasChapters, total, limit, offset, results: hits }, 200, { 'Cache-Control': 'no-store' });
|
||||
});
|
||||
|
||||
app.post('/api/admin/lyrics/search/rebuild', adminAuth, async c => {
|
||||
if (typeof db.rebuildLyricsSearch !== 'function') return c.json({ ok: false, error: 'Lyrics search rebuild is unavailable.' }, 503);
|
||||
const indexed = await db.rebuildLyricsSearch();
|
||||
await audit(db, 'admin', 'lyrics.search.rebuild', '', { indexed });
|
||||
return c.json({ ok: true, indexed });
|
||||
});
|
||||
|
||||
app.get('/api/admin/cache/health', adminAuth, async c => {
|
||||
const limit = Math.max(1, Math.min(100, Number(c.req.query('limit')) || 50));
|
||||
const offset = Math.max(0, Math.min(1_000_000, Number(c.req.query('offset')) || 0));
|
||||
|
||||
Reference in New Issue
Block a user