mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
4de577eede
Parse type and serverVersion from Subsonic ping; persist per-server identity and run a background Instant Mix probe (random songs + getSimilarSongs) for Navidrome 0.60+. Hide the AudioMuse server toggle when the probe returns no similar tracks; clear prefs when the server is no longer eligible. Artist pages fall back to Last.fm similar artists when AudioMuse is enabled but the server returns none. Instant Mix failures set a per-server issue flag with a settings warning and toast. Settings description links the official plugin repo via i18n Trans.
533 lines
22 KiB
TypeScript
533 lines
22 KiB
TypeScript
import { create } from 'zustand';
|
||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||
import { invoke } from '@tauri-apps/api/core';
|
||
import type { EntityRatingSupportLevel } from '../api/subsonic';
|
||
import {
|
||
isNavidromeAudiomuseSoftwareEligible,
|
||
type InstantMixProbeResult,
|
||
type SubsonicServerIdentity,
|
||
} from '../utils/subsonicServerIdentity';
|
||
import { usePlayerStore } from './playerStore';
|
||
|
||
export interface ServerProfile {
|
||
id: string;
|
||
name: string;
|
||
url: string;
|
||
username: string;
|
||
password: string;
|
||
}
|
||
|
||
export type SeekbarStyle = 'waveform' | 'linedot' | 'bar' | 'thick' | 'segmented' | 'neon' | 'pulsewave' | 'particletrail' | 'liquidfill' | 'retrotape';
|
||
|
||
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;
|
||
enableAppleMusicCoversDiscord: boolean;
|
||
useCustomTitlebar: boolean;
|
||
nowPlayingEnabled: boolean;
|
||
lyricsServerFirst: boolean;
|
||
showFullscreenLyrics: boolean;
|
||
showChangelogOnUpdate: boolean;
|
||
lastSeenChangelogVersion: string;
|
||
|
||
seekbarStyle: SeekbarStyle;
|
||
|
||
/** 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;
|
||
|
||
/** After this many manual skips of the same track, set track rating to 1 if still unrated (below 1 star). */
|
||
skipStarOnManualSkipsEnabled: boolean;
|
||
/** Manual skips per track before applying rating 1 (when enabled). */
|
||
skipStarManualSkipThreshold: number;
|
||
/**
|
||
* Manual Next-count per track for skip→1★. Key = `${serverId}\\u001f${trackId}`
|
||
* (empty serverId when none). Persisted; cleared when the track finishes naturally or when threshold is reached.
|
||
*/
|
||
skipStarManualSkipCountsByKey: Record<string, number>;
|
||
/** Increment skip count for current server + track; clears stored count when threshold reached. */
|
||
recordSkipStarManualAdvance: (trackId: string) => { crossedThreshold: boolean } | null;
|
||
/** Drop persisted skip count for this track on the active server (e.g. natural playback end). */
|
||
clearSkipStarManualCountForTrack: (trackId: string) => void;
|
||
|
||
/** Random mixes, random albums, home hero: drop non‑zero ratings at or below per‑axis thresholds (0 = unrated, kept). */
|
||
mixMinRatingFilterEnabled: boolean;
|
||
/** 0 = ignore; 1–3 = cutoff (UI); exclude track rating r when 0 < r ≤ cutoff. */
|
||
mixMinRatingSong: number;
|
||
/** 0 = ignore; album entity rating from payload or `getAlbum` when missing. */
|
||
mixMinRatingAlbum: number;
|
||
/** 0 = ignore; artist rating from payload / nested OpenSubsonic fields or `getArtist`. */
|
||
mixMinRatingArtist: number;
|
||
|
||
/** 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;
|
||
|
||
/**
|
||
* Per server: whether `setRating` is assumed to work for album/artist ids (OpenSubsonic-style).
|
||
* Absent key = not probed yet (`unknown` in UI).
|
||
*/
|
||
entityRatingSupportByServer: Record<string, EntityRatingSupportLevel>;
|
||
setEntityRatingSupport: (serverId: string, level: EntityRatingSupportLevel) => void;
|
||
|
||
/**
|
||
* Per server: Navidrome has the AudioMuse-AI plugin — use `getSimilarSongs` (Instant Mix) and
|
||
* `getArtistInfo2` similar artists instead of Last.fm for discovery on this server.
|
||
*/
|
||
audiomuseNavidromeByServer: Record<string, boolean>;
|
||
setAudiomuseNavidromeEnabled: (serverId: string, enabled: boolean) => void;
|
||
|
||
/** From `ping` — used to show the AudioMuse toggle only on Navidrome ≥ 0.60. */
|
||
subsonicServerIdentityByServer: Record<string, SubsonicServerIdentity>;
|
||
setSubsonicServerIdentity: (serverId: string, identity: SubsonicServerIdentity) => void;
|
||
|
||
/** Instant Mix / similar path failed while this server had AudioMuse enabled (cleared on success or toggle off). */
|
||
audiomuseNavidromeIssueByServer: Record<string, boolean>;
|
||
setAudiomuseNavidromeIssue: (serverId: string, hasIssue: boolean) => void;
|
||
|
||
/**
|
||
* `getSimilarSongs` probe per server (after ping). `empty` hides the AudioMuse row; re-run by testing connection.
|
||
*/
|
||
instantMixProbeByServer: Record<string, InstantMixProbeResult>;
|
||
setInstantMixProbe: (serverId: string, result: InstantMixProbeResult) => void;
|
||
|
||
// 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;
|
||
setEnableAppleMusicCoversDiscord: (v: boolean) => void;
|
||
setUseCustomTitlebar: (v: boolean) => void;
|
||
setNowPlayingEnabled: (v: boolean) => void;
|
||
setLyricsServerFirst: (v: boolean) => void;
|
||
setShowFullscreenLyrics: (v: boolean) => void;
|
||
setShowChangelogOnUpdate: (v: boolean) => void;
|
||
setLastSeenChangelogVersion: (v: string) => void;
|
||
setSeekbarStyle: (v: SeekbarStyle) => void;
|
||
setEnableHiRes: (v: boolean) => void;
|
||
setHotCacheEnabled: (v: boolean) => void;
|
||
setHotCacheMaxMb: (v: number) => void;
|
||
setHotCacheDebounceSec: (v: number) => void;
|
||
setHotCacheDownloadDir: (v: string) => void;
|
||
setSkipStarOnManualSkipsEnabled: (v: boolean) => void;
|
||
setSkipStarManualSkipThreshold: (v: number) => void;
|
||
setMixMinRatingFilterEnabled: (v: boolean) => void;
|
||
setMixMinRatingSong: (v: number) => void;
|
||
setMixMinRatingAlbum: (v: number) => void;
|
||
setMixMinRatingArtist: (v: number) => 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);
|
||
}
|
||
|
||
/** Upper bound for mix min-rating thresholds (UI shows five stars, only 1…this many are selectable). */
|
||
export const MIX_MIN_RATING_FILTER_MAX_STARS = 3;
|
||
|
||
function clampMixFilterMinStars(v: number): number {
|
||
if (!Number.isFinite(v)) return 0;
|
||
return Math.max(0, Math.min(MIX_MIN_RATING_FILTER_MAX_STARS, Math.round(v)));
|
||
}
|
||
|
||
function clampSkipStarThreshold(v: number): number {
|
||
if (!Number.isFinite(v)) return 3;
|
||
return Math.max(1, Math.min(99, Math.round(v)));
|
||
}
|
||
|
||
function skipStarCountStorageKey(serverId: string | null | undefined, trackId: string): string {
|
||
return `${serverId ?? ''}\u001f${trackId}`;
|
||
}
|
||
|
||
function sanitizeSkipStarCounts(raw: unknown): Record<string, number> {
|
||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
||
const next: Record<string, number> = {};
|
||
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
|
||
const n = Number(v);
|
||
if (Number.isFinite(n) && n > 0) next[k] = Math.min(Math.floor(n), 1_000_000);
|
||
}
|
||
return next;
|
||
}
|
||
|
||
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,
|
||
enableAppleMusicCoversDiscord: false,
|
||
useCustomTitlebar: false,
|
||
nowPlayingEnabled: false,
|
||
lyricsServerFirst: true,
|
||
showFullscreenLyrics: true,
|
||
showChangelogOnUpdate: true,
|
||
lastSeenChangelogVersion: '',
|
||
seekbarStyle: 'waveform',
|
||
enableHiRes: false,
|
||
hotCacheEnabled: false,
|
||
hotCacheMaxMb: 256,
|
||
hotCacheDebounceSec: 30,
|
||
hotCacheDownloadDir: '',
|
||
skipStarOnManualSkipsEnabled: false,
|
||
skipStarManualSkipThreshold: 3,
|
||
skipStarManualSkipCountsByKey: {},
|
||
mixMinRatingFilterEnabled: false,
|
||
mixMinRatingSong: 0,
|
||
mixMinRatingAlbum: 0,
|
||
mixMinRatingArtist: 0,
|
||
musicFolders: [],
|
||
musicLibraryFilterByServer: {},
|
||
musicLibraryFilterVersion: 0,
|
||
entityRatingSupportByServer: {},
|
||
audiomuseNavidromeByServer: {},
|
||
subsonicServerIdentityByServer: {},
|
||
audiomuseNavidromeIssueByServer: {},
|
||
instantMixProbeByServer: {},
|
||
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;
|
||
const { [id]: _r, ...entityRatingRest } = s.entityRatingSupportByServer;
|
||
const { [id]: _a, ...audiomuseRest } = s.audiomuseNavidromeByServer;
|
||
const { [id]: _idn, ...identityRest } = s.subsonicServerIdentityByServer;
|
||
const { [id]: _iss, ...issueRest } = s.audiomuseNavidromeIssueByServer;
|
||
const { [id]: _pr, ...probeRest } = s.instantMixProbeByServer;
|
||
return {
|
||
servers: newServers,
|
||
activeServerId: switchedAway ? (newServers[0]?.id ?? null) : s.activeServerId,
|
||
isLoggedIn: switchedAway ? false : s.isLoggedIn,
|
||
entityRatingSupportByServer: entityRatingRest,
|
||
audiomuseNavidromeByServer: audiomuseRest,
|
||
subsonicServerIdentityByServer: identityRest,
|
||
audiomuseNavidromeIssueByServer: issueRest,
|
||
instantMixProbeByServer: probeRest,
|
||
};
|
||
});
|
||
},
|
||
|
||
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 }),
|
||
setEnableAppleMusicCoversDiscord: (v) => set({ enableAppleMusicCoversDiscord: v }),
|
||
setUseCustomTitlebar: (v) => set({ useCustomTitlebar: 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 }),
|
||
|
||
setSeekbarStyle: (v) => set({ seekbarStyle: 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 }),
|
||
|
||
setSkipStarOnManualSkipsEnabled: (v) =>
|
||
set({
|
||
skipStarOnManualSkipsEnabled: v,
|
||
...(v ? {} : { skipStarManualSkipCountsByKey: {} }),
|
||
}),
|
||
setSkipStarManualSkipThreshold: (v) => set({ skipStarManualSkipThreshold: clampSkipStarThreshold(v) }),
|
||
|
||
recordSkipStarManualAdvance: (trackId: string) => {
|
||
const s = get();
|
||
if (!s.skipStarOnManualSkipsEnabled || s.skipStarManualSkipThreshold < 1) return null;
|
||
const key = skipStarCountStorageKey(s.activeServerId, trackId);
|
||
const prev = s.skipStarManualSkipCountsByKey[key] ?? 0;
|
||
const threshold = s.skipStarManualSkipThreshold;
|
||
const next = prev + 1;
|
||
if (next >= threshold) {
|
||
const { [key]: _removed, ...rest } = s.skipStarManualSkipCountsByKey;
|
||
set({ skipStarManualSkipCountsByKey: rest });
|
||
return { crossedThreshold: true };
|
||
}
|
||
set({
|
||
skipStarManualSkipCountsByKey: { ...s.skipStarManualSkipCountsByKey, [key]: next },
|
||
});
|
||
return { crossedThreshold: false };
|
||
},
|
||
|
||
clearSkipStarManualCountForTrack: (trackId: string) => {
|
||
const s = get();
|
||
const key = skipStarCountStorageKey(s.activeServerId, trackId);
|
||
if (s.skipStarManualSkipCountsByKey[key] === undefined) return;
|
||
const { [key]: _removed, ...rest } = s.skipStarManualSkipCountsByKey;
|
||
set({ skipStarManualSkipCountsByKey: rest });
|
||
},
|
||
|
||
setMixMinRatingFilterEnabled: (v) => set({ mixMinRatingFilterEnabled: v }),
|
||
setMixMinRatingSong: (v) => set({ mixMinRatingSong: clampMixFilterMinStars(v) }),
|
||
setMixMinRatingAlbum: (v) => set({ mixMinRatingAlbum: clampMixFilterMinStars(v) }),
|
||
setMixMinRatingArtist: (v) => set({ mixMinRatingArtist: clampMixFilterMinStars(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,
|
||
}));
|
||
},
|
||
|
||
setEntityRatingSupport: (serverId, level) =>
|
||
set(s => ({
|
||
entityRatingSupportByServer: { ...s.entityRatingSupportByServer, [serverId]: level },
|
||
})),
|
||
|
||
setAudiomuseNavidromeEnabled: (serverId, enabled) =>
|
||
set(s => {
|
||
const audiomuseNavidromeByServer = enabled
|
||
? { ...s.audiomuseNavidromeByServer, [serverId]: true }
|
||
: (() => {
|
||
const { [serverId]: _removed, ...rest } = s.audiomuseNavidromeByServer;
|
||
return rest;
|
||
})();
|
||
const { [serverId]: _issueRm, ...issueRest } = s.audiomuseNavidromeIssueByServer;
|
||
return { audiomuseNavidromeByServer, audiomuseNavidromeIssueByServer: issueRest };
|
||
}),
|
||
|
||
setSubsonicServerIdentity: (serverId, identity) =>
|
||
set(s => {
|
||
const subsonicServerIdentityByServer = { ...s.subsonicServerIdentityByServer, [serverId]: { ...identity } };
|
||
if (!isNavidromeAudiomuseSoftwareEligible(identity)) {
|
||
const { [serverId]: _a, ...audiomuseRest } = s.audiomuseNavidromeByServer;
|
||
const { [serverId]: _i, ...issueRest } = s.audiomuseNavidromeIssueByServer;
|
||
const { [serverId]: _p, ...probeRest } = s.instantMixProbeByServer;
|
||
return {
|
||
subsonicServerIdentityByServer,
|
||
audiomuseNavidromeByServer: audiomuseRest,
|
||
audiomuseNavidromeIssueByServer: issueRest,
|
||
instantMixProbeByServer: probeRest,
|
||
};
|
||
}
|
||
return { subsonicServerIdentityByServer };
|
||
}),
|
||
|
||
setInstantMixProbe: (serverId, result) =>
|
||
set(s => {
|
||
const instantMixProbeByServer = { ...s.instantMixProbeByServer, [serverId]: result };
|
||
if (result === 'empty') {
|
||
const { [serverId]: _a, ...audiomuseRest } = s.audiomuseNavidromeByServer;
|
||
const { [serverId]: _i, ...issueRest } = s.audiomuseNavidromeIssueByServer;
|
||
return {
|
||
instantMixProbeByServer,
|
||
audiomuseNavidromeByServer: audiomuseRest,
|
||
audiomuseNavidromeIssueByServer: issueRest,
|
||
};
|
||
}
|
||
return { instantMixProbeByServer };
|
||
}),
|
||
|
||
setAudiomuseNavidromeIssue: (serverId, hasIssue) =>
|
||
set(s =>
|
||
hasIssue
|
||
? { audiomuseNavidromeIssueByServer: { ...s.audiomuseNavidromeIssueByServer, [serverId]: true } }
|
||
: (() => {
|
||
const { [serverId]: _rm, ...rest } = s.audiomuseNavidromeIssueByServer;
|
||
return { audiomuseNavidromeIssueByServer: rest };
|
||
})(),
|
||
),
|
||
|
||
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;
|
||
},
|
||
onRehydrateStorage: () => (state, error) => {
|
||
if (error || !state) return;
|
||
useAuthStore.setState({
|
||
mixMinRatingSong: clampMixFilterMinStars(state.mixMinRatingSong as number),
|
||
mixMinRatingAlbum: clampMixFilterMinStars(state.mixMinRatingAlbum as number),
|
||
mixMinRatingArtist: clampMixFilterMinStars(state.mixMinRatingArtist as number),
|
||
skipStarManualSkipCountsByKey: sanitizeSkipStarCounts(
|
||
(state as { skipStarManualSkipCountsByKey?: unknown }).skipStarManualSkipCountsByKey,
|
||
),
|
||
});
|
||
},
|
||
}
|
||
)
|
||
);
|