mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 12:40:15 +02:00
lyrics support + lookup
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// In-memory UI state for lyrics — one entry per track path, backed by the
|
||||
// cache-first orchestrator (src/lyrics/lyrics.ts). The orchestrator already
|
||||
// dedupes in-flight network work and persists to SQLite; this store adds the
|
||||
// loading flag + last result the now-playing lyrics band renders, plus a small
|
||||
// LRU cap so revisiting tracks stays instant without growing unbounded.
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { buildLyricsQuery, getLyricsForTrack } from '@/lyrics/lyrics';
|
||||
import type { LyricsLookupResult } from '@/lyrics/types';
|
||||
import type { Track } from '@/types/audio';
|
||||
|
||||
const MAX_ENTRIES = 64;
|
||||
|
||||
export interface LyricsUiEntry {
|
||||
loading: boolean;
|
||||
result: LyricsLookupResult | null;
|
||||
}
|
||||
|
||||
interface LyricsStore {
|
||||
onlineEnabled: boolean;
|
||||
byPath: Record<string, LyricsUiEntry>;
|
||||
loadForTrack: (track: Track | null, options?: { force?: boolean }) => Promise<void>;
|
||||
setOnlineEnabled: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
// Latest request id per path — guards against a stale response overwriting a
|
||||
// newer one (e.g. rapid track changes reusing the same store entry).
|
||||
const requestIds = new Map<string, number>();
|
||||
let requestSeq = 0;
|
||||
|
||||
function pruneToLru(byPath: Record<string, LyricsUiEntry>): Record<string, LyricsUiEntry> {
|
||||
const keys = Object.keys(byPath);
|
||||
if (keys.length <= MAX_ENTRIES) return byPath;
|
||||
const next = { ...byPath };
|
||||
for (const key of keys.slice(0, keys.length - MAX_ENTRIES)) {
|
||||
delete next[key];
|
||||
requestIds.delete(key);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export const useLyricsStore = create<LyricsStore>((set, get) => ({
|
||||
onlineEnabled: true,
|
||||
byPath: {},
|
||||
|
||||
loadForTrack: async (track, options = {}) => {
|
||||
if (!track?.path) return;
|
||||
const path = track.path;
|
||||
const force = Boolean(options.force);
|
||||
|
||||
const existing = get().byPath[path];
|
||||
if (!force && existing && (existing.result || existing.loading)) return;
|
||||
|
||||
const requestId = ++requestSeq;
|
||||
requestIds.set(path, requestId);
|
||||
|
||||
const query = buildLyricsQuery(track);
|
||||
if (!query) {
|
||||
set((state) => ({
|
||||
byPath: pruneToLru({
|
||||
...state.byPath,
|
||||
[path]: { loading: false, result: { status: 'not_found', reason: 'embedded-missing' } },
|
||||
}),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
byPath: pruneToLru({
|
||||
...state.byPath,
|
||||
[path]: { loading: true, result: existing?.result ?? null },
|
||||
}),
|
||||
}));
|
||||
|
||||
let result: LyricsLookupResult;
|
||||
try {
|
||||
result = await getLyricsForTrack(query, { forceRefresh: force, onlineEnabled: get().onlineEnabled });
|
||||
} catch (error) {
|
||||
result = {
|
||||
status: 'transient_error',
|
||||
message: error instanceof Error ? error.message : 'Lyrics lookup failed.',
|
||||
};
|
||||
}
|
||||
|
||||
// Drop the response if a newer request for this path superseded it.
|
||||
if (requestIds.get(path) !== requestId) return;
|
||||
|
||||
set((state) => ({
|
||||
byPath: pruneToLru({ ...state.byPath, [path]: { loading: false, result } }),
|
||||
}));
|
||||
},
|
||||
|
||||
setOnlineEnabled: (enabled) => set({ onlineEnabled: enabled }),
|
||||
}));
|
||||
@@ -12,6 +12,7 @@ const ARTIST_GROUPING_KEY = 'artist_grouping_mode';
|
||||
const INCLUDE_SINGLES_KEY = 'album_include_singles';
|
||||
const SCOPE_MODE_KEY = 'scope_mode';
|
||||
const SCOPE_STAGE_VISIBLE_KEY = 'scope_stage_visible';
|
||||
const LYRICS_VISIBLE_KEY = 'lyrics_visible';
|
||||
|
||||
/** Which visualizer the now-playing scope stage shows. */
|
||||
export type ScopeMode = 'spectrum' | 'scope';
|
||||
@@ -34,12 +35,15 @@ interface SettingsStore {
|
||||
includeSingles: boolean;
|
||||
scopeMode: ScopeMode;
|
||||
scopeStageVisible: boolean;
|
||||
/** Whether the now-playing top half shows lyrics instead of art/scope. */
|
||||
lyricsVisible: boolean;
|
||||
loaded: boolean;
|
||||
load: () => Promise<void>;
|
||||
setArtistGroupingMode: (mode: ArtistGroupingMode) => Promise<void>;
|
||||
setIncludeSingles: (include: boolean) => Promise<void>;
|
||||
setScopeMode: (mode: ScopeMode) => Promise<void>;
|
||||
setScopeStageVisible: (visible: boolean) => Promise<void>;
|
||||
setLyricsVisible: (visible: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
@@ -47,22 +51,25 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
includeSingles: false,
|
||||
scopeMode: 'spectrum',
|
||||
scopeStageVisible: false,
|
||||
lyricsVisible: false,
|
||||
loaded: false,
|
||||
|
||||
load: async () => {
|
||||
if (get().loaded) return;
|
||||
const db = await openLibraryDb();
|
||||
const [grouping, includeSingles, scope, scopeStageVisible] = await Promise.all([
|
||||
const [grouping, includeSingles, scope, scopeStageVisible, lyricsVisible] = await Promise.all([
|
||||
getSetting(db, ARTIST_GROUPING_KEY),
|
||||
getSetting(db, INCLUDE_SINGLES_KEY),
|
||||
getSetting(db, SCOPE_MODE_KEY),
|
||||
getSetting(db, SCOPE_STAGE_VISIBLE_KEY),
|
||||
getSetting(db, LYRICS_VISIBLE_KEY),
|
||||
]);
|
||||
set({
|
||||
artistGroupingMode: parseGroupingMode(grouping),
|
||||
includeSingles: parseBoolean(includeSingles),
|
||||
scopeMode: parseScopeMode(scope),
|
||||
scopeStageVisible: parseBoolean(scopeStageVisible),
|
||||
lyricsVisible: parseBoolean(lyricsVisible),
|
||||
loaded: true,
|
||||
});
|
||||
},
|
||||
@@ -94,4 +101,11 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
const db = await openLibraryDb();
|
||||
await setSetting(db, SCOPE_STAGE_VISIBLE_KEY, visible ? 'true' : 'false');
|
||||
},
|
||||
|
||||
setLyricsVisible: async (visible) => {
|
||||
if (get().lyricsVisible === visible) return;
|
||||
set({ lyricsVisible: visible });
|
||||
const db = await openLibraryDb();
|
||||
await setSetting(db, LYRICS_VISIBLE_KEY, visible ? 'true' : 'false');
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user