mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
a41e3a624a
* feat(song-info): show absolute file path on Navidrome via native API
Subsonic's `getSong.view` returns at most a relative path (or none on
Navidrome), so the Path row in the Song Info modal stayed empty for
most users. Feishin and the Navidrome web client surface the full
server-side path by hitting Navidrome's native `/api/song/{id}` instead.
Added an `nd_get_song_path` Tauri command that logs in to the native
API with the active server's credentials, fetches the song, and returns
the `path` string. Wired it into `SongInfoModal`: the Subsonic
`getSong` call still drives the rest of the dialog, and the native call
runs in parallel only when the active server's identity is
"navidrome". When it returns a path, that absolute path replaces the
relative Subsonic value; native-API failures are silent and the modal
falls back to whatever Subsonic provided.
No token cache yet — the modal is opened occasionally enough that one
fresh login per call is fine.
Closes discussion #479.
* docs: changelog entry for PR #504
Logs the Navidrome native-API absolute file path support for the
Song Info dialog in v1.46.0 "## Added".
* docs: settings contributor entry for PR #504, drop competitor mention
Adds the song-info absolute path bullet to Psychotoxical's contributors
list in Settings, and rewords the existing CHANGELOG entry so neither
text references competing clients.
134 lines
4.1 KiB
TypeScript
134 lines
4.1 KiB
TypeScript
import { invoke } from '@tauri-apps/api/core';
|
|
|
|
export interface NdLibrary {
|
|
id: number;
|
|
name: string;
|
|
}
|
|
|
|
export interface NdUser {
|
|
id: string;
|
|
userName: string;
|
|
name: string;
|
|
email: string;
|
|
isAdmin: boolean;
|
|
libraryIds: number[];
|
|
lastLoginAt?: string | null;
|
|
lastAccessAt?: string | null;
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
}
|
|
|
|
export interface NdLoginResult {
|
|
token: string;
|
|
userId: string;
|
|
isAdmin: boolean;
|
|
}
|
|
|
|
export async function ndLogin(
|
|
serverUrl: string,
|
|
username: string,
|
|
password: string,
|
|
): Promise<NdLoginResult> {
|
|
return invoke<NdLoginResult>('navidrome_login', { serverUrl, username, password });
|
|
}
|
|
|
|
function extractLibraryIds(o: Record<string, unknown>): number[] {
|
|
const libs = o.libraries;
|
|
if (!Array.isArray(libs)) return [];
|
|
const ids: number[] = [];
|
|
for (const l of libs) {
|
|
const id = (l as Record<string, unknown>)?.id;
|
|
if (typeof id === 'number') ids.push(id);
|
|
else if (typeof id === 'string' && /^\d+$/.test(id)) ids.push(Number(id));
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
export async function ndListUsers(serverUrl: string, token: string): Promise<NdUser[]> {
|
|
const raw = await invoke<unknown>('nd_list_users', { serverUrl, token });
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.map(u => {
|
|
const o = u as Record<string, unknown>;
|
|
return {
|
|
id: String(o.id ?? ''),
|
|
userName: String(o.userName ?? ''),
|
|
name: String(o.name ?? ''),
|
|
email: String(o.email ?? ''),
|
|
isAdmin: !!o.isAdmin,
|
|
libraryIds: extractLibraryIds(o),
|
|
lastLoginAt: (o.lastLoginAt as string | null | undefined) ?? null,
|
|
lastAccessAt: (o.lastAccessAt as string | null | undefined) ?? null,
|
|
createdAt: o.createdAt as string | undefined,
|
|
updatedAt: o.updatedAt as string | undefined,
|
|
};
|
|
});
|
|
}
|
|
|
|
export async function ndListLibraries(serverUrl: string, token: string): Promise<NdLibrary[]> {
|
|
const raw = await invoke<unknown>('nd_list_libraries', { serverUrl, token });
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.map(l => {
|
|
const o = l as Record<string, unknown>;
|
|
const id = typeof o.id === 'number'
|
|
? o.id
|
|
: typeof o.id === 'string' && /^\d+$/.test(o.id) ? Number(o.id) : 0;
|
|
return { id, name: String(o.name ?? '') };
|
|
}).filter(l => l.id > 0);
|
|
}
|
|
|
|
export async function ndSetUserLibraries(
|
|
serverUrl: string,
|
|
token: string,
|
|
id: string,
|
|
libraryIds: number[],
|
|
): Promise<void> {
|
|
await invoke('nd_set_user_libraries', { serverUrl, token, id, libraryIds });
|
|
}
|
|
|
|
export async function ndCreateUser(
|
|
serverUrl: string,
|
|
token: string,
|
|
data: { userName: string; name: string; email: string; password: string; isAdmin: boolean },
|
|
): Promise<{ id: string }> {
|
|
const raw = await invoke<unknown>('nd_create_user', { serverUrl, token, ...data });
|
|
const o = (raw as Record<string, unknown> | null) ?? {};
|
|
return { id: String(o.id ?? '') };
|
|
}
|
|
|
|
export async function ndUpdateUser(
|
|
serverUrl: string,
|
|
token: string,
|
|
id: string,
|
|
data: { userName: string; name: string; email: string; password: string; isAdmin: boolean },
|
|
): Promise<void> {
|
|
await invoke('nd_update_user', { serverUrl, token, id, ...data });
|
|
}
|
|
|
|
export async function ndDeleteUser(serverUrl: string, token: string, id: string): Promise<void> {
|
|
await invoke('nd_delete_user', { serverUrl, token, id });
|
|
}
|
|
|
|
/**
|
|
* Fetch the absolute filesystem path of a song from Navidrome's native API.
|
|
* Subsonic `getSong.view` only returns a relative path (or none on Navidrome);
|
|
* the native `/api/song/:id` endpoint returns the absolute path the server
|
|
* stores the file at, the same way Feishin and the Navidrome web client get it.
|
|
*
|
|
* Returns `null` when the server doesn't expose the field (non-admin on some
|
|
* Navidrome configurations) or when the call fails — the Song Info modal then
|
|
* falls back to whatever the Subsonic response had.
|
|
*/
|
|
export async function ndGetSongPath(
|
|
serverUrl: string,
|
|
username: string,
|
|
password: string,
|
|
id: string,
|
|
): Promise<string | null> {
|
|
try {
|
|
const raw = await invoke<string | null>('nd_get_song_path', { serverUrl, username, password, id });
|
|
return raw && raw.length > 0 ? raw : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|