mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 12:40:15 +02:00
artist image search
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { create } from 'zustand';
|
||||
import { AstraLibraryData } from '../../modules/astra-library-scanner';
|
||||
import { useSettingsStore } from './settingsStore';
|
||||
|
||||
interface ArtistImageState {
|
||||
/** True while a sweep is draining the queue. */
|
||||
running: boolean;
|
||||
/** Artists resolved so far in the current sweep — one per provider request. */
|
||||
processed: number;
|
||||
/** Artists queued when the sweep started. 0 means "not counted yet". */
|
||||
total: number;
|
||||
/** Artists with no portrait from any source, refreshed when a sweep settles. */
|
||||
missing: number;
|
||||
beginSweep: (total: number) => void;
|
||||
advanceSweep: (by?: number) => void;
|
||||
endSweep: () => void;
|
||||
refreshMissing: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable progress for the artist-image sweep. The coordinator itself is a
|
||||
* plain module (it runs without any React tree mounted), so it pushes into this
|
||||
* store rather than owning the state — Settings just subscribes.
|
||||
*/
|
||||
export const useArtistImageStore = create<ArtistImageState>((set, get) => ({
|
||||
running: false,
|
||||
processed: 0,
|
||||
total: 0,
|
||||
missing: 0,
|
||||
|
||||
beginSweep: (total) => set({ running: true, processed: 0, total }),
|
||||
|
||||
advanceSweep: (by = 1) => set((state) => ({ processed: state.processed + by })),
|
||||
|
||||
endSweep: () => {
|
||||
set({ running: false, processed: 0, total: 0 });
|
||||
void get().refreshMissing();
|
||||
},
|
||||
|
||||
refreshMissing: async () => {
|
||||
try {
|
||||
const stats = await AstraLibraryData.getArtistImageStats(
|
||||
useSettingsStore.getState().artistGroupingMode,
|
||||
Date.now()
|
||||
);
|
||||
set({ missing: stats.missing });
|
||||
} catch {
|
||||
// A stale count is not worth surfacing an error for.
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type ScanResult,
|
||||
} from '@/library/scanner';
|
||||
import { endScanService, reportScanProgress } from '@/library/scanService';
|
||||
import { requeueMissingArtistImages } from '@/library/artistImageLookup';
|
||||
import { ALBUM_SORT_LABELS, type AlbumSort } from '@/lib/albumSort';
|
||||
import { ARTIST_SORT_LABELS, type ArtistSort } from '@/lib/artistSort';
|
||||
import { TRACK_SORT_LABELS, type TrackSort } from '@/lib/trackSort';
|
||||
@@ -191,6 +192,7 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
||||
artists: 0,
|
||||
};
|
||||
let anchorGeneration = 0;
|
||||
let artistImageRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Re-entrancy guards, per list and per direction, so a backward refill, a forward
|
||||
// page and a different view's load never block one another — the single shared flag
|
||||
@@ -391,6 +393,10 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
||||
} finally {
|
||||
try {
|
||||
await get().refresh();
|
||||
// A scan is the user asking Astra to look at their library again, so it
|
||||
// also re-opens artist-image lookups that previously found no match.
|
||||
// New artists queue on their own; these would never retry otherwise.
|
||||
await requeueMissingArtistImages();
|
||||
} finally {
|
||||
if (activeScanCancellation === cancellation) activeScanCancellation = null;
|
||||
set({
|
||||
@@ -490,6 +496,13 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
||||
set({ sectionAnchors: [] });
|
||||
void get().refresh();
|
||||
});
|
||||
AstraLibraryData.addListener('onArtistImagesChanged', () => {
|
||||
if (artistImageRefreshTimer) clearTimeout(artistImageRefreshTimer);
|
||||
artistImageRefreshTimer = setTimeout(() => {
|
||||
artistImageRefreshTimer = null;
|
||||
void get().refresh();
|
||||
}, 300);
|
||||
});
|
||||
useSettingsStore.subscribe((next, previous) => {
|
||||
if (next.artistGroupingMode !== previous.artistGroupingMode) {
|
||||
anchorGeneration += 1;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
resumeListeningHistoryTracking,
|
||||
} from '@/audio/listeningHistoryTracker';
|
||||
import { notifyListeningHistoryChanged } from '@/listeningStats/events';
|
||||
import type { ArtistImageAutoPolicy } from '@/types/artistImages';
|
||||
|
||||
/**
|
||||
* Persisted app preferences. SQLite (settings table) is the source of truth — this
|
||||
@@ -29,6 +30,8 @@ const LYRICS_VISIBLE_KEY = 'lyrics_visible';
|
||||
const NOW_PLAYING_COMPANION_KEY = 'now_playing_companion';
|
||||
const HOME_GREETING_TEXT_MODE_KEY = 'home_greeting_text_mode';
|
||||
const LISTENING_HISTORY_ENABLED_KEY = 'listening_history_enabled';
|
||||
const ARTIST_IMAGE_AUTO_POLICY_KEY = 'artist_image_auto_policy';
|
||||
const ARTIST_IMAGE_DISCLOSURE_KEY = 'artist_image_disclosure_seen';
|
||||
|
||||
/** Which visualizer the now-playing scope stage shows. */
|
||||
export type ScopeMode = 'spectrum' | 'scope';
|
||||
@@ -56,6 +59,10 @@ function parseBoolean(value: string | null): boolean {
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
function parseArtistImageAutoPolicy(value: string | null): ArtistImageAutoPolicy {
|
||||
return value === 'off' || value === 'any' ? value : 'wifi';
|
||||
}
|
||||
|
||||
interface SettingsStore {
|
||||
artistGroupingMode: ArtistGroupingMode;
|
||||
/** Show 1-track albums in the Albums view (desktop parity default: hidden). */
|
||||
@@ -68,6 +75,8 @@ interface SettingsStore {
|
||||
nowPlayingCompanion: NowPlayingCompanion;
|
||||
homeGreetingTextMode: HomeGreetingTextMode;
|
||||
listeningHistoryEnabled: boolean;
|
||||
artistImageAutoPolicy: ArtistImageAutoPolicy;
|
||||
artistImageDisclosureSeen: boolean;
|
||||
loaded: boolean;
|
||||
load: () => Promise<void>;
|
||||
setArtistGroupingMode: (mode: ArtistGroupingMode) => Promise<void>;
|
||||
@@ -79,6 +88,8 @@ interface SettingsStore {
|
||||
setNowPlayingCompanion: (companion: NowPlayingCompanion) => Promise<void>;
|
||||
setHomeGreetingTextMode: (mode: HomeGreetingTextMode) => Promise<void>;
|
||||
setListeningHistoryEnabled: (enabled: boolean) => Promise<void>;
|
||||
setArtistImageAutoPolicy: (policy: ArtistImageAutoPolicy) => Promise<void>;
|
||||
acknowledgeArtistImageDisclosure: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
@@ -91,6 +102,8 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
nowPlayingCompanion: 'queue',
|
||||
homeGreetingTextMode: 'messages',
|
||||
listeningHistoryEnabled: true,
|
||||
artistImageAutoPolicy: 'wifi',
|
||||
artistImageDisclosureSeen: false,
|
||||
loaded: false,
|
||||
|
||||
load: async () => {
|
||||
@@ -106,6 +119,8 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
NOW_PLAYING_COMPANION_KEY,
|
||||
HOME_GREETING_TEXT_MODE_KEY,
|
||||
LISTENING_HISTORY_ENABLED_KEY,
|
||||
ARTIST_IMAGE_AUTO_POLICY_KEY,
|
||||
ARTIST_IMAGE_DISCLOSURE_KEY,
|
||||
]);
|
||||
const grouping = values[ARTIST_GROUPING_KEY] ?? null;
|
||||
const includeSingles = values[INCLUDE_SINGLES_KEY] ?? null;
|
||||
@@ -116,6 +131,10 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
const nowPlayingCompanion = values[NOW_PLAYING_COMPANION_KEY] ?? null;
|
||||
const homeGreetingTextMode = values[HOME_GREETING_TEXT_MODE_KEY] ?? null;
|
||||
const listeningHistoryEnabled = values[LISTENING_HISTORY_ENABLED_KEY] !== '0';
|
||||
const artistImageAutoPolicy = parseArtistImageAutoPolicy(
|
||||
values[ARTIST_IMAGE_AUTO_POLICY_KEY] ?? null
|
||||
);
|
||||
const artistImageDisclosureSeen = values[ARTIST_IMAGE_DISCLOSURE_KEY] === '1';
|
||||
set({
|
||||
artistGroupingMode: parseGroupingMode(grouping),
|
||||
includeSingles: parseBoolean(includeSingles),
|
||||
@@ -126,6 +145,8 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
nowPlayingCompanion: parseNowPlayingCompanion(nowPlayingCompanion),
|
||||
homeGreetingTextMode: parseHomeGreetingTextMode(homeGreetingTextMode),
|
||||
listeningHistoryEnabled,
|
||||
artistImageAutoPolicy,
|
||||
artistImageDisclosureSeen,
|
||||
loaded: true,
|
||||
});
|
||||
},
|
||||
@@ -194,4 +215,22 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
setArtistImageAutoPolicy: async (policy) => {
|
||||
const previous = get().artistImageAutoPolicy;
|
||||
if (previous === policy) return;
|
||||
set({ artistImageAutoPolicy: policy });
|
||||
try {
|
||||
await AstraLibraryData.setSettings({ [ARTIST_IMAGE_AUTO_POLICY_KEY]: policy });
|
||||
} catch (error) {
|
||||
set({ artistImageAutoPolicy: previous });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
acknowledgeArtistImageDisclosure: async () => {
|
||||
if (get().artistImageDisclosureSeen) return;
|
||||
await AstraLibraryData.setSettings({ [ARTIST_IMAGE_DISCLOSURE_KEY]: '1' });
|
||||
set({ artistImageDisclosureSeen: true });
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user