From 1415c350c57ae8bd2c7034a84d6cccd135cca9af Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Tue, 23 Dec 2025 14:15:35 +0100 Subject: [PATCH] feat: add playlist search tab and fix playlist detail rendering --- index.html | 4 +++ js/api.js | 71 ++++++++++++++++++++++++++++++++++++++++++------------ js/ui.js | 26 ++++++++++++++++++-- 3 files changed, 83 insertions(+), 18 deletions(-) diff --git a/index.html b/index.html index f755577..1c9da6b 100644 --- a/index.html +++ b/index.html @@ -122,6 +122,7 @@ +
@@ -132,6 +133,9 @@
+
+
+
diff --git a/js/api.js b/js/api.js index aa1a2c0..2a14ea2 100644 --- a/js/api.js +++ b/js/api.js @@ -157,6 +157,10 @@ export class LosslessAPI { return album; } + preparePlaylist(playlist) { + return playlist; + } + prepareArtist(artist) { if (!artist.type && Array.isArray(artist.artistTypes) && artist.artistTypes.length > 0) { return { ...artist, type: artist.artistTypes[0] }; @@ -278,6 +282,27 @@ export class LosslessAPI { } } + async searchPlaylists(query) { + const cached = await this.cache.get('search_playlists', query); + if (cached) return cached; + + try { + const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}`); + const data = await response.json(); + const normalized = this.normalizeSearchResponse(data, 'playlists'); + const result = { + ...normalized, + items: normalized.items.map(p => this.preparePlaylist(p)) + }; + + await this.cache.set('search_playlists', query, result); + return result; + } catch (error) { + console.error('Playlist search failed:', error); + return { items: [], limit: 0, offset: 0, totalNumberOfItems: 0 }; + } + } + async getAlbum(id) { const cached = await this.cache.get('album', id); if (cached) return cached; @@ -354,26 +379,40 @@ export class LosslessAPI { // Unwrap the data property if it exists const data = jsonData.data || jsonData; - const entries = Array.isArray(data) ? data : [data]; + + let playlist = null; + let tracksSection = null; - let playlist, tracksSection; - - for (const entry of entries) { - if (!entry || typeof entry !== 'object') continue; - - if (!playlist && ('uuid' in entry || 'numberOfTracks' in entry || 'title' in entry && 'id' in entry)) { - playlist = entry; - } - - if (!tracksSection && 'items' in entry) { - tracksSection = entry; - } + // Check for direct playlist property (common in v2 responses) + if (data.playlist) { + playlist = data.playlist; } - // If still no playlist found, try using the first valid entry - if (!playlist && entries.length > 0) { + // Check for direct items property + if (data.items) { + tracksSection = { items: data.items }; + } + + // Fallback: iterate if we still missed something or if structure is flat array + if (!playlist || !tracksSection) { + const entries = Array.isArray(data) ? data : [data]; for (const entry of entries) { - if (entry && typeof entry === 'object' && ('id' in entry || 'uuid' in entry)) { + if (!entry || typeof entry !== 'object') continue; + + if (!playlist && ('uuid' in entry || 'numberOfTracks' in entry || ('title' in entry && 'id' in entry))) { + playlist = entry; + } + + if (!tracksSection && 'items' in entry) { + tracksSection = entry; + } + } + } + + // Fallback 2: If we have a list of entries but no explicit playlist object, try to find one that looks like a playlist + if (!playlist && Array.isArray(data)) { + for (const entry of data) { + if (entry && typeof entry === 'object' && ('uuid' in entry || 'numberOfTracks' in entry)) { playlist = entry; break; } diff --git a/js/ui.js b/js/ui.js index d3474cf..d90039d 100644 --- a/js/ui.js +++ b/js/ui.js @@ -103,6 +103,19 @@ export class UIRenderer { `; } + createPlaylistCardHTML(playlist) { + const imageId = playlist.squareImage || playlist.image || playlist.uuid; // Fallback or use a specific cover getter if needed + return ` + +
+ ${playlist.title} +
+

${playlist.title}

+

${playlist.numberOfTracks || 0} tracks

+
+ `; + } + createArtistCardHTML(artist) { return ` @@ -210,21 +223,25 @@ export class UIRenderer { const tracksContainer = document.getElementById('search-tracks-container'); const artistsContainer = document.getElementById('search-artists-container'); const albumsContainer = document.getElementById('search-albums-container'); + const playlistsContainer = document.getElementById('search-playlists-container'); tracksContainer.innerHTML = this.createSkeletonTracks(8, true); artistsContainer.innerHTML = this.createSkeletonCards(6, true); albumsContainer.innerHTML = this.createSkeletonCards(6, false); + playlistsContainer.innerHTML = this.createSkeletonCards(6, false); try { - const [tracksResult, artistsResult, albumsResult] = await Promise.all([ + const [tracksResult, artistsResult, albumsResult, playlistsResult] = await Promise.all([ this.api.searchTracks(query), this.api.searchArtists(query), - this.api.searchAlbums(query) + this.api.searchAlbums(query), + this.api.searchPlaylists(query) ]); let finalTracks = tracksResult.items; let finalArtists = artistsResult.items; let finalAlbums = albumsResult.items; + let finalPlaylists = playlistsResult.items; if (finalArtists.length === 0 && finalTracks.length > 0) { const artistMap = new Map(); @@ -267,12 +284,17 @@ export class UIRenderer { ? finalAlbums.map(album => this.createAlbumCardHTML(album)).join('') : createPlaceholder('No albums found.'); + playlistsContainer.innerHTML = finalPlaylists.length + ? finalPlaylists.map(playlist => this.createPlaylistCardHTML(playlist)).join('') + : createPlaceholder('No playlists found.'); + } catch (error) { console.error("Search failed:", error); const errorMsg = createPlaceholder(`Error during search. ${error.message}`); tracksContainer.innerHTML = errorMsg; artistsContainer.innerHTML = errorMsg; albumsContainer.innerHTML = errorMsg; + playlistsContainer.innerHTML = errorMsg; } }