Enhance artist page by fetching EPs and Singles via search fallback

This commit is contained in:
Julien Maille 2025-12-25 21:35:59 +01:00
parent 77ccfc8234
commit 9533155656
5 changed files with 113 additions and 39 deletions

View file

@ -226,6 +226,10 @@
<h2 class="section-title">Albums</h2>
<div class="card-grid" id="artist-detail-albums"></div>
</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 id="page-settings" class="page">

View file

@ -484,15 +484,42 @@ export class LosslessAPI {
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)
);
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())
.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);
return result;

View file

@ -418,8 +418,9 @@ document.addEventListener('DOMContentLoaded', async () => {
try {
const artist = await api.getArtist(artistId);
if (!artist.albums || artist.albums.length === 0) {
throw new Error("No albums found for this artist");
const allReleases = [...(artist.albums || []), ...(artist.eps || [])];
if (allReleases.length === 0) {
throw new Error("No albums or EPs found for this artist");
}
const trackSet = new Set();
@ -427,7 +428,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const chunks = [];
const chunkSize = 3;
const albums = artist.albums;
const albums = allReleases;
for (let i = 0; i < albums.length; i += chunkSize) {
chunks.push(albums.slice(i, i + chunkSize));

View file

@ -387,12 +387,13 @@ export async function downloadDiscography(artist, api, quality, lyricsManager =
const template = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist} - 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);
try {
for (let albumIndex = 0; albumIndex < artist.albums.length; albumIndex++) {
const album = artist.albums[albumIndex];
for (let albumIndex = 0; albumIndex < allReleases.length; albumIndex++) {
const album = allReleases[albumIndex];
updateBulkDownloadProgress(notification, albumIndex, totalAlbums, album.title);

101
js/ui.js
View file

@ -140,6 +140,17 @@ export class UIRenderer {
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 `
<a href="#album/${album.id}" class="card">
<div class="card-image-wrapper">
@ -147,7 +158,7 @@ export class UIRenderer {
</div>
<h3 class="card-title">${album.title} ${explicitBadge}</h3>
<p class="card-subtitle">${album.artist?.name ?? ''}</p>
<p class="card-subtitle">${yearDisplay}</p>
<p class="card-subtitle">${yearDisplay}${typeLabel}</p>
</a>
`;
}
@ -557,43 +568,57 @@ export class UIRenderer {
document.title = `${album.title} - ${album.artist.name} - Monochrome`;
// "More from Artist" Section
// "More from Artist" Sections
try {
// Remove any existing "More from" section if re-rendering
const existingMoreSection = document.getElementById('album-more-from-artist');
if (existingMoreSection) existingMoreSection.remove();
// Remove any existing "More from" sections if re-rendering
document.querySelectorAll('.album-more-section').forEach(el => el.remove());
document.getElementById('album-more-from-artist')?.remove(); // Legacy cleanup
const moreSection = document.createElement('section');
moreSection.id = 'album-more-from-artist';
moreSection.className = 'content-section';
moreSection.style.marginTop = '3rem';
moreSection.innerHTML = `
// Create placeholder section while loading
const placeholderSection = document.createElement('section');
placeholderSection.className = 'content-section album-more-section';
placeholderSection.style.marginTop = '3rem';
placeholderSection.innerHTML = `
<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)}
</div>
`;
document.getElementById('page-album').appendChild(moreSection);
document.getElementById('page-album').appendChild(placeholderSection);
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) {
moreContainer.innerHTML = otherAlbums.map(a => this.createAlbumCardHTML(a)).join('');
} else {
moreSection.remove(); // Remove section if no other albums
}
// Remove placeholder
placeholderSection.remove();
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) {
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) {
@ -672,6 +697,8 @@ async renderPlaylistPage(playlistId) {
const metaEl = document.getElementById('artist-detail-meta');
const tracksContainer = document.getElementById('artist-detail-tracks');
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');
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>';
tracksContainer.innerHTML = this.createSkeletonTracks(5, true);
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
if (epsContainer) epsContainer.innerHTML = this.createSkeletonCards(6, false);
if (epsSection) epsSection.style.display = 'none';
try {
const artist = await this.api.getArtist(artistId);
@ -702,9 +731,21 @@ async renderPlaylistPage(playlistId) {
`;
this.renderListWithTracks(tracksContainer, artist.tracks, true);
albumsContainer.innerHTML = artist.albums.map(album =>
this.createAlbumCardHTML(album)
).join('');
// Render Albums
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);