9d88d70be0
Full-screen overlay expanded from the persistent player: big artwork or a Lyrics tab. Synced (LRC) lyrics highlight the active line by playback position, auto-scroll, and seek on tap; plain text is the fallback; missing lyrics offers a re-fetch. Adds lyrics endpoint + LRC parser + isNowPlayingOpen player state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
50 lines
1.5 KiB
TypeScript
50 lines
1.5 KiB
TypeScript
import { api } from '../index';
|
|
import type { Lyrics } from '../types';
|
|
|
|
// `GET /tracks/{id}/lyrics` — cached lyrics for the Now Playing panel (§6.7).
|
|
// A miss is a normal 200 with `status: 'not_found'`, not an error.
|
|
|
|
interface RawLyrics {
|
|
track_id: string;
|
|
status: string;
|
|
source: string | null;
|
|
synced: string | null;
|
|
plain: string | null;
|
|
synced_available: boolean;
|
|
}
|
|
|
|
const toLyrics = (r: RawLyrics): Lyrics => ({
|
|
trackId: r.track_id,
|
|
status: r.status as Lyrics['status'],
|
|
source: r.source ?? undefined,
|
|
synced: r.synced ?? undefined,
|
|
plain: r.plain ?? undefined,
|
|
syncedAvailable: r.synced_available,
|
|
});
|
|
|
|
export const lyricsApi = api.injectEndpoints({
|
|
endpoints: (build) => ({
|
|
getLyrics: build.query<Lyrics, string>({
|
|
query: (trackId) => `/tracks/${trackId}/lyrics`,
|
|
transformResponse: (raw: RawLyrics) => toLyrics(raw),
|
|
}),
|
|
refetchLyrics: build.mutation<Lyrics, string>({
|
|
query: (trackId) => ({
|
|
url: `/tracks/${trackId}/lyrics/refetch`,
|
|
method: 'POST',
|
|
}),
|
|
transformResponse: (raw: RawLyrics) => toLyrics(raw),
|
|
// Push the fresh result straight into the getLyrics cache for this track.
|
|
async onQueryStarted(trackId, { dispatch, queryFulfilled }) {
|
|
const { data } = await queryFulfilled;
|
|
dispatch(
|
|
lyricsApi.util.upsertQueryData('getLyrics', trackId, data),
|
|
);
|
|
},
|
|
}),
|
|
}),
|
|
overrideExisting: false,
|
|
});
|
|
|
|
export const { useGetLyricsQuery, useRefetchLyricsMutation } = lyricsApi;
|