diff --git a/index.html b/index.html
index b5c6778..94988b9 100644
--- a/index.html
+++ b/index.html
@@ -24,6 +24,7 @@
- Like
- Add to Playlist
+ - Track Mix
- Play Next
- Add to Queue
- Download
@@ -342,7 +343,7 @@
- Fans Also Like
+ Similar Artists
diff --git a/js/api.js b/js/api.js
index 0796523..fd5e771 100644
--- a/js/api.js
+++ b/js/api.js
@@ -630,6 +630,26 @@ export class LosslessAPI {
}
}
+ async getSimilarAlbums(albumId) {
+ const cached = await this.cache.get('similar_albums', albumId);
+ if (cached) return cached;
+
+ try {
+ const response = await this.fetchWithRetry(`/album/similar/?id=${albumId}`, { type: 'api' });
+ const data = await response.json();
+
+ const items = data.items || data.albums || data.data || (Array.isArray(data) ? data : []);
+
+ const result = items.map(album => this.prepareAlbum(album));
+
+ await this.cache.set('similar_albums', albumId, result);
+ return result;
+ } catch (e) {
+ console.warn('Failed to fetch similar albums:', e);
+ return [];
+ }
+ }
+
normalizeTrackResponse(apiResponse) {
if (!apiResponse || typeof apiResponse !== 'object') {
return apiResponse;
diff --git a/js/events.js b/js/events.js
index e423c93..2ded01b 100644
--- a/js/events.js
+++ b/js/events.js
@@ -359,6 +359,10 @@ export async function handleTrackAction(action, item, player, api, lyricsManager
player.addNextToQueue(item);
renderQueue(player);
showNotification(`Playing next: ${item.title}`);
+ } else if (action === 'track-mix') {
+ if (item.mixes && item.mixes.TRACK_MIX) {
+ window.location.hash = `#mix/${item.mixes.TRACK_MIX}?type=track&name=${encodeURIComponent(item.title)}`;
+ }
} else if (action === 'play-card') {
try {
let tracks = [];
@@ -676,6 +680,15 @@ async function updateContextMenuLikeState(menu, track) {
const isLiked = await db.isFavorite('track', track.id);
likeItem.textContent = isLiked ? 'Remove from Favorites' : 'Add to Favorites';
}
+
+ const mixItem = menu.querySelector('[data-action="track-mix"]');
+ if (mixItem) {
+ if (track.mixes && track.mixes.TRACK_MIX) {
+ mixItem.style.display = 'block';
+ } else {
+ mixItem.style.display = 'none';
+ }
+ }
}
function positionMenu(menu, x, y, anchorRect = null) {
diff --git a/js/ui.js b/js/ui.js
index b1e0c36..0806b01 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -908,7 +908,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
const mixBtn = document.getElementById('album-mix-btn');
if (mixBtn && artistData.mixes && artistData.mixes.ARTIST_MIX) {
mixBtn.style.display = 'flex';
- mixBtn.onclick = () => window.location.hash = `#mix/${artistData.mixes.ARTIST_MIX}`;
+ mixBtn.onclick = () => window.location.hash = `#mix/${artistData.mixes.ARTIST_MIX}?type=artist&name=${encodeURIComponent(artistData.name)}`;
}
// Remove placeholder
@@ -954,7 +954,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
section.className = 'content-section album-more-section';
section.style.marginTop = '3rem';
section.innerHTML = `
- Fans Also Like
+ Similar Artists
${similar.map(a => this.createArtistCardHTML(a)).join('')}
@@ -963,6 +963,30 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
}
}).catch(e => console.warn('Failed to load similar artists:', e));
+ // Similar Albums
+ this.api.getSimilarAlbums(albumId).then(similar => {
+ if (similar && similar.length > 0) {
+ const section = document.createElement('section');
+ section.className = 'content-section album-more-section';
+ section.style.marginTop = '3rem';
+ section.innerHTML = `
+ Similar Albums
+
+ ${similar.map(a => this.createAlbumCardHTML(a)).join('')}
+
+ `;
+ document.getElementById('page-album').appendChild(section);
+
+ similar.forEach(a => {
+ const el = section.querySelector(`[data-album-id="${a.id}"]`);
+ if (el) {
+ trackDataStore.set(el, a);
+ this.updateLikeState(el, 'album', a.id);
+ }
+ });
+ }
+ }).catch(e => console.warn('Failed to load similar albums:', e));
+
} catch (err) {
console.warn('Failed to load "More from artist":', err);
document.querySelectorAll('.album-more-section').forEach(el => el.remove());
@@ -1114,8 +1138,13 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
}
}
- async renderMixPage(mixId) {
+ async renderMixPage(param) {
this.showPage('mix');
+ const [mixId, query] = param.split('?');
+ const urlParams = new URLSearchParams(query);
+ const type = urlParams.get('type');
+ const name = urlParams.get('name');
+
const imageEl = document.getElementById('mix-detail-image');
const titleEl = document.getElementById('mix-detail-title');
const metaEl = document.getElementById('mix-detail-meta');
@@ -1161,8 +1190,15 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
imageEl.style.backgroundColor = '';
- const firstTrackArtist = tracks.length > 0 ? tracks[0].artist?.name : '';
- const displayTitle = firstTrackArtist ? `${firstTrackArtist} Mix` : 'Mix';
+ let displayTitle;
+ if (type === 'artist' && name) {
+ displayTitle = `Mix for artist ${decodeURIComponent(name)}`;
+ } else if (type === 'track' && name) {
+ displayTitle = `Mix for track ${decodeURIComponent(name)}`;
+ } else {
+ const firstTrackArtist = tracks.length > 0 ? tracks[0].artist?.name : '';
+ displayTitle = mix.title || (firstTrackArtist ? `${firstTrackArtist} Mix` : 'Mix');
+ }
titleEl.textContent = displayTitle;
this.adjustTitleFontSize(titleEl, displayTitle);
@@ -1228,7 +1264,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
if (mixBtn) {
if (artist.mixes && artist.mixes.ARTIST_MIX) {
mixBtn.style.display = 'flex';
- mixBtn.onclick = () => window.location.hash = `#mix/${artist.mixes.ARTIST_MIX}`;
+ mixBtn.onclick = () => window.location.hash = `#mix/${artist.mixes.ARTIST_MIX}?type=artist&name=${encodeURIComponent(artist.name)}`;
} else {
mixBtn.style.display = 'none';
}