Files
Psychotoxical-psysonic/src/features/settings/hooks/useUserMgmtData.ts
T
Psychotoxical 7e9cb60763 refactor(api): co-locate server-protocol clients into lib/api
Move the Subsonic + Navidrome protocol clients and the library IPC facade
(subsonic*, navidrome*, library.ts + tests) from src/api/ into src/lib/api/,
the feature-free infra layer (plan M4, decision §10.3: shared client core → lib/).
Pure move: deep-path import specifiers @/api/* -> @/lib/api/* across ~150
consumers; no behaviour change. Domain REST (lyrics/events) and cover/analysis
infra stay in src/api/ pending their own M4 placement.

tsc 0, lint 0/0, full suite 319 files / 2353 tests green.
2026-06-30 07:53:25 +02:00

65 lines
2.3 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import type { TFunction } from 'i18next';
import {
ndListLibraries,
ndListUsers,
type NdLibrary,
type NdUser,
} from '@/lib/api/navidromeAdmin';
interface UseUserMgmtDataResult {
users: NdUser[];
libraries: NdLibrary[];
loading: boolean;
loadError: string | null;
load: () => Promise<void>;
}
/**
* Fetch and keep the Navidrome admin users + libraries lists in sync.
*
* The two requests are **sequential, not parallel**: some nginx setups
* with churning upstream keep-alive drop one of two parallel TLS
* connections. Doing users first then libraries keeps us on one
* connection at a time and pairs cleanly with the `nd_retry` backoff on
* the Rust side.
*
* Tauri's `invoke` rejects with a bare `string` (our Rust commands
* return `Err(String)`), so the error normalisation surfaces the real
* cause (e.g. `tls handshake eof`) instead of falling back to the
* generic i18n string.
*/
export function useUserMgmtData(serverUrl: string, token: string, t: TFunction): UseUserMgmtDataResult {
const [users, setUsers] = useState<NdUser[]>([]);
const [libraries, setLibraries] = useState<NdLibrary[]>([]);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setLoadError(null);
try {
const list = await ndListUsers(serverUrl, token);
const libs = await ndListLibraries(serverUrl, token).catch(() => [] as NdLibrary[]);
setUsers([...list].sort((a, b) => a.userName.localeCompare(b.userName)));
setLibraries([...libs].sort((a, b) => a.name.localeCompare(b.name)));
} catch (e) {
const raw = typeof e === 'string'
? e
: (e instanceof Error && e.message)
? e.message
: '';
const prefix = t('settings.userMgmtLoadError');
setLoadError(raw ? `${prefix} ${raw}` : prefix);
} finally {
setLoading(false);
}
}, [serverUrl, token, t]);
// React Compiler set-state-in-effect rule: state set from an async result resolved in this effect.
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => { void load(); }, [load]);
return { users, libraries, loading, loadError, load };
}