feat(playlist-modal): route Plex tracks through the dedicated add path
Frontend half of the fix. LocalLibraryView no longer gates Plex
tracks out of "Add to playlist" — the whole add pipeline now routes
by source:
* source === 'plex' → v2_playlist_add_plex_track(playlistId,
ratingKey, position). ratingKey comes from track.file_path, which
is where mapPlexTrack stashes it at load time.
* source in {user, qobuz_purchase, qobuz_download} →
v2_playlist_add_local_track(playlistId, localTrackId, position)
(unchanged).
* source === qobuz (Qobuz catalog track) →
v2_add_tracks_to_playlist (unchanged).
Changes:
* uiStore: new playlistModalPlexRatingKeys slot + getter + passed
through openPlaylistModal (4th arg) and closePlaylistModal.
* PlaylistModal: new plexRatingKeys prop, isPlexTracks derived flag,
and branches in handleAddToPlaylist / handleDirectAddToPlaylist /
handleCreateAndAdd that emit v2_playlist_add_plex_track. Duplicate
detection (which only covers Qobuz) is skipped for Plex adds.
tracks_count update for newly-created playlists now treats Plex
like local — 0, since the Qobuz side doesn't own those tracks.
* LocalLibraryView: adds onTrackAddPlexToPlaylist /
onBulkAddPlexToPlaylist callbacks. handleBulkAddToPlaylist and
handleAlbumBulkAddToPlaylist split the selection by source and
emit the right callback per subset. The per-row plex gate at
line 3509 is replaced with source-aware routing.
* +page.svelte: new openAddPlexToPlaylist helper + wired the two
new callbacks into the LocalLibraryView render.
No confusion with Qobuz tracks: Plex rating keys never touch the
Qobuz playlist API, and local-track ids never reach the Plex
command. The two namespaces stay physically separate at every layer.
cc
This commit is contained in:
4 files changed
+101
-20
No files matched your search
@@ -36,6 +36,7 @@
|
||||
onDelete?: (playlistId: number) => void;
|
||||
isHidden?: boolean;
|
||||
isLocalTracks?: boolean;
|
||||
plexRatingKeys?: string[];
|
||||
currentFolderId?: string | null;
|
||||
}
|
||||
|
||||
@@ -50,9 +51,12 @@
|
||||
onDelete,
|
||||
isHidden = false,
|
||||
isLocalTracks = false,
|
||||
plexRatingKeys = [],
|
||||
currentFolderId = null
|
||||
}: Props = $props();
|
||||
|
||||
const isPlexTracks = $derived(plexRatingKeys.length > 0);
|
||||
|
||||
// Form state
|
||||
let name = $state('');
|
||||
let description = $state('');
|
||||
@@ -400,13 +404,16 @@
|
||||
}
|
||||
|
||||
async function handleAddToPlaylist() {
|
||||
if (!selectedPlaylistId || trackIds.length === 0) {
|
||||
const hasIds = trackIds.length > 0 || plexRatingKeys.length > 0;
|
||||
if (!selectedPlaylistId || !hasIds) {
|
||||
error = $t('playlist.selectPlaylist');
|
||||
return;
|
||||
}
|
||||
|
||||
// For pending playlists and local tracks add directly
|
||||
if (selectedPlaylistId < 0 || isLocalTracks) {
|
||||
// For pending playlists, local tracks, and Plex tracks add directly.
|
||||
// Duplicate detection below only covers Qobuz tracks, so anything
|
||||
// non-Qobuz bypasses it.
|
||||
if (selectedPlaylistId < 0 || isLocalTracks || isPlexTracks) {
|
||||
await handleDirectAddToPlaylist();
|
||||
return;
|
||||
}
|
||||
@@ -442,7 +449,8 @@
|
||||
}
|
||||
|
||||
async function handleDirectAddToPlaylist() {
|
||||
if (!selectedPlaylistId || trackIds.length === 0) {
|
||||
const hasIds = trackIds.length > 0 || plexRatingKeys.length > 0;
|
||||
if (!selectedPlaylistId || !hasIds) {
|
||||
error = $t('playlist.selectPlaylist');
|
||||
return;
|
||||
}
|
||||
@@ -454,7 +462,7 @@
|
||||
// Check if this is a pending playlist (negative ID)
|
||||
if (selectedPlaylistId < 0) {
|
||||
const pendingId = -selectedPlaylistId; // Convert back to positive
|
||||
const qobuzTrackIds = isLocalTracks ? [] : trackIds;
|
||||
const qobuzTrackIds = isLocalTracks || isPlexTracks ? [] : trackIds;
|
||||
|
||||
// For local tracks, fetch file paths instead of using IDs
|
||||
let localTrackPaths: string[] = [];
|
||||
@@ -471,6 +479,22 @@
|
||||
qobuzTrackIds,
|
||||
localTrackPaths
|
||||
});
|
||||
} else if (isPlexTracks) {
|
||||
// Regular playlist with Plex tracks — route through the dedicated
|
||||
// plex command which stores the rating key in playlist_plex_tracks.
|
||||
// Never hits the Qobuz API.
|
||||
const playlist = userPlaylists.find(p => p.id === selectedPlaylistId);
|
||||
const qobuzCount = playlist?.tracks_count ?? 0;
|
||||
const localCount = localTrackCounts.get(selectedPlaylistId!) ?? 0;
|
||||
const startPosition = qobuzCount + localCount;
|
||||
|
||||
for (let i = 0; i < plexRatingKeys.length; i++) {
|
||||
await invoke('v2_playlist_add_plex_track', {
|
||||
playlistId: selectedPlaylistId,
|
||||
ratingKey: plexRatingKeys[i],
|
||||
position: startPosition + i
|
||||
});
|
||||
}
|
||||
} else if (isLocalTracks) {
|
||||
// Regular playlist with local tracks
|
||||
// Get current total count (Qobuz + local) to append at correct position
|
||||
@@ -596,7 +620,16 @@
|
||||
});
|
||||
|
||||
// Then add tracks
|
||||
if (trackIds.length > 0) {
|
||||
if (isPlexTracks && plexRatingKeys.length > 0) {
|
||||
// Plex tracks — dedicated command, never hits Qobuz API
|
||||
for (let i = 0; i < plexRatingKeys.length; i++) {
|
||||
await invoke('v2_playlist_add_plex_track', {
|
||||
playlistId: newPlaylist.id,
|
||||
ratingKey: plexRatingKeys[i],
|
||||
position: i
|
||||
});
|
||||
}
|
||||
} else if (trackIds.length > 0) {
|
||||
if (isLocalTracks) {
|
||||
// Add local tracks one by one
|
||||
for (let i = 0; i < trackIds.length; i++) {
|
||||
@@ -614,7 +647,9 @@
|
||||
void logPlaylistAdd(trackIds, newPlaylist.id);
|
||||
}
|
||||
// Update tracks_count to reflect added tracks (API returns 0 at creation)
|
||||
newPlaylist.tracks_count = isLocalTracks ? 0 : trackIds.length;
|
||||
// Local and Plex tracks live outside the Qobuz playlist, so the Qobuz
|
||||
// count stays at 0 — they're surfaced via localTrackCounts separately.
|
||||
newPlaylist.tracks_count = isLocalTracks || isPlexTracks ? 0 : trackIds.length;
|
||||
}
|
||||
|
||||
onSuccess?.(newPlaylist);
|
||||
|
||||
@@ -234,6 +234,8 @@
|
||||
onTrackPlayLater?: (track: LocalTrack) => void;
|
||||
onTrackAddToPlaylist?: (trackId: number) => void;
|
||||
onBulkAddToPlaylist?: (trackIds: number[]) => void;
|
||||
onTrackAddPlexToPlaylist?: (ratingKey: string) => void;
|
||||
onBulkAddPlexToPlaylist?: (ratingKeys: string[]) => void;
|
||||
onSetLocalQueue?: (trackIds: number[]) => void;
|
||||
activeTrackId?: number | null;
|
||||
isPlaybackActive?: boolean;
|
||||
@@ -249,6 +251,8 @@
|
||||
onTrackPlayLater,
|
||||
onTrackAddToPlaylist,
|
||||
onBulkAddToPlaylist,
|
||||
onTrackAddPlexToPlaylist,
|
||||
onBulkAddPlexToPlaylist,
|
||||
onSetLocalQueue,
|
||||
activeTrackId = null,
|
||||
isPlaybackActive = false
|
||||
@@ -575,12 +579,17 @@
|
||||
}
|
||||
|
||||
function handleBulkAddToPlaylist() {
|
||||
// Plex tracks can't be added to playlists (matches per-row guard at line ~3306)
|
||||
const ids = selectedLocalTracks()
|
||||
.filter(trk => trk.source !== 'plex')
|
||||
.map(trk => trk.id);
|
||||
if (ids.length === 0) return;
|
||||
onBulkAddToPlaylist?.(ids);
|
||||
// Split selection by source: local_tracks-backed rows (user / qobuz
|
||||
// purchases / qobuz downloads) route through v2_playlist_add_local_track;
|
||||
// Plex rows route through v2_playlist_add_plex_track with their
|
||||
// rating key (stored in file_path at map time). Each path stays within
|
||||
// its own namespace so nothing bleeds into the Qobuz playlist API.
|
||||
const selected = selectedLocalTracks();
|
||||
const localIds = selected.filter(trk => trk.source !== 'plex').map(trk => trk.id);
|
||||
const plexRatingKeys = selected.filter(trk => trk.source === 'plex').map(trk => trk.file_path);
|
||||
if (localIds.length === 0 && plexRatingKeys.length === 0) return;
|
||||
if (localIds.length > 0) onBulkAddToPlaylist?.(localIds);
|
||||
if (plexRatingKeys.length > 0) onBulkAddPlexToPlaylist?.(plexRatingKeys);
|
||||
trackSelectMode = false;
|
||||
selectedTrackIds = new Set();
|
||||
}
|
||||
@@ -669,15 +678,20 @@
|
||||
selectedAlbumIds = new Set();
|
||||
}
|
||||
|
||||
/** Bulk-add selected albums' tracks to a playlist. Albums → tracks →
|
||||
* playlist flow, skipping Plex tracks (mirrors per-row guard). */
|
||||
/** Bulk-add selected albums' tracks to a playlist. Splits local rows
|
||||
* from Plex rows so each goes through the right backend path — see
|
||||
* handleBulkAddToPlaylist for the same split on the tracks-tab side. */
|
||||
async function handleAlbumBulkAddToPlaylist() {
|
||||
const picked = selectedAlbums();
|
||||
if (picked.length === 0) return;
|
||||
const tracksFlat = await resolveAlbumsTracks(picked);
|
||||
const ids = tracksFlat.filter((trk) => trk.source !== 'plex').map((trk) => trk.id);
|
||||
if (ids.length === 0) return;
|
||||
onBulkAddToPlaylist?.(ids);
|
||||
const localIds = tracksFlat.filter((trk) => trk.source !== 'plex').map((trk) => trk.id);
|
||||
const plexRatingKeys = tracksFlat
|
||||
.filter((trk) => trk.source === 'plex')
|
||||
.map((trk) => trk.file_path);
|
||||
if (localIds.length === 0 && plexRatingKeys.length === 0) return;
|
||||
if (localIds.length > 0) onBulkAddToPlaylist?.(localIds);
|
||||
if (plexRatingKeys.length > 0) onBulkAddPlexToPlaylist?.(plexRatingKeys);
|
||||
albumSelectMode = false;
|
||||
selectedAlbumIds = new Set();
|
||||
}
|
||||
@@ -3506,7 +3520,9 @@
|
||||
onPlayNow: () => handleTrackPlay(track),
|
||||
onPlayNext: onTrackPlayNext ? () => onTrackPlayNext(track) : undefined,
|
||||
onPlayLater: onTrackPlayLater ? () => onTrackPlayLater(track) : undefined,
|
||||
onAddToPlaylist: onTrackAddToPlaylist && track.source !== 'plex' ? () => onTrackAddToPlaylist(track.id) : undefined
|
||||
onAddToPlaylist: track.source === 'plex'
|
||||
? (onTrackAddPlexToPlaylist ? () => onTrackAddPlexToPlaylist(track.file_path) : undefined)
|
||||
: (onTrackAddToPlaylist ? () => onTrackAddToPlaylist(track.id) : undefined)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
@@ -53,6 +53,12 @@ let isPlaylistModalOpen = false;
|
||||
let playlistModalMode: 'create' | 'edit' | 'addTrack' = 'create';
|
||||
let playlistModalTrackIds: number[] = [];
|
||||
let playlistModalTracksAreLocal = false;
|
||||
// Parallel identifier list for Plex tracks. Plex's native id is the
|
||||
// rating key (TEXT), not the numeric id used by Qobuz / local tracks,
|
||||
// so it lives in its own array. When non-empty the modal routes adds
|
||||
// through v2_playlist_add_plex_track instead of the local / Qobuz
|
||||
// paths.
|
||||
let playlistModalPlexRatingKeys: string[] = [];
|
||||
let isPlaylistImportOpen = false;
|
||||
|
||||
// Listeners
|
||||
@@ -282,11 +288,21 @@ export function getPlaylistModalTracksAreLocal(): boolean {
|
||||
return playlistModalTracksAreLocal;
|
||||
}
|
||||
|
||||
export function openPlaylistModal(mode: 'create' | 'edit' | 'addTrack', trackIds: number[] = [], isLocal = false): void {
|
||||
export function getPlaylistModalPlexRatingKeys(): string[] {
|
||||
return playlistModalPlexRatingKeys;
|
||||
}
|
||||
|
||||
export function openPlaylistModal(
|
||||
mode: 'create' | 'edit' | 'addTrack',
|
||||
trackIds: number[] = [],
|
||||
isLocal = false,
|
||||
plexRatingKeys: string[] = [],
|
||||
): void {
|
||||
isPlaylistModalOpen = true;
|
||||
playlistModalMode = mode;
|
||||
playlistModalTrackIds = trackIds;
|
||||
playlistModalTracksAreLocal = isLocal;
|
||||
playlistModalPlexRatingKeys = plexRatingKeys;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -294,6 +310,7 @@ export function closePlaylistModal(): void {
|
||||
isPlaylistModalOpen = false;
|
||||
playlistModalTrackIds = [];
|
||||
playlistModalTracksAreLocal = false;
|
||||
playlistModalPlexRatingKeys = [];
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -367,6 +384,7 @@ export interface UIState {
|
||||
playlistModalMode: 'create' | 'edit' | 'addTrack';
|
||||
playlistModalTrackIds: number[];
|
||||
playlistModalTracksAreLocal: boolean;
|
||||
playlistModalPlexRatingKeys: string[];
|
||||
isPlaylistImportOpen: boolean;
|
||||
miniPlayer: MiniPlayerState;
|
||||
}
|
||||
@@ -382,6 +400,7 @@ export function getUIState(): UIState {
|
||||
playlistModalMode,
|
||||
playlistModalTrackIds,
|
||||
playlistModalTracksAreLocal,
|
||||
playlistModalPlexRatingKeys,
|
||||
isPlaylistImportOpen,
|
||||
miniPlayer: getMiniPlayerState()
|
||||
};
|
||||
|
||||
@@ -965,6 +965,7 @@
|
||||
let playlistModalMode = $state<'create' | 'edit' | 'addTrack'>('create');
|
||||
let playlistModalTrackIds = $state<number[]>([]);
|
||||
let playlistModalTracksAreLocal = $state(false);
|
||||
let playlistModalPlexRatingKeys = $state<string[]>([]);
|
||||
let playlistModalEditPlaylist = $state<{ id: number; name: string; tracks_count: number } | undefined>(undefined);
|
||||
let playlistModalEditIsHidden = $state(false);
|
||||
let playlistModalEditCurrentFolderId = $state<string | null>(null);
|
||||
@@ -3799,6 +3800,12 @@
|
||||
openPlaylistModal('addTrack', trackIds, isLocal);
|
||||
}
|
||||
|
||||
function openAddPlexToPlaylist(ratingKeys: string[]) {
|
||||
clearPlaylistEditContext();
|
||||
userPlaylists = sidebarRef?.getPlaylists() ?? [];
|
||||
openPlaylistModal('addTrack', [], false, ratingKeys);
|
||||
}
|
||||
|
||||
function handlePlaylistCreated(playlist?: { id: number; name: string; tracks_count: number }) {
|
||||
const trackCount = playlistModalTrackIds.length;
|
||||
const isLocal = playlistModalTracksAreLocal;
|
||||
@@ -4609,6 +4616,7 @@
|
||||
playlistModalMode = uiState.playlistModalMode;
|
||||
playlistModalTrackIds = uiState.playlistModalTrackIds;
|
||||
playlistModalTracksAreLocal = uiState.playlistModalTracksAreLocal;
|
||||
playlistModalPlexRatingKeys = uiState.playlistModalPlexRatingKeys;
|
||||
if (wasPlaylistModalOpen && !uiState.isPlaylistModalOpen) {
|
||||
clearPlaylistEditContext();
|
||||
}
|
||||
@@ -5878,6 +5886,8 @@
|
||||
onTrackPlayLater={queueLocalTrackLater}
|
||||
onTrackAddToPlaylist={(trackId) => openAddToPlaylist([trackId], true)}
|
||||
onBulkAddToPlaylist={(trackIds) => openAddToPlaylist(trackIds, true)}
|
||||
onTrackAddPlexToPlaylist={(ratingKey) => openAddPlexToPlaylist([ratingKey])}
|
||||
onBulkAddPlexToPlaylist={(ratingKeys) => openAddPlexToPlaylist(ratingKeys)}
|
||||
onSetLocalQueue={handleSetLocalQueue}
|
||||
onQobuzArtistClick={handleArtistClick}
|
||||
activeTrackId={currentTrack?.id ?? null}
|
||||
@@ -6588,6 +6598,7 @@
|
||||
playlist={playlistModalMode === 'edit' ? playlistModalEditPlaylist : undefined}
|
||||
trackIds={playlistModalTrackIds}
|
||||
isLocalTracks={playlistModalTracksAreLocal}
|
||||
plexRatingKeys={playlistModalPlexRatingKeys}
|
||||
isHidden={playlistModalMode === 'edit' ? playlistModalEditIsHidden : false}
|
||||
currentFolderId={playlistModalMode === 'edit' ? playlistModalEditCurrentFolderId : null}
|
||||
{userPlaylists}
|
||||
|
||||
Reference in new issue
Block a user