new: mix for artists, use new api

This commit is contained in:
Julien Maille 2026-01-01 19:27:29 +01:00
parent 4ffac0ae0a
commit 57f3e42dbe
8 changed files with 271 additions and 117 deletions

View file

@ -41,34 +41,33 @@
<!-- Content injected dynamically -->
</div>
</div>
<div id="fullscreen-cover-overlay" style="display: none;">
<div class="fullscreen-cover-content">
<button id="close-fullscreen-cover-btn" title="Close">&times;</button>
<button id="toggle-fullscreen-lyrics-btn" class="fullscreen-lyrics-toggle" title="Toggle Lyrics">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="22"/>
<line x1="8" y1="22" x2="16" y2="22"/>
</svg>
</button>
<div class="fullscreen-main-view">
<img id="fullscreen-cover-image" src="" alt="Album Cover">
<div class="fullscreen-track-info">
<h2 id="fullscreen-track-title"></h2>
<h3 id="fullscreen-track-artist"></h3>
<div id="fullscreen-next-track" style="display: none;">
<span class="label">Up Next: </span>
<span class="value"></span>
<div id="fullscreen-cover-overlay" style="display: none;">
<div class="fullscreen-cover-content">
<button id="close-fullscreen-cover-btn" title="Close">&times;</button>
<button id="toggle-fullscreen-lyrics-btn" class="fullscreen-lyrics-toggle" title="Toggle Lyrics">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="22"/>
<line x1="8" y1="22" x2="16" y2="22"/>
</svg>
</button>
<div class="fullscreen-main-view">
<img id="fullscreen-cover-image" src="" alt="Album Cover">
<div class="fullscreen-track-info">
<h2 id="fullscreen-track-title"></h2>
<h3 id="fullscreen-track-artist"></h3>
<div id="fullscreen-next-track" style="display: none;">
<span class="label">Up Next: </span>
<span class="value"></span>
</div>
</div>
</div>
</div>
<div class="fullscreen-lyrics-container" id="fullscreen-lyrics-container" style="display: none;">
<div class="fullscreen-lyrics-container" id="fullscreen-lyrics-container" style="display: none;">
</div>
</div>
</div>
</div>
<div id="playlist-modal" class="modal" style="display: none;">
<div class="modal-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;">
@ -89,11 +88,11 @@
<aside class="sidebar">
<div>
<div class="sidebar-logo">
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="14.75 14.75 70.5 70.5" >
<g fill="currentColor" >
<path d="M38.25 14.75H85.25V61.75H61.75V38.25H38.25ZM14.75 38.25H38.25V61.75H61.75V85.25H14.75Z" />
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="14.75 14.75 70.5 70.5" >
<g fill="currentColor" >
<path d="M38.25 14.75H85.25V61.75H61.75V38.25H38.25ZM14.75 38.25H38.25V61.75H61.75V85.25H14.75Z" />
</g>
</svg>
<span>Monochrome</span>
</div>
<nav class="sidebar-nav">
@ -199,29 +198,32 @@
</div>
<div id="page-library" class="page">
<h2 class="section-title">Your Library</h2>
<section class="content-section">
<h2 class="section-title">My Playlists <button id="create-playlist-btn" class="btn-secondary">Create Playlist</button></h2>
<div class="card-grid" id="my-playlists-container"></div>
</section>
<div class="search-tabs">
<button class="search-tab active" data-tab="tracks">Liked 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="library-tab-tracks">
<div class="track-list" id="library-tracks-container"></div>
</div>
<div class="search-tab-content" id="library-tab-albums">
<div class="card-grid" id="library-albums-container"></div>
</div>
<div class="search-tab-content" id="library-tab-artists">
<div class="card-grid" id="library-artists-container"></div>
</div>
<div class="search-tab-content" id="library-tab-playlists">
<div class="card-grid" id="library-playlists-container"></div>
</div>
<section class="content-section">
<h2 class="section-title">Your Favorites</h2>
<div class="search-tabs">
<button class="search-tab active" data-tab="tracks">Liked 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="library-tab-tracks">
<div class="track-list" id="library-tracks-container"></div>
</div>
<div class="search-tab-content" id="library-tab-albums">
<div class="card-grid" id="library-albums-container"></div>
</div>
<div class="search-tab-content" id="library-tab-artists">
<div class="card-grid" id="library-artists-container"></div>
</div>
<div class="search-tab-content" id="library-tab-playlists">
<div class="card-grid" id="library-playlists-container"></div>
</div>
</section>
</div>
<div id="page-recent" class="page">
@ -240,10 +242,16 @@
<button id="play-album-btn" class="btn-primary">
<span>Play Album</span>
</button>
<button id="download-album-btn" class="btn-primary">
<span>Download Album</span>
</button>
<button id="like-album-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="album" title="Save to Library">
<button id="album-mix-btn" class="btn-primary" style="display: none;">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
</svg>
<span>Mix</span>
</button>
<button id="download-album-btn" class="btn-primary">
<span>Download</span>
</button>
<button id="like-album-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="album" title="Save to Favorites">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
</button>
</div>
@ -252,29 +260,46 @@
<div class="track-list" id="album-detail-tracklist"></div>
</div>
<section id="page-playlist" class="page">
<div class="detail-header">
<img id="playlist-detail-image" class="detail-cover" alt="Playlist Cover">
<div class="detail-header-info">
<h1 class="title" id="playlist-detail-title"></h1>
<p class="meta" id="playlist-detail-meta"></p>
<p id="playlist-detail-description" class="detail-description"></p>
<div class="detail-actions">
<button id="play-playlist-btn" class="btn-primary">
<span>Play</span>
</button>
<!-- Like button not typically for own playlists, but good for "followed" ones if we support that. For now, maybe skip or add "Edit" if it's user playlist -->
<button id="download-playlist-btn" class="btn-primary">
<span>Download</span>
</button>
<button id="like-playlist-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="playlist" title="Save to Library">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
</button>
</div>
</div>
</div>
<div id="playlist-detail-tracklist" class="track-list"></div>
</section>
<section id="page-playlist" class="page">
<div class="detail-header">
<img id="playlist-detail-image" class="detail-cover" alt="Playlist Cover">
<div class="detail-header-info">
<h1 class="title" id="playlist-detail-title"></h1>
<p class="meta" id="playlist-detail-meta"></p>
<p id="playlist-detail-description" class="detail-description"></p>
<div class="detail-actions">
<button id="play-playlist-btn" class="btn-primary">
<span>Play</span>
</button>
<!-- Like button not typically for own playlists, but good for "followed" ones if we support that. For now, maybe skip or add "Edit" if it's user playlist -->
<button id="download-playlist-btn" class="btn-primary">
<span>Download</span>
</button>
<button id="like-playlist-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="playlist" title="Save to Favorites">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
</button>
</div>
</div>
</div>
<div id="playlist-detail-tracklist" class="track-list"></div>
</section>
<section id="page-mix" class="page">
<div class="detail-header">
<img id="mix-detail-image" class="detail-cover" alt="Mix Cover">
<div class="detail-header-info">
<h1 class="title" id="mix-detail-title"></h1>
<p class="meta" id="mix-detail-meta"></p>
<p id="mix-detail-description" class="detail-description"></p>
<div class="detail-actions">
<button id="play-mix-btn" class="btn-primary">
<span>Play</span>
</button>
</div>
</div>
</div>
<div id="mix-detail-tracklist" class="track-list"></div>
</section>
<div id="page-artist" class="page">
<header class="detail-header">
@ -289,10 +314,16 @@
</svg>
<span>Artist Radio</span>
</button>
<button id="artist-mix-btn" class="btn-primary" style="display: none;">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
</svg>
<span>Mix</span>
</button>
<button id="download-discography-btn" class="btn-primary">
<span>Download Discography</span>
</button>
<button id="like-artist-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="artist" title="Save to Library">
<button id="like-artist-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="artist" title="Save to Favorites">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
</button>
</div>
@ -652,7 +683,7 @@
</div>
<div class="volume-controls">
<div class="player-actions-row">
<button id="now-playing-like-btn" class="like-btn" data-action="toggle-like" title="Save to Library" style="display: none;">
<button id="now-playing-like-btn" class="like-btn" data-action="toggle-like" title="Save to Favorites" style="display: none;">
</button>
<button id="toggle-lyrics-btn" title="Lyrics">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mic"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="8" y1="22" x2="16" y2="22"/></svg>
@ -696,7 +727,6 @@ if ('serviceWorker' in navigator) {
location.reload();
});
}
</script>
</body>
</html>

