From b705bfcce46899f42ae4100c02f3dec5aa05ed08 Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 15 Oct 2026 13:16:18 -0400 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP --- apps/api/src/services/lyrics.ts | 6 ++---- apps/api/test/lyrics.test.ts | 14 ++++++++++++++ apps/mobile/src/app/(tabs)/index.tsx | 6 +++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/apps/api/src/services/lyrics.ts b/apps/api/src/services/lyrics.ts index 776e2c1..ddf06fd 100644 --- a/apps/api/src/services/lyrics.ts +++ b/apps/api/src/services/lyrics.ts @@ -65,10 +65,8 @@ async function lookup(track: { title: string; artist: string; album: string | nu if (track.durationMs) { const params = new URLSearchParams({ track_name: track.title, artist_name: artist, duration: String(Math.round(track.durationMs / 1000)) }); if (track.album) params.set('album_name', track.album); - const exact = await fetchJson(`${env.LRCLIB_URL}/api/get?${params}`, { headers }).catch((e: unknown) => { - if (e instanceof HttpError && e.status === 404) return null; - throw e; - }); + // Not found, or LRCLIB's exact lookup is overloaded (it rate-limits /api/get separately): try the search. + const exact = await fetchJson(`${env.LRCLIB_URL}/api/get?${params}`, { headers }).catch(() => null); if (exact) return exact; } const params = new URLSearchParams({ track_name: track.title, artist_name: artist }); diff --git a/apps/api/test/lyrics.test.ts b/apps/api/test/lyrics.test.ts index 5b8b1e4..9be33b0 100644 --- a/apps/api/test/lyrics.test.ts +++ b/apps/api/test/lyrics.test.ts @@ -55,6 +55,20 @@ describe('lyrics', () => { 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)]])); diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index 0cf410b..35a94c0 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -1,7 +1,7 @@ import { router } from 'expo-router'; import { useEffect, useState } from 'react'; import { Pressable, RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native'; -import Animated, { FadeIn, LinearTransition } from 'react-native-reanimated'; +import Animated, { FadeIn } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; import useSWR, { useSWRConfig } from 'swr'; import { sortFeed, type ConnectionDTO, type FeedItemDTO, type FollowListDTO, type ProfileDTO } from '@musicshare/shared'; @@ -85,9 +85,9 @@ export default function FeedScreen() { return ( - {/* Cards fade in and slide to new positions when someone pins or starts playing. */} + {/* No itemLayoutAnimation: it leaves the cards behind when the header above them changes height + (e.g. your own card's lyrics loading), opening a gap under "Friends listening". */} item.user.id} contentContainerStyle={styles.list}