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
+9 -10
View File
@@ -1,9 +1,11 @@
import { star, unstar } from '../api/subsonicStarRating';
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
import { usePlaybackCoverArt } from '../hooks/usePlaybackCoverArt';
import type { Track } from '../store/playerStoreTypes';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
import React, { useState, useCallback, useMemo, useRef, useEffect, useSyncExternalStore, CSSProperties } from 'react';
import { useNavigate } from 'react-router-dom';
import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate';
import { useEnsurePlaybackServerOnMount } from '../hooks/useEnsurePlaybackServerOnMount';
import { useTranslation } from 'react-i18next';
import {
ChevronDown, Play, Pause, SkipBack, SkipForward,
@@ -152,6 +154,8 @@ function LyricsDrawer({ onClose, currentTrack }: { onClose: () => void; currentT
export default function MobilePlayerView() {
const { t } = useTranslation();
const navigate = useNavigate();
const navigatePlaybackLibrary = usePlaybackLibraryNavigate();
useEnsurePlaybackServerOnMount();
// Lock body scroll while full-screen player is mounted
useEffect(() => {
@@ -185,12 +189,7 @@ export default function MobilePlayerView() {
const duration = currentTrack?.duration ?? 0;
// Cover art
const coverFetchUrl = useMemo(
() => currentTrack?.coverArt ? buildCoverArtUrl(currentTrack.coverArt, 800) : '',
[currentTrack?.coverArt]
);
const coverKey = currentTrack?.coverArt ? coverArtCacheKey(currentTrack.coverArt, 800) : '';
const { src: coverFetchUrl, cacheKey: coverKey } = usePlaybackCoverArt(currentTrack?.coverArt, 800);
const resolvedCover = useCachedUrl(coverFetchUrl, coverKey);
// Dynamic background color extracted from cover art
@@ -332,7 +331,7 @@ export default function MobilePlayerView() {
<OpenArtistRefInline
refs={currentTrack.artists}
fallbackName={currentTrack.artist}
onGoArtist={id => navigate(`/artist/${id}`)}
onGoArtist={id => { void navigatePlaybackLibrary(`/artist/${id}`); }}
as="none"
linkTag="span"
linkClassName="mp-artist-link"
@@ -341,12 +340,12 @@ export default function MobilePlayerView() {
<span
role={currentTrack.artistId ? 'link' : undefined}
tabIndex={currentTrack.artistId ? 0 : undefined}
onClick={() => currentTrack.artistId && navigate(`/artist/${currentTrack.artistId}`)}
onClick={() => currentTrack.artistId && void navigatePlaybackLibrary(`/artist/${currentTrack.artistId}`)}
onKeyDown={e => {
if (!currentTrack.artistId) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
navigate(`/artist/${currentTrack.artistId}`);
void navigatePlaybackLibrary(`/artist/${currentTrack.artistId}`);
}
}}
style={{ cursor: currentTrack.artistId ? 'pointer' : 'default' }}