View file

@ -1,6 +1,6 @@
{
"api": [
"https://tidal-api.binimum.org"
"https://monochrome-api.samidy.com"
],
"streaming": [
"https://triton.squid.wtf",

View file

@ -480,6 +480,40 @@ export class LosslessAPI {
return result;
}
async getMix(id) {
const cached = await this.cache.get('mix', id);
if (cached) return cached;
const response = await this.fetchWithRetry(`/mix/?id=${id}`, { type: 'api' });
const data = await response.json();
let mix = null;
let tracks = [];
// Mix response structure might vary, handle likely cases
const items = data.items || data.tracks || (Array.isArray(data) ? data : []);
// If data has mix info, use it. Otherwise, fabricate one or look for it.
if (data.mix) {
mix = data.mix;
} else if (!Array.isArray(data) && data.id) {
mix = data;
}
if (!mix) {
// Basic placeholder if mix metadata isn't explicitly separated
mix = { id, title: 'Mix', description: 'Generated Mix' };
}
if (items.length > 0) {
tracks = items.map(i => this.prepareTrack(i.item || i));
}
const result = { mix, tracks };
await this.cache.set('mix', id, result);
return result;
}
async getArtist(artistId) {
const cached = await this.cache.get('artist', artistId);
if (cached) return cached;

View file

@ -423,7 +423,7 @@ export async function handleTrackAction(action, item, player, api, lyricsManager
}
}
btn.classList.toggle('active', added);
btn.title = added ? 'Remove from Library' : 'Add to Library';
btn.title = added ? 'Remove from Favorites' : 'Add to Favorites';
});
// Handle Library Page Update
@ -674,7 +674,7 @@ async function updateContextMenuLikeState(menu, track) {
const likeItem = menu.querySelector('[data-action="toggle-like"]');
if (likeItem) {
const isLiked = await db.isFavorite('track', track.id);
likeItem.textContent = isLiked ? 'Remove from Library' : 'Add to Library';
likeItem.textContent = isLiked ? 'Remove from Favorites' : 'Add to Favorites';
}
}

