Fetch lyrics from LRCLIB (synced when available) and serve an admin media library of uploaded video and audio with cover art and embedded lyrics

This commit is contained in:
Jonathan Sykes
2026-09-20 05:59:50 +08:00
parent 9c1ec4ac51
commit b983c6ca3c
14 changed files with 963 additions and 25 deletions

View File

@@ -106,6 +106,36 @@ describe('pure helpers', () => {
expect(N.pickCaptionTrack({ automatic_captions: { es: [{ ext: 'vtt', url: 's' }] } })).toBeNull();
});
test('cleanTitle / cleanArtist strip YouTube noise', () => {
expect(N.cleanTitle('Hosanna - Hillsong Worship (Official Live Video) [HD]')).toBe('Hosanna - Hillsong Worship');
expect(N.cleanTitle('Kay Buti-Buti Mo Panginoon Karaoke | Minus-One | Instrumental')).toBe('Kay Buti-Buti Mo Panginoon');
expect(N.cleanTitle('Still (Lyrics)')).toBe('Still');
expect(N.cleanArtist('Hillsong Worship - Topic')).toBe('Hillsong Worship');
expect(N.cleanArtist('BethelVEVO')).toBe('Bethel');
});
test('parseLrc reads synced and plain lyrics', () => {
const synced = N.parseLrc('[ar:Someone]\n[00:12.50]first line here\n[00:20.00][01:05.25]repeated line\n');
expect(synced).toEqual([
{ t: 12.5, text: 'first line here', kind: 'line' },
{ t: 20, text: 'repeated line', kind: 'line' },
{ t: 65.25, text: 'repeated line', kind: 'line' },
]);
const plain = N.parseLrc('one line\n\nanother line');
expect(plain).toEqual([{ t: null, text: 'one line', kind: 'line' }, { t: null, text: 'another line', kind: 'line' }]);
});
test('pickLrclib prefers a title match, synced lyrics and the closest duration', () => {
const list = [
{ trackName: 'As The Deer', duration: 120, plainLyrics: 'x' },
{ trackName: 'As The Deer', duration: 249, syncedLyrics: '[00:01.00]x', plainLyrics: 'x' },
{ trackName: 'As The Deer (Instrumental)', duration: 248, instrumental: true, plainLyrics: 'x' },
{ trackName: 'Something else', duration: 250, syncedLyrics: '[00:01.00]y' },
];
expect(N.pickLrclib(list, { title: 'As the Deer', duration: 249 }).duration).toBe(249);
expect(N.pickLrclib(list, { title: 'No such song', duration: 249 })).toBeNull();
});
test('admin cookie signature and expiry', () => {
const v = N.signAdminCookie('k', 2000);
expect(N.verifyAdminCookie('k', v, 1000)).toBe(true);