mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
70f0641439
Adds library scoping per server: users can select one Navidrome music
folder or 'All' from a new sidebar dropdown. Choice is persisted per
server ID in localStorage. A musicLibraryFilterVersion counter triggers
refetches across browsing pages when the scope changes.
- getMusicFolders() + libraryFilterParams() in subsonic.ts
- New fields in authStore: musicFolders, musicLibraryFilterByServer,
musicLibraryFilterVersion, setMusicFolders, setMusicLibraryFilter
- Sidebar dropdown via createPortal; collapses to icon in narrow mode
- API scoping applied to: getAlbumList2, getRandomSongs, getArtists,
getStarred2, search3
- Full i18n coverage (en, de, fr, nl, zh, nb, ru)
Review fixes applied (co-authored):
- Sidebar: title={} → data-tooltip + data-tooltip-pos='right'
- authStore: setMusicFolders merged into single set() call
- Locales: removed dead libraryScopeHint key from all 7 files
- Genres.tsx: removed musicLibraryFilterVersion dep (getGenres unscoped)
Co-authored-by: cucadmuh <cucadmuh@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
298 lines
11 KiB
TypeScript
298 lines
11 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { usePlayerStore } from './playerStore';
|
|
|
|
export interface ServerProfile {
|
|
id: string;
|
|
name: string;
|
|
url: string;
|
|
username: string;
|
|
password: string;
|
|
}
|
|
|
|
interface AuthState {
|
|
// Multi-server
|
|
servers: ServerProfile[];
|
|
activeServerId: string | null;
|
|
|
|
// Last.fm (global)
|
|
lastfmApiKey: string;
|
|
lastfmApiSecret: string;
|
|
lastfmSessionKey: string;
|
|
lastfmUsername: string;
|
|
|
|
// Settings (global)
|
|
scrobblingEnabled: boolean;
|
|
maxCacheMb: number;
|
|
downloadFolder: string;
|
|
offlineDownloadDir: string;
|
|
excludeAudiobooks: boolean;
|
|
customGenreBlacklist: string[];
|
|
replayGainEnabled: boolean;
|
|
replayGainMode: 'track' | 'album';
|
|
crossfadeEnabled: boolean;
|
|
crossfadeSecs: number;
|
|
gaplessEnabled: boolean;
|
|
preloadMode: 'balanced' | 'early' | 'custom';
|
|
preloadCustomSeconds: number;
|
|
infiniteQueueEnabled: boolean;
|
|
showArtistImages: boolean;
|
|
showTrayIcon: boolean;
|
|
minimizeToTray: boolean;
|
|
discordRichPresence: boolean;
|
|
nowPlayingEnabled: boolean;
|
|
lyricsServerFirst: boolean;
|
|
showFullscreenLyrics: boolean;
|
|
showChangelogOnUpdate: boolean;
|
|
lastSeenChangelogVersion: string;
|
|
|
|
/** Alpha: native hi-res sample rate output (disabled = safe 44.1 kHz mode) */
|
|
enableHiRes: boolean;
|
|
|
|
/** Alpha: ephemeral queue prefetch cache on disk */
|
|
hotCacheEnabled: boolean;
|
|
hotCacheMaxMb: number;
|
|
hotCacheDebounceSec: number;
|
|
/** Parent directory; actual cache is `<dir>/psysonic-hot-cache/`. Empty = app data. */
|
|
hotCacheDownloadDir: string;
|
|
|
|
/** Subsonic music folders for the active server (not persisted; refetched on login / server change). */
|
|
musicFolders: Array<{ id: string; name: string }>;
|
|
/**
|
|
* Per server: `all` = no musicFolderId param; otherwise a single folder id.
|
|
* Only one library or all — no multi-folder merge.
|
|
*/
|
|
musicLibraryFilterByServer: Record<string, 'all' | string>;
|
|
/** Bumps when `setMusicLibraryFilter` runs so pages refetch catalog data. */
|
|
musicLibraryFilterVersion: number;
|
|
|
|
// Status
|
|
isLoggedIn: boolean;
|
|
isConnecting: boolean;
|
|
connectionError: string | null;
|
|
lastfmSessionError: boolean;
|
|
|
|
// Actions
|
|
addServer: (profile: Omit<ServerProfile, 'id'>) => string;
|
|
updateServer: (id: string, data: Partial<Omit<ServerProfile, 'id'>>) => void;
|
|
removeServer: (id: string) => void;
|
|
setActiveServer: (id: string) => void;
|
|
setLoggedIn: (v: boolean) => void;
|
|
setConnecting: (v: boolean) => void;
|
|
setConnectionError: (e: string | null) => void;
|
|
setLastfm: (apiKey: string, apiSecret: string, sessionKey: string, username: string) => void;
|
|
connectLastfm: (sessionKey: string, username: string) => void;
|
|
disconnectLastfm: () => void;
|
|
setLastfmSessionError: (v: boolean) => void;
|
|
setScrobblingEnabled: (v: boolean) => void;
|
|
setMaxCacheMb: (v: number) => void;
|
|
setDownloadFolder: (v: string) => void;
|
|
setOfflineDownloadDir: (v: string) => void;
|
|
setExcludeAudiobooks: (v: boolean) => void;
|
|
setCustomGenreBlacklist: (v: string[]) => void;
|
|
setReplayGainEnabled: (v: boolean) => void;
|
|
setReplayGainMode: (v: 'track' | 'album') => void;
|
|
setCrossfadeEnabled: (v: boolean) => void;
|
|
setCrossfadeSecs: (v: number) => void;
|
|
setGaplessEnabled: (v: boolean) => void;
|
|
setPreloadMode: (v: 'balanced' | 'early' | 'custom') => void;
|
|
setPreloadCustomSeconds: (v: number) => void;
|
|
setInfiniteQueueEnabled: (v: boolean) => void;
|
|
setShowArtistImages: (v: boolean) => void;
|
|
setShowTrayIcon: (v: boolean) => void;
|
|
setMinimizeToTray: (v: boolean) => void;
|
|
setDiscordRichPresence: (v: boolean) => void;
|
|
setNowPlayingEnabled: (v: boolean) => void;
|
|
setLyricsServerFirst: (v: boolean) => void;
|
|
setShowFullscreenLyrics: (v: boolean) => void;
|
|
setShowChangelogOnUpdate: (v: boolean) => void;
|
|
setLastSeenChangelogVersion: (v: string) => void;
|
|
setEnableHiRes: (v: boolean) => void;
|
|
setHotCacheEnabled: (v: boolean) => void;
|
|
setHotCacheMaxMb: (v: number) => void;
|
|
setHotCacheDebounceSec: (v: number) => void;
|
|
setHotCacheDownloadDir: (v: string) => void;
|
|
setMusicFolders: (folders: Array<{ id: string; name: string }>) => void;
|
|
setMusicLibraryFilter: (folderId: 'all' | string) => void;
|
|
logout: () => void;
|
|
|
|
// Derived
|
|
getBaseUrl: () => string;
|
|
getActiveServer: () => ServerProfile | undefined;
|
|
}
|
|
|
|
function generateId(): string {
|
|
return Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
}
|
|
|
|
export const useAuthStore = create<AuthState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
servers: [],
|
|
activeServerId: null,
|
|
lastfmApiKey: '',
|
|
lastfmApiSecret: '',
|
|
lastfmSessionKey: '',
|
|
lastfmUsername: '',
|
|
scrobblingEnabled: true,
|
|
maxCacheMb: 500,
|
|
downloadFolder: '',
|
|
offlineDownloadDir: '',
|
|
excludeAudiobooks: false,
|
|
customGenreBlacklist: [],
|
|
replayGainEnabled: false,
|
|
replayGainMode: 'track',
|
|
crossfadeEnabled: false,
|
|
crossfadeSecs: 3,
|
|
gaplessEnabled: false,
|
|
preloadMode: 'balanced',
|
|
preloadCustomSeconds: 30,
|
|
infiniteQueueEnabled: false,
|
|
showArtistImages: false,
|
|
showTrayIcon: true,
|
|
minimizeToTray: false,
|
|
discordRichPresence: false,
|
|
nowPlayingEnabled: false,
|
|
lyricsServerFirst: true,
|
|
showFullscreenLyrics: true,
|
|
showChangelogOnUpdate: true,
|
|
lastSeenChangelogVersion: '',
|
|
enableHiRes: false,
|
|
hotCacheEnabled: false,
|
|
hotCacheMaxMb: 256,
|
|
hotCacheDebounceSec: 30,
|
|
hotCacheDownloadDir: '',
|
|
musicFolders: [],
|
|
musicLibraryFilterByServer: {},
|
|
musicLibraryFilterVersion: 0,
|
|
isLoggedIn: false,
|
|
isConnecting: false,
|
|
connectionError: null,
|
|
lastfmSessionError: false,
|
|
|
|
addServer: (profile) => {
|
|
const id = generateId();
|
|
set(s => ({ servers: [...s.servers, { ...profile, id }] }));
|
|
return id;
|
|
},
|
|
|
|
updateServer: (id, data) => {
|
|
set(s => ({
|
|
servers: s.servers.map(srv => srv.id === id ? { ...srv, ...data } : srv),
|
|
}));
|
|
},
|
|
|
|
removeServer: (id) => {
|
|
set(s => {
|
|
const newServers = s.servers.filter(srv => srv.id !== id);
|
|
const switchedAway = s.activeServerId === id;
|
|
return {
|
|
servers: newServers,
|
|
activeServerId: switchedAway ? (newServers[0]?.id ?? null) : s.activeServerId,
|
|
isLoggedIn: switchedAway ? false : s.isLoggedIn,
|
|
};
|
|
});
|
|
},
|
|
|
|
setActiveServer: (id) => set({ activeServerId: id, musicFolders: [] }),
|
|
|
|
setLoggedIn: (v) => set({ isLoggedIn: v }),
|
|
setConnecting: (v) => set({ isConnecting: v }),
|
|
setConnectionError: (e) => set({ connectionError: e }),
|
|
|
|
setLastfm: (apiKey, apiSecret, sessionKey, username) =>
|
|
set({ lastfmApiKey: apiKey, lastfmApiSecret: apiSecret, lastfmSessionKey: sessionKey, lastfmUsername: username }),
|
|
|
|
connectLastfm: (sessionKey, username) =>
|
|
set({ lastfmSessionKey: sessionKey, lastfmUsername: username }),
|
|
|
|
disconnectLastfm: () =>
|
|
set({ lastfmSessionKey: '', lastfmUsername: '', lastfmSessionError: false }),
|
|
|
|
setLastfmSessionError: (v) => set({ lastfmSessionError: v }),
|
|
|
|
setScrobblingEnabled: (v) => set({ scrobblingEnabled: v }),
|
|
setMaxCacheMb: (v) => set({ maxCacheMb: v }),
|
|
setDownloadFolder: (v) => set({ downloadFolder: v }),
|
|
setOfflineDownloadDir: (v) => set({ offlineDownloadDir: v }),
|
|
setExcludeAudiobooks: (v) => set({ excludeAudiobooks: v }),
|
|
setCustomGenreBlacklist: (v) => set({ customGenreBlacklist: v }),
|
|
setReplayGainEnabled: (v) => {
|
|
set({ replayGainEnabled: v });
|
|
usePlayerStore.getState().updateReplayGainForCurrentTrack();
|
|
},
|
|
setReplayGainMode: (v) => {
|
|
set({ replayGainMode: v });
|
|
usePlayerStore.getState().updateReplayGainForCurrentTrack();
|
|
},
|
|
setCrossfadeEnabled: (v) => set({ crossfadeEnabled: v }),
|
|
setCrossfadeSecs: (v) => set({ crossfadeSecs: v }),
|
|
setGaplessEnabled: (v) => set({ gaplessEnabled: v }),
|
|
setPreloadMode: (v: 'balanced' | 'early' | 'custom') => set({ preloadMode: v }),
|
|
setPreloadCustomSeconds: (v: number) => set({ preloadCustomSeconds: v }),
|
|
setInfiniteQueueEnabled: (v) => set({ infiniteQueueEnabled: v }),
|
|
setShowArtistImages: (v) => set({ showArtistImages: v }),
|
|
setShowTrayIcon: (v) => set({ showTrayIcon: v }),
|
|
setMinimizeToTray: (v) => set({ minimizeToTray: v }),
|
|
setDiscordRichPresence: (v) => set({ discordRichPresence: v }),
|
|
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
|
|
setLyricsServerFirst: (v: boolean) => set({ lyricsServerFirst: v }),
|
|
setShowFullscreenLyrics: (v: boolean) => set({ showFullscreenLyrics: v }),
|
|
setShowChangelogOnUpdate: (v) => set({ showChangelogOnUpdate: v }),
|
|
setLastSeenChangelogVersion: (v) => set({ lastSeenChangelogVersion: v }),
|
|
|
|
setEnableHiRes: (v) => set({ enableHiRes: v }),
|
|
setHotCacheEnabled: (v) => set({ hotCacheEnabled: v }),
|
|
setHotCacheMaxMb: (v) => set({ hotCacheMaxMb: v }),
|
|
setHotCacheDebounceSec: (v) => set({ hotCacheDebounceSec: v }),
|
|
setHotCacheDownloadDir: (v) => set({ hotCacheDownloadDir: v }),
|
|
|
|
setMusicFolders: (folders) => {
|
|
const sid = get().activeServerId;
|
|
set(s => {
|
|
const f = sid ? s.musicLibraryFilterByServer[sid] : undefined;
|
|
const invalidFilter = f && f !== 'all' && !folders.some(x => x.id === f);
|
|
return {
|
|
musicFolders: folders,
|
|
...(sid && invalidFilter
|
|
? { musicLibraryFilterByServer: { ...s.musicLibraryFilterByServer, [sid]: 'all' } }
|
|
: {}),
|
|
};
|
|
});
|
|
},
|
|
|
|
setMusicLibraryFilter: (folderId) => {
|
|
const sid = get().activeServerId;
|
|
if (!sid) return;
|
|
set(s => ({
|
|
musicLibraryFilterByServer: { ...s.musicLibraryFilterByServer, [sid]: folderId },
|
|
musicLibraryFilterVersion: s.musicLibraryFilterVersion + 1,
|
|
}));
|
|
},
|
|
|
|
logout: () => set({ isLoggedIn: false, musicFolders: [] }),
|
|
|
|
getBaseUrl: () => {
|
|
const s = get();
|
|
const server = s.servers.find(srv => srv.id === s.activeServerId);
|
|
if (!server?.url) return '';
|
|
return server.url.startsWith('http') ? server.url : `http://${server.url}`;
|
|
},
|
|
|
|
getActiveServer: () => {
|
|
const s = get();
|
|
return s.servers.find(srv => srv.id === s.activeServerId);
|
|
},
|
|
}),
|
|
{
|
|
name: 'psysonic-auth',
|
|
storage: createJSONStorage(() => localStorage),
|
|
partialize: state => {
|
|
const { musicFolders: _mf, musicLibraryFilterVersion: _fv, ...rest } = state;
|
|
return rest;
|
|
},
|
|
}
|
|
)
|
|
);
|