diff --git a/app.py b/app.py index a60b752..773652b 100755 --- a/app.py +++ b/app.py @@ -68,6 +68,11 @@ def create_app(): def serve_track(id): # The id parameter is captured, but you can use it as needed. return render_template('track.html') + + @app.route('/artist/') + def serve_artist(id): + # The id parameter is captured, but you can use it as needed. + return render_template('artist.html') @app.route('/static/') def serve_static(path): diff --git a/static/css/artist/artist.css b/static/css/artist/artist.css new file mode 100644 index 0000000..39105b1 --- /dev/null +++ b/static/css/artist/artist.css @@ -0,0 +1,355 @@ +/* Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + } + + body { + background-color: #121212; + color: #ffffff; + min-height: 100vh; + line-height: 1.4; + } + + /* Main App Container */ + #app { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + position: relative; + z-index: 1; + } + + /* Artist Header */ + #artist-header { + display: flex; + gap: 20px; + margin-bottom: 2rem; + align-items: center; + padding-bottom: 1.5rem; + border-bottom: 1px solid #2a2a2a; + flex-wrap: wrap; /* Allow wrapping on narrow screens */ + background: linear-gradient(135deg, rgba(0,0,0,0.5), transparent); + padding: 20px; + border-radius: 8px; + } + + #artist-image { + width: 200px; + height: 200px; + object-fit: cover; + border-radius: 8px; + flex-shrink: 0; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + } + + #artist-info { + flex: 1; + min-width: 0; + } + + #artist-name { + font-size: 2rem; + margin-bottom: 0.5rem; + } + + #artist-stats { + font-size: 1rem; + color: #b3b3b3; + margin-bottom: 0.5rem; + } + + /* Albums Container */ + #albums-container { + margin-top: 2rem; + } + + #albums-container h2 { + font-size: 1.5rem; + margin-bottom: 1rem; + border-bottom: 1px solid #2a2a2a; + padding-bottom: 0.5rem; + } + + /* Album Groups */ + .album-group { + margin-bottom: 2rem; + } + + .album-group-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding: 0.5rem 0; + border-bottom: 1px solid #2a2a2a; + } + + .album-group-header h3 { + font-size: 1.5rem; + margin: 0; + text-transform: capitalize; + } + + .albums-list { + display: flex; + flex-direction: column; + gap: 1rem; + } + + /* Track Card (for Albums) */ + .track { + display: flex; + align-items: center; + padding: 1rem; + background: #181818; + border-radius: 8px; + transition: background 0.3s ease; + flex-wrap: wrap; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + } + + .track:hover { + background: #2a2a2a; + } + + .track-number { + width: 30px; + font-size: 1rem; + font-weight: 500; + text-align: center; + margin-right: 1rem; + flex-shrink: 0; + } + + .track-info { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + align-items: flex-start; + } + + .track-name { + font-size: 1rem; + font-weight: bold; + word-wrap: break-word; + } + + .track-artist { + font-size: 0.9rem; + color: #b3b3b3; + } + + .track-album { + max-width: 200px; + font-size: 0.9rem; + color: #b3b3b3; + margin-left: 1rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; + } + + .track-duration { + width: 60px; + text-align: right; + font-size: 0.9rem; + color: #b3b3b3; + margin-left: 1rem; + flex-shrink: 0; + } + + /* Loading and Error States */ + .loading, + .error { + width: 100%; + text-align: center; + font-size: 1rem; + padding: 1rem; + } + + .error { + color: #c0392b; + } + + /* Utility Classes */ + .hidden { + display: none !important; + } + + /* Unified Download Button Base Style */ + .download-btn { + background-color: #1db954; + color: #fff; + border: none; + border-radius: 4px; + padding: 0.5rem 1rem; + font-size: 0.95rem; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0.5rem; + } + + .download-btn:hover { + background-color: #17a44b; + } + + .download-btn:active { + transform: scale(0.98); + } + + /* Circular Variant for Compact Areas */ + .download-btn--circle { + width: 32px; + height: 32px; + padding: 0; + border-radius: 50%; + font-size: 0; + } + + .download-btn--circle::before { + content: "↓"; + font-size: 16px; + color: #fff; + display: inline-block; + } + + /* Back Button Styling */ + .back-btn { + background-color: #333; + color: #fff; + border: none; + border-radius: 4px; + padding: 0.5rem 1rem; + font-size: 0.95rem; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.2s ease; + margin-right: 1rem; + } + + .back-btn:hover { + background-color: #444; + } + + .back-btn:active { + transform: scale(0.98); + } + + /* Download Queue Toggle Button */ + .queue-toggle { + position: fixed; + bottom: 20px; + right: 20px; + background: #1db954; + color: #fff; + border: none; + border-radius: 50%; + width: 56px; + height: 56px; + cursor: pointer; + font-size: 0.9rem; + font-weight: bold; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + transition: background-color 0.3s ease, transform 0.2s ease; + z-index: 1002; + } + + .queue-toggle:hover { + background: #1ed760; + transform: scale(1.05); + } + + .queue-toggle:active { + transform: scale(1); + } + + /* Responsive Styles */ + + /* Medium Devices (Tablets) */ + @media (max-width: 768px) { + #artist-header { + flex-direction: column; + align-items: flex-start; + } + + #artist-image { + width: 100%; + height: auto; + margin-bottom: 1rem; + } + + .track { + flex-direction: column; + align-items: flex-start; + } + + .track-album, + .track-duration { + margin-left: 0; + margin-top: 0.5rem; + width: 100%; + text-align: left; + } + + .album-group-header { + flex-direction: row; + justify-content: space-between; + align-items: center; + } + + .album-group-header h3 { + font-size: 1.3rem; + } + } + + /* Small Devices (Mobile Phones) */ + @media (max-width: 480px) { + #app { + padding: 10px; + } + + #artist-name { + font-size: 1.5rem; + } + + /* Adjust track layout to vertical & centered */ + .track { + padding: 0.8rem; + flex-direction: column; + align-items: center; + text-align: center; + } + + .track-number { + font-size: 0.9rem; + margin-right: 0; + margin-bottom: 0.5rem; + } + + .track-info { + align-items: center; + } + + .track-album, + .track-duration { + margin-left: 0; + margin-top: 0.5rem; + width: 100%; + text-align: center; + } + + /* Adjust album-group header layout */ + .album-group-header { + flex-direction: column; + align-items: center; + gap: 0.5rem; + } + } + \ No newline at end of file diff --git a/static/js/album.js b/static/js/album.js index 90a2073..219e6f8 100644 --- a/static/js/album.js +++ b/static/js/album.js @@ -1,23 +1,26 @@ +// Import the downloadQueue singleton from your working queue.js implementation. import { downloadQueue } from './queue.js'; document.addEventListener('DOMContentLoaded', () => { + // Parse artist ID from the URL (expected route: /artist/{id}) const pathSegments = window.location.pathname.split('/'); - const albumId = pathSegments[pathSegments.indexOf('album') + 1]; + const artistId = pathSegments[pathSegments.indexOf('artist') + 1]; - if (!albumId) { - showError('No album ID provided.'); + if (!artistId) { + showError('No artist ID provided.'); return; } - fetch(`/api/album/info?id=${encodeURIComponent(albumId)}`) + // Fetch the artist info (which includes a list of albums) + fetch(`/api/artist/info?id=${encodeURIComponent(artistId)}`) .then(response => { if (!response.ok) throw new Error('Network response was not ok'); return response.json(); }) - .then(data => renderAlbum(data)) + .then(data => renderArtist(data)) .catch(error => { console.error('Error:', error); - showError('Failed to load album.'); + showError('Failed to load artist info.'); }); const queueIcon = document.getElementById('queueIcon'); @@ -28,132 +31,225 @@ document.addEventListener('DOMContentLoaded', () => { } }); -function renderAlbum(album) { +/** + * Renders the artist header and groups the albums by type. + * + * The API response is expected to have the following structure: + * { + * "href": "...", + * "limit": 50, + * "next": null, + * "offset": 0, + * "previous": null, + * "total": 5, + * "items": [ { album object }, { album object }, ... ] + * } + */ +function renderArtist(artistData) { + // Hide loading and error messages document.getElementById('loading').classList.add('hidden'); document.getElementById('error').classList.add('hidden'); - // Album header info - document.getElementById('album-name').textContent = album.name; - document.getElementById('album-artist').textContent = - `By ${album.artists.map(artist => artist.name).join(', ')}`; - - const releaseYear = new Date(album.release_date).getFullYear(); - document.getElementById('album-stats').textContent = - `${releaseYear} • ${album.total_tracks} songs • ${album.label}`; - - document.getElementById('album-copyright').textContent = - album.copyrights.map(c => c.text).join(' • '); + // Use the first album to extract artist details + const firstAlbum = artistData.items[0]; + const artistName = firstAlbum?.artists[0]?.name || 'Unknown Artist'; + const artistImage = firstAlbum?.images[0]?.url || 'placeholder.jpg'; + document.getElementById('artist-name').textContent = artistName; + document.getElementById('artist-stats').textContent = `${artistData.total} albums`; + document.getElementById('artist-image').src = artistImage; - const image = album.images[0]?.url || 'placeholder.jpg'; - document.getElementById('album-image').src = image; - - // Back Button + // --- Add Back Button --- let backButton = document.getElementById('backButton'); if (!backButton) { backButton = document.createElement('button'); backButton.id = 'backButton'; backButton.textContent = 'Back'; backButton.className = 'back-btn'; - const headerContainer = document.getElementById('album-header'); + // Insert the back button at the beginning of the header container. + const headerContainer = document.getElementById('artist-header'); headerContainer.insertBefore(backButton, headerContainer.firstChild); } backButton.addEventListener('click', () => { + // Navigate to the site's base URL. window.location.href = window.location.origin; }); - // Download Album Button - let downloadAlbumBtn = document.getElementById('downloadAlbumBtn'); - if (!downloadAlbumBtn) { - downloadAlbumBtn = document.createElement('button'); - downloadAlbumBtn.id = 'downloadAlbumBtn'; - downloadAlbumBtn.textContent = 'Download Full Album'; - downloadAlbumBtn.className = 'download-btn download-btn--main'; - document.getElementById('album-header').appendChild(downloadAlbumBtn); + // --- Add "Download Whole Artist" Button --- + let downloadArtistBtn = document.getElementById('downloadArtistBtn'); + if (!downloadArtistBtn) { + downloadArtistBtn = document.createElement('button'); + downloadArtistBtn.id = 'downloadArtistBtn'; + downloadArtistBtn.textContent = 'Download Whole Artist'; + downloadArtistBtn.className = 'download-btn download-btn--main'; + // Insert the button into the header container. + const headerContainer = document.getElementById('artist-header'); + headerContainer.appendChild(downloadArtistBtn); } - - downloadAlbumBtn.addEventListener('click', () => { + downloadArtistBtn.addEventListener('click', () => { + // Remove individual album download buttons (but leave the whole artist button). document.querySelectorAll('.download-btn').forEach(btn => { - if (btn.id !== 'downloadAlbumBtn') btn.remove(); + if (btn.id !== 'downloadArtistBtn') { + btn.remove(); + } }); - downloadAlbumBtn.disabled = true; - downloadAlbumBtn.textContent = 'Queueing...'; + // Disable the whole artist button to prevent repeated clicks. + downloadArtistBtn.disabled = true; + downloadArtistBtn.textContent = 'Queueing...'; - downloadWholeAlbum(album).then(() => { - downloadAlbumBtn.textContent = 'Queued!'; + // Initiate the artist download. + downloadWholeArtist(artistData).then(() => { + downloadArtistBtn.textContent = 'Queued!'; }).catch(err => { - showError('Failed to queue album download: ' + err.message); - downloadAlbumBtn.disabled = false; + showError('Failed to queue artist download: ' + err.message); + downloadArtistBtn.disabled = false; }); }); - // Render tracks - const tracksList = document.getElementById('tracks-list'); - tracksList.innerHTML = ''; + // Group albums by album type. + const albumGroups = {}; + artistData.items.forEach(album => { + // Normalize album type to lower-case for grouping. + const type = album.album_type.toLowerCase(); + if (!albumGroups[type]) { + albumGroups[type] = []; + } + albumGroups[type].push(album); + }); - album.tracks.items.forEach((track, index) => { - const trackElement = document.createElement('div'); - trackElement.className = 'track'; - trackElement.innerHTML = ` -
${index + 1}
-
-
${track.name}
-
${track.artists.map(a => a.name).join(', ')}
-
-
${msToTime(track.duration_ms)}
- `; - tracksList.appendChild(trackElement); - }); + groupSection.appendChild(header); - document.getElementById('album-header').classList.remove('hidden'); - document.getElementById('tracks-container').classList.remove('hidden'); + // Container for the individual albums in this group. + const albumsContainer = document.createElement('div'); + albumsContainer.className = 'albums-list'; + albums.forEach((album, index) => { + const albumElement = document.createElement('div'); + albumElement.className = 'track'; // reusing the same CSS classes as in the playlist view + albumElement.innerHTML = ` +
${index + 1}
+ Album cover +
+
${album.name}
+
${album.album_type}
+
+
${album.release_date}
+
${album.total_tracks} tracks
+ + `; + albumsContainer.appendChild(albumElement); + }); + groupSection.appendChild(albumsContainer); + groupsContainer.appendChild(groupSection); + } + + // Reveal header and albums container + document.getElementById('artist-header').classList.remove('hidden'); + document.getElementById('albums-container').classList.remove('hidden'); + + // Attach event listeners for individual album download buttons. attachDownloadListeners(); + // Attach event listeners for group download buttons. + attachGroupDownloadListeners(); } -async function downloadWholeAlbum(album) { - const url = album.external_urls.spotify; - startDownload(url, 'album', { name: album.name }); -} - -function msToTime(duration) { - const minutes = Math.floor(duration / 60000); - const seconds = ((duration % 60000) / 1000).toFixed(0); - return `${minutes}:${seconds.padStart(2, '0')}`; -} - +/** + * Displays an error message in the UI. + */ function showError(message) { const errorEl = document.getElementById('error'); errorEl.textContent = message; errorEl.classList.remove('hidden'); } +/** + * Attaches event listeners to all individual album download buttons. + */ function attachDownloadListeners() { - document.querySelectorAll('.download-btn').forEach((btn) => { - if (btn.id === 'downloadAlbumBtn') return; - btn.addEventListener('click', (e) => { - e.stopPropagation(); - const url = e.currentTarget.dataset.url; - const type = e.currentTarget.dataset.type; - const name = e.currentTarget.dataset.name || extractName(url); - const albumType = e.currentTarget.dataset.albumType; - - // Remove the button after click - e.currentTarget.remove(); - - // Start the download for this track. - startDownload(url, type, { name }, albumType); - }); + document.querySelectorAll('.download-btn').forEach((btn) => { + // Skip group and whole artist download buttons. + if (btn.id === 'downloadArtistBtn' || btn.classList.contains('group-download-btn')) return; + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const url = e.currentTarget.dataset.url; + const type = e.currentTarget.dataset.type; + const name = e.currentTarget.dataset.name || extractName(url); + const albumType = e.currentTarget.dataset.albumType; + // Remove the button after click. + e.currentTarget.remove(); + // Start the download for this album. + startDownload(url, type, { name }, albumType); }); + }); } +/** + * Attaches event listeners to all group download buttons. + */ +function attachGroupDownloadListeners() { + document.querySelectorAll('.group-download-btn').forEach((btn) => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const albumType = e.currentTarget.dataset.albumType; + const artistUrl = e.currentTarget.dataset.artistUrl; + // Disable the button to prevent repeated clicks. + e.currentTarget.disabled = true; + e.currentTarget.textContent = `Queueing ${capitalize(albumType)}s...`; + // Initiate a download for this album group. + startDownload(artistUrl, 'artist', { name: `All ${capitalize(albumType)}s` }, albumType) + .then(() => { + e.currentTarget.textContent = `Queued!`; + }) + .catch(err => { + showError('Failed to queue group download: ' + err.message); + e.currentTarget.disabled = false; + }); + }); + }); +} + +/** + * Initiates the whole artist download by calling the artist endpoint. + */ +async function downloadWholeArtist(artistData) { + // Use the artist external URL from the first album's artist object. + const artistUrl = artistData.items[0]?.artists[0]?.external_urls.spotify; + if (!artistUrl) throw new Error('Artist URL not found.'); + // Queue the whole artist download with the descriptive artist name. + startDownload(artistUrl, 'artist', { name: artistData.items[0]?.artists[0]?.name || 'Artist' }); +} + +/** + * Starts the download process by building the API URL, + * fetching download details, and then adding the download to the queue. + */ async function startDownload(url, type, item, albumType) { - // Retrieve configuration (if any) from localStorage + // Retrieve configuration (if any) from localStorage. const config = JSON.parse(localStorage.getItem('activeConfig')) || {}; const { fallback = false, @@ -168,12 +264,11 @@ async function startDownload(url, type, item, albumType) { let apiUrl = ''; // Build API URL based on the download type. - if (type === 'album') { - apiUrl = `/api/album/download?service=${service}&url=${encodeURIComponent(url)}`; - } else if (type === 'artist') { + if (type === 'artist') { + // Use the dedicated artist download endpoint. apiUrl = `/api/artist/download?service=${service}&artist_url=${encodeURIComponent(url)}&album_type=${encodeURIComponent(albumType || 'album,single,compilation')}`; } else { - // Default is track download. + // Default: track or other type. apiUrl = `/api/${type}/download?service=${service}&url=${encodeURIComponent(url)}`; } @@ -198,4 +293,19 @@ async function startDownload(url, type, item, albumType) { } catch (error) { showError('Download failed: ' + error.message); } -} \ No newline at end of file +} + +/** + * A helper function to extract a display name from the URL. + */ +function extractName(url) { + return url; +} + +/** + * Helper to capitalize the first letter of a string. + */ +function capitalize(str) { + if (!str) return ''; + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/static/js/artist.js b/static/js/artist.js new file mode 100644 index 0000000..55f2def --- /dev/null +++ b/static/js/artist.js @@ -0,0 +1,310 @@ +// Import the downloadQueue singleton from your working queue.js implementation. +import { downloadQueue } from './queue.js'; + +document.addEventListener('DOMContentLoaded', () => { + // Parse artist ID from the URL (expected route: /artist/{id}) + const pathSegments = window.location.pathname.split('/'); + const artistId = pathSegments[pathSegments.indexOf('artist') + 1]; + + if (!artistId) { + showError('No artist ID provided.'); + return; + } + + // Fetch the artist info (which includes a list of albums) + fetch(`/api/artist/info?id=${encodeURIComponent(artistId)}`) + .then(response => { + if (!response.ok) throw new Error('Network response was not ok'); + return response.json(); + }) + .then(data => renderArtist(data)) + .catch(error => { + console.error('Error:', error); + showError('Failed to load artist info.'); + }); + + const queueIcon = document.getElementById('queueIcon'); + if (queueIcon) { + queueIcon.addEventListener('click', () => { + downloadQueue.toggleVisibility(); + }); + } +}); + +/** + * Renders the artist header and groups the albums by type. + * + * The API response is expected to have the following structure: + * { + * "href": "...", + * "limit": 50, + * "next": null, + * "offset": 0, + * "previous": null, + * "total": 5, + * "items": [ { album object }, { album object }, ... ] + * } + */ +function renderArtist(artistData) { + // Hide loading and error messages + document.getElementById('loading').classList.add('hidden'); + document.getElementById('error').classList.add('hidden'); + + // Use the first album to extract artist details + const firstAlbum = artistData.items[0]; + const artistName = firstAlbum?.artists[0]?.name || 'Unknown Artist'; + const artistImage = firstAlbum?.images[0]?.url || 'placeholder.jpg'; + document.getElementById('artist-name').textContent = artistName; + document.getElementById('artist-stats').textContent = `${artistData.total} albums`; + document.getElementById('artist-image').src = artistImage; + + // --- Add Back Button --- + let backButton = document.getElementById('backButton'); + if (!backButton) { + backButton = document.createElement('button'); + backButton.id = 'backButton'; + backButton.textContent = 'Back'; + backButton.className = 'back-btn'; + // Insert the back button at the beginning of the header container. + const headerContainer = document.getElementById('artist-header'); + headerContainer.insertBefore(backButton, headerContainer.firstChild); + } + backButton.addEventListener('click', () => { + // Navigate to the site's base URL. + window.location.href = window.location.origin; + }); + + // --- Add "Download Whole Artist" Button --- + let downloadArtistBtn = document.getElementById('downloadArtistBtn'); + if (!downloadArtistBtn) { + downloadArtistBtn = document.createElement('button'); + downloadArtistBtn.id = 'downloadArtistBtn'; + downloadArtistBtn.textContent = 'Download Whole Artist'; + downloadArtistBtn.className = 'download-btn download-btn--main'; + // Insert the button into the header container. + const headerContainer = document.getElementById('artist-header'); + headerContainer.appendChild(downloadArtistBtn); + } + downloadArtistBtn.addEventListener('click', () => { + // Remove individual album download buttons (but leave the whole artist button). + document.querySelectorAll('.download-btn').forEach(btn => { + if (btn.id !== 'downloadArtistBtn') { + btn.remove(); + } + }); + + // Disable the whole artist button to prevent repeated clicks. + downloadArtistBtn.disabled = true; + downloadArtistBtn.textContent = 'Queueing...'; + + // Initiate the artist download. + downloadWholeArtist(artistData).then(() => { + downloadArtistBtn.textContent = 'Queued!'; + }).catch(err => { + showError('Failed to queue artist download: ' + err.message); + downloadArtistBtn.disabled = false; + }); + }); + + // Group albums by album type. + const albumGroups = {}; + artistData.items.forEach(album => { + const type = album.album_type.toLowerCase(); + if (!albumGroups[type]) { + albumGroups[type] = []; + } + albumGroups[type].push(album); + }); + + // Render groups into the #album-groups container. + const groupsContainer = document.getElementById('album-groups'); + groupsContainer.innerHTML = ''; // Clear any previous content + + // For each album type, render a section header, a "Download All" button, and the album list. + for (const [groupType, albums] of Object.entries(albumGroups)) { + const groupSection = document.createElement('section'); + groupSection.className = 'album-group'; + + // Header for the album group with a download-all button. + const header = document.createElement('div'); + header.className = 'album-group-header'; + header.innerHTML = ` +

