fix(playback): pin queue playback to source server when browsing another library (#717)

* fix(playback): pin queue streams, cover art, and library links to queue server

When the active server changes while a queue from another server is playing,
keep streams and UI on queueServerId; switch back for artist/album links and
queue or player-bar context menus.

* fix(playback): switch to queue server when opening Now Playing

Ensure active server matches queueServerId before Subsonic fetches on the
Now Playing page, mobile player route, and queue info panel; scope caches
by server id.

* docs(credits): mention Now Playing in PR #717 contribution line

* fix(playback): route scrobble and queue sync to queue server

Address PR review: apiForServer for scrobble/now-playing/savePlayQueue,
clear queueServerId on server removal, mini-player queueServerId sync,
block cross-server enqueue with toast, and regression tests.
This commit is contained in:
cucadmuh
2026-05-15 16:08:41 +03:00
committed by GitHub
parent a9b50b9244
commit 45e0e1206f
58 changed files with 701 additions and 178 deletions
+25 -12
View File
@@ -7,6 +7,7 @@ import { Info } from 'lucide-react';
import { open as shellOpen } from '@tauri-apps/plugin-shell';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useEnsurePlaybackServerOnMount } from '../hooks/useEnsurePlaybackServerOnMount';
import { fetchBandsintownEvents, type BandsintownEvent } from '../api/bandsintown';
import CachedImage from './CachedImage';
import OverlayScrollArea from './OverlayScrollArea';
@@ -72,21 +73,31 @@ function buildContributorRows(
return out;
}
function queuePanelCacheKey(serverId: string, id: string): string {
return serverId ? `${serverId}:${id}` : id;
}
export default function NowPlayingInfo() {
const { t } = useTranslation();
const currentTrack = usePlayerStore(s => s.currentTrack);
const enableBandsintown = useAuthStore(s => s.enableBandsintown);
const setEnableBandsintown = useAuthStore(s => s.setEnableBandsintown);
const subsonicReady = useEnsurePlaybackServerOnMount();
const subsonicServerId = useAuthStore(s => s.activeServerId ?? '');
const artistName = currentTrack?.artist || '';
const artistId = currentTrack?.artistId || '';
const songId = currentTrack?.id || '';
const [artistInfo, setArtistInfo] = useState<SubsonicArtistInfo | null>(
artistId ? artistInfoCache.get(artistId) ?? null : null,
artistId && subsonicServerId
? artistInfoCache.get(queuePanelCacheKey(subsonicServerId, artistId)) ?? null
: null,
);
const [songDetail, setSongDetail] = useState<SubsonicSong | null>(
songId ? songDetailCache.get(songId) ?? null : null,
songId && subsonicServerId
? songDetailCache.get(queuePanelCacheKey(subsonicServerId, songId)) ?? null
: null,
);
const [tourEvents, setTourEvents] = useState<BandsintownEvent[]>([]);
const [tourLoading, setTourLoading] = useState(false);
@@ -101,27 +112,29 @@ export default function NowPlayingInfo() {
// Artist bio + image
useEffect(() => {
if (!artistId) { setArtistInfo(null); return; }
const cached = artistInfoCache.get(artistId);
if (!subsonicReady || !subsonicServerId || !artistId) { setArtistInfo(null); return; }
const cacheKey = queuePanelCacheKey(subsonicServerId, artistId);
const cached = artistInfoCache.get(cacheKey);
if (cached !== undefined) { setArtistInfo(cached); return; }
let cancelled = false;
getArtistInfo(artistId)
.then(info => { if (!cancelled) { artistInfoCache.set(artistId, info ?? null); setArtistInfo(info ?? null); } })
.catch(() => { if (!cancelled) { artistInfoCache.set(artistId, null); setArtistInfo(null); } });
.then(info => { if (!cancelled) { artistInfoCache.set(cacheKey, info ?? null); setArtistInfo(info ?? null); } })
.catch(() => { if (!cancelled) { artistInfoCache.set(cacheKey, null); setArtistInfo(null); } });
return () => { cancelled = true; };
}, [artistId]);
}, [subsonicReady, subsonicServerId, artistId]);
// Song detail (for OpenSubsonic contributors[])
useEffect(() => {
if (!songId) { setSongDetail(null); return; }
const cached = songDetailCache.get(songId);
if (!subsonicReady || !subsonicServerId || !songId) { setSongDetail(null); return; }
const cacheKey = queuePanelCacheKey(subsonicServerId, songId);
const cached = songDetailCache.get(cacheKey);
if (cached !== undefined) { setSongDetail(cached); return; }
let cancelled = false;
getSong(songId)
.then(song => { if (!cancelled) { songDetailCache.set(songId, song ?? null); setSongDetail(song ?? null); } })
.catch(() => { if (!cancelled) { songDetailCache.set(songId, null); setSongDetail(null); } });
.then(song => { if (!cancelled) { songDetailCache.set(cacheKey, song ?? null); setSongDetail(song ?? null); } })
.catch(() => { if (!cancelled) { songDetailCache.set(cacheKey, null); setSongDetail(null); } });
return () => { cancelled = true; };
}, [songId]);
}, [subsonicReady, subsonicServerId, songId]);
// Bandsintown — only when opt-in toggle is on
useEffect(() => {