diff --git a/.dockerignore b/.dockerignore
index 3d50c99..0da008a 100755
--- a/.dockerignore
+++ b/.dockerignore
@@ -13,3 +13,4 @@
/Dockerfile
/docker-compose.yaml
/README.md
+/config
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index b39477a..33ebb82 100755
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@ routes/utils/__pycache__/__init__.cpython-312.pyc
routes/utils/__pycache__/credentials.cpython-312.pyc
routes/utils/__pycache__/search.cpython-312.pyc
search_test.py
+config/main.json
diff --git a/app.py b/app.py
index 1d9da05..31eccc0 100755
--- a/app.py
+++ b/app.py
@@ -6,6 +6,8 @@ from routes.album import album_bp
from routes.track import track_bp
from routes.playlist import playlist_bp
from routes.prgs import prgs_bp
+from routes.config import config_bp
+from routes.artist import artist_bp
import logging
import time
from pathlib import Path
@@ -34,12 +36,15 @@ def create_app():
CORS(app)
# Register blueprints
+ app.register_blueprint(config_bp, url_prefix='/api')
app.register_blueprint(search_bp, url_prefix='/api')
app.register_blueprint(credentials_bp, url_prefix='/api/credentials')
app.register_blueprint(album_bp, url_prefix='/api/album')
app.register_blueprint(track_bp, url_prefix='/api/track')
app.register_blueprint(playlist_bp, url_prefix='/api/playlist')
+ app.register_blueprint(artist_bp, url_prefix='/api/artist')
app.register_blueprint(prgs_bp, url_prefix='/api/prgs')
+
# Serve frontend
@app.route('/')
diff --git a/routes/artist.py b/routes/artist.py
new file mode 100644
index 0000000..0376ea5
--- /dev/null
+++ b/routes/artist.py
@@ -0,0 +1,309 @@
+#!/usr/bin/env python3
+"""
+Artist endpoint blueprint.
+"""
+
+from flask import Blueprint, Response, request
+import json
+import os
+import random
+import string
+import sys
+import traceback
+from multiprocessing import Process
+
+artist_bp = Blueprint('artist', __name__)
+
+# Global dictionary to keep track of running download processes.
+download_processes = {}
+
+def generate_random_filename(length=6):
+ chars = string.ascii_lowercase + string.digits
+ return ''.join(random.choice(chars) for _ in range(length)) + '.artist.prg'
+
+class FlushingFileWrapper:
+ def __init__(self, file):
+ self.file = file
+
+ def write(self, text):
+ # Only write lines that start with '{'
+ for line in text.split('\n'):
+ line = line.strip()
+ if line and line.startswith('{'):
+ try:
+ obj = json.loads(line)
+ # Skip writing if the JSON object has type "track"
+ if obj.get("type") == "track":
+ continue
+ except ValueError:
+ # If not valid JSON, write the line as is.
+ pass
+ self.file.write(line + '\n')
+ self.file.flush()
+
+ def flush(self):
+ self.file.flush()
+
+def download_artist_task(service, artist_url, main, fallback, quality, fall_quality, real_time,
+ album_type, prg_path, orig_request, custom_dir_format, custom_track_format):
+ """
+ This function wraps the call to download_artist_albums, writes the original
+ request data to the progress file, and then writes JSON status updates.
+ """
+ try:
+ from routes.utils.artist import download_artist_albums
+ with open(prg_path, 'w') as f:
+ flushing_file = FlushingFileWrapper(f)
+ original_stdout = sys.stdout
+ sys.stdout = flushing_file # Redirect stdout to our flushing file wrapper
+
+ # Write the original request data to the progress file.
+ try:
+ flushing_file.write(json.dumps({"original_request": orig_request}) + "\n")
+ except Exception as e:
+ flushing_file.write(json.dumps({
+ "status": "error",
+ "message": f"Failed to write original request data: {str(e)}"
+ }) + "\n")
+
+ try:
+ download_artist_albums(
+ service=service,
+ artist_url=artist_url,
+ main=main,
+ fallback=fallback,
+ quality=quality,
+ fall_quality=fall_quality,
+ real_time=real_time,
+ album_type=album_type,
+ custom_dir_format=custom_dir_format,
+ custom_track_format=custom_track_format
+ )
+ flushing_file.write(json.dumps({"status": "complete"}) + "\n")
+ except Exception as e:
+ error_data = json.dumps({
+ "status": "error",
+ "message": str(e),
+ "traceback": traceback.format_exc()
+ })
+ flushing_file.write(error_data + "\n")
+ finally:
+ sys.stdout = original_stdout # Restore stdout
+ except Exception as e:
+ with open(prg_path, 'w') as f:
+ error_data = json.dumps({
+ "status": "error",
+ "message": str(e),
+ "traceback": traceback.format_exc()
+ })
+ f.write(error_data + "\n")
+
+@artist_bp.route('/download', methods=['GET'])
+def handle_artist_download():
+ """
+ Starts the artist album download process.
+ Expected query parameters:
+ - artist_url: string (e.g., a Spotify artist URL)
+ - service: string (e.g., "deezer" or "spotify")
+ - main: string (e.g., "MX")
+ - fallback: string (optional, e.g., "JP")
+ - quality: string (e.g., "MP3_128")
+ - fall_quality: string (optional, e.g., "HIGH")
+ - real_time: bool (e.g., "true" or "false")
+ - album_type: string(s); one or more of "album", "single", "appears_on", "compilation" (if multiple, comma-separated)
+ - custom_dir_format: string (optional, default: "%ar_album%/%album%/%copyright%")
+ - custom_track_format: string (optional, default: "%tracknum%. %music% - %artist%")
+ """
+ service = request.args.get('service')
+ artist_url = request.args.get('artist_url')
+ main = request.args.get('main')
+ fallback = request.args.get('fallback')
+ quality = request.args.get('quality')
+ fall_quality = request.args.get('fall_quality')
+ album_type = request.args.get('album_type')
+ real_time_arg = request.args.get('real_time', 'false')
+ real_time = real_time_arg.lower() in ['true', '1', 'yes']
+
+ # New query parameters for custom formatting.
+ custom_dir_format = request.args.get('custom_dir_format', "%ar_album%/%album%")
+ custom_track_format = request.args.get('custom_track_format', "%tracknum%. %music% - %artist%")
+
+ # Sanitize main and fallback to prevent directory traversal
+ if main:
+ main = os.path.basename(main)
+ if fallback:
+ fallback = os.path.basename(fallback)
+
+ # Check for required parameters.
+ if not all([service, artist_url, main, quality, album_type]):
+ return Response(
+ json.dumps({"error": "Missing parameters"}),
+ status=400,
+ mimetype='application/json'
+ )
+
+ # Validate credentials based on the selected service.
+ try:
+ if service == 'spotify':
+ if fallback:
+ # When using Spotify as the main service with a fallback, assume main credentials for Deezer and fallback for Spotify.
+ deezer_creds_path = os.path.abspath(os.path.join('./creds/deezer', main, 'credentials.json'))
+ if not os.path.isfile(deezer_creds_path):
+ return Response(
+ json.dumps({"error": "Invalid Deezer credentials directory"}),
+ status=400,
+ mimetype='application/json'
+ )
+ spotify_fallback_path = os.path.abspath(os.path.join('./creds/spotify', fallback, 'credentials.json'))
+ if not os.path.isfile(spotify_fallback_path):
+ return Response(
+ json.dumps({"error": "Invalid Spotify fallback credentials directory"}),
+ status=400,
+ mimetype='application/json'
+ )
+ else:
+ # Validate Spotify main credentials.
+ spotify_creds_path = os.path.abspath(os.path.join('./creds/spotify', main, 'credentials.json'))
+ if not os.path.isfile(spotify_creds_path):
+ return Response(
+ json.dumps({"error": "Invalid Spotify credentials directory"}),
+ status=400,
+ mimetype='application/json'
+ )
+ elif service == 'deezer':
+ # Validate Deezer main credentials.
+ deezer_creds_path = os.path.abspath(os.path.join('./creds/deezer', main, 'credentials.json'))
+ if not os.path.isfile(deezer_creds_path):
+ return Response(
+ json.dumps({"error": "Invalid Deezer credentials directory"}),
+ status=400,
+ mimetype='application/json'
+ )
+ else:
+ return Response(
+ json.dumps({"error": "Unsupported service"}),
+ status=400,
+ mimetype='application/json'
+ )
+ except Exception as e:
+ return Response(
+ json.dumps({"error": f"Credential validation failed: {str(e)}"}),
+ status=500,
+ mimetype='application/json'
+ )
+
+ # Create a random filename for the progress file.
+ filename = generate_random_filename()
+ prg_dir = './prgs'
+ os.makedirs(prg_dir, exist_ok=True)
+ prg_path = os.path.join(prg_dir, filename)
+
+ # Capture the original request parameters as a dictionary.
+ orig_request = request.args.to_dict()
+
+ # Create and start the download process.
+ process = Process(
+ target=download_artist_task,
+ args=(
+ service,
+ artist_url,
+ main,
+ fallback,
+ quality,
+ fall_quality,
+ real_time,
+ album_type,
+ prg_path,
+ orig_request,
+ custom_dir_format,
+ custom_track_format
+ )
+ )
+ process.start()
+ download_processes[filename] = process
+
+ return Response(
+ json.dumps({"prg_file": filename}),
+ status=202,
+ mimetype='application/json'
+ )
+
+@artist_bp.route('/download/cancel', methods=['GET'])
+def cancel_artist_download():
+ """
+ Cancel a running artist download process by its prg file name.
+ """
+ prg_file = request.args.get('prg_file')
+ if not prg_file:
+ return Response(
+ json.dumps({"error": "Missing process id (prg_file) parameter"}),
+ status=400,
+ mimetype='application/json'
+ )
+
+ process = download_processes.get(prg_file)
+ prg_dir = './prgs'
+ prg_path = os.path.join(prg_dir, prg_file)
+
+ if process and process.is_alive():
+ process.terminate()
+ process.join() # Wait for termination
+ del download_processes[prg_file]
+
+ try:
+ with open(prg_path, 'a') as f:
+ f.write(json.dumps({"status": "cancel"}) + "\n")
+ except Exception as e:
+ return Response(
+ json.dumps({"error": f"Failed to write cancel status to file: {str(e)}"}),
+ status=500,
+ mimetype='application/json'
+ )
+
+ return Response(
+ json.dumps({"status": "cancel"}),
+ status=200,
+ mimetype='application/json'
+ )
+ else:
+ return Response(
+ json.dumps({"error": "Process not found or already terminated"}),
+ 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'
+ )
\ No newline at end of file
diff --git a/routes/config.py b/routes/config.py
new file mode 100644
index 0000000..cba627b
--- /dev/null
+++ b/routes/config.py
@@ -0,0 +1,45 @@
+from flask import Blueprint, jsonify, request
+import json
+from pathlib import Path
+import logging
+
+config_bp = Blueprint('config_bp', __name__)
+CONFIG_PATH = Path('./config/main.json')
+
+def get_config():
+ try:
+ if not CONFIG_PATH.exists():
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
+ CONFIG_PATH.write_text('{}')
+ return {}
+
+ with open(CONFIG_PATH, 'r') as f:
+ return json.load(f)
+ except Exception as e:
+ logging.error(f"Error reading config: {str(e)}")
+ return None
+
+@config_bp.route('/config', methods=['GET'])
+def handle_config():
+ config = get_config()
+ if config is None:
+ return jsonify({"error": "Could not read config file"}), 500
+ return jsonify(config)
+
+@config_bp.route('/config', methods=['POST', 'PUT'])
+def update_config():
+ try:
+ new_config = request.get_json()
+ if not isinstance(new_config, dict):
+ return jsonify({"error": "Invalid config format"}), 400
+
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
+ with open(CONFIG_PATH, 'w') as f:
+ json.dump(new_config, f, indent=2)
+
+ return jsonify({"message": "Config updated successfully"})
+ except json.JSONDecodeError:
+ return jsonify({"error": "Invalid JSON data"}), 400
+ except Exception as e:
+ logging.error(f"Error updating config: {str(e)}")
+ return jsonify({"error": "Failed to update config"}), 500
\ No newline at end of file
diff --git a/routes/prgs.py b/routes/prgs.py
index 86ec1ef..78bd370 100755
--- a/routes/prgs.py
+++ b/routes/prgs.py
@@ -49,17 +49,9 @@ def get_prg_file(filename):
if len(lines) > 1:
try:
second_line = json.loads(lines[1])
+ # Directly extract 'type' and 'name' from the JSON
resource_type = second_line.get("type", "")
- if resource_type == "track":
- resource_name = second_line.get("song", "")
- elif resource_type == "album":
- resource_name = second_line.get("album", "")
- elif resource_type == "playlist":
- resource_name = second_line.get("name", "")
- elif resource_type == "artist":
- resource_name = second_line.get("artist", "")
- else:
- resource_name = ""
+ resource_name = second_line.get("name", "")
except Exception:
resource_type = ""
resource_name = ""
diff --git a/routes/utils/artist.py b/routes/utils/artist.py
new file mode 100644
index 0000000..507bd73
--- /dev/null
+++ b/routes/utils/artist.py
@@ -0,0 +1,126 @@
+import json
+import traceback
+
+from deezspot.easy_spoty import Spo
+from deezspot.libutils.utils import get_ids, link_is_valid
+from routes.utils.album import download_album # Assumes album.py is in routes/utils/
+
+def log_json(message_dict):
+ """Helper function to output a JSON-formatted log message."""
+ print(json.dumps(message_dict))
+
+
+def get_artist_discography(url, album_type='album,single,compilation,appears_on'):
+ if not url:
+ message = "No artist URL provided."
+ log_json({"status": "error", "message": message})
+ raise ValueError(message)
+
+ try:
+ # Validate the URL (this function should raise an error if invalid).
+ link_is_valid(link=url)
+ except Exception as validation_error:
+ message = f"Link validation failed: {validation_error}"
+ log_json({"status": "error", "message": message})
+ raise ValueError(message)
+
+ try:
+ # Extract the artist ID from the URL.
+ artist_id = get_ids(url)
+ except Exception as id_error:
+ message = f"Failed to extract artist ID from URL: {id_error}"
+ log_json({"status": "error", "message": message})
+ raise ValueError(message)
+
+ try:
+ # Retrieve the discography using the artist ID.
+ discography = Spo.get_artist(artist_id, album_type=album_type)
+ return discography
+ except Exception as fetch_error:
+ message = f"An error occurred while fetching the discography: {fetch_error}"
+ log_json({"status": "error", "message": message})
+ raise
+
+
+def download_artist_albums(service, artist_url, main, fallback=None, quality=None,
+ fall_quality=None, real_time=False, album_type='album,single,compilation,appears_on',
+ custom_dir_format="%ar_album%/%album%/%copyright%",
+ custom_track_format="%tracknum%. %music% - %artist%"):
+ try:
+ discography = get_artist_discography(artist_url, album_type=album_type)
+ except Exception as e:
+ log_json({"status": "error", "message": f"Error retrieving artist discography: {e}"})
+ raise
+ albums = discography.get('items', [])
+ # Extract artist name from the first album's artists as fallback.
+ artist_name = artist_url
+ if albums:
+ first_album = albums[0]
+ artists = first_album.get('artists', [])
+ if artists:
+ artist_name = artists[0].get('name', artist_url)
+
+ if not albums:
+ log_json({
+ "status": "done",
+ "type": "artist",
+ "artist": artist_name,
+ "album_type": album_type,
+ "message": "No albums found for the artist."
+ })
+ return
+
+ log_json({
+ "status": "initializing",
+ "type": "artist",
+ "artist": artist_name,
+ "total_albums": len(albums),
+ "album_type": album_type
+ })
+
+ for album in albums:
+ try:
+ album_url = album.get('external_urls', {}).get('spotify')
+ album_name = album.get('name', 'Unknown Album')
+ # Extract artist names if available.
+ artists = []
+ if "artists" in album:
+ artists = [artist.get("name", "Unknown") for artist in album["artists"]]
+ if not album_url:
+ log_json({
+ "status": "warning",
+ "type": "album",
+ "album": album_name,
+ "artist": artists,
+ "message": "No Spotify URL found; skipping."
+ })
+ continue
+
+ download_album(
+ service=service,
+ url=album_url,
+ main=main,
+ fallback=fallback,
+ quality=quality,
+ fall_quality=fall_quality,
+ real_time=real_time,
+ custom_dir_format=custom_dir_format,
+ custom_track_format=custom_track_format
+ )
+
+ except Exception as album_error:
+ log_json({
+ "status": "error",
+ "type": "album",
+ "album": album.get('name', 'Unknown'),
+ "error": str(album_error)
+ })
+ traceback.print_exc()
+
+ # When everything has been processed, print the final status.
+ log_json({
+ "status": "done",
+ "type": "artist",
+ "artist": artist_name,
+ "album_type": album_type
+ })
\ No newline at end of file
diff --git a/routes/utils/queue.py b/routes/utils/queue.py
index 44fcc28..3ea2873 100644
--- a/routes/utils/queue.py
+++ b/routes/utils/queue.py
@@ -12,8 +12,18 @@ from queue import Queue, Empty
# ------------------------------------------------------------------------------
# Configuration
# ------------------------------------------------------------------------------
-MAX_CONCURRENT_DL = 3 # maximum number of concurrent download processes
-PRG_DIR = './prgs' # directory where .prg files will be stored
+
+# Load configuration from ./config/main.json and get the max_concurrent_dl value.
+CONFIG_PATH = './config/main.json'
+try:
+ with open(CONFIG_PATH, 'r') as f:
+ config_data = json.load(f)
+ MAX_CONCURRENT_DL = config_data.get("maxConcurrentDownloads", 3)
+except Exception as e:
+ # Fallback to a default value if there's an error reading the config.
+ MAX_CONCURRENT_DL = 3
+
+PRG_DIR = './prgs' # directory where .prg files will be stored
# ------------------------------------------------------------------------------
# Utility Functions and Classes
@@ -124,7 +134,8 @@ class DownloadQueueManager:
self.pending_tasks = Queue() # holds tasks waiting to run
self.running_downloads = {} # maps prg_filename -> Process instance
- self.lock = threading.Lock() # protects access to running_downloads
+ self.cancelled_tasks = set() # holds prg_filenames of tasks that have been cancelled
+ self.lock = threading.Lock() # protects access to running_downloads and cancelled_tasks
self.worker_thread = threading.Thread(target=self.queue_worker, daemon=True)
self.running = False
@@ -185,19 +196,20 @@ class DownloadQueueManager:
def cancel_task(self, prg_filename):
"""
- Cancel a running download task by terminating its process.
- If the task is found and alive, it is terminated and a cancellation
- status is appended to its .prg file.
+ Cancel a download task (either queued or running) by marking it as cancelled or terminating its process.
+ If the task is running, its process is terminated.
+ If the task is queued, it is marked as cancelled so that it won't be started.
+ In either case, a cancellation status is appended to its .prg file.
Returns a dictionary indicating the result.
"""
+ prg_path = os.path.join(self.prg_dir, prg_filename)
with self.lock:
process = self.running_downloads.get(prg_filename)
if process and process.is_alive():
process.terminate()
process.join()
del self.running_downloads[prg_filename]
- prg_path = os.path.join(self.prg_dir, prg_filename)
try:
with open(prg_path, 'a') as f:
f.write(json.dumps({"status": "cancel"}) + "\n")
@@ -205,7 +217,14 @@ class DownloadQueueManager:
return {"error": f"Failed to write cancel status: {str(e)}"}
return {"status": "cancelled"}
else:
- return {"error": "Task not found or already terminated"}
+ # Task is not running; mark it as cancelled if it's still pending.
+ self.cancelled_tasks.add(prg_filename)
+ try:
+ with open(prg_path, 'a') as f:
+ f.write(json.dumps({"status": "cancel"}) + "\n")
+ except Exception as e:
+ return {"error": f"Failed to write cancel status: {str(e)}"}
+ return {"status": "cancelled"}
def queue_worker(self):
"""
@@ -217,7 +236,7 @@ class DownloadQueueManager:
# First, clean up any finished processes.
with self.lock:
finished = []
- for prg_filename, process in self.running_downloads.items():
+ for prg_filename, process in list(self.running_downloads.items()):
if not process.is_alive():
finished.append(prg_filename)
for prg_filename in finished:
@@ -231,6 +250,13 @@ class DownloadQueueManager:
time.sleep(0.5)
continue
+ # Check if the task was cancelled while it was still queued.
+ with self.lock:
+ if prg_filename in self.cancelled_tasks:
+ # Task has been cancelled; remove it from the set and skip processing.
+ self.cancelled_tasks.remove(prg_filename)
+ continue
+
prg_path = task.get('prg_path')
# Create and start a new process for the task.
p = Process(
diff --git a/static/css/queue/queue.css b/static/css/queue/queue.css
index 650d8af..2c8d2d9 100644
--- a/static/css/queue/queue.css
+++ b/static/css/queue/queue.css
@@ -13,8 +13,12 @@
padding: 20px;
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1001;
- overflow-y: auto;
+ /* Remove overflow-y here to delegate scrolling to the queue items container */
box-shadow: -20px 0 30px rgba(0, 0, 0, 0.4);
+
+ /* Added for flex layout */
+ display: flex;
+ flex-direction: column;
}
/* When active, the sidebar slides into view */
@@ -65,8 +69,10 @@
/* Container for all queue items */
#queueItems {
- max-height: 60vh;
+ /* Allow the container to fill all available space in the sidebar */
+ flex: 1;
overflow-y: auto;
+ /* Removed max-height: 60vh; */
}
/* Each download queue item */
diff --git a/static/js/album.js b/static/js/album.js
index 8de0208..a1abac1 100644
--- a/static/js/album.js
+++ b/static/js/album.js
@@ -156,7 +156,12 @@ function renderAlbum(album) {
async function downloadWholeAlbum(album) {
const url = album.external_urls.spotify;
- return startDownload(url, 'album', { name: album.name });
+ try {
+ await downloadQueue.startAlbumDownload(url, { name: album.name });
+ } catch (error) {
+ showError('Album download failed: ' + error.message);
+ throw error;
+ }
}
function msToTime(duration) {
diff --git a/static/js/artist.js b/static/js/artist.js
index 1f51e66..cecdf47 100644
--- a/static/js/artist.js
+++ b/static/js/artist.js
@@ -1,8 +1,7 @@
-// Import the downloadQueue singleton from your working queue.js implementation.
+// Import the downloadQueue singleton
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];
@@ -11,13 +10,12 @@ document.addEventListener('DOMContentLoaded', () => {
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, artistId)) // Pass artistId along
+ .then(data => renderArtist(data, artistId))
.catch(error => {
console.error('Error:', error);
showError('Failed to load artist info.');
@@ -25,114 +23,82 @@ document.addEventListener('DOMContentLoaded', () => {
const queueIcon = document.getElementById('queueIcon');
if (queueIcon) {
- queueIcon.addEventListener('click', () => {
- downloadQueue.toggleVisibility();
- });
+ queueIcon.addEventListener('click', () => downloadQueue.toggleVisibility());
}
});
-/**
- * Renders the artist header and groups the albums by type.
- */
function renderArtist(artistData, artistId) {
- // 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';
- // Embed the artist name in a link
document.getElementById('artist-name').innerHTML =
`${artistName}`;
document.getElementById('artist-stats').textContent = `${artistData.total} albums`;
document.getElementById('artist-image').src = artistImage;
- // --- Add Home Button ---
+ // Home Button
let homeButton = document.getElementById('homeButton');
if (!homeButton) {
homeButton = document.createElement('button');
homeButton.id = 'homeButton';
homeButton.className = 'home-btn';
- homeButton.innerHTML = ``;
- const headerContainer = document.getElementById('artist-header');
- headerContainer.insertBefore(homeButton, headerContainer.firstChild);
+ homeButton.innerHTML = `
`;
+ document.getElementById('artist-header').prepend(homeButton);
}
- homeButton.addEventListener('click', () => {
- window.location.href = window.location.origin;
- });
+ homeButton.addEventListener('click', () => window.location.href = window.location.origin);
- // --- Add "Download Whole Artist" Button ---
+ // 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';
- const headerContainer = document.getElementById('artist-header');
- headerContainer.appendChild(downloadArtistBtn);
+ downloadArtistBtn.textContent = 'Download All Albums';
+ document.getElementById('artist-header').appendChild(downloadArtistBtn);
}
- downloadArtistBtn.addEventListener('click', () => {
- // Remove individual album and group download buttons (but leave the whole artist button).
- document.querySelectorAll('.download-btn').forEach(btn => {
- if (btn.id !== 'downloadArtistBtn') {
- btn.remove();
- }
- });
+ downloadArtistBtn.addEventListener('click', () => {
+ document.querySelectorAll('.download-btn:not(#downloadArtistBtn)').forEach(btn => btn.remove());
downloadArtistBtn.disabled = true;
downloadArtistBtn.textContent = 'Queueing...';
- downloadWholeArtist(artistData)
- .then(() => {
- downloadArtistBtn.textContent = 'Queued!';
- })
- .catch(err => {
- showError('Failed to queue artist download: ' + err.message);
- downloadArtistBtn.disabled = false;
- });
+ queueAllAlbums(artistData.items, downloadArtistBtn);
});
- // Group albums by album type.
- const albumGroups = {};
- artistData.items.forEach(album => {
+ // Group albums by type
+ const albumGroups = artistData.items.reduce((groups, album) => {
const type = album.album_type.toLowerCase();
- if (!albumGroups[type]) {
- albumGroups[type] = [];
- }
- albumGroups[type].push(album);
- });
+ if (!groups[type]) groups[type] = [];
+ groups[type].push(album);
+ return groups;
+ }, {});
- // Render groups into the #album-groups container.
+ // Render album groups
const groupsContainer = document.getElementById('album-groups');
- groupsContainer.innerHTML = ''; // Clear previous content
+ groupsContainer.innerHTML = '';
- // 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 with a download-all button.
- const header = document.createElement('div');
- header.className = 'album-group-header';
- header.innerHTML = `
-