mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat(server): capability framework, AudioMuse sonic routing & PsyLab Connections (#1033)
* feat(server): probe AudioMuse via OpenSubsonic and add PsyLab Connections tab Navidrome ≥0.62: detect sonicSimilarity extension for reliable plugin signal; older servers keep the legacy getSimilarSongs probe. PsyLab gets a Connections tab with session, endpoint, and active-server capability details. * feat(psylab): polish Connections tab, admin role probe, and tab bar layout Status badges and Navidrome admin/user validation in Connections; prevent PsyLab tab row from vertically collapsing under the Logs flex layout. * docs: add CHANGELOG and credits for PR #1032 * feat(settings): auto-enable AudioMuse on Navidrome 0.62+ with status indicator Replace the per-server manual toggle with a probe-driven badge when sonicSimilarity is available; pre-0.62 Navidrome keeps the legacy toggle. * feat(server): capability framework with AudioMuse sonic routing Add a declarative server-capability catalog (src/serverCapabilities/) that picks a feature strategy per server generation, runs only the needed probes, and routes API calls. AudioMuse Instant Mix now prefers the OpenSubsonic sonicSimilarity endpoint (getSonicSimilarTracks) on Navidrome 0.62+ and falls back to legacy getSimilarSongs. - catalog/context/resolve: eligibility, detection, activation, call routing - storeView: read facade over the existing per-server probe maps - getSonicSimilarTracks API client + fetchSimilarTracksRouted router - route Instant Mix and Lucky Mix through the resolver - ServersTab + PsyLab Connections read the resolver (auto status vs toggle) - tests: resolve, storeView, router * docs: update CHANGELOG and credits for PR #1033 Renamed branch supersedes PR #1032: point changelog/credits at #1033 and document the server-capability framework, auto-managed AudioMuse indicator, and sonic Instant Mix routing. * refactor(server): address PR #1033 review — idempotent probe, drop dead code - Make scheduleInstantMixProbeForServer idempotent: skip when a definitive result is cached; re-probe only on force (add/edit/test server), a prior error, or a server version/type change (invalidated in setSubsonicServerIdentity). Removes the steady-state 120 s re-probe, the present→probing→present flicker, and the momentary legacy-fallback routing window. - Remove now-dead identity helpers (showAudiomuseNavidromeServerSetting, isAudiomusePluginAutoManaged, isNavidromeSonicSimilarityEligible, resolveAudiomusePluginProbeUiStatus + type) and the superseded probeAudiomusePluginWithCredentials; the catalog is the single source of truth. - Drop the never-emitted 'unsupported' AudiomusePluginProbeResult variant. - Fill audiomuseStatus* keys in all 8 non-en locales. - Tests: probe idempotency + version-change invalidation; retarget OpenSubsonic test to fetchOpenSubsonicExtensionsWithCredentials.
This commit is contained in:
@@ -17,10 +17,9 @@ type SetState = (
|
||||
* 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.
|
||||
* - `setInstantMixProbe` — legacy probe (pre-0.62). If `empty`, wipe AudioMuse caps.
|
||||
* - `setAudiomusePluginProbe` — Navidrome ≥ 0.62 `sonicSimilarity` extension probe.
|
||||
* - `setAudiomuseNavidromeIssue` — set/clear the "current session saw a failure" flag.
|
||||
*/
|
||||
export function createPerServerCapabilityActions(set: SetState): Pick<
|
||||
AuthState,
|
||||
@@ -28,6 +27,7 @@ export function createPerServerCapabilityActions(set: SetState): Pick<
|
||||
| 'setAudiomuseNavidromeEnabled'
|
||||
| 'setSubsonicServerIdentity'
|
||||
| 'setInstantMixProbe'
|
||||
| 'setAudiomusePluginProbe'
|
||||
| 'setAudiomuseNavidromeIssue'
|
||||
> {
|
||||
return {
|
||||
@@ -50,16 +50,31 @@ export function createPerServerCapabilityActions(set: SetState): Pick<
|
||||
|
||||
setSubsonicServerIdentity: (serverId, identity) =>
|
||||
set(s => {
|
||||
const prev = s.subsonicServerIdentityByServer[serverId];
|
||||
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;
|
||||
const { [serverId]: _pp, ...pluginProbeRest } = s.audiomusePluginProbeByServer;
|
||||
return {
|
||||
subsonicServerIdentityByServer,
|
||||
audiomuseNavidromeByServer: audiomuseRest,
|
||||
audiomuseNavidromeIssueByServer: issueRest,
|
||||
instantMixProbeByServer: probeRest,
|
||||
audiomusePluginProbeByServer: pluginProbeRest,
|
||||
};
|
||||
}
|
||||
// Server generation changed (version/type) → drop cached capability probes
|
||||
// so the next probe re-runs against the new generation. The user opt-in
|
||||
// (`audiomuseNavidromeByServer`) is preserved.
|
||||
if (prev && (prev.serverVersion !== identity.serverVersion || prev.type !== identity.type)) {
|
||||
const { [serverId]: _p, ...probeRest } = s.instantMixProbeByServer;
|
||||
const { [serverId]: _pp, ...pluginProbeRest } = s.audiomusePluginProbeByServer;
|
||||
return {
|
||||
subsonicServerIdentityByServer,
|
||||
instantMixProbeByServer: probeRest,
|
||||
audiomusePluginProbeByServer: pluginProbeRest,
|
||||
};
|
||||
}
|
||||
return { subsonicServerIdentityByServer };
|
||||
@@ -80,6 +95,27 @@ export function createPerServerCapabilityActions(set: SetState): Pick<
|
||||
return { instantMixProbeByServer };
|
||||
}),
|
||||
|
||||
setAudiomusePluginProbe: (serverId, result) =>
|
||||
set(s => {
|
||||
const audiomusePluginProbeByServer = { ...s.audiomusePluginProbeByServer, [serverId]: result };
|
||||
if (result === 'present') {
|
||||
return {
|
||||
audiomusePluginProbeByServer,
|
||||
audiomuseNavidromeByServer: { ...s.audiomuseNavidromeByServer, [serverId]: true },
|
||||
};
|
||||
}
|
||||
if (result === 'absent' || result === 'error') {
|
||||
const { [serverId]: _a, ...audiomuseRest } = s.audiomuseNavidromeByServer;
|
||||
const { [serverId]: _i, ...issueRest } = s.audiomuseNavidromeIssueByServer;
|
||||
return {
|
||||
audiomusePluginProbeByServer,
|
||||
audiomuseNavidromeByServer: audiomuseRest,
|
||||
audiomuseNavidromeIssueByServer: issueRest,
|
||||
};
|
||||
}
|
||||
return { audiomusePluginProbeByServer };
|
||||
}),
|
||||
|
||||
setAudiomuseNavidromeIssue: (serverId, hasIssue) =>
|
||||
set(s =>
|
||||
hasIssue
|
||||
|
||||
@@ -56,6 +56,7 @@ export function createServerProfileActions(set: SetState): Pick<
|
||||
const { [id]: _idn, ...identityRest } = s.subsonicServerIdentityByServer;
|
||||
const { [id]: _iss, ...issueRest } = s.audiomuseNavidromeIssueByServer;
|
||||
const { [id]: _pr, ...probeRest } = s.instantMixProbeByServer;
|
||||
const { [id]: _ppl, ...pluginProbeRest } = s.audiomusePluginProbeByServer;
|
||||
return {
|
||||
servers: newServers,
|
||||
activeServerId: switchedAway ? (newServers[0]?.id ?? null) : s.activeServerId,
|
||||
@@ -65,6 +66,7 @@ export function createServerProfileActions(set: SetState): Pick<
|
||||
subsonicServerIdentityByServer: identityRest,
|
||||
audiomuseNavidromeIssueByServer: issueRest,
|
||||
instantMixProbeByServer: probeRest,
|
||||
audiomusePluginProbeByServer: pluginProbeRest,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@@ -255,6 +255,18 @@ describe('per-server bookkeeping setters', () => {
|
||||
'srv-1': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('setAudiomusePluginProbe auto-enables AudioMuse when sonicSimilarity is present', () => {
|
||||
useAuthStore.getState().setAudiomusePluginProbe('srv-1', 'present');
|
||||
expect(useAuthStore.getState().audiomusePluginProbeByServer).toEqual({ 'srv-1': 'present' });
|
||||
expect(useAuthStore.getState().audiomuseNavidromeByServer).toEqual({ 'srv-1': true });
|
||||
});
|
||||
|
||||
it('setAudiomusePluginProbe clears AudioMuse when the extension is absent', () => {
|
||||
useAuthStore.getState().setAudiomuseNavidromeEnabled('srv-1', true);
|
||||
useAuthStore.getState().setAudiomusePluginProbe('srv-1', 'absent');
|
||||
expect(useAuthStore.getState().audiomuseNavidromeByServer).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('genre blacklist + audio output device', () => {
|
||||
|
||||
@@ -121,6 +121,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
subsonicServerIdentityByServer: {},
|
||||
audiomuseNavidromeIssueByServer: {},
|
||||
instantMixProbeByServer: {},
|
||||
audiomusePluginProbeByServer: {},
|
||||
isLoggedIn: false,
|
||||
isConnecting: false,
|
||||
connectionError: null,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { EntityRatingSupportLevel } from '../api/subsonicTypes';
|
||||
import type {
|
||||
AudiomusePluginProbeResult,
|
||||
InstantMixProbeResult,
|
||||
SubsonicServerIdentity,
|
||||
} from '../utils/server/subsonicServerIdentity';
|
||||
@@ -250,7 +251,8 @@ export interface AuthState {
|
||||
setEntityRatingSupport: (serverId: string, level: EntityRatingSupportLevel) => void;
|
||||
|
||||
/**
|
||||
* Per server: Navidrome has the AudioMuse-AI plugin — use `getSimilarSongs` (Instant Mix) and
|
||||
* Per server: AudioMuse-AI features active — manual opt-in on pre-0.62 Navidrome; auto-set on
|
||||
* 0.62+ when `sonicSimilarity` probe is `present`. Uses `getSimilarSongs` (Instant Mix) and
|
||||
* `getArtistInfo2` similar artists instead of Last.fm for discovery on this server.
|
||||
*/
|
||||
audiomuseNavidromeByServer: Record<string, boolean>;
|
||||
@@ -265,11 +267,17 @@ export interface AuthState {
|
||||
setAudiomuseNavidromeIssue: (serverId: string, hasIssue: boolean) => void;
|
||||
|
||||
/**
|
||||
* `getSimilarSongs` probe per server (after ping). `empty` hides the AudioMuse row; re-run by testing connection.
|
||||
* `getSimilarSongs` probe per server (after ping). `empty` hides the AudioMuse row on pre-0.62 Navidrome.
|
||||
*/
|
||||
instantMixProbeByServer: Record<string, InstantMixProbeResult>;
|
||||
setInstantMixProbe: (serverId: string, result: InstantMixProbeResult) => void;
|
||||
|
||||
/**
|
||||
* Navidrome ≥ 0.62: `sonicSimilarity` extension probe (`present` = AudioMuse-style plugin active).
|
||||
*/
|
||||
audiomusePluginProbeByServer: Record<string, AudiomusePluginProbeResult>;
|
||||
setAudiomusePluginProbe: (serverId: string, result: AudiomusePluginProbeResult) => void;
|
||||
|
||||
// Status
|
||||
isLoggedIn: boolean;
|
||||
isConnecting: boolean;
|
||||
|
||||
Reference in New Issue
Block a user