${capitalize(groupType)}s

+ + `; + groupSection.appendChild(header); + + // Container for the individual albums in this group. + const albumsContainer = document.createElement('div'); + albumsContainer.className = 'albums-list'; + albums.forEach((album, index) => { + const albumElement = document.createElement('div'); + albumElement.className = 'track'; // reusing the same CSS classes as in the playlist view + albumElement.innerHTML = ` +
${index + 1}
+ Album cover +
+
${album.name}
+
+
+
${album.release_date}
+
${album.total_tracks} tracks
+ + `; + albumsContainer.appendChild(albumElement); + }); + groupSection.appendChild(albumsContainer); + groupsContainer.appendChild(groupSection); + } + + // Reveal header and albums container + document.getElementById('artist-header').classList.remove('hidden'); + document.getElementById('albums-container').classList.remove('hidden'); + + // Attach event listeners for individual album download buttons. + attachDownloadListeners(); + // Attach event listeners for group download buttons. + attachGroupDownloadListeners(); +} + +/** + * Displays an error message in the UI. + */ +function showError(message) { + const errorEl = document.getElementById('error'); + errorEl.textContent = message; + errorEl.classList.remove('hidden'); +} + +/** + * Attaches event listeners to all individual download buttons. + */ +function attachDownloadListeners() { + document.querySelectorAll('.download-btn').forEach((btn) => { + // Skip the whole artist button and group download buttons. + if (btn.id === 'downloadArtistBtn' || btn.classList.contains('group-download-btn')) return; + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const url = e.currentTarget.dataset.url; + const type = e.currentTarget.dataset.type; + const name = e.currentTarget.dataset.name || extractName(url); + const albumType = e.currentTarget.dataset.albumType; + // Remove the button after click. + e.currentTarget.remove(); + // Start the download for this album. + startDownload(url, type, { name }, albumType); + }); + }); +} + +/** + * Attaches event listeners to all group download buttons. + */ +function attachGroupDownloadListeners() { + document.querySelectorAll('.group-download-btn').forEach((btn) => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const albumType = e.currentTarget.dataset.albumType; + const artistUrl = e.currentTarget.dataset.artistUrl; + // Disable the button to prevent repeated clicks. + e.currentTarget.disabled = true; + e.currentTarget.textContent = `Queueing ${capitalize(albumType)}s...`; + // Initiate a download for this album group. + startDownload(artistUrl, 'artist', { name: `All ${capitalize(albumType)}s` }, albumType) + .then(() => { + e.currentTarget.textContent = `Queued!`; + }) + .catch(err => { + showError('Failed to queue group download: ' + err.message); + e.currentTarget.disabled = false; + }); + }); + }); +} + +/** + * Initiates the whole artist download by calling the artist endpoint. + */ +async function downloadWholeArtist(artistData) { + // Use the artist external URL from the first album's artist object. + const artistUrl = artistData.items[0]?.artists[0]?.external_urls.spotify; + if (!artistUrl) throw new Error('Artist URL not found.'); + // Queue the whole artist download with the descriptive artist name. + startDownload(artistUrl, 'artist', { name: artistData.items[0]?.artists[0]?.name || 'Artist' }); +} + +/** + * Starts the download process by building the API URL, + * fetching download details, and then adding the download to the queue. + */ +async function startDownload(url, type, item, albumType) { + // Retrieve configuration (if any) from localStorage. + const config = JSON.parse(localStorage.getItem('activeConfig')) || {}; + const { + fallback = false, + spotify = '', + deezer = '', + spotifyQuality = 'NORMAL', + deezerQuality = 'MP3_128', + realTime = false + } = config; + + const service = url.includes('open.spotify.com') ? 'spotify' : 'deezer'; + let apiUrl = ''; + + // Build API URL based on the download type. + if (type === 'artist') { + // Use the dedicated artist download endpoint. + apiUrl = `/api/artist/download?service=${service}&artist_url=${encodeURIComponent(url)}&album_type=${encodeURIComponent(albumType || 'album,single,compilation')}`; + } else { + // Default: use a generic endpoint. + apiUrl = `/api/${type}/download?service=${service}&url=${encodeURIComponent(url)}`; + } + + // Append account and quality details. + if (fallback && service === 'spotify') { + apiUrl += `&main=${deezer}&fallback=${spotify}`; + apiUrl += `&quality=${deezerQuality}&fall_quality=${spotifyQuality}`; + } else { + const mainAccount = service === 'spotify' ? spotify : deezer; + apiUrl += `&main=${mainAccount}&quality=${service === 'spotify' ? spotifyQuality : deezerQuality}`; + } + + if (realTime) { + apiUrl += '&real_time=true'; + } + + try { + const response = await fetch(apiUrl); + const data = await response.json(); + // Add the download to the queue using the working queue implementation. + downloadQueue.addDownload(item, type, data.prg_file); + } catch (error) { + showError('Download failed: ' + error.message); + } +} + +/** + * A helper function to extract a display name from the URL. + */ +function extractName(url) { + return url; +} + +/** + * Helper to capitalize the first letter of a string. + */ +function capitalize(str) { + if (!str) return ''; + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/templates/artist.html b/templates/artist.html new file mode 100644 index 0000000..e368283 --- /dev/null +++ b/templates/artist.html @@ -0,0 +1,42 @@ + + + + + + Artist Viewer + + + + + + + +
+ + + + + + +
Loading...
+ +
+ + + + +