diff --git a/index.html b/index.html
index f755577..1c9da6b 100644
--- a/index.html
+++ b/index.html
@@ -122,6 +122,7 @@
+
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.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;
}
}