mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat(song-info): show absolute file path on Navidrome via native API (#504)
* 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.
This commit is contained in:
committed by
GitHub
parent
726f3f0ff2
commit
a41e3a624a
@@ -4,6 +4,8 @@ import { X } from 'lucide-react';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { getSong, SubsonicSong } from '../api/subsonic';
|
||||
import { ndGetSongPath } from '../api/navidromeAdmin';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { copyTextToClipboard } from '../utils/serverMagicString';
|
||||
import { showToast } from '../utils/toast';
|
||||
@@ -62,17 +64,39 @@ export default function SongInfoModal() {
|
||||
);
|
||||
const [song, setSong] = useState<SubsonicSong | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
// Absolute filesystem path resolved via Navidrome's native API in parallel
|
||||
// with the Subsonic getSong call. Subsonic only ever returns a relative
|
||||
// path (or none on Navidrome); the native endpoint is what Feishin and the
|
||||
// Navidrome web client use to surface the full server-side location.
|
||||
const [absolutePath, setAbsolutePath] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!songInfoModal.isOpen || !songInfoModal.songId) {
|
||||
setSong(null);
|
||||
setAbsolutePath(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
getSong(songInfoModal.songId).then(s => {
|
||||
setAbsolutePath(null);
|
||||
const songId = songInfoModal.songId;
|
||||
getSong(songId).then(s => {
|
||||
if (!cancelled) { setSong(s); setLoading(false); }
|
||||
});
|
||||
// Try the native API in parallel; only when the active server is Navidrome
|
||||
// and we have credentials. Failures are silent — modal falls back to
|
||||
// whatever the Subsonic `path` field carried (typically nothing).
|
||||
const auth = useAuthStore.getState();
|
||||
const sid = auth.activeServerId;
|
||||
const profile = sid ? auth.servers.find(p => p.id === sid) : null;
|
||||
const identity = sid ? auth.subsonicServerIdentityByServer[sid] : undefined;
|
||||
const isNavidrome = identity?.type?.trim().toLowerCase() === 'navidrome';
|
||||
if (isNavidrome && profile?.url && profile.username && profile.password) {
|
||||
const serverUrl = (profile.url.startsWith('http') ? profile.url : `http://${profile.url}`).replace(/\/$/, '');
|
||||
ndGetSongPath(serverUrl, profile.username, profile.password, songId).then(p => {
|
||||
if (!cancelled && p) setAbsolutePath(p);
|
||||
});
|
||||
}
|
||||
return () => { cancelled = true; };
|
||||
}, [songInfoModal.isOpen, songInfoModal.songId]);
|
||||
|
||||
@@ -139,10 +163,10 @@ export default function SongInfoModal() {
|
||||
<Row label={t('songInfo.channels')} value={channels} />
|
||||
<Row label={t('songInfo.fileSize')} value={formatSize(song.size)} />
|
||||
|
||||
{song.path && (
|
||||
{(absolutePath || song.path) && (
|
||||
<>
|
||||
<Divider />
|
||||
<Row label={t('songInfo.path')} value={<span className="song-info-path">{song.path}</span>} />
|
||||
<Row label={t('songInfo.path')} value={<span className="song-info-path">{absolutePath ?? song.path}</span>} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user