feat: add playlist search tab and fix playlist detail rendering
This commit is contained in:
parent
70c942e39b
commit
1415c350c5
3 changed files with 83 additions and 18 deletions
|
|
@ -122,6 +122,7 @@
|
|||
<button class="search-tab active" data-tab="tracks">Tracks</button>
|
||||
<button class="search-tab" data-tab="albums">Albums</button>
|
||||
<button class="search-tab" data-tab="artists">Artists</button>
|
||||
<button class="search-tab" data-tab="playlists">Playlists</button>
|
||||
</div>
|
||||
<div class="search-tab-content active" id="search-tab-tracks">
|
||||
<div class="track-list" id="search-tracks-container"></div>
|
||||
|
|
@ -132,6 +133,9 @@
|
|||
<div class="search-tab-content" id="search-tab-artists">
|
||||
<div class="card-grid" id="search-artists-container"></div>
|
||||
</div>
|
||||
<div class="search-tab-content" id="search-tab-playlists">
|
||||
<div class="card-grid" id="search-playlists-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-album" class="page">
|
||||
|
|
|
|||
71
js/api.js
71
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;
|
||||
}
|
||||
|
|
|
|||
26
js/ui.js
26
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 `
|
||||
<a href="#playlist/${playlist.uuid}" class="card">
|
||||
<div class="card-image-wrapper">
|
||||
<img src="${this.api.getCoverUrl(imageId, '320')}" alt="${playlist.title}" class="card-image" loading="lazy">
|
||||
</div>
|
||||
<h3 class="card-title">${playlist.title}</h3>
|
||||
<p class="card-subtitle">${playlist.numberOfTracks || 0} tracks</p>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
createArtistCardHTML(artist) {
|
||||
return `
|
||||
<a href="#artist/${artist.id}" class="card artist">
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue