unreleased rework
This commit is contained in:
parent
9ef3d6573f
commit
db777a7923
9 changed files with 1413 additions and 451 deletions
66
index.html
66
index.html
|
|
@ -45,6 +45,8 @@
|
|||
<li data-action="go-to-artist" data-type-filter="track,album">Go to artist</li>
|
||||
<li data-action="go-to-album" data-type-filter="track">Go to album</li>
|
||||
<li data-action="copy-link">Copy link</li>
|
||||
<li data-action="track-info" data-type-filter="track">Track Info</li>
|
||||
<li data-action="open-original-url" data-type-filter="track">Open Original URL</li>
|
||||
<li data-action="download">Download</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -795,6 +797,27 @@
|
|||
<span>Recent</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/unreleased">
|
||||
<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"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
<span>Unreleased</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/settings">
|
||||
<svg
|
||||
|
|
@ -1338,6 +1361,35 @@
|
|||
<div class="track-list" id="recent-tracks-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="page-unreleased" class="page">
|
||||
<div id="unreleased-content" style="padding: 1rem 0;"></div>
|
||||
</div>
|
||||
|
||||
<div id="page-tracker-artist" class="page">
|
||||
<header class="detail-header">
|
||||
<img
|
||||
id="tracker-artist-detail-image"
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt=""
|
||||
class="detail-header-image artist"
|
||||
/>
|
||||
<div class="detail-header-info">
|
||||
<div class="type">Unreleased Artist</div>
|
||||
<h1 class="title" id="tracker-artist-detail-name"></h1>
|
||||
<div class="meta" id="tracker-artist-detail-meta"></div>
|
||||
<div class="detail-header-actions">
|
||||
<button id="play-tracker-artist-btn" class="btn-primary">
|
||||
<span>Shuffle Play</span>
|
||||
</button>
|
||||
<button id="download-tracker-artist-btn" class="btn-primary">
|
||||
<span>Download All</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div id="tracker-artist-projects-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="page-album" class="page">
|
||||
<header class="detail-header">
|
||||
<img
|
||||
|
|
@ -1704,11 +1756,13 @@
|
|||
<h2 class="section-title">EPs and Singles</h2>
|
||||
<div class="card-grid" id="artist-detail-eps"></div>
|
||||
</section>
|
||||
<div
|
||||
id="artist-tracker-section"
|
||||
class="content-section"
|
||||
style="display: none; margin-top: 2rem"
|
||||
></div>
|
||||
<section class="content-section" id="artist-section-load-unreleased" style="display: none; margin: 1.5rem 0;">
|
||||
<button id="load-unreleased-btn" class="btn-primary">Load Unreleased Projects</button>
|
||||
</section>
|
||||
<section class="content-section" id="artist-section-unreleased" style="display: none">
|
||||
<h2 class="section-title">Unreleased Music</h2>
|
||||
<div class="card-grid" id="artist-detail-unreleased"></div>
|
||||
</section>
|
||||
<section class="content-section" id="artist-section-similar" style="display: none">
|
||||
<h2 class="section-title">Similar Artists</h2>
|
||||
<div class="card-grid" id="artist-detail-similar"></div>
|
||||
|
|
@ -2429,7 +2483,7 @@
|
|||
</a>
|
||||
</div>
|
||||
<div class="about-footer">
|
||||
<p class="version">Version 2.1.0</p>
|
||||
<p class="version">Version 2.2.0</p>
|
||||
<p class="disclaimer">
|
||||
This is an independent client and is not affiliated with or endorsed by TIDAL or any
|
||||
music streaming service.
|
||||
|
|
|
|||
1
js/db.js
1
js/db.js
|
|
@ -231,6 +231,7 @@ export class MusicDatabase {
|
|||
// Keep mix info
|
||||
mixes: item.mixes || null,
|
||||
isTracker: item.isTracker || (item.id && String(item.id).startsWith('tracker-')),
|
||||
trackerInfo: item.trackerInfo || null,
|
||||
audioUrl: item.remoteUrl || item.audioUrl || null,
|
||||
remoteUrl: item.remoteUrl || null,
|
||||
audioQuality: item.audioQuality || null,
|
||||
|
|
|
|||
267
js/events.js
267
js/events.js
|
|
@ -8,6 +8,7 @@ import {
|
|||
trackDataStore,
|
||||
formatTime,
|
||||
SVG_BIN,
|
||||
getTrackArtists,
|
||||
} from './utils.js';
|
||||
import { lastFMStorage, waveformSettings } from './storage.js';
|
||||
import { showNotification, downloadTrackWithMetadata, downloadAlbumAsZip, downloadPlaylistAsZip } from './downloads.js';
|
||||
|
|
@ -579,6 +580,112 @@ function initializeSmoothSliders(audioPlayer, player) {
|
|||
);
|
||||
}
|
||||
|
||||
// Standalone function to show add to playlist modal
|
||||
export async function showAddToPlaylistModal(track) {
|
||||
const modal = document.getElementById('playlist-select-modal');
|
||||
const list = document.getElementById('playlist-select-list');
|
||||
const cancelBtn = document.getElementById('playlist-select-cancel');
|
||||
const overlay = modal.querySelector('.modal-overlay');
|
||||
|
||||
const renderModal = async () => {
|
||||
const playlists = await db.getPlaylists(true);
|
||||
|
||||
const trackId = track.id;
|
||||
const playlistsWithTrack = new Set();
|
||||
|
||||
for (const playlist of playlists) {
|
||||
if (playlist.tracks && playlist.tracks.some((t) => t.id == trackId)) {
|
||||
playlistsWithTrack.add(playlist.id);
|
||||
}
|
||||
}
|
||||
|
||||
list.innerHTML =
|
||||
`
|
||||
<div class="modal-option create-new-option" style="border-bottom: 1px solid var(--border); margin-bottom: 0.5rem;">
|
||||
<span style="font-weight: 600; color: var(--primary);">+ Create New Playlist</span>
|
||||
</div>
|
||||
` +
|
||||
playlists
|
||||
.map((p) => {
|
||||
const alreadyContains = playlistsWithTrack.has(p.id);
|
||||
return `
|
||||
<div class="modal-option ${alreadyContains ? 'already-contains' : ''}" data-id="${p.id}">
|
||||
<span>${p.name}</span>
|
||||
${
|
||||
alreadyContains
|
||||
? `<button class="remove-from-playlist-btn-modal" title="Remove from playlist" style="background: transparent; border: none; color: inherit; cursor: pointer; padding: 4px; display: flex; align-items: center;">${SVG_BIN}</button>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!(await renderModal())) return;
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
cleanup();
|
||||
};
|
||||
|
||||
const handleOptionClick = async (e) => {
|
||||
const removeBtn = e.target.closest('.remove-from-playlist-btn-modal');
|
||||
const option = e.target.closest('.modal-option');
|
||||
|
||||
if (!option) return;
|
||||
|
||||
if (option.classList.contains('create-new-option')) {
|
||||
closeModal();
|
||||
const createModal = document.getElementById('playlist-modal');
|
||||
document.getElementById('playlist-modal-title').textContent = 'Create Playlist';
|
||||
document.getElementById('playlist-name-input').value = '';
|
||||
document.getElementById('playlist-cover-input').value = '';
|
||||
createModal.dataset.editingId = '';
|
||||
document.getElementById('csv-import-section').style.display = 'none';
|
||||
|
||||
// Pass track
|
||||
createModal._pendingTracks = [track];
|
||||
|
||||
createModal.classList.add('active');
|
||||
document.getElementById('playlist-name-input').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const playlistId = option.dataset.id;
|
||||
|
||||
if (removeBtn) {
|
||||
e.stopPropagation();
|
||||
await db.removeTrackFromPlaylist(playlistId, track.id);
|
||||
const updatedPlaylist = await db.getPlaylist(playlistId);
|
||||
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
|
||||
showNotification(`Removed from playlist: ${option.querySelector('span').textContent}`);
|
||||
await renderModal();
|
||||
} else {
|
||||
if (option.classList.contains('already-contains')) return;
|
||||
|
||||
await db.addTrackToPlaylist(playlistId, track);
|
||||
const updatedPlaylist = await db.getPlaylist(playlistId);
|
||||
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
|
||||
showNotification(`Added to playlist: ${option.querySelector('span').textContent}`);
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
cancelBtn.removeEventListener('click', closeModal);
|
||||
overlay.removeEventListener('click', closeModal);
|
||||
list.removeEventListener('click', handleOptionClick);
|
||||
};
|
||||
|
||||
cancelBtn.addEventListener('click', closeModal);
|
||||
overlay.addEventListener('click', closeModal);
|
||||
list.addEventListener('click', handleOptionClick);
|
||||
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
export async function handleTrackAction(
|
||||
action,
|
||||
item,
|
||||
|
|
@ -942,6 +1049,143 @@ export async function handleTrackAction(
|
|||
navigator.clipboard.writeText(url).then(() => {
|
||||
showNotification('Link copied to clipboard!');
|
||||
});
|
||||
} else if (action === 'track-info') {
|
||||
// Show detailed track info modal
|
||||
const isTracker = item.isTracker;
|
||||
let infoHTML = '';
|
||||
|
||||
if (isTracker && item.trackerInfo) {
|
||||
// Detailed unreleased/tracker track info
|
||||
const releaseDate = item.trackerInfo.releaseDate || item.streamStartDate;
|
||||
const dateDisplay = releaseDate ? new Date(releaseDate).toLocaleDateString() : 'Unknown';
|
||||
const addedDate = item.trackerInfo.addedDate ? new Date(item.trackerInfo.addedDate).toLocaleDateString() : 'Unknown';
|
||||
|
||||
infoHTML = `
|
||||
<div style="padding: 1.5rem; max-width: 500px; max-height: 80vh; overflow-y: auto;">
|
||||
<h3 style="margin-bottom: 1rem; font-size: 1.3rem; font-weight: 600;">${item.title}</h3>
|
||||
<div style="color: var(--muted-foreground); font-size: 0.9rem; line-height: 1.8;">
|
||||
<div style="margin-bottom: 1rem; padding: 0.75rem; background: var(--accent); border-radius: 8px;">
|
||||
<p style="color: var(--primary); font-weight: 500;">Unreleased Track</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 0.5rem;">
|
||||
${item.artists ? `<p><strong style="color: var(--foreground);">Artist:</strong> ${Array.isArray(item.artists) ? item.artists.map(a => a.name || a).join(', ') : item.artists}</p>` : ''}
|
||||
${item.trackerInfo.artist ? `<p><strong style="color: var(--foreground);">Tracked Artist:</strong> ${item.trackerInfo.artist}</p>` : ''}
|
||||
${item.trackerInfo.project ? `<p><strong style="color: var(--foreground);">Project:</strong> ${item.trackerInfo.project}</p>` : ''}
|
||||
${item.trackerInfo.era ? `<p><strong style="color: var(--foreground);">Era:</strong> ${item.trackerInfo.era}</p>` : ''}
|
||||
${item.trackerInfo.timeline ? `<p><strong style="color: var(--foreground);">Timeline:</strong> ${item.trackerInfo.timeline}</p>` : ''}
|
||||
${item.trackerInfo.category ? `<p><strong style="color: var(--foreground);">Category:</strong> ${item.trackerInfo.category}</p>` : ''}
|
||||
${item.trackerInfo.trackNumber ? `<p><strong style="color: var(--foreground);">Track Number:</strong> ${item.trackerInfo.trackNumber}</p>` : ''}
|
||||
<p><strong style="color: var(--foreground);">Duration:</strong> ${formatTime(item.duration)}</p>
|
||||
${releaseDate !== 'Unknown' ? `<p><strong style="color: var(--foreground);">Release Date:</strong> ${dateDisplay}</p>` : ''}
|
||||
${item.trackerInfo.addedDate ? `<p><strong style="color: var(--foreground);">Added to Tracker:</strong> ${addedDate}</p>` : ''}
|
||||
${item.trackerInfo.leakedDate ? `<p><strong style="color: var(--foreground);">Leak Date:</strong> ${new Date(item.trackerInfo.leakedDate).toLocaleDateString()}</p>` : ''}
|
||||
${item.trackerInfo.recordingDate ? `<p><strong style="color: var(--foreground);">Recording Date:</strong> ${new Date(item.trackerInfo.recordingDate).toLocaleDateString()}</p>` : ''}
|
||||
</div>
|
||||
|
||||
${item.trackerInfo.description ? `
|
||||
<div style="margin-top: 1rem; padding: 0.75rem; background: var(--accent); border-radius: 8px;">
|
||||
<p style="color: var(--foreground); font-weight: 500; margin-bottom: 0.5rem;">Description</p>
|
||||
<p style="font-size: 0.85rem; line-height: 1.6;">${item.trackerInfo.description}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${item.trackerInfo.notes ? `
|
||||
<div style="margin-top: 1rem; padding: 0.75rem; background: var(--accent); border-radius: 8px;">
|
||||
<p style="color: var(--foreground); font-weight: 500; margin-bottom: 0.5rem;">Notes</p>
|
||||
<p style="font-size: 0.85rem; line-height: 1.6;">${item.trackerInfo.notes}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${item.trackerInfo.sourceUrl ? `
|
||||
<div style="margin-top: 1rem;">
|
||||
<p style="margin-bottom: 0.5rem;"><strong style="color: var(--foreground);">Source URL:</strong></p>
|
||||
<a href="${item.trackerInfo.sourceUrl}" target="_blank" style="color: var(--primary); word-break: break-all; font-size: 0.85rem; display: block; padding: 0.5rem; background: var(--accent); border-radius: 6px; text-decoration: none;">
|
||||
${item.trackerInfo.sourceUrl}
|
||||
</a>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${item.id ? `<p style="margin-top: 1rem; font-size: 0.8rem; color: var(--muted);"><strong>Track ID:</strong> ${item.id}</p>` : ''}
|
||||
</div>
|
||||
<button onclick="this.closest('.modal-overlay').remove()" class="btn-primary" style="margin-top: 1.5rem; width: 100%;">Close</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// Detailed normal track info
|
||||
const releaseDate = item.album?.releaseDate || item.streamStartDate;
|
||||
const dateDisplay = releaseDate ? new Date(releaseDate).toLocaleDateString() : 'Unknown';
|
||||
const quality = item.audioQuality || 'Unknown';
|
||||
const bitrate = item.bitrate ? `${item.bitrate} kbps` : '';
|
||||
|
||||
infoHTML = `
|
||||
<div style="padding: 1.5rem; max-width: 500px; max-height: 80vh; overflow-y: auto;">
|
||||
<h3 style="margin-bottom: 1rem; font-size: 1.3rem; font-weight: 600;">${item.title}</h3>
|
||||
<div style="color: var(--muted-foreground); font-size: 0.9rem; line-height: 1.8;">
|
||||
<div style="display: grid; gap: 0.5rem;">
|
||||
<p><strong style="color: var(--foreground);">Artist:</strong> ${getTrackArtists(item)}</p>
|
||||
<p><strong style="color: var(--foreground);">Album:</strong> ${item.album?.title || 'Unknown'}</p>
|
||||
${item.album?.artist?.name ? `<p><strong style="color: var(--foreground);">Album Artist:</strong> ${item.album.artist.name}</p>` : ''}
|
||||
<p><strong style="color: var(--foreground);">Release Date:</strong> ${dateDisplay}</p>
|
||||
<p><strong style="color: var(--foreground);">Duration:</strong> ${formatTime(item.duration)}</p>
|
||||
${item.trackNumber ? `<p><strong style="color: var(--foreground);">Track Number:</strong> ${item.trackNumber}</p>` : ''}
|
||||
${item.discNumber ? `<p><strong style="color: var(--foreground);">Disc Number:</strong> ${item.discNumber}</p>` : ''}
|
||||
${item.version ? `<p><strong style="color: var(--foreground);">Version:</strong> ${item.version}</p>` : ''}
|
||||
${item.explicit ? `<p><strong style="color: var(--foreground);">Explicit:</strong> Yes</p>` : ''}
|
||||
<p><strong style="color: var(--foreground);">Quality:</strong> ${quality} ${bitrate ? `(${bitrate})` : ''}</p>
|
||||
</div>
|
||||
|
||||
${item.credits && item.credits.length > 0 ? `
|
||||
<div style="margin-top: 1rem; padding: 0.75rem; background: var(--accent); border-radius: 8px;">
|
||||
<p style="color: var(--foreground); font-weight: 500; margin-bottom: 0.5rem;">Credits</p>
|
||||
<div style="font-size: 0.85rem; line-height: 1.6;">
|
||||
${item.credits.map(c => `<p>${c.type}: ${c.name}</p>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${item.composers && item.composers.length > 0 ? `
|
||||
<p style="margin-top: 0.5rem;"><strong style="color: var(--foreground);">Composers:</strong> ${item.composers.map(c => c.name).join(', ')}</p>
|
||||
` : ''}
|
||||
|
||||
${item.lyrics?.text ? `
|
||||
<div style="margin-top: 1rem; padding: 0.75rem; background: var(--accent); border-radius: 8px;">
|
||||
<p style="color: var(--foreground); font-weight: 500; margin-bottom: 0.5rem;">Has Lyrics</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${item.id ? `<p style="margin-top: 1rem; font-size: 0.8rem; color: var(--muted);"><strong>Track ID:</strong> ${item.id}</p>` : ''}
|
||||
${item.album?.id ? `<p style="font-size: 0.8rem; color: var(--muted);"><strong>Album ID:</strong> ${item.album.id}</p>` : ''}
|
||||
</div>
|
||||
<button onclick="this.closest('.modal-overlay').remove()" class="btn-primary" style="margin-top: 1.5rem; width: 100%;">Close</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Create and show modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10000;';
|
||||
modal.innerHTML = infoHTML;
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) modal.remove();
|
||||
};
|
||||
document.body.appendChild(modal);
|
||||
} else if (action === 'open-original-url') {
|
||||
// Open the original source URL for the track
|
||||
let url = null;
|
||||
|
||||
if (item.isTracker && item.trackerInfo && item.trackerInfo.sourceUrl) {
|
||||
url = item.trackerInfo.sourceUrl;
|
||||
} else if (item.remoteUrl) {
|
||||
url = item.remoteUrl;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
showNotification('No original URL available for this track.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -961,6 +1205,13 @@ async function updateContextMenuLikeState(contextMenu, contextTrack) {
|
|||
trackMixItem.style.display = hasMix ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Show/hide "Open Original URL" only for unreleased/tracker tracks
|
||||
const openOriginalUrlItem = contextMenu.querySelector('li[data-action="open-original-url"]');
|
||||
if (openOriginalUrlItem) {
|
||||
const isUnreleased = contextTrack.isTracker || (contextTrack.trackerInfo && contextTrack.trackerInfo.sourceUrl);
|
||||
openOriginalUrlItem.style.display = isUnreleased ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Filter items based on type
|
||||
const type = contextMenu._contextType || 'track';
|
||||
contextMenu.querySelectorAll('li[data-action]').forEach((item) => {
|
||||
|
|
@ -1229,7 +1480,11 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
if (link) {
|
||||
e.stopPropagation();
|
||||
const artistId = link.dataset.artistId;
|
||||
if (artistId) {
|
||||
const trackerSheetId = link.dataset.trackerSheetId;
|
||||
if (trackerSheetId) {
|
||||
// Navigate to tracker artist page
|
||||
navigate(`/unreleased/${trackerSheetId}`);
|
||||
} else if (artistId) {
|
||||
navigate(`/artist/${artistId}`);
|
||||
}
|
||||
return;
|
||||
|
|
@ -1237,8 +1492,14 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
|
||||
// Fallback for non-link clicks (e.g. separators) or single artist legacy
|
||||
const track = player.currentTrack;
|
||||
if (track?.artist?.id) {
|
||||
navigate(`/artist/${track.artist.id}`);
|
||||
if (track) {
|
||||
// Check if this is a tracker track
|
||||
const isTracker = track.isTracker || (track.id && String(track.id).startsWith('tracker-'));
|
||||
if (isTracker && track.trackerInfo?.sheetId) {
|
||||
navigate(`/unreleased/${track.trackerInfo.sheetId}`);
|
||||
} else if (track.artist?.id) {
|
||||
navigate(`/artist/${track.artist.id}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
10
js/lastfm.js
10
js/lastfm.js
|
|
@ -180,9 +180,10 @@ export class LastFMScrobbler {
|
|||
this.clearScrobbleTimer();
|
||||
|
||||
try {
|
||||
const scrobbleTitle = track.cleanTitle || track.title;
|
||||
const params = {
|
||||
artist: this._getScrobbleArtist(track),
|
||||
track: track.title,
|
||||
track: scrobbleTitle,
|
||||
};
|
||||
|
||||
if (track.album?.title) {
|
||||
|
|
@ -199,7 +200,7 @@ export class LastFMScrobbler {
|
|||
|
||||
await this.makeRequest('track.updateNowPlaying', params, true);
|
||||
|
||||
console.log('Now playing updated:', track.title);
|
||||
console.log('Now playing updated:', scrobbleTitle);
|
||||
|
||||
this.scrobbleThreshold = Math.min(track.duration / 2, 240);
|
||||
this.scheduleScrobble(this.scrobbleThreshold * 1000);
|
||||
|
|
@ -228,10 +229,11 @@ export class LastFMScrobbler {
|
|||
|
||||
try {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const scrobbleTitle = this.currentTrack.cleanTitle || this.currentTrack.title;
|
||||
|
||||
const params = {
|
||||
artist: this._getScrobbleArtist(this.currentTrack),
|
||||
track: this.currentTrack.title,
|
||||
track: scrobbleTitle,
|
||||
timestamp: timestamp,
|
||||
};
|
||||
|
||||
|
|
@ -250,7 +252,7 @@ export class LastFMScrobbler {
|
|||
await this.makeRequest('track.scrobble', params, true);
|
||||
|
||||
this.hasScrobbled = true;
|
||||
console.log('Scrobbled:', this.currentTrack.title);
|
||||
console.log('Scrobbled:', this.currentTrack.cleanTitle || this.currentTrack.title);
|
||||
} catch (error) {
|
||||
console.error('Failed to scrobble:', error);
|
||||
}
|
||||
|
|
|
|||
26
js/player.js
26
js/player.js
|
|
@ -356,6 +356,32 @@ export class Player {
|
|||
this.applyReplayGain();
|
||||
|
||||
this.audio.src = streamUrl;
|
||||
|
||||
// Wait for audio to be ready before playing (prevents restart issues with blob URLs)
|
||||
if (isTracker) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const onCanPlay = () => {
|
||||
this.audio.removeEventListener('canplay', onCanPlay);
|
||||
this.audio.removeEventListener('error', onError);
|
||||
resolve();
|
||||
};
|
||||
const onError = (e) => {
|
||||
this.audio.removeEventListener('canplay', onCanPlay);
|
||||
this.audio.removeEventListener('error', onError);
|
||||
reject(e);
|
||||
};
|
||||
this.audio.addEventListener('canplay', onCanPlay);
|
||||
this.audio.addEventListener('error', onError);
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
this.audio.removeEventListener('canplay', onCanPlay);
|
||||
this.audio.removeEventListener('error', onError);
|
||||
reject(new Error('Timeout waiting for audio to load'));
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
if (startTime > 0) {
|
||||
this.audio.currentTime = startTime;
|
||||
}
|
||||
|
|
|
|||
20
js/router.js
20
js/router.js
|
|
@ -52,7 +52,11 @@ export function createRouter(ui) {
|
|||
await ui.renderMixPage(param);
|
||||
break;
|
||||
case 'track':
|
||||
await ui.renderTrackPage(param);
|
||||
if (param.startsWith('tracker-')) {
|
||||
await ui.renderTrackerTrackPage(param);
|
||||
} else {
|
||||
await ui.renderTrackPage(param);
|
||||
}
|
||||
break;
|
||||
case 'library':
|
||||
await ui.renderLibraryPage();
|
||||
|
|
@ -60,6 +64,20 @@ export function createRouter(ui) {
|
|||
case 'recent':
|
||||
await ui.renderRecentPage();
|
||||
break;
|
||||
case 'unreleased':
|
||||
if (param) {
|
||||
const parts = param.split('/');
|
||||
const sheetId = parts[0];
|
||||
const projectName = parts[1] ? decodeURIComponent(parts[1]) : null;
|
||||
if (projectName) {
|
||||
await ui.renderTrackerProjectPage(sheetId, projectName);
|
||||
} else {
|
||||
await ui.renderTrackerArtistPage(sheetId);
|
||||
}
|
||||
} else {
|
||||
await ui.renderUnreleasedPage();
|
||||
}
|
||||
break;
|
||||
case 'home':
|
||||
await ui.renderHomePage();
|
||||
break;
|
||||
|
|
|
|||
1324
js/tracker.js
1324
js/tracker.js
File diff suppressed because it is too large
Load diff
139
js/ui.js
139
js/ui.js
|
|
@ -23,6 +23,7 @@ import { getVibrantColorFromImage } from './vibrant-color.js';
|
|||
import { syncManager } from './accounts/pocketbase.js';
|
||||
import { Visualizer } from './visualizer.js';
|
||||
import { navigate } from './router.js';
|
||||
import { renderUnreleasedPage as renderUnreleasedTrackerPage, renderTrackerArtistPage as renderTrackerArtistContent, renderTrackerProjectPage as renderTrackerProjectContent, renderTrackerTrackPage as renderTrackerTrackContent, findTrackerArtistByName, getArtistUnreleasedProjects, createProjectCardHTML } from './tracker.js';
|
||||
|
||||
export class UIRenderer {
|
||||
constructor(api, player) {
|
||||
|
|
@ -109,31 +110,42 @@ export class UIRenderer {
|
|||
if (track) {
|
||||
const isLocal = track.isLocal;
|
||||
const isTracker = track.isTracker || (track.id && String(track.id).startsWith('tracker-'));
|
||||
const shouldHide = isLocal || isTracker;
|
||||
const shouldHideLikes = isLocal || isTracker;
|
||||
|
||||
if (likeBtn) {
|
||||
if (shouldHide) {
|
||||
if (shouldHideLikes) {
|
||||
likeBtn.style.display = 'none';
|
||||
} else {
|
||||
likeBtn.style.display = 'flex';
|
||||
this.updateLikeState(likeBtn.parentElement, 'track', track.id);
|
||||
}
|
||||
}
|
||||
|
||||
// For tracker tracks: show add playlist, hide lyrics
|
||||
// For normal tracks: hide add playlist, show lyrics (unless local)
|
||||
if (addPlaylistBtn) {
|
||||
if (shouldHide) addPlaylistBtn.style.setProperty('display', 'none', 'important');
|
||||
else addPlaylistBtn.style.removeProperty('display');
|
||||
if (isTracker) {
|
||||
addPlaylistBtn.style.removeProperty('display');
|
||||
addPlaylistBtn.style.display = 'flex';
|
||||
} else {
|
||||
addPlaylistBtn.style.setProperty('display', 'none', 'important');
|
||||
}
|
||||
}
|
||||
if (mobileAddPlaylistBtn) {
|
||||
if (shouldHide) mobileAddPlaylistBtn.style.setProperty('display', 'none', 'important');
|
||||
else mobileAddPlaylistBtn.style.removeProperty('display');
|
||||
if (isTracker) {
|
||||
mobileAddPlaylistBtn.style.removeProperty('display');
|
||||
mobileAddPlaylistBtn.style.display = 'flex';
|
||||
} else {
|
||||
mobileAddPlaylistBtn.style.setProperty('display', 'none', 'important');
|
||||
}
|
||||
}
|
||||
if (lyricsBtn) {
|
||||
if (isLocal) lyricsBtn.style.display = 'none';
|
||||
if (isLocal || isTracker) lyricsBtn.style.display = 'none';
|
||||
else lyricsBtn.style.removeProperty('display');
|
||||
}
|
||||
|
||||
if (fsLikeBtn) {
|
||||
if (shouldHide) {
|
||||
if (shouldHideLikes) {
|
||||
fsLikeBtn.style.display = 'none';
|
||||
} else {
|
||||
fsLikeBtn.style.display = 'flex';
|
||||
|
|
@ -141,7 +153,7 @@ export class UIRenderer {
|
|||
}
|
||||
}
|
||||
if (fsAddPlaylistBtn) {
|
||||
if (shouldHide) fsAddPlaylistBtn.style.display = 'none';
|
||||
if (shouldHideLikes) fsAddPlaylistBtn.style.display = 'none';
|
||||
else fsAddPlaylistBtn.style.display = 'flex';
|
||||
}
|
||||
} else {
|
||||
|
|
@ -2192,6 +2204,8 @@ export class UIRenderer {
|
|||
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
|
||||
if (epsContainer) epsContainer.innerHTML = this.createSkeletonCards(6, false);
|
||||
if (epsSection) epsSection.style.display = 'none';
|
||||
const loadUnreleasedSection = document.getElementById('artist-section-load-unreleased');
|
||||
if (loadUnreleasedSection) loadUnreleasedSection.style.display = 'none';
|
||||
if (similarContainer) similarContainer.innerHTML = this.createSkeletonCards(6, true);
|
||||
if (similarSection) similarSection.style.display = 'block';
|
||||
|
||||
|
|
@ -2299,6 +2313,89 @@ export class UIRenderer {
|
|||
}
|
||||
});
|
||||
|
||||
// Check for unreleased projects
|
||||
const unreleasedSection = document.getElementById('artist-section-unreleased');
|
||||
const unreleasedContainer = document.getElementById('artist-detail-unreleased');
|
||||
const loadUnreleasedBtn = document.getElementById('load-unreleased-btn');
|
||||
const loadUnreleasedSection = document.getElementById('artist-section-load-unreleased');
|
||||
if (unreleasedSection && unreleasedContainer && loadUnreleasedBtn && loadUnreleasedSection) {
|
||||
// Initially hide the unreleased section
|
||||
unreleasedSection.style.display = 'none';
|
||||
loadUnreleasedSection.style.display = 'none';
|
||||
|
||||
// Check if artist has unreleased projects
|
||||
const trackerArtist = findTrackerArtistByName(artist.name);
|
||||
if (trackerArtist) {
|
||||
// Show the load button section
|
||||
loadUnreleasedSection.style.display = 'block';
|
||||
|
||||
// Add click handler to load and display unreleased projects
|
||||
loadUnreleasedBtn.onclick = async () => {
|
||||
loadUnreleasedBtn.disabled = true;
|
||||
loadUnreleasedBtn.textContent = 'Loading...';
|
||||
|
||||
try {
|
||||
const unreleasedData = await getArtistUnreleasedProjects(artist.name);
|
||||
if (unreleasedData && unreleasedData.eras.length > 0) {
|
||||
const { artist: trackerArtistData, sheetId, eras } = unreleasedData;
|
||||
|
||||
unreleasedContainer.innerHTML = eras.map(e => {
|
||||
let trackCount = 0;
|
||||
if (e.data) {
|
||||
Object.values(e.data).forEach((songs) => {
|
||||
if (songs && songs.length) trackCount += songs.length;
|
||||
});
|
||||
}
|
||||
return createProjectCardHTML(e, trackerArtistData, sheetId, trackCount);
|
||||
}).join('');
|
||||
|
||||
unreleasedSection.style.display = 'block';
|
||||
loadUnreleasedBtn.style.display = 'none';
|
||||
|
||||
// Add click handlers
|
||||
unreleasedContainer.querySelectorAll('.card').forEach((card) => {
|
||||
const eraName = decodeURIComponent(card.dataset.trackerProjectId);
|
||||
const era = eras.find(e => e.name === eraName);
|
||||
if (!era) return;
|
||||
|
||||
card.onclick = (e) => {
|
||||
if (e.target.closest('.card-play-btn')) {
|
||||
e.stopPropagation();
|
||||
let eraTracks = [];
|
||||
if (era.data) {
|
||||
Object.values(era.data).forEach((songs) => {
|
||||
if (songs && songs.length) {
|
||||
songs.forEach((song, index) => {
|
||||
const track = createTrackFromSong(song, era, trackerArtistData.name, eraTracks.length, sheetId);
|
||||
eraTracks.push(track);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const availableTracks = eraTracks.filter(t => !t.unavailable);
|
||||
if (availableTracks.length > 0) {
|
||||
globalPlayer.setQueue(availableTracks, 0);
|
||||
globalPlayer.playTrackFromQueue();
|
||||
}
|
||||
} else if (e.target.closest('.card-menu-btn')) {
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
navigate(`/unreleased/${sheetId}/${encodeURIComponent(era.name)}`);
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
loadUnreleasedBtn.textContent = 'No unreleased projects';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load unreleased projects:', error);
|
||||
loadUnreleasedBtn.textContent = 'Failed to load';
|
||||
loadUnreleasedBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
recentActivityManager.addArtist(artist);
|
||||
|
||||
document.title = artist.name;
|
||||
|
|
@ -2376,6 +2473,30 @@ export class UIRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
async renderUnreleasedPage() {
|
||||
this.showPage('unreleased');
|
||||
const container = document.getElementById('unreleased-content');
|
||||
await renderUnreleasedTrackerPage(container);
|
||||
}
|
||||
|
||||
async renderTrackerArtistPage(sheetId) {
|
||||
this.showPage('tracker-artist');
|
||||
const container = document.getElementById('tracker-artist-projects-container');
|
||||
await renderTrackerArtistContent(sheetId, container);
|
||||
}
|
||||
|
||||
async renderTrackerProjectPage(sheetId, projectName) {
|
||||
this.showPage('album'); // Use album page template
|
||||
const container = document.getElementById('album-detail-tracklist');
|
||||
await renderTrackerProjectContent(sheetId, projectName, container, this);
|
||||
}
|
||||
|
||||
async renderTrackerTrackPage(trackId) {
|
||||
this.showPage('album'); // Use album page template
|
||||
const container = document.getElementById('album-detail-tracklist');
|
||||
await renderTrackerTrackContent(trackId, container, this);
|
||||
}
|
||||
|
||||
updatePlaylistHeaderActions(playlist, isOwned, tracks, showShare = false, onSort = null) {
|
||||
const actionsDiv = document.getElementById('page-playlist').querySelector('.detail-header-actions');
|
||||
|
||||
|
|
|
|||
11
js/utils.js
11
js/utils.js
|
|
@ -274,7 +274,16 @@ export const getTrackArtists = (track = {}, { fallback = 'Unknown Artist' } = {}
|
|||
export const getTrackArtistsHTML = (track = {}, { fallback = 'Unknown Artist' } = {}) => {
|
||||
if (track?.artists?.length) {
|
||||
return track.artists
|
||||
.map((artist) => `<span class="artist-link" data-artist-id="${artist.id}">${artist.name}</span>`)
|
||||
.map((artist) => {
|
||||
// Check if this is a tracker/unreleased track
|
||||
const isTracker = track.isTracker || (track.id && String(track.id).startsWith('tracker-'));
|
||||
if (isTracker && track.trackerInfo?.sheetId) {
|
||||
// For tracker tracks, link to the tracker artist page
|
||||
return `<span class="artist-link tracker-artist-link" data-tracker-sheet-id="${track.trackerInfo.sheetId}">${artist.name}</span>`;
|
||||
}
|
||||
// For normal tracks, use the artist ID
|
||||
return `<span class="artist-link" data-artist-id="${artist.id}">${artist.name}</span>`;
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue