mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(auth): E.46 — extract logic-bearing factories (#611)
Three factories for the actions that carry real logic (not just
`set({ field: v })` pass-through):
- `createSkipStarActions` — skip-to-1★ counter with threshold check
and per-`<activeServerId><trackId>` storage key. Disabling the
feature wipes the counter map so a re-enable doesn't resume from
stale partial counts. Crossing the threshold deletes the key so
the next session starts fresh.
- `createMusicLibraryActions` — `setMusicFolders` falls back to
`'all'` when the persisted filter points at a folder the server
no longer reports; `setMusicLibraryFilter` bumps
`musicLibraryFilterVersion` so subscribed pages refetch.
- `createPerServerCapabilityActions` — `setEntityRatingSupport`,
`setAudiomuseNavidromeEnabled`, `setSubsonicServerIdentity`,
`setInstantMixProbe`, `setAudiomuseNavidromeIssue`. Each branch
knows which neighbouring per-server maps to wipe when the input
invalidates them (identity not AudioMuse-eligible / probe empty /
audiomuse disabled).
Pure code-move. authStore.ts: 384 → 270 LOC (−114). All trivial-setter
and logic-bearing action bodies are now out of the store body; only
the persist config + `onRehydrateStorage` migration block remains as
a non-factory chunk (target for E.47).
This commit is contained in:
committed by
GitHub
parent
fe6acdaa4c
commit
2c0aef9538
@@ -0,0 +1,47 @@
|
|||||||
|
import type { AuthState } from './authStoreTypes';
|
||||||
|
|
||||||
|
type SetState = (
|
||||||
|
partial: Partial<AuthState> | ((state: AuthState) => Partial<AuthState>),
|
||||||
|
) => void;
|
||||||
|
type GetState = () => AuthState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-server music-folder selection. `setMusicFolders` is called
|
||||||
|
* after login / server change with the fresh Subsonic folder list;
|
||||||
|
* if the currently-persisted filter for that server points at a
|
||||||
|
* folder that no longer exists on the server, it falls back to
|
||||||
|
* `'all'` so the page doesn't end up filtering by a stale id.
|
||||||
|
*
|
||||||
|
* `setMusicLibraryFilter` writes the new filter and bumps
|
||||||
|
* `musicLibraryFilterVersion` so subscribed pages refetch their
|
||||||
|
* catalog data.
|
||||||
|
*/
|
||||||
|
export function createMusicLibraryActions(set: SetState, get: GetState): Pick<
|
||||||
|
AuthState,
|
||||||
|
'setMusicFolders' | 'setMusicLibraryFilter'
|
||||||
|
> {
|
||||||
|
return {
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { isNavidromeAudiomuseSoftwareEligible } from '../utils/subsonicServerIdentity';
|
||||||
|
import type { AuthState } from './authStoreTypes';
|
||||||
|
|
||||||
|
type SetState = (
|
||||||
|
partial: Partial<AuthState> | ((state: AuthState) => Partial<AuthState>),
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-server capability flags learned from pings/probes:
|
||||||
|
*
|
||||||
|
* - `setEntityRatingSupport` — does setRating apply to album/artist
|
||||||
|
* ids on this server? Stored as `unknown`/`yes`/`no`.
|
||||||
|
* - `setAudiomuseNavidromeEnabled` — user opted in/out of the
|
||||||
|
* AudioMuse-AI Instant Mix path for this Navidrome. Disable also
|
||||||
|
* clears the related issue flag.
|
||||||
|
* - `setSubsonicServerIdentity` — server's identity from ping. If the
|
||||||
|
* ping reveals the server isn't AudioMuse-eligible (wrong type or
|
||||||
|
* too old), wipe the related caps for that id so the UI doesn't
|
||||||
|
* keep a stale toggle.
|
||||||
|
* - `setInstantMixProbe` — probe result for getSimilarSongs. If
|
||||||
|
* `empty`, wipe the related AudioMuse caps so the row hides.
|
||||||
|
* - `setAudiomuseNavidromeIssue` — set/clear the "current session
|
||||||
|
* saw a failure" flag.
|
||||||
|
*/
|
||||||
|
export function createPerServerCapabilityActions(set: SetState): Pick<
|
||||||
|
AuthState,
|
||||||
|
| 'setEntityRatingSupport'
|
||||||
|
| 'setAudiomuseNavidromeEnabled'
|
||||||
|
| 'setSubsonicServerIdentity'
|
||||||
|
| 'setInstantMixProbe'
|
||||||
|
| 'setAudiomuseNavidromeIssue'
|
||||||
|
> {
|
||||||
|
return {
|
||||||
|
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 };
|
||||||
|
})(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
clampSkipStarThreshold,
|
||||||
|
skipStarCountStorageKey,
|
||||||
|
} from './authStoreHelpers';
|
||||||
|
import type { AuthState } from './authStoreTypes';
|
||||||
|
|
||||||
|
type SetState = (
|
||||||
|
partial: Partial<AuthState> | ((state: AuthState) => Partial<AuthState>),
|
||||||
|
) => void;
|
||||||
|
type GetState = () => AuthState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip-to-1★ feature: after N manual skips of the same track, set
|
||||||
|
* its rating to 1 if still unrated. Counter is per
|
||||||
|
* `<activeServerId><trackId>` key, persisted in
|
||||||
|
* `skipStarManualSkipCountsByKey`, and cleared either when the
|
||||||
|
* threshold is crossed (so the next session starts fresh) or when
|
||||||
|
* the track ends naturally without a skip.
|
||||||
|
*
|
||||||
|
* Disabling the feature wipes the counter map so re-enabling later
|
||||||
|
* doesn't resume from stale partial counts.
|
||||||
|
*/
|
||||||
|
export function createSkipStarActions(set: SetState, get: GetState): Pick<
|
||||||
|
AuthState,
|
||||||
|
| 'setSkipStarOnManualSkipsEnabled'
|
||||||
|
| 'setSkipStarManualSkipThreshold'
|
||||||
|
| 'recordSkipStarManualAdvance'
|
||||||
|
| 'clearSkipStarManualCountForTrack'
|
||||||
|
> {
|
||||||
|
return {
|
||||||
|
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 });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+6
-120
@@ -1,8 +1,5 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
import {
|
|
||||||
isNavidromeAudiomuseSoftwareEligible,
|
|
||||||
} from '../utils/subsonicServerIdentity';
|
|
||||||
import { IS_LINUX } from '../utils/platform';
|
import { IS_LINUX } from '../utils/platform';
|
||||||
import {
|
import {
|
||||||
LOUDNESS_PRE_ANALYSIS_REF_TARGET_LUFS,
|
LOUDNESS_PRE_ANALYSIS_REF_TARGET_LUFS,
|
||||||
@@ -14,8 +11,11 @@ import { createCacheStorageActions } from './authCacheStorageActions';
|
|||||||
import { createDiscordSettingsActions } from './authDiscordSettingsActions';
|
import { createDiscordSettingsActions } from './authDiscordSettingsActions';
|
||||||
import { createDiscoveryActions } from './authDiscoveryActions';
|
import { createDiscoveryActions } from './authDiscoveryActions';
|
||||||
import { createLyricsSettingsActions } from './authLyricsSettingsActions';
|
import { createLyricsSettingsActions } from './authLyricsSettingsActions';
|
||||||
|
import { createMusicLibraryActions } from './authMusicLibraryActions';
|
||||||
|
import { createPerServerCapabilityActions } from './authPerServerCapabilityActions';
|
||||||
import { createPlumbingSettingsActions } from './authPlumbingActions';
|
import { createPlumbingSettingsActions } from './authPlumbingActions';
|
||||||
import { createServerProfileActions } from './authServerProfileActions';
|
import { createServerProfileActions } from './authServerProfileActions';
|
||||||
|
import { createSkipStarActions } from './authSkipStarActions';
|
||||||
import { createTrackPreviewActions } from './authTrackPreviewActions';
|
import { createTrackPreviewActions } from './authTrackPreviewActions';
|
||||||
import { createUiAppearanceActions } from './authUiAppearanceActions';
|
import { createUiAppearanceActions } from './authUiAppearanceActions';
|
||||||
import {
|
import {
|
||||||
@@ -26,11 +26,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
clampMixFilterMinStars,
|
clampMixFilterMinStars,
|
||||||
clampRandomMixSize,
|
clampRandomMixSize,
|
||||||
clampSkipStarThreshold,
|
|
||||||
sanitizeLoudnessLufsPreset,
|
sanitizeLoudnessLufsPreset,
|
||||||
sanitizeLoudnessPreAnalysisFromStorage,
|
sanitizeLoudnessPreAnalysisFromStorage,
|
||||||
sanitizeSkipStarCounts,
|
sanitizeSkipStarCounts,
|
||||||
skipStarCountStorageKey,
|
|
||||||
} from './authStoreHelpers';
|
} from './authStoreHelpers';
|
||||||
import type {
|
import type {
|
||||||
AuthState,
|
AuthState,
|
||||||
@@ -143,121 +141,9 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
...createTrackPreviewActions(set),
|
...createTrackPreviewActions(set),
|
||||||
...createDiscoveryActions(set),
|
...createDiscoveryActions(set),
|
||||||
...createPlumbingSettingsActions(set),
|
...createPlumbingSettingsActions(set),
|
||||||
|
...createSkipStarActions(set, get),
|
||||||
setSkipStarOnManualSkipsEnabled: (v) =>
|
...createMusicLibraryActions(set, get),
|
||||||
set({
|
...createPerServerCapabilityActions(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 });
|
|
||||||
},
|
|
||||||
|
|
||||||
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 };
|
|
||||||
})(),
|
|
||||||
),
|
|
||||||
|
|
||||||
getBaseUrl: () => {
|
getBaseUrl: () => {
|
||||||
const s = get();
|
const s = get();
|
||||||
|
|||||||
Reference in New Issue
Block a user