diff --git a/plugins/multipleplaylists/src/index.ts b/plugins/multipleplaylists/src/index.ts index 58794c8..a09aeaf 100644 --- a/plugins/multipleplaylists/src/index.ts +++ b/plugins/multipleplaylists/src/index.ts @@ -1,5 +1,5 @@ import { LunaUnload, Tracer } from "@luna/core"; -import { MediaItem, redux, ContextMenu, Playlist } from "@luna/lib"; +import { MediaItem, redux, ContextMenu, Playlist, TidalApi } from "@luna/lib"; export const { trace, errSignal } = Tracer("[MultiplePlaylists]"); @@ -9,6 +9,9 @@ export { Settings } from "./Settings"; // Functions in unloads are called when plugin is unloaded. export const unloads = new Set(); +// Local tracking of songs added to playlists (server data can be stale) +const addedSongs = new Map>(); // playlistId -> Set + // Function to show playlist selector modal async function showPlaylistSelector(song: MediaItem) { @@ -122,19 +125,56 @@ async function showPlaylistSelector(song: MediaItem) { }); } -// Type definition for playlist object structure -interface PlaylistObject { - uuid: string; - title?: string; - numberOfTracks?: number; - type: string; - isEditable?: boolean; - isAutoGenerated?: boolean; - isSuggested?: boolean; - isRecommended?: boolean; - creator?: { - id?: string; - }; +// Fetch user's playlists via Tidal API (proven approach from squadgazzz/PlaylistTools) +async function fetchUserPlaylists(): Promise> { + const state = redux.store.getState(); + const userId = state.session?.userId; + if (!userId) throw new Error("Not logged in"); + + const headers = await TidalApi.getAuthHeaders(); + const queryArgs = TidalApi.queryArgs(); + const res = await fetch( + `https://api.tidal.com/v1/users/${userId}/playlists?${queryArgs}&limit=999`, + { headers } + ); + if (!res.ok) throw new Error(`Failed to fetch playlists: ${res.status}`); + + const data = await res.json(); + // Only show playlists the user owns (can add songs to) + return (data.items || []) + .filter((p: any) => p.creator?.id === userId) + .map((p: any) => ({ + uuid: p.uuid, + title: p.title, + numberOfTracks: p.numberOfTracks + })); +} + +// Fallback: load playlists from favorites store + Playlist.fromId() +async function fetchPlaylistsFromFavorites(): Promise> { + const state = redux.store.getState(); + const userId = state.session?.userId; + const favoriteUUIDs: string[] = state.favorites?.playlists || []; + if (favoriteUUIDs.length === 0) return []; + + const results: Array<{uuid: string, title: string, numberOfTracks: number}> = []; + for (const uuid of favoriteUUIDs) { + try { + const playlist = await Playlist.fromId(uuid); + if (playlist) { + // Only show playlists the user owns (can add songs to) + if (userId && playlist.tidalPlaylist?.creator?.id !== userId) continue; + results.push({ + uuid, + title: await playlist.title() || 'Untitled Playlist', + numberOfTracks: await playlist.count() || 0 + }); + } + } catch (error) { + console.warn(`[MultiplePlaylists] Failed to load playlist ${uuid} from favorites:`, error); + } + } + return results; } // Function to populate the playlist list @@ -143,67 +183,48 @@ async function populatePlaylistList(song?: MediaItem) { if (!playlistContainer) return; try { - // Get playlists from redux store - const state = redux.store.getState(); - const playlists = state.content?.playlists || {}; - const currentUser = state.auth?.user || {}; + // Show loading state + playlistContainer.innerHTML = '
Loading playlists...
'; - if (Object.keys(playlists).length === 0) { + // Try fetching playlists: API first, then favorites fallback + let playlistsArray: Array<{uuid: string, title: string, numberOfTracks: number}> = []; + try { + playlistsArray = await fetchUserPlaylists(); + console.log(`[MultiplePlaylists] Loaded ${playlistsArray.length} playlists via Tidal API`); + } catch (apiError) { + console.warn('[MultiplePlaylists] API fetch failed, trying favorites fallback:', apiError); + try { + playlistsArray = await fetchPlaylistsFromFavorites(); + console.log(`[MultiplePlaylists] Loaded ${playlistsArray.length} playlists from favorites`); + } catch (favError) { + console.warn('[MultiplePlaylists] Favorites fallback also failed:', favError); + } + } + + if (playlistsArray.length === 0) { playlistContainer.innerHTML = '

No playlists found. Create some playlists first!

'; return; } - // Filter for user's own playlists more specifically - const playlistsArray = Object.values(playlists).filter((playlist: any): playlist is PlaylistObject => { - if (!playlist || playlist.type !== 'USER') return false; - - // Additional filtering to exclude recommended/suggested playlists - // Check for properties that indicate ownership - const playlistData = playlist as any; - - // Exclude playlists that are marked as suggested/recommended - if (playlistData.isSuggested || playlistData.isRecommended) return false; - - // Only include playlists that are editable (user owns them) - if (playlistData.isEditable === false) return false; - - // Exclude auto-generated playlists - if (playlistData.isAutoGenerated) return false; - - // Additional check: if creator info is available, ensure it's the current user - if (playlistData.creator?.id && currentUser.id && playlistData.creator.id !== currentUser.id) { - return false; - } - - return true; - }); - - console.log(`[MultiplePlaylists] Processing ${playlistsArray.length} user playlists for duplicate detection`); - - // Show loading state initially - playlistContainer.innerHTML = '
Loading playlists...
'; - - // Process playlists in batches to avoid blocking the UI + // Process playlists in batches for duplicate detection const BATCH_SIZE = 5; const playlistBatches = []; for (let i = 0; i < playlistsArray.length; i += BATCH_SIZE) { playlistBatches.push(playlistsArray.slice(i, i + BATCH_SIZE)); } - + // Clear container and start building the list playlistContainer.innerHTML = ''; - + for (const [batchIndex, batch] of playlistBatches.entries()) { - console.log(`[MultiplePlaylists] Processing batch ${batchIndex + 1}/${playlistBatches.length} (${batch.length} playlists)`); - // Process batch with Promise.allSettled for fault tolerance const batchResults = await Promise.allSettled( - batch.map(async (playlist: PlaylistObject) => { + batch.map(async (playlist) => { try { const isAlreadyInPlaylist = song ? await isSongInPlaylist(song, playlist.uuid) : false; const statusText = isAlreadyInPlaylist ? ' (Already added)' : ''; const opacity = isAlreadyInPlaylist ? '0.6' : '1'; - + return `