import axios from 'axios'; import md5 from 'md5'; import { version } from '../../package.json'; import { useAuthStore } from '../store/authStore'; import type { ServerProfile } from '../store/authStoreTypes'; export const SUBSONIC_CLIENT = `psysonic/${version}`; export function secureRandomSalt(): string { const buf = new Uint8Array(8); crypto.getRandomValues(buf); return Array.from(buf, b => b.toString(16).padStart(2, '0')).join(''); } export function getAuthParams(username: string, password: string) { const salt = secureRandomSalt(); const token = md5(password + salt); return { u: username, t: token, s: salt, v: '1.16.1', c: SUBSONIC_CLIENT, f: 'json' }; } export function restBaseFromUrl(serverUrl: string): string { const base = serverUrl.startsWith('http') ? serverUrl.replace(/\/$/, '') : `http://${serverUrl.replace(/\/$/, '')}`; return `${base}/rest`; } export async function apiWithCredentials( serverUrl: string, username: string, password: string, endpoint: string, extra: Record = {}, timeout = 15000, ): Promise { const params = { ...getAuthParams(username, password), ...extra }; const resp = await axios.get(`${restBaseFromUrl(serverUrl)}/${endpoint}`, { params, paramsSerializer: { indexes: null }, timeout, }); const data = resp.data?.['subsonic-response']; if (!data) throw new Error('Invalid response from server (possibly not a Subsonic server)'); if (data.status !== 'ok') throw new Error(data.error?.message ?? 'Subsonic API error'); return data as T; } export function getClient() { const { getBaseUrl, getActiveServer } = useAuthStore.getState(); const server = getActiveServer(); const baseUrl = getBaseUrl(); if (!baseUrl) throw new Error('No server configured'); const params = getAuthParams(server?.username ?? '', server?.password ?? ''); return { baseUrl: `${baseUrl}/rest`, params }; } export function getServerById(serverId: string): ServerProfile | undefined { return useAuthStore.getState().servers.find(s => s.id === serverId); } /** Subsonic REST call against an explicit saved server (not necessarily the active one). */ export async function apiForServer( serverId: string, endpoint: string, extra: Record = {}, timeout = 15000, ): Promise { const server = getServerById(serverId); if (!server) throw new Error(`Unknown server: ${serverId}`); return apiWithCredentials(server.url, server.username, server.password, endpoint, extra, timeout); } export async function api(endpoint: string, extra: Record = {}, timeout = 15000): Promise { const { baseUrl, params } = getClient(); const resp = await axios.get(`${baseUrl}/${endpoint}`, { params: { ...params, ...extra }, paramsSerializer: { indexes: null }, timeout, }); const data = resp.data?.['subsonic-response']; if (!data) throw new Error('Invalid response from server (possibly not a Subsonic server)'); if (data.status !== 'ok') throw new Error(data.error?.message ?? 'Subsonic API error'); return data as T; } /** Optional `musicFolderId` when the user narrowed browsing to one Subsonic library (see `getMusicFolders`). */ export function libraryFilterParams(): Record { const { activeServerId } = useAuthStore.getState(); return activeServerId ? libraryFilterParamsForServer(activeServerId) : {}; } /** Library folder filter for an explicit saved server (e.g. Now Playing while browsing another). */ export function libraryFilterParamsForServer(serverId: string): Record { const f = useAuthStore.getState().musicLibraryFilterByServer[serverId]; if (f === undefined || f === 'all') return {}; return { musicFolderId: f }; }