fix(playlist-detail): route plex tracks through v2_plex_play_track
Plex tracks added to a Qobuz playlist weren't playable:
* Clicking the row hit onTrackPlay → playTrack with no source arg →
backend treated the id as a Qobuz track id and rejected it with
"invalid track id for remote handoff: -1000017964" (the negative
plexDisplayId UI-only offset was the id we sent).
* Auto-advancing from the previous (local) track mapped the plex
DisplayTrack's negative id through Math.abs into the local-track
queue, which then looked 1_000_017_964 up in local_tracks and
errored with "Track not found".
Changes:
1. plexTrackToDisplay now uses Number(ratingKey) as id when the
ratingKey parses as a positive integer, matching the shape Plex
backend / LocalLibraryView already use (PlexCachedTrack.id =
playback_track_id(rating_key) = the numeric rating_key for the
common case). The negative-offset hash stays as the fallback for
non-numeric rating keys — those still need UI-unique ids but are
rare enough that no-play is an acceptable degradation.
2. isLocal stays true so the Qobuz context-builder and blacklist
filter keep their existing semantics; the new isPlex flag drives
routing at the dispatch sites.
3. PlaylistDetailView.Props gains onPlexTrackPlay (track) → void.
handleTrackClick checks track.isPlex first and dispatches through
it; handleTrackPlayNext / handleTrackPlayLater no-op on plex for
now (backend queue doesn't understand plex yet).
4. removeTrackFromPlaylist checks track.isPlex first and calls
v2_playlist_remove_plex_track with ratingKey: String(track.id) so
the "Remove from playlist" menu item works on plex rows too.
5. +page.svelte gets handleDisplayPlexTrackPlay — mirrors
handleLocalTrackPlay's plex branch (playTrack with source='plex',
isLocal:false). Wired via onPlexTrackPlay on the PlaylistDetailView
render.
6. buildQueueTracks filters plex out entirely. The local-track queue
path in the backend looks ids up in local_tracks; plex ratingKeys
aren't there and the mismatch was the source of the auto-advance
"Track not found" toast. Plex tracks keep their direct-click
playback; gapless auto-advance across plex will require backend
queue awareness (deferred).
Known limitation carried forward: Play Next / Play Later / auto-
advance across plex tracks are no-ops until the backend queue learns
to cross-reference plex_cache_tracks. Direct click on a plex row
plays correctly.
cc
This commit is contained in:
2 files changed
+89
-8
No files matched your search
@@ -217,6 +217,14 @@
|
||||
onLocalTrackPlay?: (track: LocalLibraryTrack) => void;
|
||||
onLocalTrackPlayNext?: (track: LocalLibraryTrack) => void;
|
||||
onLocalTrackPlayLater?: (track: LocalLibraryTrack) => void;
|
||||
/**
|
||||
* Play a Plex track from the playlist. Fired when the user clicks
|
||||
* a row whose isPlex flag is set. The track argument carries the
|
||||
* DisplayTrack shape (including title/artist/album/artwork) plus
|
||||
* id = Number(ratingKey) — the +page.svelte handler routes to
|
||||
* v2_plex_play_track via playTrack with source='plex'.
|
||||
*/
|
||||
onPlexTrackPlay?: (track: DisplayTrack) => void;
|
||||
onSetLocalQueue?: (trackIds: number[]) => void;
|
||||
onPlaylistCountUpdate?: (playlistId: number, qobuzCount: number, localCount: number) => void;
|
||||
onPlaylistUpdated?: () => void;
|
||||
@@ -249,6 +257,7 @@
|
||||
onLocalTrackPlay,
|
||||
onLocalTrackPlayNext,
|
||||
onLocalTrackPlayLater,
|
||||
onPlexTrackPlay,
|
||||
onSetLocalQueue,
|
||||
onPlaylistCountUpdate,
|
||||
onPlaylistUpdated,
|
||||
@@ -1549,10 +1558,21 @@
|
||||
return `${base}${path}${separator}X-Plex-Token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
// Convert plex tracks to DisplayTrack format
|
||||
// Convert plex tracks to DisplayTrack format.
|
||||
// id: parse the ratingKey to a Number when possible — this matches
|
||||
// the LocalLibraryView.PlexCachedTrack.id shape (playback_track_id
|
||||
// on the backend) and is what v2_plex_play_track expects as its
|
||||
// ratingKey arg (via String(track.id)). The negative-offset fallback
|
||||
// is only used for non-numeric rating keys, which are rare and only
|
||||
// need frontend uniqueness.
|
||||
function plexTrackToDisplay(track: PlaylistPlexTrack, index: number): DisplayTrack {
|
||||
const numericId = Number(track.ratingKey);
|
||||
const idForUi =
|
||||
Number.isFinite(numericId) && numericId > 0
|
||||
? numericId
|
||||
: plexDisplayId(track.ratingKey);
|
||||
return {
|
||||
id: plexDisplayId(track.ratingKey),
|
||||
id: idForUi,
|
||||
number: index + 1,
|
||||
title: track.title,
|
||||
artist: track.artist,
|
||||
@@ -1563,8 +1583,8 @@
|
||||
hires: (track.bit_depth && track.bit_depth >= 24) || track.sample_rate > 48000,
|
||||
bitDepth: track.bit_depth,
|
||||
samplingRate: track.sample_rate / 1000,
|
||||
isLocal: true,
|
||||
isPlex: true,
|
||||
isLocal: true, // kept for context-builder / blacklist filters
|
||||
isPlex: true, // drives plex-specific playback routing
|
||||
artworkPath: track.artwork_path,
|
||||
};
|
||||
}
|
||||
@@ -1783,8 +1803,16 @@
|
||||
}
|
||||
|
||||
function buildQueueTracks(tracks: DisplayTrack[]) {
|
||||
// Filter out blacklisted artists before building queue
|
||||
// Filter out blacklisted artists AND plex tracks from the queue.
|
||||
// Plex exclusion is a short-term safety: the local-track queue path
|
||||
// in the backend looks every id up in local_tracks, and Plex rating
|
||||
// keys don't live there — they'd crash auto-advance with
|
||||
// "Track not found". Plex tracks in a playlist still play when
|
||||
// clicked directly (handleTrackClick dispatches via isPlex below);
|
||||
// gapless auto-advance to/from plex is deferred until the backend
|
||||
// queue learns to cross-reference plex_cache_tracks.
|
||||
const filteredTracks = tracks.filter(trk => {
|
||||
if (trk.isPlex) return false;
|
||||
if (trk.isLocal) return true; // Local tracks are never blacklisted
|
||||
if (!trk.artistId) return true; // No artist ID, can't check blacklist
|
||||
return !isArtistBlacklisted(trk.artistId);
|
||||
@@ -1830,7 +1858,7 @@
|
||||
.map(trk => trk.id);
|
||||
|
||||
const contextIndex = trackIds.indexOf(track.id);
|
||||
|
||||
|
||||
if (contextIndex >= 0 && trackIds.length > 0) {
|
||||
await setPlaybackContext(
|
||||
'playlist',
|
||||
@@ -1851,7 +1879,12 @@
|
||||
console.error('Failed to set queue:', err);
|
||||
}
|
||||
|
||||
if (track.isLocal && track.localTrackId) {
|
||||
if (track.isPlex) {
|
||||
// Plex tracks go through the dedicated plex handler — parent
|
||||
// routes to playTrack with source='plex', which hits
|
||||
// v2_plex_play_track(ratingKey=String(track.id)).
|
||||
onPlexTrackPlay?.(track);
|
||||
} else if (track.isLocal && track.localTrackId) {
|
||||
// Handle local track play
|
||||
const localTrack = localTracksMap.get(track.localTrackId);
|
||||
if (localTrack && onLocalTrackPlay) {
|
||||
@@ -1863,6 +1896,11 @@
|
||||
}
|
||||
|
||||
function handleTrackPlayNext(track: DisplayTrack) {
|
||||
// Plex Play Next / Later aren't wired on the backend queue yet —
|
||||
// the queue's local-track path looks ids up in local_tracks, which
|
||||
// doesn't cover plex rating keys. Drop silently so the menu item
|
||||
// is effectively a no-op for plex until queue support lands.
|
||||
if (track.isPlex) return;
|
||||
if (track.isLocal && track.localTrackId) {
|
||||
const localTrack = localTracksMap.get(track.localTrackId);
|
||||
if (localTrack && onLocalTrackPlayNext) {
|
||||
@@ -1874,6 +1912,7 @@
|
||||
}
|
||||
|
||||
function handleTrackPlayLater(track: DisplayTrack) {
|
||||
if (track.isPlex) return;
|
||||
if (track.isLocal && track.localTrackId) {
|
||||
const localTrack = localTracksMap.get(track.localTrackId);
|
||||
if (localTrack && onLocalTrackPlayLater) {
|
||||
@@ -1886,7 +1925,16 @@
|
||||
|
||||
async function removeTrackFromPlaylist(track: DisplayTrack) {
|
||||
try {
|
||||
if (track.isLocal && track.localTrackId) {
|
||||
if (track.isPlex) {
|
||||
// Plex tracks live in playlist_plex_tracks, keyed by ratingKey
|
||||
// (string). track.id is Number(ratingKey) — stringify it back.
|
||||
await invoke('v2_playlist_remove_plex_track', {
|
||||
playlistId,
|
||||
ratingKey: String(track.id),
|
||||
});
|
||||
await Promise.all([loadLocalTracks(), loadPlexTracks()]);
|
||||
notifyParentOfCounts();
|
||||
} else if (track.isLocal && track.localTrackId) {
|
||||
// Remove local track
|
||||
await invoke('v2_playlist_remove_local_track', { playlistId, localTrackId: track.localTrackId });
|
||||
await Promise.all([loadLocalTracks(), loadPlexTracks()]);
|
||||
|
||||
@@ -3683,6 +3683,38 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a Plex-sourced DisplayTrack. Mirrors handleLocalTrackPlay's
|
||||
* Plex branch: routes through playTrack with source='plex', which
|
||||
* hits v2_plex_play_track(ratingKey=String(track.id)). track.id is
|
||||
* already Number(ratingKey) by the time this callback fires — set
|
||||
* in PlaylistDetailView.plexTrackToDisplay.
|
||||
*/
|
||||
function handleDisplayPlexTrackPlay(track: DisplayTrack): void {
|
||||
console.log('Playing plex display track:', track.id, track.title);
|
||||
const quality = track.bitDepth && track.samplingRate
|
||||
? `${track.bitDepth}bit/${track.samplingRate}kHz`
|
||||
: track.hires
|
||||
? 'Hi-Res'
|
||||
: '-';
|
||||
playTrack(
|
||||
{
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.artist || 'Unknown Artist',
|
||||
album: track.album || 'Playlist',
|
||||
artwork: track.albumArt || '',
|
||||
duration: track.durationSeconds,
|
||||
quality,
|
||||
bitDepth: track.bitDepth,
|
||||
samplingRate: track.samplingRate,
|
||||
isLocal: false,
|
||||
source: 'plex',
|
||||
},
|
||||
{ isLocal: false, source: 'plex' },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Create context and play display track
|
||||
*/
|
||||
@@ -5926,6 +5958,7 @@
|
||||
onLocalTrackPlay={handleLocalTrackPlay}
|
||||
onLocalTrackPlayNext={queueLocalTrackNext}
|
||||
onLocalTrackPlayLater={queueLocalTrackLater}
|
||||
onPlexTrackPlay={handleDisplayPlexTrackPlay}
|
||||
onSetLocalQueue={handleSetLocalQueue}
|
||||
onPlaylistCountUpdate={(playlistId, qobuzCount, localCount) =>
|
||||
sidebarRef?.updatePlaylistCounts(playlistId, qobuzCount, localCount)
|
||||
|
||||
Reference in new issue
Block a user