From 0a356a3e675fb4955d8d2bc814ef158972a715e2 Mon Sep 17 00:00:00 2001 From: "cool.gitter.choco" Date: Mon, 3 Feb 2025 20:22:07 -0600 Subject: [PATCH] added playlist view --- app.py | 12 +- routes/album.py | 36 ++++ routes/artist.py | 36 ++++ routes/playlist.py | 36 ++++ routes/track.py | 36 ++++ routes/utils/get_info.py | 19 ++ static/css/main/icons.css | 36 +++- static/css/playlist/playlist.css | 325 +++++++++++++++++++++++++++++++ static/css/queue/queue.css | 42 ---- static/js/main.js | 40 ++-- static/js/playlist.js | 243 +++++++++++++++++++++++ templates/playlist.html | 40 ++++ 12 files changed, 839 insertions(+), 62 deletions(-) create mode 100644 routes/utils/get_info.py create mode 100644 static/css/playlist/playlist.css create mode 100644 static/js/playlist.js create mode 100644 templates/playlist.html diff --git a/app.py b/app.py index da2799d..a160f8e 100755 --- a/app.py +++ b/app.py @@ -16,7 +16,7 @@ def create_app(): app = Flask(__name__) # Configure basic logging - log_file='flask_server.log' + log_file = 'flask_server.log' logging.basicConfig( level=logging.INFO, format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s', @@ -48,11 +48,17 @@ def create_app(): def serve_index(): return render_template('main.html') - # Add this new route for config page + # Config page route @app.route('/config') def serve_config(): return render_template('config.html') + # New route: Serve playlist.html under /playlist/ + @app.route('/playlist/') + def serve_playlist(id): + # The id parameter is captured, but you can use it as needed. + return render_template('playlist.html') + @app.route('/static/') def serve_static(path): return send_from_directory('static', path) @@ -90,4 +96,4 @@ if __name__ == '__main__': app = create_app() logging.info("Starting Flask server on port 7171") from waitress import serve - serve(app, host='0.0.0.0', port=7171) \ No newline at end of file + serve(app, host='0.0.0.0', port=7171) diff --git a/routes/album.py b/routes/album.py index e27c7d5..86957b5 100755 --- a/routes/album.py +++ b/routes/album.py @@ -219,3 +219,39 @@ def cancel_download(): status=404, mimetype='application/json' ) + +# NEW ENDPOINT: Get Album Information +@album_bp.route('/info', methods=['GET']) +def get_album_info(): + """ + Retrieve Spotify album metadata given a Spotify album ID. + Expects a query parameter 'id' that contains the Spotify album ID. + """ + spotify_id = request.args.get('id') + if not spotify_id: + return Response( + json.dumps({"error": "Missing parameter: id"}), + status=400, + mimetype='application/json' + ) + + try: + # Import the get_spotify_info function from the utility module. + from routes.utils.get_info import get_spotify_info + # Call the function with the album type. + album_info = get_spotify_info(spotify_id, "album") + return Response( + json.dumps(album_info), + status=200, + mimetype='application/json' + ) + except Exception as e: + error_data = { + "error": str(e), + "traceback": traceback.format_exc() + } + return Response( + json.dumps(error_data), + status=500, + mimetype='application/json' + ) diff --git a/routes/artist.py b/routes/artist.py index ea5dac0..4595bb9 100644 --- a/routes/artist.py +++ b/routes/artist.py @@ -236,3 +236,39 @@ def cancel_artist_download(): status=404, mimetype='application/json' ) + +# NEW ENDPOINT: Get Artist Information +@artist_bp.route('/info', methods=['GET']) +def get_artist_info(): + """ + Retrieve Spotify artist metadata given a Spotify ID. + Expects a query parameter 'id' that contains the Spotify artist ID. + """ + spotify_id = request.args.get('id') + if not spotify_id: + return Response( + json.dumps({"error": "Missing parameter: id"}), + status=400, + mimetype='application/json' + ) + + try: + # Import the get_spotify_info function from the utility module. + from routes.utils.get_info import get_spotify_info + # Call the function with the artist type. + artist_info = get_spotify_info(spotify_id, "artist") + return Response( + json.dumps(artist_info), + status=200, + mimetype='application/json' + ) + except Exception as e: + error_data = { + "error": str(e), + "traceback": traceback.format_exc() + } + return Response( + json.dumps(error_data), + status=500, + mimetype='application/json' + ) diff --git a/routes/playlist.py b/routes/playlist.py index 4ebbb95..c8eba7b 100755 --- a/routes/playlist.py +++ b/routes/playlist.py @@ -163,3 +163,39 @@ def cancel_download(): status=404, mimetype='application/json' ) + +# NEW ENDPOINT: Get Playlist Information +@playlist_bp.route('/info', methods=['GET']) +def get_playlist_info(): + """ + Retrieve Spotify playlist metadata given a Spotify ID. + Expects a query parameter 'id' that contains the Spotify playlist ID. + """ + spotify_id = request.args.get('id') + if not spotify_id: + return Response( + json.dumps({"error": "Missing parameter: id"}), + status=400, + mimetype='application/json' + ) + + try: + # Import the get_spotify_info function from the utility module. + from routes.utils.get_info import get_spotify_info + # Call the function with the playlist type. + playlist_info = get_spotify_info(spotify_id, "playlist") + return Response( + json.dumps(playlist_info), + status=200, + mimetype='application/json' + ) + except Exception as e: + error_data = { + "error": str(e), + "traceback": traceback.format_exc() + } + return Response( + json.dumps(error_data), + status=500, + mimetype='application/json' + ) diff --git a/routes/track.py b/routes/track.py index 80c8f59..7ffb95a 100755 --- a/routes/track.py +++ b/routes/track.py @@ -208,3 +208,39 @@ def cancel_download(): status=404, mimetype='application/json' ) + +# NEW ENDPOINT: Get Track Information +@track_bp.route('/info', methods=['GET']) +def get_track_info(): + """ + Retrieve Spotify track metadata given a Spotify track ID. + Expects a query parameter 'id' that contains the Spotify track ID. + """ + spotify_id = request.args.get('id') + if not spotify_id: + return Response( + json.dumps({"error": "Missing parameter: id"}), + status=400, + mimetype='application/json' + ) + + try: + # Import the get_spotify_info function from the utility module. + from routes.utils.get_info import get_spotify_info + # Call the function with the track type. + track_info = get_spotify_info(spotify_id, "track") + return Response( + json.dumps(track_info), + status=200, + mimetype='application/json' + ) + except Exception as e: + error_data = { + "error": str(e), + "traceback": traceback.format_exc() + } + return Response( + json.dumps(error_data), + status=500, + mimetype='application/json' + ) diff --git a/routes/utils/get_info.py b/routes/utils/get_info.py new file mode 100644 index 0000000..86543ec --- /dev/null +++ b/routes/utils/get_info.py @@ -0,0 +1,19 @@ +#!/usr/bin/python3 + +from deezspot.easy_spoty import Spo + +Spo() + +def get_spotify_info(spotify_id, spotify_type): + if spotify_type == "track": + return Spo.get_track(spotify_id) + elif spotify_type == "album": + return Spo.get_album(spotify_id) + elif spotify_type == "playlist": + return Spo.get_playlist(spotify_id) + elif spotify_type == "artist": + return Spo.get_artist(spotify_id) + elif spotify_type == "episode": + return Spo.get_episode(spotify_id) + else: + raise ValueError(f"Unsupported Spotify type: {spotify_type}") diff --git a/static/css/main/icons.css b/static/css/main/icons.css index 6601381..4fee28a 100644 --- a/static/css/main/icons.css +++ b/static/css/main/icons.css @@ -37,4 +37,38 @@ width: 18px; height: 18px; margin-right: 0.3rem; -} \ No newline at end of file +} + +/* Base styles for View buttons */ +.view-btn { + background-color: #4CAF50; /* Same green tone as other buttons */ + color: #fff; + border: none; + padding: 4px 8px; /* Smaller padding than the download button */ + margin: 4px 4px 4px 8px; /* Adjust margins to keep spacing neat */ + border-radius: 4px; + cursor: pointer; + font-size: 12px; /* Smaller font size */ + transition: background-color 0.3s ease, transform 0.2s ease; +} + +/* Hover effect */ +.view-btn:hover { + background-color: #45a049; + transform: translateY(-1px); +} + +/* Focus outline for accessibility */ +.view-btn:focus { + outline: 2px solid #2e7d32; + outline-offset: 2px; +} + +/* Mobile compatibility tweaks */ +@media (max-width: 600px) { + .view-btn { + padding: 6px 10px; /* Slightly larger padding on mobile for easier tap targets */ + font-size: 13px; /* Ensure readability on smaller screens */ + margin: 4px; /* Reduce margins to better fit mobile layouts */ + } +} diff --git a/static/css/playlist/playlist.css b/static/css/playlist/playlist.css new file mode 100644 index 0000000..1c7c111 --- /dev/null +++ b/static/css/playlist/playlist.css @@ -0,0 +1,325 @@ +/* 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; + } + + /* Playlist Header */ + #playlist-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 */ + } + + #playlist-image { + width: 200px; + height: 200px; + object-fit: cover; + border-radius: 8px; + flex-shrink: 0; + } + + #playlist-info { + flex: 1; + min-width: 0; + } + + #playlist-name { + font-size: 2rem; + margin-bottom: 0.5rem; + } + + #playlist-owner, + #playlist-stats, + #playlist-description { + font-size: 1rem; + color: #b3b3b3; + margin-bottom: 0.5rem; + } + + /* Tracks Container */ + #tracks-container { + margin-top: 2rem; + } + + #tracks-container h2 { + font-size: 1.5rem; + margin-bottom: 1rem; + border-bottom: 1px solid #2a2a2a; + padding-bottom: 0.5rem; + } + + /* Tracks List */ + #tracks-list { + display: flex; + flex-direction: column; + gap: 1rem; + } + + /* Individual Track Styling */ + .track { + display: flex; + align-items: center; + padding: 1rem; + background: #181818; + border-radius: 8px; + transition: background 0.3s ease; + flex-wrap: wrap; + } + + .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; /* default left alignment */ + } + + .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 (e.g., in a queue list) */ + .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; + } + + /* Icon next to text */ + .download-btn .btn-icon { + margin-right: 0.5rem; + display: inline-flex; + align-items: center; + } + + /* 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) { + #playlist-header { + flex-direction: column; + align-items: flex-start; + } + + #playlist-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; + } + } + + /* Small Devices (Mobile Phones) */ + @media (max-width: 480px) { + #app { + padding: 10px; + } + + #playlist-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; + } + + /* Center the album and duration info */ + .track-album, + .track-duration { + margin-left: 0; + margin-top: 0.5rem; + width: 100%; + text-align: center; + } + + } + \ No newline at end of file diff --git a/static/css/queue/queue.css b/static/css/queue/queue.css index b22dd00..3a4df6a 100644 --- a/static/css/queue/queue.css +++ b/static/css/queue/queue.css @@ -141,42 +141,6 @@ color: #1DB954; } -/* Download button inside queue items (if needed) */ -.download-btn { - background-color: #1DB954; - color: white; - border: none; - padding: 8px 15px; - border-radius: 20px; - cursor: pointer; - font-size: 14px; - font-weight: 500; - transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease; - display: flex; - align-items: center; - gap: 8px; - width: 100%; - justify-content: center; -} - -.download-btn:hover { - background-color: #1ed760; - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(29, 185, 84, 0.3); -} - -.download-btn:active { - transform: translateY(0); - box-shadow: none; -} - -/* Optional: icon before the download button text */ -.download-btn::before { - content: "↓"; - font-weight: bold; - font-size: 16px; -} - /* Optional status message colors (if using state classes) */ .log--success { color: #1DB954 !important; @@ -309,12 +273,6 @@ margin-bottom: 12px; } - /* Adjust the download button padding and font size */ - .download-btn { - padding: 8px 10px; - font-size: 13px; - } - /* Ensure text remains legible on smaller screens */ .queue-item .log, .queue-item .type { diff --git a/static/js/main.js b/static/js/main.js index 744cf52..9f27623 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -139,6 +139,15 @@ function msToMinutesSeconds(ms) { } function createResultCard(item, type) { + let newUrl = '#'; + try { + const spotifyUrl = item.external_urls.spotify; + const parsedUrl = new URL(spotifyUrl); + newUrl = window.location.origin + parsedUrl.pathname; + } catch (e) { + console.error('Error parsing URL:', e); + } + let imageUrl, title, subtitle, details; switch(type) { @@ -146,10 +155,9 @@ function createResultCard(item, type) { imageUrl = item.album.images[0]?.url || ''; title = item.name; subtitle = item.artists.map(a => a.name).join(', '); - details = ` - ${item.album.name} - ${msToMinutesSeconds(item.duration_ms)} - `; + details = + `${item.album.name} + ${msToMinutesSeconds(item.duration_ms)}`; return `
@@ -163,16 +171,16 @@ function createResultCard(item, type) { data-type="${type}"> Download +
`; case 'playlist': imageUrl = item.images[0]?.url || ''; title = item.name; subtitle = item.owner.display_name; - details = ` - ${item.tracks.total} tracks - ${item.description || 'No description'} - `; + details = + `${item.tracks.total} tracks + ${item.description || 'No description'}`; return `
@@ -186,16 +194,16 @@ function createResultCard(item, type) { data-type="${type}"> Download +
`; case 'album': imageUrl = item.images[0]?.url || ''; title = item.name; subtitle = item.artists.map(a => a.name).join(', '); - details = ` - ${item.release_date} - ${item.total_tracks} tracks - `; + details = + `${item.release_date} + ${item.total_tracks} tracks`; return `
@@ -209,6 +217,7 @@ function createResultCard(item, type) { data-type="${type}"> Download +
`; case 'artist': @@ -225,7 +234,6 @@ function createResultCard(item, type) {
${subtitle}
${details}
- - - +
+
`; } -} +} \ No newline at end of file diff --git a/static/js/playlist.js b/static/js/playlist.js new file mode 100644 index 0000000..4e2f12f --- /dev/null +++ b/static/js/playlist.js @@ -0,0 +1,243 @@ +// Import the downloadQueue singleton from your working queue.js implementation. +import { downloadQueue } from './queue.js'; + +document.addEventListener('DOMContentLoaded', () => { + // Parse playlist ID from URL + const pathSegments = window.location.pathname.split('/'); + const playlistId = pathSegments[pathSegments.indexOf('playlist') + 1]; + + if (!playlistId) { + showError('No playlist ID provided.'); + return; + } + + // Fetch playlist info and render it + fetch(`/api/playlist/info?id=${encodeURIComponent(playlistId)}`) + .then(response => { + if (!response.ok) throw new Error('Network response was not ok'); + return response.json(); + }) + .then(data => renderPlaylist(data)) + .catch(error => { + console.error('Error:', error); + showError('Failed to load playlist.'); + }); + + const queueIcon = document.getElementById('queueIcon'); + if (queueIcon) { + queueIcon.addEventListener('click', () => { + downloadQueue.toggleVisibility(); + }); + } +}); + +/** + * Renders playlist header and tracks. + */ +function renderPlaylist(playlist) { + // Hide loading and error messages + document.getElementById('loading').classList.add('hidden'); + document.getElementById('error').classList.add('hidden'); + + // Update header info + document.getElementById('playlist-name').textContent = playlist.name; + document.getElementById('playlist-owner').textContent = `By ${playlist.owner.display_name}`; + document.getElementById('playlist-stats').textContent = + `${playlist.followers.total} followers • ${playlist.tracks.total} songs`; + document.getElementById('playlist-description').textContent = playlist.description; + const image = playlist.images[0]?.url || 'placeholder.jpg'; + document.getElementById('playlist-image').src = image; + + // --- 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('playlist-header'); + headerContainer.insertBefore(backButton, headerContainer.firstChild); + } + backButton.addEventListener('click', () => { + // Navigate to the site's base URL. For example, if the current URL is + // cool.com/bla/bla, this will take you to cool.com. + window.location.href = window.location.origin; + }); + + // --- Add "Download Whole Playlist" Button --- + let downloadPlaylistBtn = document.getElementById('downloadPlaylistBtn'); + if (!downloadPlaylistBtn) { + downloadPlaylistBtn = document.createElement('button'); + downloadPlaylistBtn.id = 'downloadPlaylistBtn'; + downloadPlaylistBtn.textContent = 'Download Whole Playlist'; + downloadPlaylistBtn.className = 'download-btn download-btn--main'; + // Insert the button into the header container (e.g. after the description) + const headerContainer = document.getElementById('playlist-header'); + headerContainer.appendChild(downloadPlaylistBtn); + } + downloadPlaylistBtn.addEventListener('click', () => { + // Remove individual track download buttons (but leave the whole playlist button). + document.querySelectorAll('.download-btn').forEach(btn => { + if (btn.id !== 'downloadPlaylistBtn') { + btn.remove(); + } + }); + + // Disable the whole playlist button to prevent repeated clicks. + downloadPlaylistBtn.disabled = true; + downloadPlaylistBtn.textContent = 'Queueing...'; + + // Initiate the playlist download. + downloadWholePlaylist(playlist).then(() => { + downloadPlaylistBtn.textContent = 'Queued!'; + }).catch(err => { + showError('Failed to queue playlist download: ' + err.message); + downloadPlaylistBtn.disabled = false; + }); + }); + + // Render tracks list + const tracksList = document.getElementById('tracks-list'); + tracksList.innerHTML = ''; // Clear any existing content + + playlist.tracks.items.forEach((item, index) => { + const track = item.track; + const trackElement = document.createElement('div'); + trackElement.className = 'track'; + trackElement.innerHTML = ` +
${index + 1}
+
+
${track.name}
+
${track.artists[0].name}
+
+
${track.album.name}
+
${msToTime(track.duration_ms)}
+ + `; + tracksList.appendChild(trackElement); + }); + + // Reveal header and tracks container + document.getElementById('playlist-header').classList.remove('hidden'); + document.getElementById('tracks-container').classList.remove('hidden'); + + // Attach download listeners to newly rendered download buttons + attachDownloadListeners(); +} + +/** + * Converts milliseconds to minutes:seconds. + */ +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 download buttons. + */ +function attachDownloadListeners() { + document.querySelectorAll('.download-btn').forEach((btn) => { + // Skip the whole playlist button. + if (btn.id === 'downloadPlaylistBtn') 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); + }); + }); +} + +/** + * Initiates the whole playlist download by calling the playlist endpoint. + */ +async function downloadWholePlaylist(playlist) { + // Use the playlist external URL (assumed available) for the download. + const url = playlist.external_urls.spotify; + // Queue the whole playlist download with the descriptive playlist name. + startDownload(url, 'playlist', { name: playlist.name }); +} + +/** + * 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 === 'playlist') { + // Use the dedicated playlist download endpoint. + apiUrl = `/api/playlist/download?service=${service}&url=${encodeURIComponent(url)}`; + } else if (type === 'artist') { + apiUrl = `/api/artist/download?service=${service}&artist_url=${encodeURIComponent(url)}&album_type=${encodeURIComponent(albumType || 'album,single,compilation')}`; + } else { + // Default is track download. + 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; +} diff --git a/templates/playlist.html b/templates/playlist.html new file mode 100644 index 0000000..c64111a --- /dev/null +++ b/templates/playlist.html @@ -0,0 +1,40 @@ + + + + + + Playlist Viewer + + + + + + +
+ + + + +
Loading...
+ +
+ + + + +