now it remembers the queue
This commit is contained in:
@@ -14,16 +14,26 @@ download_processes = {}
|
|||||||
|
|
||||||
def generate_random_filename(length=6):
|
def generate_random_filename(length=6):
|
||||||
chars = string.ascii_lowercase + string.digits
|
chars = string.ascii_lowercase + string.digits
|
||||||
return ''.join(random.choice(chars) for _ in range(length)) + '.prg'
|
return ''.join(random.choice(chars) for _ in range(length)) + '.album.prg'
|
||||||
|
|
||||||
class FlushingFileWrapper:
|
class FlushingFileWrapper:
|
||||||
def __init__(self, file):
|
def __init__(self, file):
|
||||||
self.file = file
|
self.file = file
|
||||||
|
|
||||||
def write(self, text):
|
def write(self, text):
|
||||||
# Filter lines to only write JSON objects
|
# Process each line separately.
|
||||||
for line in text.split('\n'):
|
for line in text.split('\n'):
|
||||||
if line.startswith('{'):
|
line = line.strip()
|
||||||
|
# Only process non-empty lines that look like JSON objects.
|
||||||
|
if line and line.startswith('{'):
|
||||||
|
try:
|
||||||
|
obj = json.loads(line)
|
||||||
|
# Skip writing if the JSON object has a "type" of "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.write(line + '\n')
|
||||||
self.file.flush()
|
self.file.flush()
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ download_processes = {}
|
|||||||
|
|
||||||
def generate_random_filename(length=6):
|
def generate_random_filename(length=6):
|
||||||
chars = string.ascii_lowercase + string.digits
|
chars = string.ascii_lowercase + string.digits
|
||||||
return ''.join(random.choice(chars) for _ in range(length)) + '.prg'
|
return ''.join(random.choice(chars) for _ in range(length)) + '.artist.prg'
|
||||||
|
|
||||||
class FlushingFileWrapper:
|
class FlushingFileWrapper:
|
||||||
def __init__(self, file):
|
def __init__(self, file):
|
||||||
@@ -28,7 +28,16 @@ class FlushingFileWrapper:
|
|||||||
def write(self, text):
|
def write(self, text):
|
||||||
# Only write lines that start with '{'
|
# Only write lines that start with '{'
|
||||||
for line in text.split('\n'):
|
for line in text.split('\n'):
|
||||||
if line.startswith('{'):
|
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.write(line + '\n')
|
||||||
self.file.flush()
|
self.file.flush()
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ playlist_processes = {}
|
|||||||
|
|
||||||
def generate_random_filename(length=6):
|
def generate_random_filename(length=6):
|
||||||
chars = string.ascii_lowercase + string.digits
|
chars = string.ascii_lowercase + string.digits
|
||||||
return ''.join(random.choice(chars) for _ in range(length)) + '.prg'
|
return ''.join(random.choice(chars) for _ in range(length)) + '.playlist.prg'
|
||||||
|
|
||||||
class FlushingFileWrapper:
|
class FlushingFileWrapper:
|
||||||
def __init__(self, file):
|
def __init__(self, file):
|
||||||
@@ -22,7 +22,18 @@ class FlushingFileWrapper:
|
|||||||
|
|
||||||
def write(self, text):
|
def write(self, text):
|
||||||
for line in text.split('\n'):
|
for line in text.split('\n'):
|
||||||
if line.startswith('{'):
|
line = line.strip()
|
||||||
|
# Only process non-empty lines that start with '{'
|
||||||
|
if line and line.startswith('{'):
|
||||||
|
try:
|
||||||
|
# Try to parse the line as JSON
|
||||||
|
obj = json.loads(line)
|
||||||
|
# If the object has a "type" key with the value "track", skip writing it.
|
||||||
|
if obj.get("type") == "track":
|
||||||
|
continue
|
||||||
|
except ValueError:
|
||||||
|
# If the line isn't valid JSON, we don't filter it.
|
||||||
|
pass
|
||||||
self.file.write(line + '\n')
|
self.file.write(line + '\n')
|
||||||
self.file.flush()
|
self.file.flush()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from flask import Blueprint, abort
|
from flask import Blueprint, abort, jsonify
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
prgs_bp = Blueprint('prgs', __name__, url_prefix='/api/prgs')
|
prgs_bp = Blueprint('prgs', __name__, url_prefix='/api/prgs')
|
||||||
|
|
||||||
@@ -9,23 +10,104 @@ PRGS_DIR = os.path.join(os.getcwd(), 'prgs')
|
|||||||
@prgs_bp.route('/<filename>', methods=['GET'])
|
@prgs_bp.route('/<filename>', methods=['GET'])
|
||||||
def get_prg_file(filename):
|
def get_prg_file(filename):
|
||||||
"""
|
"""
|
||||||
Return the last line of the specified file from the prgs directory.
|
Return a JSON object with the resource type, its name (title) and the last line (progress update) of the PRG file.
|
||||||
|
If the file is empty, return default values.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Security check to prevent path traversal attacks
|
# Security check to prevent path traversal attacks.
|
||||||
if '..' in filename or '/' in filename:
|
if '..' in filename or '/' in filename:
|
||||||
abort(400, "Invalid file request")
|
abort(400, "Invalid file request")
|
||||||
|
|
||||||
filepath = os.path.join(PRGS_DIR, filename)
|
filepath = os.path.join(PRGS_DIR, filename)
|
||||||
|
|
||||||
# Read the last line of the file
|
|
||||||
with open(filepath, 'r') as f:
|
with open(filepath, 'r') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
lines = content.splitlines()
|
lines = content.splitlines()
|
||||||
last_line = lines[-1] if lines else ''
|
|
||||||
|
|
||||||
return last_line
|
# If the file is empty, return default values.
|
||||||
|
if not lines:
|
||||||
|
return jsonify({
|
||||||
|
"type": "",
|
||||||
|
"name": "",
|
||||||
|
"last_line": None
|
||||||
|
})
|
||||||
|
|
||||||
|
# Process the initialization line (first line) to extract type and name.
|
||||||
|
try:
|
||||||
|
init_data = json.loads(lines[0])
|
||||||
|
except Exception as e:
|
||||||
|
# If parsing fails, use defaults.
|
||||||
|
init_data = {}
|
||||||
|
|
||||||
|
resource_type = init_data.get("type", "")
|
||||||
|
# Determine the name based on type.
|
||||||
|
if resource_type == "track":
|
||||||
|
resource_name = init_data.get("song", "")
|
||||||
|
elif resource_type == "album":
|
||||||
|
resource_name = init_data.get("album", "")
|
||||||
|
elif resource_type == "playlist":
|
||||||
|
resource_name = init_data.get("name", "")
|
||||||
|
elif resource_type == "artist":
|
||||||
|
resource_name = init_data.get("artist", "")
|
||||||
|
else:
|
||||||
|
resource_name = ""
|
||||||
|
|
||||||
|
# Get the last line from the file.
|
||||||
|
last_line_raw = lines[-1]
|
||||||
|
# Try to parse the last line as JSON.
|
||||||
|
try:
|
||||||
|
last_line_parsed = json.loads(last_line_raw)
|
||||||
|
except Exception:
|
||||||
|
last_line_parsed = last_line_raw # Fallback to returning raw string if JSON parsing fails.
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"type": resource_type,
|
||||||
|
"name": resource_name,
|
||||||
|
"last_line": last_line_parsed
|
||||||
|
})
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
abort(404, "File not found")
|
abort(404, "File not found")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
abort(500, f"An error occurred: {e}")
|
abort(500, f"An error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@prgs_bp.route('/delete/<filename>', methods=['DELETE'])
|
||||||
|
def delete_prg_file(filename):
|
||||||
|
"""
|
||||||
|
Delete the specified .prg file from the prgs directory.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Security checks to prevent path traversal and ensure correct file type.
|
||||||
|
if '..' in filename or '/' in filename:
|
||||||
|
abort(400, "Invalid file request")
|
||||||
|
if not filename.endswith('.prg'):
|
||||||
|
abort(400, "Only .prg files can be deleted")
|
||||||
|
|
||||||
|
filepath = os.path.join(PRGS_DIR, filename)
|
||||||
|
|
||||||
|
if not os.path.isfile(filepath):
|
||||||
|
abort(404, "File not found")
|
||||||
|
|
||||||
|
os.remove(filepath)
|
||||||
|
return {'message': f'File {filename} deleted successfully'}, 200
|
||||||
|
except FileNotFoundError:
|
||||||
|
abort(404, "File not found")
|
||||||
|
except Exception as e:
|
||||||
|
abort(500, f"An error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@prgs_bp.route('/list', methods=['GET'])
|
||||||
|
def list_prg_files():
|
||||||
|
"""
|
||||||
|
Retrieve a list of all .prg files in the prgs directory.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
prg_files = []
|
||||||
|
if os.path.isdir(PRGS_DIR):
|
||||||
|
with os.scandir(PRGS_DIR) as entries:
|
||||||
|
for entry in entries:
|
||||||
|
if entry.is_file() and entry.name.endswith('.prg'):
|
||||||
|
prg_files.append(entry.name)
|
||||||
|
return jsonify(prg_files)
|
||||||
|
except Exception as e:
|
||||||
|
abort(500, f"An error occurred: {e}")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ track_processes = {}
|
|||||||
|
|
||||||
def generate_random_filename(length=6):
|
def generate_random_filename(length=6):
|
||||||
chars = string.ascii_lowercase + string.digits
|
chars = string.ascii_lowercase + string.digits
|
||||||
return ''.join(random.choice(chars) for _ in range(length)) + '.prg'
|
return ''.join(random.choice(chars) for _ in range(length)) + '.track.prg'
|
||||||
|
|
||||||
class FlushingFileWrapper:
|
class FlushingFileWrapper:
|
||||||
def __init__(self, file):
|
def __init__(self, file):
|
||||||
|
|||||||
@@ -49,10 +49,14 @@ def download_artist_albums(service, artist_url, main, fallback=None, quality=Non
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_json({"status": "error", "message": f"Error retrieving artist discography: {e}"})
|
log_json({"status": "error", "message": f"Error retrieving artist discography: {e}"})
|
||||||
raise
|
raise
|
||||||
|
|
||||||
albums = discography.get('items', [])
|
albums = discography.get('items', [])
|
||||||
# Attempt to extract the artist name from the discography; fallback to artist_url if not available.
|
# Extract artist name from the first album's artists
|
||||||
artist_name = discography.get("name", artist_url)
|
artist_name = artist_url # default fallback
|
||||||
|
if albums:
|
||||||
|
first_album = albums[0]
|
||||||
|
artists = first_album.get('artists', [])
|
||||||
|
if artists:
|
||||||
|
artist_name = artists[0].get('name', artist_url)
|
||||||
|
|
||||||
if not albums:
|
if not albums:
|
||||||
log_json({
|
log_json({
|
||||||
@@ -64,7 +68,7 @@ def download_artist_albums(service, artist_url, main, fallback=None, quality=Non
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
||||||
log_json({"status": "initializing", "type": "artist", "total_albums": len(albums)})
|
log_json({"status": "initializing", "type": "artist", "artist": artist_name, "total_albums": len(albums)})
|
||||||
|
|
||||||
for album in albums:
|
for album in albums:
|
||||||
try:
|
try:
|
||||||
@@ -109,4 +113,4 @@ def download_artist_albums(service, artist_url, main, fallback=None, quality=Non
|
|||||||
"type": "artist",
|
"type": "artist",
|
||||||
"artist": artist_name,
|
"artist": artist_name,
|
||||||
"album_type": album_type
|
"album_type": album_type
|
||||||
})
|
})
|
||||||
210
static/js/app.js
210
static/js/app.js
@@ -98,7 +98,6 @@ async function initConfig() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add quality select listeners with null checks
|
|
||||||
const spotifyQuality = document.getElementById('spotifyQualitySelect');
|
const spotifyQuality = document.getElementById('spotifyQualitySelect');
|
||||||
if (spotifyQuality) {
|
if (spotifyQuality) {
|
||||||
spotifyQuality.addEventListener('change', saveConfig);
|
spotifyQuality.addEventListener('change', saveConfig);
|
||||||
@@ -108,8 +107,10 @@ async function initConfig() {
|
|||||||
if (deezerQuality) {
|
if (deezerQuality) {
|
||||||
deezerQuality.addEventListener('change', saveConfig);
|
deezerQuality.addEventListener('change', saveConfig);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Load existing PRG files after initial setup
|
||||||
|
await loadExistingPrgFiles();
|
||||||
|
}
|
||||||
|
|
||||||
async function updateAccountSelectors() {
|
async function updateAccountSelectors() {
|
||||||
try {
|
try {
|
||||||
@@ -496,43 +497,35 @@ async function startEntryMonitoring(queueId) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/prgs/${entry.prgFile}`);
|
const response = await fetch(`/api/prgs/${entry.prgFile}`);
|
||||||
const lastLine = (await response.text()).trim();
|
const data = await response.json();
|
||||||
|
// data contains: { type, name, last_line }
|
||||||
|
const progress = data.last_line;
|
||||||
|
|
||||||
// Handle empty response
|
if (entry.type !== 'track' && progress?.type === 'track') {
|
||||||
if (!lastLine) {
|
return; // Skip track-type messages for non-track downloads
|
||||||
|
}
|
||||||
|
// If there is no progress data, handle as inactivity.
|
||||||
|
if (!progress) {
|
||||||
handleInactivity(entry, queueId, logElement);
|
handleInactivity(entry, queueId, logElement);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Check for unchanged status to handle inactivity.
|
||||||
const data = JSON.parse(lastLine);
|
if (JSON.stringify(entry.lastStatus) === JSON.stringify(progress)) {
|
||||||
|
handleInactivity(entry, queueId, logElement);
|
||||||
// Check for status changes
|
return;
|
||||||
if (JSON.stringify(entry.lastStatus) === JSON.stringify(data)) {
|
|
||||||
handleInactivity(entry, queueId, logElement);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update entry state
|
|
||||||
entry.lastStatus = data;
|
|
||||||
entry.lastUpdated = Date.now();
|
|
||||||
entry.status = data.status;
|
|
||||||
logElement.textContent = getStatusMessage(data);
|
|
||||||
|
|
||||||
// Handle terminal states
|
|
||||||
if (data.status === 'error' || data.status === 'complete') {
|
|
||||||
handleTerminalState(entry, queueId, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Invalid PRG line:', lastLine);
|
|
||||||
logElement.textContent = 'Error parsing status update';
|
|
||||||
handleTerminalState(entry, queueId, {
|
|
||||||
status: 'error',
|
|
||||||
message: 'Invalid status format'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update entry state and log.
|
||||||
|
entry.lastStatus = progress;
|
||||||
|
entry.lastUpdated = Date.now();
|
||||||
|
entry.status = progress.status;
|
||||||
|
logElement.textContent = getStatusMessage(progress);
|
||||||
|
|
||||||
|
// Handle terminal states.
|
||||||
|
if (progress.status === 'error' || progress.status === 'complete') {
|
||||||
|
handleTerminalState(entry, queueId, progress);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Status check failed:', error);
|
console.error('Status check failed:', error);
|
||||||
handleTerminalState(entry, queueId, {
|
handleTerminalState(entry, queueId, {
|
||||||
@@ -543,6 +536,8 @@ async function startEntryMonitoring(queueId) {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function handleInactivity(entry, queueId, logElement) {
|
function handleInactivity(entry, queueId, logElement) {
|
||||||
// Check if real time downloading is enabled
|
// Check if real time downloading is enabled
|
||||||
const realTimeEnabled = document.getElementById('realTimeToggle')?.checked;
|
const realTimeEnabled = document.getElementById('realTimeToggle')?.checked;
|
||||||
@@ -595,10 +590,43 @@ function cleanupEntry(queueId) {
|
|||||||
if (entry) {
|
if (entry) {
|
||||||
clearInterval(entry.intervalId);
|
clearInterval(entry.intervalId);
|
||||||
entry.element.remove();
|
entry.element.remove();
|
||||||
|
const prgFile = entry.prgFile;
|
||||||
delete downloadQueue[queueId];
|
delete downloadQueue[queueId];
|
||||||
|
// Send delete request for the PRG file
|
||||||
|
fetch(`/api/prgs/delete/${encodeURIComponent(prgFile)}`, { method: 'DELETE' })
|
||||||
|
.catch(err => console.error('Error deleting PRG file:', err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadExistingPrgFiles() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/prgs/list');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch PRG files');
|
||||||
|
const prgFiles = await response.json();
|
||||||
|
for (const prgFile of prgFiles) {
|
||||||
|
try {
|
||||||
|
const prgResponse = await fetch(`/api/prgs/${prgFile}`);
|
||||||
|
const prgData = await prgResponse.json();
|
||||||
|
// If name is empty, fallback to using the prgFile as title.
|
||||||
|
const title = prgData.name || prgFile;
|
||||||
|
const type = prgData.type || "unknown";
|
||||||
|
const dummyItem = {
|
||||||
|
name: title,
|
||||||
|
external_urls: {} // You can expand this if needed.
|
||||||
|
};
|
||||||
|
addToQueue(dummyItem, type, prgFile);
|
||||||
|
} catch (innerError) {
|
||||||
|
console.error('Error processing PRG file', prgFile, ':', innerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading existing PRG files:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function createQueueItem(item, type, prgFile, queueId) {
|
function createQueueItem(item, type, prgFile, queueId) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'queue-item';
|
div.className = 'queue-item';
|
||||||
@@ -827,48 +855,84 @@ function showSidebarError(message) {
|
|||||||
|
|
||||||
function getStatusMessage(data) {
|
function getStatusMessage(data) {
|
||||||
switch (data.status) {
|
switch (data.status) {
|
||||||
case 'downloading':
|
case 'downloading':
|
||||||
return `Downloading ${data.song || 'track'} by ${data.artist || 'artist'}...`;
|
// For track downloads only.
|
||||||
case 'progress':
|
if (data.type === 'track') {
|
||||||
if (data.type === 'album') {
|
return `Downloading track "${data.song}" by ${data.artist}...`;
|
||||||
return `Processing track ${data.current_track}/${data.total_tracks} (${data.percentage.toFixed(1)}%): ${data.song} from ${data.album}`;
|
|
||||||
} else {
|
|
||||||
return `${data.percentage.toFixed(1)}% complete`;
|
|
||||||
}
|
|
||||||
case 'done':
|
|
||||||
if (data.type === 'track') {
|
|
||||||
return `Finished: ${data.song} by ${data.artist}`;
|
|
||||||
} else if (data.type === 'album'){
|
|
||||||
return `Finished: ${data.album} by ${data.artist}`;
|
|
||||||
}
|
|
||||||
else if (data.type === 'artist'){
|
|
||||||
return `Finished: Artist's ${data.album_type}s`;
|
|
||||||
}
|
|
||||||
case 'initializing':
|
|
||||||
if (data.type === 'artist') {
|
|
||||||
return `Initializing artist download, ${data.total_albums} albums to process...`;
|
|
||||||
}
|
|
||||||
return `Initializing ${data.type} download for ${data.album || data.artist}...`;
|
|
||||||
case 'retrying':
|
|
||||||
return `Track ${data.song} by ${data.artist} failed, retrying (${data.retries}/${data.max_retries}) in ${data.seconds_left}s`;
|
|
||||||
case 'error':
|
|
||||||
return `Error: ${data.message || 'Unknown error'}`;
|
|
||||||
case 'complete':
|
|
||||||
return 'Download completed successfully';
|
|
||||||
case 'skipped':
|
|
||||||
return `Track ${data.song} skipped, it already exists!`;
|
|
||||||
case 'real_time': {
|
|
||||||
// Convert milliseconds to minutes and seconds.
|
|
||||||
const totalMs = data.time_elapsed;
|
|
||||||
const minutes = Math.floor(totalMs / 60000);
|
|
||||||
const seconds = Math.floor((totalMs % 60000) / 1000);
|
|
||||||
const paddedSeconds = seconds < 10 ? '0' + seconds : seconds;
|
|
||||||
return `Real-time downloading track ${data.song} by ${data.artist} (${(data.percentage * 100).toFixed(1)}%). Time elapsed: ${minutes}:${paddedSeconds}`;
|
|
||||||
}
|
}
|
||||||
default:
|
return `Downloading ${data.type}...`;
|
||||||
return data.status;
|
|
||||||
|
case 'initializing':
|
||||||
|
if (data.type === 'playlist') {
|
||||||
|
return `Initializing playlist download "${data.name}" with ${data.total_tracks} tracks...`;
|
||||||
|
} else if (data.type === 'album') {
|
||||||
|
return `Initializing album download "${data.album}" by ${data.artist}...`;
|
||||||
|
} else if (data.type === 'artist') {
|
||||||
|
return `Initializing artist download for ${data.artist} with ${data.total_albums} album(s) [${data.album_type}]...`;
|
||||||
|
}
|
||||||
|
return `Initializing ${data.type} download...`;
|
||||||
|
|
||||||
|
case 'progress':
|
||||||
|
// Expect progress messages for playlists, albums (or artist’s albums) to include a "track" and "current_track".
|
||||||
|
if (data.track && data.current_track) {
|
||||||
|
// current_track is a string in the format "current/total"
|
||||||
|
const parts = data.current_track.split('/');
|
||||||
|
const current = parts[0];
|
||||||
|
const total = parts[1] || '?';
|
||||||
|
|
||||||
|
if (data.type === 'playlist') {
|
||||||
|
return `Downloading playlist: Track ${current} of ${total} - ${data.track}`;
|
||||||
|
} else if (data.type === 'album') {
|
||||||
|
// For album progress, the "album" and "artist" fields may be available on a done message.
|
||||||
|
// In some cases (like artist downloads) only track info is passed.
|
||||||
|
if (data.album && data.artist) {
|
||||||
|
return `Downloading album "${data.album}" by ${data.artist}: track ${current} of ${total} - ${data.track}`;
|
||||||
|
} else {
|
||||||
|
return `Downloading track ${current} of ${total}: ${data.track} from ${data.album}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback if fields are missing:
|
||||||
|
return `Progress: ${data.status}...`;
|
||||||
|
|
||||||
|
case 'done':
|
||||||
|
if (data.type === 'track') {
|
||||||
|
return `Finished track "${data.song}" by ${data.artist}`;
|
||||||
|
} else if (data.type === 'playlist') {
|
||||||
|
return `Finished playlist "${data.name}" with ${data.total_tracks} tracks`;
|
||||||
|
} else if (data.type === 'album') {
|
||||||
|
return `Finished album "${data.album}" by ${data.artist}`;
|
||||||
|
} else if (data.type === 'artist') {
|
||||||
|
return `Finished artist "${data.artist}" (${data.album_type})`;
|
||||||
|
}
|
||||||
|
return `Finished ${data.type}`;
|
||||||
|
|
||||||
|
case 'retrying':
|
||||||
|
return `Track "${data.song}" by ${data.artist}" failed, retrying (${data.retry_count}/10) in ${data.seconds_left}s`;
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
return `Error: ${data.message || 'Unknown error'}`;
|
||||||
|
|
||||||
|
case 'complete':
|
||||||
|
return 'Download completed successfully';
|
||||||
|
|
||||||
|
case 'skipped':
|
||||||
|
return `Track "${data.song}" skipped, it already exists!`;
|
||||||
|
|
||||||
|
case 'real_time': {
|
||||||
|
// Convert milliseconds to minutes and seconds.
|
||||||
|
const totalMs = data.time_elapsed;
|
||||||
|
const minutes = Math.floor(totalMs / 60000);
|
||||||
|
const seconds = Math.floor((totalMs % 60000) / 1000);
|
||||||
|
const paddedSeconds = seconds < 10 ? '0' + seconds : seconds;
|
||||||
|
return `Real-time downloading track "${data.song}" by ${data.artist} (${(data.percentage * 100).toFixed(1)}%). Time elapsed: ${minutes}:${paddedSeconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return data.status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function saveConfig() {
|
function saveConfig() {
|
||||||
const config = {
|
const config = {
|
||||||
|
|||||||
Reference in New Issue
Block a user