Add a lyrics-only service mode view with the sung line centred and auto-sized, and a free local faster-whisper script to transcribe saved songs
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
* POST /api/admin/login | /api/admin/logout, GET /api/admin/me
|
||||
* GET|POST /api/admin/tokens, DELETE /api/admin/tokens/:id
|
||||
* GET /api/admin/notes/recent, GET /api/admin/notes/export
|
||||
* GET /api/admin/media (admin or token) saved videos + lyrics status
|
||||
* POST /api/admin/notes/:id/:kind/restore { rev }
|
||||
* GET /admin → admin.html
|
||||
* ========================================================================== */
|
||||
@@ -436,6 +437,32 @@ export function registerNoteRoutes(app, deps) {
|
||||
return ok ? c.json({ ok: true }) : c.json({ ok: false, error: 'not found' }, 404);
|
||||
});
|
||||
|
||||
// Admin cookie OR an API token — for scripts (e.g. scripts/lyrics).
|
||||
const requireAdminOrToken = async (c, next) => {
|
||||
const who = await resolveWriter(c, {});
|
||||
if (!who || who.invalid || who.via === 'user') return c.json({ ok: false, error: 'API token or admin login required' }, 401);
|
||||
await next();
|
||||
};
|
||||
|
||||
// Saved (server-cached) videos with whether each already has lyrics — the
|
||||
// work list for batch lyric injection.
|
||||
app.get('/api/admin/media', requireAdminOrToken, async (c) => {
|
||||
const rows = (await db.listMedia()).filter((r) => r.status === 'ready');
|
||||
const out = [];
|
||||
for (const r of rows) {
|
||||
let meta = {};
|
||||
try { meta = JSON.parse(r.meta || '{}'); } catch { /* corrupt meta */ }
|
||||
const n = await db.getNotes(r.video_id);
|
||||
out.push({
|
||||
id: r.video_id, title: meta.title || '', channel: meta.channel || meta.uploader || '',
|
||||
duration: Number(r.duration) || 0, lastAccess: Number(r.last_access) || 0,
|
||||
lyricsRev: n.lyrics ? n.lyrics.rev : 0, lyricsLines: n.lyrics ? n.lyrics.data.lines.length : 0,
|
||||
});
|
||||
}
|
||||
out.sort((a, b) => b.lastAccess - a.lastAccess);
|
||||
return c.json({ ok: true, media: out });
|
||||
});
|
||||
|
||||
app.get('/api/admin/notes/recent', requireAdmin, async (c) => c.json({ ok: true, revs: await db.recentNoteRevs(150) }));
|
||||
|
||||
app.post('/api/admin/notes/:id/:kind/restore', requireAdmin, async (c) => {
|
||||
|
||||
@@ -191,6 +191,10 @@ describe('routes', () => {
|
||||
const recent = await (await app.request('/api/admin/notes/recent', { headers: { Cookie: cookie } })).json();
|
||||
expect(recent.revs[0].rev).toBe(4);
|
||||
|
||||
// Media work list: token or admin only.
|
||||
expect((await app.request('/api/admin/media')).status).toBe(401);
|
||||
expect((await app.request('/api/admin/media', { headers: auth })).status).toBe(200);
|
||||
|
||||
// Revoked tokens stop working.
|
||||
await app.request(`/api/admin/tokens/${created.id}`, { method: 'DELETE', headers: { Cookie: cookie } });
|
||||
res = await app.request(`/api/notes/${VID}/lyrics`, json('PUT', { data: { lines: [] }, baseRev: 4 }, auth));
|
||||
|
||||
Reference in New Issue
Block a user