mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
@@ -1,11 +1,14 @@
|
||||
import { getArtists } from '@/lib/api/subsonicArtists';
|
||||
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
|
||||
import type { ArtistCreditMode } from '@/lib/api/library';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { dedupeById } from '@/lib/util/dedupeById';
|
||||
import {
|
||||
fetchLocalArtistCatalogChunk,
|
||||
fetchNetworkStarredArtists,
|
||||
} from '@/lib/library/browseTextSearch';
|
||||
import {
|
||||
fetchNetworkArtistCatalog,
|
||||
fetchStarredArtistsForBrowse,
|
||||
} from '@/features/artist/utils/artistBrowseCreditMode';
|
||||
import { useOfflineBrowseContext } from '@/features/offline';
|
||||
import { useOfflineBrowseReloadToken } from '@/features/offline';
|
||||
import {
|
||||
@@ -23,6 +26,8 @@ export type UseArtistsBrowseCatalogArgs = {
|
||||
serverId: string | null | undefined;
|
||||
indexEnabled: boolean;
|
||||
starredOnly: boolean;
|
||||
creditMode: ArtistCreditMode;
|
||||
letterFilter: string;
|
||||
musicLibraryFilterVersion: number;
|
||||
};
|
||||
|
||||
@@ -30,6 +35,8 @@ export function useArtistsBrowseCatalog({
|
||||
serverId,
|
||||
indexEnabled,
|
||||
starredOnly,
|
||||
creditMode,
|
||||
letterFilter,
|
||||
musicLibraryFilterVersion,
|
||||
}: UseArtistsBrowseCatalogArgs) {
|
||||
const offlineBrowseActive = useOfflineBrowseContext().active;
|
||||
@@ -56,8 +63,14 @@ export function useArtistsBrowseCatalog({
|
||||
serverId,
|
||||
catalogOffsetRef.current,
|
||||
ARTIST_CATALOG_CHUNK_SIZE,
|
||||
creditMode,
|
||||
letterFilter,
|
||||
);
|
||||
if (generation !== loadGenerationRef.current || chunk == null) return;
|
||||
if (generation !== loadGenerationRef.current) return;
|
||||
if (chunk == null) {
|
||||
if (append) setCatalogHasMore(false);
|
||||
return;
|
||||
}
|
||||
if (append) {
|
||||
setCatalogArtists(prev => {
|
||||
const merged = dedupeById([...prev, ...chunk.artists]);
|
||||
@@ -75,8 +88,14 @@ export function useArtistsBrowseCatalog({
|
||||
serverId,
|
||||
catalogOffsetRef.current,
|
||||
ARTIST_CATALOG_CHUNK_SIZE,
|
||||
creditMode,
|
||||
letterFilter,
|
||||
);
|
||||
if (generation !== loadGenerationRef.current || chunk == null) return;
|
||||
if (generation !== loadGenerationRef.current) return;
|
||||
if (chunk == null) {
|
||||
if (append) setCatalogHasMore(false);
|
||||
return;
|
||||
}
|
||||
if (append) {
|
||||
setCatalogArtists(prev => {
|
||||
const merged = dedupeById([...prev, ...chunk.artists]);
|
||||
@@ -95,7 +114,7 @@ export function useArtistsBrowseCatalog({
|
||||
setCatalogLoadingMore(false);
|
||||
}
|
||||
}
|
||||
}, [offlineBrowseActive, serverId]);
|
||||
}, [creditMode, letterFilter, offlineBrowseActive, serverId]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -115,12 +134,20 @@ export function useArtistsBrowseCatalog({
|
||||
if (offlineBrowseActive) {
|
||||
if (!cancelled && generation === loadGenerationRef.current) {
|
||||
if (serverId && starredOnly && offlineLocalBrowseEnabled(serverId)) {
|
||||
setCatalogArtists((await fetchOfflineLocalStarredArtists(serverId)) ?? []);
|
||||
try {
|
||||
setCatalogArtists(
|
||||
await fetchStarredArtistsForBrowse(creditMode, serverId, true),
|
||||
);
|
||||
} catch {
|
||||
setCatalogArtists((await fetchOfflineLocalStarredArtists(serverId)) ?? []);
|
||||
}
|
||||
} else if (serverId && !starredOnly && offlineLocalBrowseEnabled(serverId)) {
|
||||
const first = await fetchOfflineLocalArtistCatalogChunk(
|
||||
serverId,
|
||||
0,
|
||||
ARTIST_CATALOG_CHUNK_SIZE,
|
||||
creditMode,
|
||||
letterFilter,
|
||||
);
|
||||
setCatalogArtists(first?.artists ?? []);
|
||||
catalogOffsetRef.current = first?.artists.length ?? 0;
|
||||
@@ -135,7 +162,11 @@ export function useArtistsBrowseCatalog({
|
||||
}
|
||||
if (starredOnly) {
|
||||
if (!cancelled && generation === loadGenerationRef.current) {
|
||||
setCatalogArtists(await fetchNetworkStarredArtists());
|
||||
setCatalogArtists(
|
||||
await fetchStarredArtistsForBrowse(creditMode, serverId, indexEnabled),
|
||||
);
|
||||
setBrowseMode('network');
|
||||
setCatalogHasMore(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -144,6 +175,8 @@ export function useArtistsBrowseCatalog({
|
||||
serverId,
|
||||
0,
|
||||
ARTIST_CATALOG_CHUNK_SIZE,
|
||||
creditMode,
|
||||
letterFilter,
|
||||
);
|
||||
if (cancelled || generation !== loadGenerationRef.current) return;
|
||||
if (first != null) {
|
||||
@@ -155,12 +188,12 @@ export function useArtistsBrowseCatalog({
|
||||
}
|
||||
}
|
||||
if (!cancelled && generation === loadGenerationRef.current) {
|
||||
setCatalogArtists(await getArtists());
|
||||
setCatalogArtists(await fetchNetworkArtistCatalog(creditMode));
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
} finally {
|
||||
if (!cancelled && generation === loadGenerationRef.current) {
|
||||
if (generation === loadGenerationRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
@@ -169,7 +202,7 @@ export function useArtistsBrowseCatalog({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [musicLibraryFilterVersion, indexEnabled, offlineBrowseActive, offlineBrowseReloadTs, serverId, starredOnly]);
|
||||
}, [creditMode, letterFilter, musicLibraryFilterVersion, indexEnabled, offlineBrowseActive, offlineBrowseReloadTs, serverId, starredOnly]);
|
||||
|
||||
return {
|
||||
catalogArtists,
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
isArtistsBrowsePath,
|
||||
useArtistBrowseSessionStore,
|
||||
} from '@/features/artist/store/artistBrowseSessionStore';
|
||||
import type { ArtistCreditMode } from '@/lib/api/library';
|
||||
import { isArtistDetailPath } from '@/features/album';
|
||||
import { ALL_SENTINEL } from '@/features/artist/utils/artistsHelpers';
|
||||
import { shouldRestoreArtistBrowseSession } from '@/lib/navigation/albumDetailNavigation';
|
||||
import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
|
||||
|
||||
@@ -38,11 +40,13 @@ export function useArtistsBrowseFilters(
|
||||
const navigationType = useNavigationType();
|
||||
const location = useLocation();
|
||||
const setShowArtistImages = useAuthStore(s => s.setShowArtistImages);
|
||||
const creditMode = useAuthStore(s => s.artistBrowseCreditMode);
|
||||
const setArtistBrowseCreditMode = useAuthStore(s => s.setArtistBrowseCreditMode);
|
||||
|
||||
const [letterFilter, setLetterFilter] = useState(
|
||||
() => returnStateForNavigation(serverId, navigationType, location.state).letterFilter,
|
||||
);
|
||||
const [starredOnly, setStarredOnly] = useState(
|
||||
const [starredOnly, setStarredOnlyRaw] = useState(
|
||||
() => returnStateForNavigation(serverId, navigationType, location.state).starredOnly,
|
||||
);
|
||||
const [viewMode, setViewMode] = useState<ArtistBrowseViewMode>(
|
||||
@@ -59,10 +63,22 @@ export function useArtistsBrowseFilters(
|
||||
filter: useLiveSearchScopeStore.getState().query,
|
||||
letterFilter,
|
||||
starredOnly,
|
||||
creditMode,
|
||||
viewMode,
|
||||
showArtistImages,
|
||||
};
|
||||
|
||||
const setCreditMode = (next: ArtistCreditMode) => {
|
||||
if (next === creditMode) return;
|
||||
setArtistBrowseCreditMode(next);
|
||||
setLetterFilter(ALL_SENTINEL);
|
||||
};
|
||||
|
||||
const setStarredOnly = (next: boolean) => {
|
||||
if (next) useLiveSearchScopeStore.getState().setQuery('');
|
||||
setStarredOnlyRaw(next);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
restoredFromStashRef.current = false;
|
||||
}, [serverId]);
|
||||
@@ -78,7 +94,8 @@ export function useArtistsBrowseFilters(
|
||||
// React Compiler set-state-in-effect rule: local state synced with store/prop inputs when the effect’s dependencies change.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setLetterFilter(restored.letterFilter);
|
||||
setStarredOnly(restored.starredOnly);
|
||||
setStarredOnlyRaw(restored.starredOnly);
|
||||
setArtistBrowseCreditMode(restored.creditMode ?? 'album');
|
||||
setViewMode(restored.viewMode);
|
||||
setShowArtistImages(restored.showArtistImages);
|
||||
}
|
||||
@@ -90,9 +107,9 @@ export function useArtistsBrowseFilters(
|
||||
useArtistBrowseSessionStore.getState().clearReturnStash(serverId);
|
||||
useLiveSearchScopeStore.getState().setQuery('');
|
||||
setLetterFilter(DEFAULT_ARTIST_BROWSE_RETURN_STATE.letterFilter);
|
||||
setStarredOnly(false);
|
||||
setStarredOnlyRaw(false);
|
||||
setViewMode('grid');
|
||||
}, [serverId, navigationType, location.state, setShowArtistImages]);
|
||||
}, [serverId, navigationType, location.state, setShowArtistImages, setArtistBrowseCreditMode]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -119,6 +136,8 @@ export function useArtistsBrowseFilters(
|
||||
setLetterFilter,
|
||||
starredOnly,
|
||||
setStarredOnly,
|
||||
creditMode,
|
||||
setCreditMode,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
|
||||
import type { ArtistCreditMode } from '@/lib/api/library';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
BROWSE_TEXT_DEBOUNCE_NETWORK_MS,
|
||||
@@ -23,6 +24,8 @@ export function useBrowseArtistTextSearch(
|
||||
indexEnabled: boolean,
|
||||
serverId: string | null | undefined,
|
||||
surface: LibrarySearchSurface = 'artists_browse',
|
||||
creditMode: ArtistCreditMode = 'album',
|
||||
starredOnly = false,
|
||||
) {
|
||||
const offlineBrowseActive = useOfflineBrowseContext().active;
|
||||
const [debouncedFilter, setDebouncedFilter] = useState('');
|
||||
@@ -38,7 +41,7 @@ export function useBrowseArtistTextSearch(
|
||||
|
||||
useEffect(() => {
|
||||
const q = debouncedFilter;
|
||||
if (!q || !indexEnabled || !serverId) {
|
||||
if (starredOnly || !q || !indexEnabled || !serverId) {
|
||||
// React Compiler set-state-in-effect rule: state set from a timer/animation callback.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setTextSearchArtists(null);
|
||||
@@ -53,7 +56,7 @@ export function useBrowseArtistTextSearch(
|
||||
void (async () => {
|
||||
if (offlineBrowseActive) {
|
||||
const artists = offlineLocalBrowseEnabled(serverId)
|
||||
? await searchOfflineLocalArtists(serverId, q)
|
||||
? await searchOfflineLocalArtists(serverId, q, creditMode)
|
||||
: [];
|
||||
if (isStale()) return;
|
||||
setTextSearchArtists(artists);
|
||||
@@ -62,8 +65,8 @@ export function useBrowseArtistTextSearch(
|
||||
}
|
||||
const outcome = await raceBrowseWithLocalFallback(
|
||||
isStale,
|
||||
() => runLocalBrowseArtists(serverId, q),
|
||||
() => runNetworkBrowseArtists(q),
|
||||
() => runLocalBrowseArtists(serverId, q, creditMode),
|
||||
() => runNetworkBrowseArtists(q, creditMode),
|
||||
{
|
||||
surface,
|
||||
query: q,
|
||||
@@ -75,7 +78,7 @@ export function useBrowseArtistTextSearch(
|
||||
setTextSearchArtists(outcome?.result ?? null);
|
||||
setTextSearchLoading(false);
|
||||
})();
|
||||
}, [debouncedFilter, indexEnabled, offlineBrowseActive, serverId, surface]);
|
||||
}, [creditMode, debouncedFilter, indexEnabled, offlineBrowseActive, serverId, starredOnly, surface]);
|
||||
|
||||
const effectiveFilter = textSearchArtists != null ? '' : filter;
|
||||
return { textSearchArtists, textSearchLoading, effectiveFilter };
|
||||
|
||||
@@ -36,6 +36,7 @@ import { useArtistsBrowseScrollRestore } from '@/features/artist/hooks/useArtist
|
||||
import { useArtistsBrowseScrollReset } from '@/features/artist/hooks/useArtistsBrowseScrollReset';
|
||||
import { useNavigateToArtist } from '@/features/artist/hooks/useNavigateToArtist';
|
||||
import { peekArtistBrowseScrollRestore } from '@/features/artist/store/artistBrowseSessionStore';
|
||||
import { nextArtistCreditMode } from '@/features/artist/utils/artistBrowseCreditMode';
|
||||
import { readArtistBrowseRestore } from '@/lib/navigation/albumDetailNavigation';
|
||||
|
||||
import { useScopedBrowseSearchQuery } from '@/store/liveSearchScopeStore';
|
||||
@@ -58,6 +59,8 @@ export default function Artists() {
|
||||
setLetterFilter,
|
||||
starredOnly,
|
||||
setStarredOnly,
|
||||
creditMode,
|
||||
setCreditMode,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
} = useArtistsBrowseFilters(serverId, scrollSnapshotRef);
|
||||
@@ -90,6 +93,8 @@ export default function Artists() {
|
||||
serverId,
|
||||
indexEnabled,
|
||||
starredOnly,
|
||||
creditMode,
|
||||
letterFilter,
|
||||
musicLibraryFilterVersion,
|
||||
});
|
||||
|
||||
@@ -97,10 +102,13 @@ export default function Artists() {
|
||||
artistsSearchQuery,
|
||||
indexEnabled,
|
||||
serverId,
|
||||
'artists_browse',
|
||||
creditMode,
|
||||
starredOnly,
|
||||
);
|
||||
const artists = textSearchArtists ?? catalogArtists;
|
||||
const loading = catalogLoading || textSearchLoading;
|
||||
const textSearchActive = textSearchArtists != null;
|
||||
const artists = starredOnly ? catalogArtists : (textSearchArtists ?? catalogArtists);
|
||||
const loading = starredOnly ? catalogLoading : (catalogLoading || textSearchLoading);
|
||||
const textSearchActive = !starredOnly && textSearchArtists != null;
|
||||
/** Scoped/plain text filter — canonical CSS grid, not row virtualization (small result sets). */
|
||||
const artistBrowsePlainLayout =
|
||||
perfFlags.disableMainstageVirtualLists
|
||||
@@ -113,7 +121,7 @@ export default function Artists() {
|
||||
loadMore: sliceLoadMore,
|
||||
} = useClientSliceInfiniteScroll({
|
||||
pageSize: PAGE_SIZE,
|
||||
resetDeps: [artistsSearchQuery, letterFilter, starredOnly, viewMode, musicLibraryFilterVersion, serverId],
|
||||
resetDeps: [artistsSearchQuery, letterFilter, starredOnly, creditMode, viewMode, musicLibraryFilterVersion, serverId],
|
||||
getScrollRoot: getArtistsScrollRoot,
|
||||
scrollRootEl: artistsScrollBodyEl,
|
||||
restoreDisplayCount: restoreVisibleCountRef.current,
|
||||
@@ -294,6 +302,7 @@ export default function Artists() {
|
||||
artistsSearchQuery,
|
||||
letterFilter,
|
||||
starredOnly,
|
||||
creditMode,
|
||||
viewMode,
|
||||
serverId,
|
||||
musicLibraryFilterVersion,
|
||||
@@ -331,7 +340,37 @@ export default function Artists() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
{!(selectionMode && selectedIds.size > 0) && (<>
|
||||
<StarFilterButton size="compact" active={starredOnly} onChange={setStarredOnly} />
|
||||
<StarFilterButton
|
||||
size="compact"
|
||||
active={starredOnly}
|
||||
onChange={setStarredOnly}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-surface${creditMode === 'track' ? ' btn-sort-active' : ''}`}
|
||||
onClick={() => setCreditMode(nextArtistCreditMode(creditMode))}
|
||||
data-tooltip={
|
||||
creditMode === 'album'
|
||||
? t('artists.browse.creditMode.tooltipTrack')
|
||||
: t('artists.browse.creditMode.tooltipAlbum')
|
||||
}
|
||||
data-tooltip-wrap
|
||||
data-tooltip-pos="bottom"
|
||||
aria-label={
|
||||
creditMode === 'album'
|
||||
? t('artists.browse.creditMode.tooltipTrack')
|
||||
: t('artists.browse.creditMode.tooltipAlbum')
|
||||
}
|
||||
style={
|
||||
creditMode === 'track'
|
||||
? { background: 'var(--accent)', color: 'var(--text-on-accent)' }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{creditMode === 'album'
|
||||
? t('artists.browse.creditMode.track')
|
||||
: t('artists.browse.creditMode.album')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-surface`}
|
||||
onClick={() => setShowArtistImages(!showArtistImages)}
|
||||
|
||||
@@ -24,6 +24,7 @@ describe('artistBrowseSessionStore', () => {
|
||||
filter: 'mozart',
|
||||
letterFilter: 'M',
|
||||
starredOnly: false,
|
||||
creditMode: 'album',
|
||||
viewMode: 'list',
|
||||
showArtistImages: true,
|
||||
scrollTop: 240,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from 'zustand';
|
||||
import { ALL_SENTINEL } from '@/features/artist/utils/artistsHelpers';
|
||||
import type { ArtistCreditMode } from '@/lib/api/library';
|
||||
|
||||
export type ArtistBrowseViewMode = 'grid' | 'list';
|
||||
|
||||
@@ -8,6 +9,7 @@ export interface ArtistBrowseReturnState {
|
||||
filter: string;
|
||||
letterFilter: string;
|
||||
starredOnly: boolean;
|
||||
creditMode: ArtistCreditMode;
|
||||
viewMode: ArtistBrowseViewMode;
|
||||
showArtistImages: boolean;
|
||||
scrollTop?: number;
|
||||
@@ -18,6 +20,7 @@ export const DEFAULT_ARTIST_BROWSE_RETURN_STATE: ArtistBrowseReturnState = {
|
||||
filter: '',
|
||||
letterFilter: ALL_SENTINEL,
|
||||
starredOnly: false,
|
||||
creditMode: 'album',
|
||||
viewMode: 'grid',
|
||||
showArtistImages: true,
|
||||
};
|
||||
@@ -41,6 +44,7 @@ export const useArtistBrowseSessionStore = create<ArtistBrowseSessionStore>((set
|
||||
filter: state.filter,
|
||||
letterFilter: state.letterFilter,
|
||||
starredOnly: state.starredOnly,
|
||||
creditMode: state.creditMode,
|
||||
viewMode: state.viewMode,
|
||||
showArtistImages: state.showArtistImages,
|
||||
...(typeof state.scrollTop === 'number' ? { scrollTop: state.scrollTop } : {}),
|
||||
@@ -65,6 +69,7 @@ export const useArtistBrowseSessionStore = create<ArtistBrowseSessionStore>((set
|
||||
filter: stash.filter,
|
||||
letterFilter: stash.letterFilter,
|
||||
starredOnly: stash.starredOnly,
|
||||
creditMode: stash.creditMode ?? 'album',
|
||||
viewMode: stash.viewMode,
|
||||
showArtistImages: stash.showArtistImages,
|
||||
...(typeof stash.scrollTop === 'number' ? { scrollTop: stash.scrollTop } : {}),
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { getArtists } from '@/lib/api/subsonicArtists';
|
||||
import { getStarred } from '@/lib/api/subsonicStarRating';
|
||||
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
|
||||
import { libraryAdvancedSearch } from '@/lib/api/library';
|
||||
import type { ArtistCreditMode } from '@/lib/api/library';
|
||||
import { libraryScopeForServer } from '@/lib/api/subsonicClient';
|
||||
import { libraryIsReady } from '@/lib/library/libraryReady';
|
||||
import { ndListArtistsByRole } from '@/lib/api/navidromeBrowse';
|
||||
|
||||
/** Network artist catalog before local index is ready (#1209). */
|
||||
export async function fetchNetworkArtistCatalog(
|
||||
creditMode: ArtistCreditMode,
|
||||
): Promise<SubsonicArtist[]> {
|
||||
if (creditMode === 'track') {
|
||||
try {
|
||||
return await ndListArtistsByRole('performer', 0, 10_000);
|
||||
} catch {
|
||||
return getArtists();
|
||||
}
|
||||
}
|
||||
return getArtists();
|
||||
}
|
||||
|
||||
/** Artist ids in the current credit-mode catalog (local index). */
|
||||
async function fetchLocalArtistIdsForMode(
|
||||
serverId: string,
|
||||
creditMode: ArtistCreditMode,
|
||||
): Promise<Set<string> | null> {
|
||||
if (!(await libraryIsReady(serverId))) return null;
|
||||
try {
|
||||
const resp = await libraryAdvancedSearch({
|
||||
serverId,
|
||||
libraryScope: libraryScopeForServer(serverId) ?? undefined,
|
||||
entityTypes: ['artist'],
|
||||
artistCreditMode: creditMode,
|
||||
limit: 100_000,
|
||||
offset: 0,
|
||||
skipTotals: true,
|
||||
});
|
||||
if (resp.source !== 'local') return null;
|
||||
return new Set(resp.artists.map(a => a.id));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNetworkArtistIdsForMode(
|
||||
creditMode: ArtistCreditMode,
|
||||
): Promise<Set<string>> {
|
||||
return new Set((await fetchNetworkArtistCatalog(creditMode)).map(a => a.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* `getStarred2` artist slice intersected with the active credit-mode catalog
|
||||
* (album-artist index or full track performer set) — works in both modes (#1209).
|
||||
*/
|
||||
export async function fetchStarredArtistsForBrowse(
|
||||
creditMode: ArtistCreditMode,
|
||||
serverId: string | null | undefined,
|
||||
indexEnabled: boolean,
|
||||
): Promise<SubsonicArtist[]> {
|
||||
const { artists: starredRaw } = await getStarred();
|
||||
const starred = starredRaw.map(a => ({ ...a, starred: a.starred ?? 'true' }));
|
||||
if (starred.length === 0) return starred;
|
||||
|
||||
let scopeIds: Set<string> | null = null;
|
||||
if (indexEnabled && serverId) {
|
||||
scopeIds = await fetchLocalArtistIdsForMode(serverId, creditMode);
|
||||
}
|
||||
if (!scopeIds) {
|
||||
try {
|
||||
scopeIds = await fetchNetworkArtistIdsForMode(creditMode);
|
||||
} catch {
|
||||
return starred;
|
||||
}
|
||||
}
|
||||
return starred.filter(a => scopeIds!.has(a.id));
|
||||
}
|
||||
|
||||
export function nextArtistCreditMode(mode: ArtistCreditMode): ArtistCreditMode {
|
||||
return mode === 'album' ? 'track' : 'album';
|
||||
}
|
||||
|
||||
export type { ArtistCreditMode };
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LibraryTrackDto } from '@/lib/api/library';
|
||||
import type { ArtistCreditMode, LibraryTrackDto } from '@/lib/api/library';
|
||||
import { libraryAdvancedSearch, libraryGetTracksBatchChunked, libraryGetTracksByAlbum } from '@/lib/api/library';
|
||||
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
|
||||
@@ -181,12 +181,49 @@ export async function fetchOfflineLocalStarredArtists(serverId: string): Promise
|
||||
return aggregateArtistsFromTracks(tracks, serverId);
|
||||
}
|
||||
|
||||
async function fetchOfflineLocalArtistCatalogFromIndex(
|
||||
serverId: string,
|
||||
offset: number,
|
||||
chunkSize: number,
|
||||
creditMode: ArtistCreditMode,
|
||||
letterBucket?: string | null,
|
||||
): Promise<{ artists: SubsonicArtist[]; hasMore: boolean } | null> {
|
||||
const bucket = letterBucket && letterBucket !== 'ALL' ? letterBucket : undefined;
|
||||
try {
|
||||
const resp = await libraryAdvancedSearch({
|
||||
serverId,
|
||||
entityTypes: ['artist'],
|
||||
artistCreditMode: creditMode,
|
||||
...(bucket ? { artistLetterBucket: bucket } : {}),
|
||||
sort: [{ field: 'name', dir: 'asc' }],
|
||||
limit: chunkSize,
|
||||
offset,
|
||||
skipTotals: true,
|
||||
});
|
||||
if (resp.source !== 'local') return null;
|
||||
const artists = resp.artists.map(artistToArtist).map(a => ({ ...a, serverId }));
|
||||
return { artists, hasMore: artists.length === chunkSize };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchOfflineLocalArtistCatalogChunk(
|
||||
serverId: string,
|
||||
offset: number,
|
||||
chunkSize: number,
|
||||
creditMode: ArtistCreditMode = 'album',
|
||||
letterBucket?: string | null,
|
||||
): Promise<{ artists: SubsonicArtist[]; hasMore: boolean } | null> {
|
||||
if (!offlineLocalBrowseEnabled(serverId)) return null;
|
||||
const fromIndex = await fetchOfflineLocalArtistCatalogFromIndex(
|
||||
serverId,
|
||||
offset,
|
||||
chunkSize,
|
||||
creditMode,
|
||||
letterBucket,
|
||||
);
|
||||
if (fromIndex) return fromIndex;
|
||||
const tracks = await fetchBrowsableLocalTrackDtos(serverId);
|
||||
const artists = aggregateArtistsFromTracks(tracks, serverId);
|
||||
const slice = artists.slice(offset, offset + chunkSize);
|
||||
@@ -199,10 +236,30 @@ export async function fetchOfflineLocalArtistCatalogChunk(
|
||||
export async function searchOfflineLocalArtists(
|
||||
serverId: string,
|
||||
query: string,
|
||||
creditMode: ArtistCreditMode = 'album',
|
||||
): Promise<SubsonicArtist[] | null> {
|
||||
if (!offlineLocalBrowseEnabled(serverId)) return null;
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return [];
|
||||
try {
|
||||
const resp = await libraryAdvancedSearch({
|
||||
serverId,
|
||||
entityTypes: ['artist'],
|
||||
artistCreditMode: creditMode,
|
||||
query: q,
|
||||
limit: 500,
|
||||
offset: 0,
|
||||
skipTotals: true,
|
||||
});
|
||||
if (resp.source === 'local') {
|
||||
return resp.artists
|
||||
.map(artistToArtist)
|
||||
.map(a => ({ ...a, serverId }))
|
||||
.filter(a => a.name.toLowerCase().includes(q));
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
const tracks = await fetchBrowsableLocalTrackDtos(serverId);
|
||||
return aggregateArtistsFromTracks(tracks, serverId)
|
||||
.filter(a => a.name.toLowerCase().includes(q));
|
||||
|
||||
Reference in New Issue
Block a user