Add resumable bulk lyrics operations
This commit is contained in:
@@ -13,13 +13,15 @@ const { registerAdminToolsRoutes } = await import('./admin-tools.js');
|
||||
const app = new Hono();
|
||||
const backupDir = join(root, 'backups');
|
||||
const auth = async (c, next) => c.req.header('x-admin') === 'yes' ? next() : c.json({ ok: false }, 401);
|
||||
let tools;
|
||||
|
||||
beforeAll(async () => {
|
||||
await db.initDb();
|
||||
registerAdminToolsRoutes(app, { db, adminAuth: auth, backupDir, backfillStep: async () => 3 });
|
||||
tools = registerAdminToolsRoutes(app, { db, adminAuth: auth, backupDir, backfillStep: async () => 3,
|
||||
pullLyrics: async () => ({ ok: true, lines: 2 }), queueWhisper: async () => ({ status: 202, body: { ok: true, job: { status: 'queued' } } }), autoStartJobs: false });
|
||||
await db.db.execute({ sql: 'INSERT INTO video_meta(id,card,hay,seen,updated_at) VALUES(?,?,?,?,?)', args: ['QualMiss001', JSON.stringify({ title: 'Missing lyric sample', channel: 'Choir' }), 'missing lyric sample choir', 1, Date.now()] });
|
||||
await db.db.execute({ sql: 'INSERT INTO video_meta(id,card,hay,seen,updated_at) VALUES(?,?,?,?,?)', args: ['Untimely001', JSON.stringify({ title: 'Untimed sample', channel: 'Band' }), 'untimed sample band', 1, Date.now()] });
|
||||
await db.db.execute({ sql: "INSERT INTO video_notes(video_id,kind,data,rev,source) VALUES(?, 'lyrics', ?, 1, 'user')", args: ['Untimely001', JSON.stringify({ lines: [{ t: null, text: 'A line' }, { t: 4, text: 'Next' }] })] });
|
||||
await db.saveNote({ videoId: 'Untimely001', kind: 'lyrics', data: { lines: [{ t: null, text: 'A line' }, { t: 4, text: 'Next' }] }, source: 'user', force: true });
|
||||
await db.db.execute({ sql: "INSERT INTO media_cache(video_id,status,size,gen) VALUES('CacheItem001','ready',700,1),('CacheItem002','ready',400,1)" });
|
||||
await db.db.execute({ sql: 'INSERT INTO video_meta(id,card,hay,seen,updated_at,url) VALUES(?,?,?,?,?,?)', args: ['IndexSong001', JSON.stringify({ title: 'Indexed song' }), 'indexed song', 1, Date.now(), 'https://www.youtube.com/watch?v=IndexSong001'] });
|
||||
await db.db.execute({ sql: "INSERT INTO video_meta_sources(video_id,source,last_seen) VALUES('IndexSong001','deep',?)", args: [Date.now()] });
|
||||
@@ -88,7 +90,7 @@ describe('lyrics quality queue', () => {
|
||||
const restored = await (await app.request(`/api/admin/backups/${created.date}/restore`, { method: 'POST', headers, body: JSON.stringify({ videoId: 'BackupSong01' }) })).json();
|
||||
expect(restored.ok).toBe(true);
|
||||
expect((await db.getNotes('BackupSong01')).lyrics.data.lines[0].text).toBe('Before');
|
||||
expect(existsSync(restored.safetySnapshot)).toBe(true);
|
||||
expect(existsSync(join(backupDir, 'snapshots', restored.safetySnapshot))).toBe(true);
|
||||
const dump = await app.request('/api/admin/backups/export/sqlite-dump', { headers: { 'x-admin': 'yes' } });
|
||||
expect(dump.status).toBe(200);
|
||||
expect(await dump.text()).toContain('CREATE TABLE');
|
||||
@@ -112,4 +114,44 @@ describe('lyrics quality queue', () => {
|
||||
expect(rebuilt.ok).toBe(true);
|
||||
expect(rebuilt.indexed).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test('bulk lyrics jobs page the library, resume, clear with revision undo, export ZIP, and cancel', async () => {
|
||||
const headers = { 'x-admin': 'yes', 'content-type': 'application/json' };
|
||||
expect((await app.request('/api/admin/lyrics/bulk/library')).status).toBe(401);
|
||||
const library = await (await app.request('/api/admin/lyrics/bulk/library?q=untimed', { headers })).json();
|
||||
expect(library.songs[0]).toMatchObject({ id: 'Untimely001', title: 'Untimed sample' });
|
||||
const create = async body => (await app.request('/api/admin/lyrics/jobs', { method: 'POST', headers, body: JSON.stringify(body) })).json();
|
||||
|
||||
const pull = (await create({ action: 'lrclib', ids: ['Untimely001'] })).job;
|
||||
expect((await app.request(`/api/admin/lyrics/jobs/${pull.id}/pause`, { method: 'POST', headers, body: '{}' })).status).toBe(200);
|
||||
expect((await app.request(`/api/admin/lyrics/jobs/${pull.id}/resume`, { method: 'POST', headers, body: '{}' })).status).toBe(200);
|
||||
await tools.runBulkJob();
|
||||
let current = await (await app.request(`/api/admin/lyrics/jobs/${pull.id}`, { headers: { 'x-admin': 'yes' } })).json();
|
||||
expect(current.job).toMatchObject({ status: 'complete', state: { done: 1, results: [{ ok: true, id: 'Untimely001' }] } });
|
||||
|
||||
const clear = (await create({ action: 'clear', ids: ['Untimely001'], confirmClear: true })).job;
|
||||
await tools.runBulkJob();
|
||||
expect((await db.getNotes('Untimely001')).lyrics.data.lines).toEqual([]);
|
||||
expect((await db.getNoteRev('Untimely001', 'lyrics', 1)).data.lines[0].text).toBe('A line');
|
||||
|
||||
await db.db.execute({ sql: 'INSERT INTO video_meta(id,card,hay,seen,updated_at) VALUES(?,?,?,?,?)', args: ['ZipSong0001', JSON.stringify({ title: 'Zip Song' }), 'zip song', 1, Date.now()] });
|
||||
await db.saveNote({ videoId: 'ZipSong0001', kind: 'lyrics', data: { lines: [{ t: 1.2, text: 'Saved line' }] }, source: 'user', force: true });
|
||||
const exportJob = (await create({ action: 'export', ids: ['ZipSong0001'] })).job;
|
||||
await tools.runBulkJob();
|
||||
const archive = await app.request(`/api/admin/lyrics/jobs/${exportJob.id}/export`, { headers: { 'x-admin': 'yes' } });
|
||||
expect(archive.status).toBe(200);
|
||||
expect([...new Uint8Array(await archive.arrayBuffer())].slice(0, 2)).toEqual([0x50, 0x4b]);
|
||||
|
||||
const failingApp = new Hono();
|
||||
const failingTools = registerAdminToolsRoutes(failingApp, { db, adminAuth: auth, backupDir: null, autoStartJobs: false });
|
||||
const failingExport = (await (await failingApp.request('/api/admin/lyrics/jobs', { method: 'POST', headers, body: JSON.stringify({ action: 'export', ids: ['ZipSong0001'] }) })).json()).job;
|
||||
await failingTools.runBulkJob();
|
||||
const failed = await (await failingApp.request(`/api/admin/lyrics/jobs/${failingExport.id}`, { headers: { 'x-admin': 'yes' } })).json();
|
||||
expect(failed.job).toMatchObject({ status: 'failed', error: 'Export storage is unavailable.' });
|
||||
|
||||
const cancelled = (await create({ action: 'whisper', ids: ['Untimely001'] })).job;
|
||||
await app.request(`/api/admin/lyrics/jobs/${cancelled.id}/cancel`, { method: 'POST', headers, body: '{}' });
|
||||
current = await (await app.request(`/api/admin/lyrics/jobs/${cancelled.id}`, { headers: { 'x-admin': 'yes' } })).json();
|
||||
expect(current.job.status).toBe('cancelled');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user