Files
chrisandClaude Opus 5.5 b705bfcce4 Fix a gap under Friends listening, and lyrics failing while LRCLIB's exact lookup is overloaded
The feed's itemLayoutAnimation left the cards behind when the header above them shrank
(your own card's lyrics loading). LRCLIB rate-limits /api/get on its own; fall back to search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
2026-10-15 13:16:18 -04:00

83 lines
3.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { upsertTrack } from '../src/services/tracks';
import { json, routeFetch, useTestApp } from './helpers';
const app = useTestApp();
afterEach(() => vi.unstubAllGlobals());
const record = (over: object = {}) => ({
id: 70627,
trackName: 'Midnight City',
artistName: 'M83',
duration: 241,
instrumental: false,
plainLyrics: 'Waiting in a car\nWaiting for a ride in the dark',
syncedLyrics: '[00:01.00] Waiting in a car\n[00:04.50] Waiting for a ride in the dark',
...over,
});
const lyrics = (id: string) => app().inject({ url: `/tracks/${id}/lyrics` });
describe('lyrics', () => {
it('looks up synced lyrics once and serves them from the cache after', async () => {
const { id } = await upsertTrack({ title: 'Midnight City', artist: 'M83', durationMs: 241_000 });
const calls: string[] = [];
vi.stubGlobal('fetch', routeFetch([[/lrclib\.net\/api\/get\?/, (url) => (calls.push(url), json(record()))]]));
const first = (await lyrics(id)).json();
expect(first.synced).toEqual([
{ timeMs: 1000, text: 'Waiting in a car' },
{ timeMs: 4500, text: 'Waiting for a ride in the dark' },
]);
expect(first.source.name).toBe('LRCLIB');
expect(new URL(calls[0]!).searchParams.get('duration')).toBe('241');
expect((await lyrics(id)).json()).toEqual(first);
expect(calls).toHaveLength(1);
});
it('falls back to a search, skipping recordings of a different length', async () => {
const { id } = await upsertTrack({ title: 'Song', artist: 'Band', durationMs: 200_000 });
vi.stubGlobal(
'fetch',
routeFetch([
[/\/api\/get\?/, () => json({ code: 404 }, 404)],
[
/\/api\/search\?/,
() =>
json([
record({ id: 1, trackName: 'Song', artistName: 'Band', duration: 320, plainLyrics: 'extended mix' }),
record({ id: 2, trackName: 'Song', artistName: 'Band', duration: 201, syncedLyrics: null, plainLyrics: 'the one' }),
]),
],
]),
);
const body = (await lyrics(id)).json();
expect(body.synced).toBeNull();
expect(body.plain).toBe('the one');
});
it("uses the search when LRCLIB's exact lookup is rate-limited", async () => {
const { id } = await upsertTrack({ title: 'Midnight City', artist: 'M83', durationMs: 241_000 });
vi.stubGlobal(
'fetch',
routeFetch([
[/\/api\/get\?/, () => json({ message: 'slow down' }, 429)],
[/\/api\/search\?/, () => json([record()])],
]),
);
const res = await lyrics(id);
expect(res.statusCode).toBe(200);
expect(res.json().synced).toHaveLength(2);
});
it('returns null when nothing matches, and 503 when LRCLIB is down', async () => {
const { id } = await upsertTrack({ title: 'Unknown', artist: 'Nobody', durationMs: 100_000 });
vi.stubGlobal('fetch', routeFetch([[/lrclib/, () => json({ message: 'down' }, 500)]]));
expect((await lyrics(id)).statusCode).toBe(503);
vi.stubGlobal('fetch', routeFetch([[/\/api\/get\?/, () => json({}, 404)], [/\/api\/search\?/, () => json([])]]));
const res = await lyrics(id);
expect(res.statusCode).toBe(200);
expect(res.json()).toBeNull();
expect((await lyrics('not-a-uuid')).statusCode).toBe(404);
});
});