Add admin analytics, metadata collection, and grouped lyric cues

Queue reviewable Whisper drafts from the song list and lyrics editor. Preserve line breaks within one timed cue across editing, saving, reporting, and service views.

Add storage and listening analytics with a durable metadata collector, related-search depth, video limits, thumbnail storage, and a browsable metadata library.
This commit is contained in:
Jonathan Sykes
2026-10-03 07:54:11 +08:00
parent 716b61ffee
commit 2b38717c05
23 changed files with 1181 additions and 48 deletions

View File

@@ -84,6 +84,12 @@ describe('pure helpers', () => {
expect(d.offset).toBe(30);
});
test('lyrics retain tight line breaks while other metadata strips controls', () => {
const result = N.sanitizeLyrics({ lines: [{ t: 12, text: ' Because You are God\r\n You can do anything\u0007 ', kind: 'line' }], tags: ['Key\nG'] });
expect(result.lines).toEqual([{ t: 12, text: 'Because You are God\nYou can do anything', kind: 'line' }]);
expect(result.tags).toEqual(['Key G']);
});
test('sanitizeChapters requires time + title and sorts', () => {
const d = N.sanitizeChapters({ items: [{ t: 30, title: 'B' }, { t: 5, title: 'A', note: 'n' }, { t: null, title: 'no time' }] });
expect(d.items).toEqual([{ t: 5, title: 'A', note: 'n' }, { t: 30, title: 'B', note: '' }]);
@@ -245,3 +251,19 @@ describe('routes', () => {
expect(await res.text()).toContain('admin');
});
});
test('saving a grouped cue preserves one timestamp in current and revision documents', async () => {
const id = 'groupedCue1';
const lines = [{ t: 12, text: 'Because You are God\nYou can do anything', kind: 'line' }];
const response = await app.request(`/api/notes/${id}/lyrics`, json('PUT', { data: { lines }, baseRev: 0, profile: 'josh' }));
expect(response.status).toBe(200);
const current = await (await app.request(`/api/notes/${id}`)).json();
expect(current.lyrics.data.lines).toEqual(lines);
const revision = await (await app.request(`/api/notes/${id}/lyrics/revs/1`)).json();
expect(revision.data.lines).toEqual(lines);
const flag = await app.request(`/api/notes/${id}/flags`, json('POST', { text: lines[0].text, profile: 'josh', reason: 'words' }));
expect(flag.status).toBe(200);
const flags = await (await app.request(`/api/notes/${id}/flags`)).json();
expect(flags.flags[0].text).toBe(lines[0].text);
});