View file

@ -20,6 +20,9 @@ export function createRouter(ui) {
case 'userplaylist':
ui.renderPlaylistPage(param);
break;
case 'mix':
ui.renderMixPage(param);
break;
case 'library':
ui.renderLibraryPage();
break;

View file

@ -53,7 +53,7 @@ export const apiSettings = {
} catch (error) {
console.error('Failed to load instances from GitHub:', error);
this.defaultInstances = {
api: ['https://tidal-api.binimum.org'],
api: ["https://monochrome-api.samidy.com"],
streaming: [
"https://triton.squid.wtf",
"https://wolf.qqdl.site",

100
js/ui.js
View file

@ -797,6 +797,8 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
if (playBtn) playBtn.innerHTML = `${SVG_PLAY}<span>Play Album</span>`;
const dlBtn = document.getElementById('download-album-btn');
if (dlBtn) dlBtn.innerHTML = `${SVG_DOWNLOAD}<span>Download Album</span>`;
const mixBtn = document.getElementById('album-mix-btn');
if (mixBtn) mixBtn.style.display = 'none';
imageEl.src = '';
imageEl.style.backgroundColor = 'var(--muted)';
@ -902,6 +904,13 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
const artistData = await this.api.getArtist(album.artist.id);
// Add Mix/Radio Button to header
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}`;
}
// Remove placeholder
placeholderSection.remove();
@ -1105,6 +1114,86 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
}
}
async renderMixPage(mixId) {
this.showPage('mix');
const imageEl = document.getElementById('mix-detail-image');
const titleEl = document.getElementById('mix-detail-title');
const metaEl = document.getElementById('mix-detail-meta');
const descEl = document.getElementById('mix-detail-description');
const tracklistContainer = document.getElementById('mix-detail-tracklist');
const playBtn = document.getElementById('play-mix-btn');
if (playBtn) playBtn.innerHTML = `${SVG_PLAY}<span>Play</span>`;
// Skeleton loading
imageEl.src = '';
imageEl.style.backgroundColor = 'var(--muted)';
titleEl.innerHTML = '<div class="skeleton" style="height: 48px; width: 300px; max-width: 90%;"></div>';
metaEl.innerHTML = '<div class="skeleton" style="height: 16px; width: 200px; max-width: 80%;"></div>';
descEl.innerHTML = '<div class="skeleton" style="height: 16px; width: 100%;"></div>';
tracklistContainer.innerHTML = `
<div class="track-list-header">
<span style="width: 40px; text-align: center;">#</span>
<span>Title</span>
<span class="duration-header">Duration</span>
</div>
${this.createSkeletonTracks(10, true)}
`;
try {
const { mix, tracks } = await this.api.getMix(mixId);
// Mixes usually have covers from Tidal resources, similar to playlists
const imageId = mix.images?.medium?.source || mix.image || mix.id;
// Fallback for cover: if mix.id matches a pattern or we can just try generic mix cover
// Often mix ID isn't directly an image ID.
// If API returns explicit image URL/ID use it.
// For now assume standard playlist-like cover or placeholder.
if (imageId && imageId !== mix.id) {
imageEl.src = this.api.getCoverUrl(imageId, '1080');
} else {
// Try to get cover from first track album
if (tracks.length > 0 && tracks[0].album?.cover) {
imageEl.src = this.api.getCoverUrl(tracks[0].album.cover, '1080');
} else {
imageEl.src = 'assets/appicon.png';
}
}
imageEl.style.backgroundColor = '';
const firstTrackArtist = tracks.length > 0 ? tracks[0].artist?.name : '';
const displayTitle = firstTrackArtist ? `${firstTrackArtist} Mix` : 'Mix';
titleEl.textContent = displayTitle;
this.adjustTitleFontSize(titleEl, displayTitle);
const totalDuration = calculateTotalDuration(tracks);
metaEl.textContent = `${tracks.length} tracks • ${formatDuration(totalDuration)}`;
descEl.textContent = mix.subTitle || mix.description || '';
tracklistContainer.innerHTML = `
<div class="track-list-header">
<span style="width: 40px; text-align: center;">#</span>
<span>Title</span>
<span class="duration-header">Duration</span>
</div>
`;
this.renderListWithTracks(tracklistContainer, tracks, true);
// Set play button action
playBtn.onclick = () => {
player.playTracks(tracks, 0);
};
document.title = `${displayTitle} - Monochrome`;
} catch (error) {
console.error("Failed to load mix:", error);
tracklistContainer.innerHTML = createPlaceholder(`Could not load mix details. ${error.message}`);
}
}
async renderArtistPage(artistId) {
this.showPage('artist');
@ -1134,6 +1223,17 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
try {
const artist = await this.api.getArtist(artistId);
// Handle Artist Mix Button
const mixBtn = document.getElementById('artist-mix-btn');
if (mixBtn) {
if (artist.mixes && artist.mixes.ARTIST_MIX) {
mixBtn.style.display = 'flex';
mixBtn.onclick = () => window.location.hash = `#mix/${artist.mixes.ARTIST_MIX}`;
} else {
mixBtn.style.display = 'none';
}
}
// Similar Artists
if (similarContainer && similarSection) {
this.api.getSimilarArtists(artistId).then(similar => {

View file

@ -3061,7 +3061,8 @@ input:checked + .slider::before {
opacity: 1;
}
#page-playlist .detail-header {
#page-playlist .detail-header,
#page-mix .detail-header {
display: flex;
align-items: flex-top;
gap: var(--spacing-xl);
@ -3069,7 +3070,8 @@ input:checked + .slider::before {
padding-bottom: var(--spacing-md);
}
#page-playlist .detail-cover {
#page-playlist .detail-cover,
#page-mix .detail-cover {
width: 200px;
height: 200px;
flex-shrink: 0;
@ -3080,12 +3082,14 @@ input:checked + .slider::before {
transition: opacity 0.3s ease-in-out;
}
#playlist-detail-image.loading {
#playlist-detail-image.loading,
#mix-detail-image.loading {
opacity: 0.3;
}
#playlist-detail-title {
#playlist-detail-title,
#mix-detail-title {
font-size: 3rem;
font-weight: 800;
line-height: 1.1;
@ -3096,7 +3100,8 @@ input:checked + .slider::before {
flex-wrap: wrap;
}
#playlist-detail-meta {
#playlist-detail-meta,
#mix-detail-meta {
color: var(--muted-foreground);
font-size: 0.95rem;
margin-bottom: 0.75rem;
@ -3106,7 +3111,8 @@ input:checked + .slider::before {
flex-wrap: wrap;
}
#playlist-detail-description {
#playlist-detail-description,
#mix-detail-description {
color: var(--foreground);
font-size: 0.9rem;
line-height: 1.6;
@ -3114,7 +3120,8 @@ input:checked + .slider::before {
max-width: 600px;
}
#page-playlist .detail-actions {
#page-playlist .detail-actions,
#page-mix .detail-actions {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
@ -3178,7 +3185,8 @@ input:checked + .slider::before {
}
@media (max-width: 768px) {
#page-playlist .detail-header {
#page-playlist .detail-header,
#page-mix .detail-header {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-lg);
@ -3186,42 +3194,21 @@ input:checked + .slider::before {
margin-bottom: var(--spacing-lg);
}
#playlist-detail-image {
#playlist-detail-image,
#mix-detail-image {
width: 150px;
height: 150px;
}
#playlist-detail-title {
#playlist-detail-title,
#mix-detail-title {
font-size: 2rem;
line-height: 1.2;
}
#playlist-detail-meta {
#playlist-detail-meta,
#mix-detail-meta {
font-size: 0.85rem;
gap: 0.35rem;
}
#playlist-detail-description {
font-size: 0.85rem;
max-width: none;
}
#page-playlist .detail-actions {
width: auto;
flex-direction: row;
}
#play-playlist-btn,
#download-playlist-btn {
width: auto;
padding: 0.875rem;
border-radius: 50%;
aspect-ratio: 1/1;
}
#play-playlist-btn span,
#download-playlist-btn span {
display: none;
}
}