Enhance artist page by fetching EPs and Singles via search fallback
This commit is contained in:
parent
77ccfc8234
commit
9533155656
5 changed files with 113 additions and 39 deletions
|
|
@ -226,6 +226,10 @@
|
||||||
<h2 class="section-title">Albums</h2>
|
<h2 class="section-title">Albums</h2>
|
||||||
<div class="card-grid" id="artist-detail-albums"></div>
|
<div class="card-grid" id="artist-detail-albums"></div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="content-section" id="artist-section-eps" style="display: none;">
|
||||||
|
<h2 class="section-title">EPs and Singles</h2>
|
||||||
|
<div class="card-grid" id="artist-detail-eps"></div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="page-settings" class="page">
|
<div id="page-settings" class="page">
|
||||||
|
|
|
||||||
33
js/api.js
33
js/api.js
|
|
@ -484,15 +484,42 @@ export class LosslessAPI {
|
||||||
|
|
||||||
entries.forEach(entry => scan(entry));
|
entries.forEach(entry => scan(entry));
|
||||||
|
|
||||||
const albums = Array.from(albumMap.values()).sort((a, b) =>
|
// Attempt to find more albums/EPs via search since the direct feed might be limited
|
||||||
|
try {
|
||||||
|
const searchResults = await this.searchAlbums(artist.name);
|
||||||
|
if (searchResults && searchResults.items) {
|
||||||
|
const numericArtistId = Number(artistId);
|
||||||
|
|
||||||
|
for (const item of searchResults.items) {
|
||||||
|
const itemArtistId = item.artist?.id;
|
||||||
|
const matchesArtist = itemArtistId === numericArtistId ||
|
||||||
|
(Array.isArray(item.artists) && item.artists.some(a => a.id === numericArtistId));
|
||||||
|
|
||||||
|
if (matchesArtist && !albumMap.has(item.id)) {
|
||||||
|
albumMap.set(item.id, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fetch additional albums via search:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allReleases = Array.from(albumMap.values()).sort((a, b) =>
|
||||||
new Date(b.releaseDate || 0) - new Date(a.releaseDate || 0)
|
new Date(b.releaseDate || 0) - new Date(a.releaseDate || 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const eps = allReleases.filter(a =>
|
||||||
|
a.type === 'EP' ||
|
||||||
|
a.type === 'SINGLE' ||
|
||||||
|
(a.numberOfTracks < 7 && !a.type)
|
||||||
|
);
|
||||||
|
const albums = allReleases.filter(a => !eps.includes(a));
|
||||||
|
|
||||||
const tracks = Array.from(trackMap.values())
|
const tracks = Array.from(trackMap.values())
|
||||||
.sort((a, b) => (b.popularity || 0) - (a.popularity || 0))
|
.sort((a, b) => (b.popularity || 0) - (a.popularity || 0))
|
||||||
.slice(0, 10);
|
.slice(0, 15);
|
||||||
|
|
||||||
const result = { ...artist, albums, tracks };
|
const result = { ...artist, albums, eps, tracks };
|
||||||
|
|
||||||
await this.cache.set('artist', artistId, result);
|
await this.cache.set('artist', artistId, result);
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -418,8 +418,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
try {
|
try {
|
||||||
const artist = await api.getArtist(artistId);
|
const artist = await api.getArtist(artistId);
|
||||||
|
|
||||||
if (!artist.albums || artist.albums.length === 0) {
|
const allReleases = [...(artist.albums || []), ...(artist.eps || [])];
|
||||||
throw new Error("No albums found for this artist");
|
if (allReleases.length === 0) {
|
||||||
|
throw new Error("No albums or EPs found for this artist");
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackSet = new Set();
|
const trackSet = new Set();
|
||||||
|
|
@ -427,7 +428,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
const chunkSize = 3;
|
const chunkSize = 3;
|
||||||
const albums = artist.albums;
|
const albums = allReleases;
|
||||||
|
|
||||||
for (let i = 0; i < albums.length; i += chunkSize) {
|
for (let i = 0; i < albums.length; i += chunkSize) {
|
||||||
chunks.push(albums.slice(i, i + chunkSize));
|
chunks.push(albums.slice(i, i + chunkSize));
|
||||||
|
|
|
||||||
|
|
@ -387,12 +387,13 @@ export async function downloadDiscography(artist, api, quality, lyricsManager =
|
||||||
const template = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist} - monochrome.tf';
|
const template = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist} - monochrome.tf';
|
||||||
const rootFolder = `${sanitizeForFilename(artist.name)} discography - monochrome.tf`;
|
const rootFolder = `${sanitizeForFilename(artist.name)} discography - monochrome.tf`;
|
||||||
|
|
||||||
const totalAlbums = artist.albums.length;
|
const allReleases = [...(artist.albums || []), ...(artist.eps || [])];
|
||||||
|
const totalAlbums = allReleases.length;
|
||||||
const notification = createBulkDownloadNotification('discography', artist.name, totalAlbums);
|
const notification = createBulkDownloadNotification('discography', artist.name, totalAlbums);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (let albumIndex = 0; albumIndex < artist.albums.length; albumIndex++) {
|
for (let albumIndex = 0; albumIndex < allReleases.length; albumIndex++) {
|
||||||
const album = artist.albums[albumIndex];
|
const album = allReleases[albumIndex];
|
||||||
|
|
||||||
updateBulkDownloadProgress(notification, albumIndex, totalAlbums, album.title);
|
updateBulkDownloadProgress(notification, albumIndex, totalAlbums, album.title);
|
||||||
|
|
||||||
|
|
|
||||||
101
js/ui.js
101
js/ui.js
|
|
@ -140,6 +140,17 @@ export class UIRenderer {
|
||||||
yearDisplay = `${date.getFullYear()}`;
|
yearDisplay = `${date.getFullYear()}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let typeLabel = '';
|
||||||
|
if (album.type === 'EP') {
|
||||||
|
typeLabel = ' • EP';
|
||||||
|
} else if (album.type === 'SINGLE') {
|
||||||
|
typeLabel = ' • Single';
|
||||||
|
} else if (!album.type && album.numberOfTracks) {
|
||||||
|
if (album.numberOfTracks <= 3) typeLabel = ' • Single';
|
||||||
|
else if (album.numberOfTracks <= 6) typeLabel = ' • EP';
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<a href="#album/${album.id}" class="card">
|
<a href="#album/${album.id}" class="card">
|
||||||
<div class="card-image-wrapper">
|
<div class="card-image-wrapper">
|
||||||
|
|
@ -147,7 +158,7 @@ export class UIRenderer {
|
||||||
</div>
|
</div>
|
||||||
<h3 class="card-title">${album.title} ${explicitBadge}</h3>
|
<h3 class="card-title">${album.title} ${explicitBadge}</h3>
|
||||||
<p class="card-subtitle">${album.artist?.name ?? ''}</p>
|
<p class="card-subtitle">${album.artist?.name ?? ''}</p>
|
||||||
<p class="card-subtitle">${yearDisplay}</p>
|
<p class="card-subtitle">${yearDisplay}${typeLabel}</p>
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
@ -557,43 +568,57 @@ export class UIRenderer {
|
||||||
|
|
||||||
document.title = `${album.title} - ${album.artist.name} - Monochrome`;
|
document.title = `${album.title} - ${album.artist.name} - Monochrome`;
|
||||||
|
|
||||||
// "More from Artist" Section
|
// "More from Artist" Sections
|
||||||
try {
|
try {
|
||||||
// Remove any existing "More from" section if re-rendering
|
// Remove any existing "More from" sections if re-rendering
|
||||||
const existingMoreSection = document.getElementById('album-more-from-artist');
|
document.querySelectorAll('.album-more-section').forEach(el => el.remove());
|
||||||
if (existingMoreSection) existingMoreSection.remove();
|
document.getElementById('album-more-from-artist')?.remove(); // Legacy cleanup
|
||||||
|
|
||||||
const moreSection = document.createElement('section');
|
// Create placeholder section while loading
|
||||||
moreSection.id = 'album-more-from-artist';
|
const placeholderSection = document.createElement('section');
|
||||||
moreSection.className = 'content-section';
|
placeholderSection.className = 'content-section album-more-section';
|
||||||
moreSection.style.marginTop = '3rem';
|
placeholderSection.style.marginTop = '3rem';
|
||||||
moreSection.innerHTML = `
|
placeholderSection.innerHTML = `
|
||||||
<h2 class="section-title">More from ${album.artist.name}</h2>
|
<h2 class="section-title">More from ${album.artist.name}</h2>
|
||||||
<div class="card-grid" id="album-more-albums">
|
<div class="card-grid">
|
||||||
${this.createSkeletonCards(6, false)}
|
${this.createSkeletonCards(6, false)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.getElementById('page-album').appendChild(moreSection);
|
document.getElementById('page-album').appendChild(placeholderSection);
|
||||||
|
|
||||||
const artistData = await this.api.getArtist(album.artist.id);
|
const artistData = await this.api.getArtist(album.artist.id);
|
||||||
// Filter out current album and duplicates
|
|
||||||
const otherAlbums = artistData.albums
|
|
||||||
.filter(a => a.id != album.id)
|
|
||||||
.filter((a, index, self) =>
|
|
||||||
index === self.findIndex((t) => t.title === a.title) // Dedup by title
|
|
||||||
)
|
|
||||||
.slice(0, 12); // Limit to 12
|
|
||||||
|
|
||||||
const moreContainer = document.getElementById('album-more-albums');
|
|
||||||
|
|
||||||
if (otherAlbums.length > 0) {
|
// Remove placeholder
|
||||||
moreContainer.innerHTML = otherAlbums.map(a => this.createAlbumCardHTML(a)).join('');
|
placeholderSection.remove();
|
||||||
} else {
|
|
||||||
moreSection.remove(); // Remove section if no other albums
|
const renderSection = (title, items) => {
|
||||||
}
|
const filtered = (items || [])
|
||||||
|
.filter(a => a.id != album.id)
|
||||||
|
.filter((a, index, self) =>
|
||||||
|
index === self.findIndex((t) => t.title === a.title) // Dedup by title
|
||||||
|
)
|
||||||
|
.slice(0, 12);
|
||||||
|
|
||||||
|
if (filtered.length === 0) return;
|
||||||
|
|
||||||
|
const section = document.createElement('section');
|
||||||
|
section.className = 'content-section album-more-section';
|
||||||
|
section.style.marginTop = '3rem';
|
||||||
|
section.innerHTML = `
|
||||||
|
<h2 class="section-title">${title}</h2>
|
||||||
|
<div class="card-grid">
|
||||||
|
${filtered.map(a => this.createAlbumCardHTML(a)).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('page-album').appendChild(section);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderSection(`More albums from ${album.artist.name}`, artistData.albums);
|
||||||
|
renderSection(`EPs and Singles from ${album.artist.name}`, artistData.eps);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to load "More from artist":', err);
|
console.warn('Failed to load "More from artist":', err);
|
||||||
document.getElementById('album-more-from-artist')?.remove();
|
document.querySelectorAll('.album-more-section').forEach(el => el.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -672,6 +697,8 @@ async renderPlaylistPage(playlistId) {
|
||||||
const metaEl = document.getElementById('artist-detail-meta');
|
const metaEl = document.getElementById('artist-detail-meta');
|
||||||
const tracksContainer = document.getElementById('artist-detail-tracks');
|
const tracksContainer = document.getElementById('artist-detail-tracks');
|
||||||
const albumsContainer = document.getElementById('artist-detail-albums');
|
const albumsContainer = document.getElementById('artist-detail-albums');
|
||||||
|
const epsContainer = document.getElementById('artist-detail-eps');
|
||||||
|
const epsSection = document.getElementById('artist-section-eps');
|
||||||
const dlBtn = document.getElementById('download-discography-btn');
|
const dlBtn = document.getElementById('download-discography-btn');
|
||||||
if (dlBtn) dlBtn.innerHTML = `${SVG_DOWNLOAD}<span>Download Discography</span>`;
|
if (dlBtn) dlBtn.innerHTML = `${SVG_DOWNLOAD}<span>Download Discography</span>`;
|
||||||
|
|
||||||
|
|
@ -681,6 +708,8 @@ async renderPlaylistPage(playlistId) {
|
||||||
metaEl.innerHTML = '<div class="skeleton" style="height: 16px; width: 150px;"></div>';
|
metaEl.innerHTML = '<div class="skeleton" style="height: 16px; width: 150px;"></div>';
|
||||||
tracksContainer.innerHTML = this.createSkeletonTracks(5, true);
|
tracksContainer.innerHTML = this.createSkeletonTracks(5, true);
|
||||||
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
|
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
|
||||||
|
if (epsContainer) epsContainer.innerHTML = this.createSkeletonCards(6, false);
|
||||||
|
if (epsSection) epsSection.style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const artist = await this.api.getArtist(artistId);
|
const artist = await this.api.getArtist(artistId);
|
||||||
|
|
@ -702,9 +731,21 @@ async renderPlaylistPage(playlistId) {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.renderListWithTracks(tracksContainer, artist.tracks, true);
|
this.renderListWithTracks(tracksContainer, artist.tracks, true);
|
||||||
albumsContainer.innerHTML = artist.albums.map(album =>
|
|
||||||
this.createAlbumCardHTML(album)
|
// Render Albums
|
||||||
).join('');
|
albumsContainer.innerHTML = artist.albums.length
|
||||||
|
? artist.albums.map(album => this.createAlbumCardHTML(album)).join('')
|
||||||
|
: createPlaceholder('No albums found.');
|
||||||
|
|
||||||
|
// Render EPs and Singles
|
||||||
|
if (epsContainer && epsSection) {
|
||||||
|
if (artist.eps && artist.eps.length > 0) {
|
||||||
|
epsContainer.innerHTML = artist.eps.map(album => this.createAlbumCardHTML(album)).join('');
|
||||||
|
epsSection.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
epsSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
recentActivityManager.addArtist(artist);
|
recentActivityManager.addArtist(artist);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue