From 1415c350c57ae8bd2c7034a84d6cccd135cca9af Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Tue, 23 Dec 2025 14:15:35 +0100 Subject: [PATCH 1/2] 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; } } From 46042d48512a717d7547413cdd9fa1c08eb25a18 Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Tue, 23 Dec 2025 15:37:44 +0100 Subject: [PATCH 2/2] feat: show recent playlists on home screen --- index.html | 4 ++++ js/storage.js | 10 ++++++++-- js/ui.js | 16 ++++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index 1c9da6b..ae79a18 100644 --- a/index.html +++ b/index.html @@ -110,6 +110,10 @@

Recent Albums

+
+

Recent Playlists

+
+

Recent Artists

diff --git a/js/storage.js b/js/storage.js index 58660e4..88d9a69 100644 --- a/js/storage.js +++ b/js/storage.js @@ -193,9 +193,11 @@ export const recentActivityManager = { _get() { try { const data = localStorage.getItem(this.STORAGE_KEY); - return data ? JSON.parse(data) : { artists: [], albums: [] }; + const parsed = data ? JSON.parse(data) : { artists: [], albums: [], playlists: [] }; + if (!parsed.playlists) parsed.playlists = []; + return parsed; } catch (e) { - return { artists: [], albums: [] }; + return { artists: [], albums: [], playlists: [] }; } }, @@ -221,6 +223,10 @@ export const recentActivityManager = { addAlbum(album) { this._add('albums', album); + }, + + addPlaylist(playlist) { + this._add('playlists', playlist); } }; diff --git a/js/ui.js b/js/ui.js index d90039d..4d55efe 100644 --- a/js/ui.js +++ b/js/ui.js @@ -206,6 +206,7 @@ export class UIRenderer { const albumsContainer = document.getElementById('home-recent-albums'); const artistsContainer = document.getElementById('home-recent-artists'); + const playlistsContainer = document.getElementById('home-recent-playlists'); albumsContainer.innerHTML = recents.albums.length ? recents.albums.map(album => this.createAlbumCardHTML(album)).join('') @@ -214,6 +215,12 @@ export class UIRenderer { artistsContainer.innerHTML = recents.artists.length ? recents.artists.map(artist => this.createArtistCardHTML(artist)).join('') : createPlaceholder("You haven't viewed any artists yet. Search for music to get started!"); + + if (playlistsContainer) { + playlistsContainer.innerHTML = recents.playlists && recents.playlists.length + ? recents.playlists.map(playlist => this.createPlaylistCardHTML(playlist)).join('') + : createPlaceholder("You haven't viewed any playlists yet. Search for music to get started!"); + } } async renderSearchPage(query) { @@ -427,10 +434,11 @@ async renderPlaylistPage(playlistId) {
`; - this.renderListWithTracks(tracklistContainer, tracks, true); - - document.title = `${playlist.title} - Monochrome`; - } catch (error) { + this.renderListWithTracks(tracklistContainer, tracks, true); + + recentActivityManager.addPlaylist(playlist); + + document.title = `${playlist.title || 'Artist Mix'} - Monochrome`; } catch (error) { console.error("Failed to load playlist:", error); tracklistContainer.innerHTML = createPlaceholder(`Could not load playlist details. ${error.message}`); }