13
.env
Normal file
13
.env
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Docker Compose environment variables
|
||||||
|
|
||||||
|
# Redis connection (external or internal)
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_DB=0
|
||||||
|
REDIS_PASSWORD=CHANGE_ME
|
||||||
|
|
||||||
|
EXPLICIT_FILTER=false # Set to true to filter out explicit content
|
||||||
|
|
||||||
|
PUID=1000 # User ID for the container
|
||||||
|
PGID=1000 # Group ID for the container
|
||||||
|
UMASK=0022 # Optional: Sets the default file permissions for newly created files within the container.
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ queue_state.json
|
|||||||
search_demo.py
|
search_demo.py
|
||||||
celery_worker.log
|
celery_worker.log
|
||||||
logs/spotizerr.log
|
logs/spotizerr.log
|
||||||
|
/.venv
|
||||||
25
README.md
25
README.md
@@ -49,9 +49,13 @@ Music downloader which combines the best of two worlds: Spotify's catalog and De
|
|||||||
mkdir spotizerr && cd spotizerr
|
mkdir spotizerr && cd spotizerr
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Copy `docker-compose.yml` from this repo
|
2. Copy the `.env` file from this repo and update all variables (e.g. Redis credentials, PUID/PGID, UMASK).
|
||||||
|
3. Copy `docker-compose.yml` from this repo.
|
||||||
3. Launch container:
|
4. Create required directories:
|
||||||
|
```bash
|
||||||
|
mkdir -p creds config downloads logs cache
|
||||||
|
```
|
||||||
|
5. Launch containers:
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
@@ -234,6 +238,21 @@ Copy that value and paste it into the correspondant setting in Spotizerr
|
|||||||
- Track number padding (01. Track or 1. Track)
|
- Track number padding (01. Track or 1. Track)
|
||||||
- Adjust retry parameters (max attempts, delay, delay increase)
|
- Adjust retry parameters (max attempts, delay, delay increase)
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Define your variables in the `.env` file in the project root:
|
||||||
|
```dotenv
|
||||||
|
REDIS_HOST=redis # Redis host name
|
||||||
|
REDIS_PORT=6379 # Redis port number
|
||||||
|
REDIS_DB=0 # Redis DB index
|
||||||
|
REDIS_PASSWORD=CHANGE_ME # Redis AUTH password
|
||||||
|
EXPLICIT_FILTER=false # Filter explicit content
|
||||||
|
PUID=1000 # Container user ID
|
||||||
|
PGID=1000 # Container group ID
|
||||||
|
UMASK=0022 # Default file permission mask
|
||||||
|
SPOTIPY_CACHE_PATH=/app/cache/.cache # Spotify token cache path
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**Common Issues**:
|
**Common Issues**:
|
||||||
|
|||||||
16
app.py
16
app.py
@@ -17,6 +17,7 @@ import atexit
|
|||||||
import sys
|
import sys
|
||||||
import redis
|
import redis
|
||||||
import socket
|
import socket
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
# Import Celery configuration and manager
|
# Import Celery configuration and manager
|
||||||
from routes.utils.celery_tasks import celery_app
|
from routes.utils.celery_tasks import celery_app
|
||||||
@@ -85,11 +86,16 @@ def check_redis_connection():
|
|||||||
redis_port = 6379 # default
|
redis_port = 6379 # default
|
||||||
|
|
||||||
# Parse from REDIS_URL if possible
|
# Parse from REDIS_URL if possible
|
||||||
if REDIS_URL and "://" in REDIS_URL:
|
if REDIS_URL:
|
||||||
parts = REDIS_URL.split("://")[1].split(":")
|
# parse hostname and port (handles optional auth)
|
||||||
if len(parts) >= 2:
|
try:
|
||||||
redis_host = parts[0]
|
parsed = urlparse(REDIS_URL)
|
||||||
redis_port = int(parts[1].split("/")[0])
|
if parsed.hostname:
|
||||||
|
redis_host = parsed.hostname
|
||||||
|
if parsed.port:
|
||||||
|
redis_port = parsed.port
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Log Redis connection details
|
# Log Redis connection details
|
||||||
logging.info(f"Checking Redis connection to {redis_host}:{redis_port}")
|
logging.info(f"Checking Redis connection to {redis_host}:{redis_port}")
|
||||||
|
|||||||
@@ -9,19 +9,20 @@ services:
|
|||||||
- ./logs:/app/logs # <-- Volume for persistent logs
|
- ./logs:/app/logs # <-- Volume for persistent logs
|
||||||
ports:
|
ports:
|
||||||
- 7171:7171
|
- 7171:7171
|
||||||
image: cooldockerizer93/spotizerr
|
image: cooldockerizer93/spotizerr:dev
|
||||||
container_name: spotizerr-app
|
container_name: spotizerr-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- PUID=1000 # Replace with your desired user ID | Remove both if you want to run as root (not recommended, might result in unreadable files)
|
- PUID=${PUID} # Replace with your desired user ID | Remove both if you want to run as root (not recommended, might result in unreadable files)
|
||||||
- PGID=1000 # Replace with your desired group ID | The user must have write permissions in the volume mapped to /app/downloads
|
- PGID=${PGID} # Replace with your desired group ID | The user must have write permissions in the volume mapped to /app/downloads
|
||||||
- UMASK=0022 # Optional: Sets the default file permissions for newly created files within the container.
|
- UMASK=${UMASK} # Optional: Sets the default file permissions for newly created files within the container.
|
||||||
- REDIS_HOST=redis
|
- REDIS_HOST=${REDIS_HOST}
|
||||||
- REDIS_PORT=6379
|
- REDIS_PORT=${REDIS_PORT}
|
||||||
- REDIS_DB=0
|
- REDIS_DB=${REDIS_DB}
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_PASSWORD=${REDIS_PASSWORD} # Optional, Redis AUTH password. Leave empty if not using authentication
|
||||||
- REDIS_BACKEND=redis://redis:6379/0
|
- REDIS_URL=redis://:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB}
|
||||||
- EXPLICIT_FILTER=false # Set to true to filter out explicit content
|
- REDIS_BACKEND=redis://:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB}
|
||||||
|
- EXPLICIT_FILTER=${EXPLICIT_FILTER} # Set to true to filter out explicit content
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
|
|
||||||
@@ -29,9 +30,11 @@ services:
|
|||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
container_name: spotizerr-redis
|
container_name: spotizerr-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- redis-data:/data
|
- redis-data:/data
|
||||||
command: redis-server --appendonly yes
|
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
redis-data:
|
redis-data:
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ else
|
|||||||
|
|
||||||
# Ensure proper permissions for all app directories
|
# Ensure proper permissions for all app directories
|
||||||
echo "Setting permissions for /app directories..."
|
echo "Setting permissions for /app directories..."
|
||||||
chown -R "${USER_NAME}:${GROUP_NAME}" /app/downloads /app/config /app/creds /app/logs || true
|
chown -R "${USER_NAME}:${GROUP_NAME}" /app/downloads /app/config /app/creds /app/logs /app/cache || true
|
||||||
|
# Ensure Spotipy cache file exists and is writable
|
||||||
|
touch /app/.cache || true
|
||||||
|
chown "${USER_NAME}:${GROUP_NAME}" /app/.cache || true
|
||||||
|
|
||||||
# Run as specified user
|
# Run as specified user
|
||||||
echo "Starting application as ${USER_NAME}..."
|
echo "Starting application as ${USER_NAME}..."
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ def handle_download():
|
|||||||
|
|
||||||
# Add the task to the queue with only essential parameters
|
# Add the task to the queue with only essential parameters
|
||||||
# The queue manager will now handle all config parameters
|
# The queue manager will now handle all config parameters
|
||||||
|
# Include full original request URL in metadata
|
||||||
|
orig_params = request.args.to_dict()
|
||||||
|
orig_params["original_url"] = request.url
|
||||||
task_id = download_queue_manager.add_task({
|
task_id = download_queue_manager.add_task({
|
||||||
"download_type": "album",
|
"download_type": "album",
|
||||||
"url": url,
|
"url": url,
|
||||||
"name": name,
|
"name": name,
|
||||||
"artist": artist,
|
"artist": artist,
|
||||||
"orig_request": request.args.to_dict()
|
"orig_request": orig_params
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ def handle_download():
|
|||||||
|
|
||||||
# Add the task to the queue with only essential parameters
|
# Add the task to the queue with only essential parameters
|
||||||
# The queue manager will now handle all config parameters
|
# The queue manager will now handle all config parameters
|
||||||
|
# Include full original request URL in metadata
|
||||||
|
orig_params = request.args.to_dict()
|
||||||
|
orig_params["original_url"] = request.url
|
||||||
task_id = download_queue_manager.add_task({
|
task_id = download_queue_manager.add_task({
|
||||||
"download_type": "playlist",
|
"download_type": "playlist",
|
||||||
"url": url,
|
"url": url,
|
||||||
"name": name,
|
"name": name,
|
||||||
"artist": artist,
|
"artist": artist,
|
||||||
"orig_request": request.args.to_dict()
|
"orig_request": orig_params
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|||||||
316
routes/prgs.py
316
routes/prgs.py
@@ -20,8 +20,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
prgs_bp = Blueprint('prgs', __name__, url_prefix='/api/prgs')
|
prgs_bp = Blueprint('prgs', __name__, url_prefix='/api/prgs')
|
||||||
|
|
||||||
# The old path for PRG files (keeping for backward compatibility during transition)
|
# (Old .prg file system removed. Using new task system only.)
|
||||||
PRGS_DIR = os.path.join(os.getcwd(), 'prgs')
|
|
||||||
|
|
||||||
@prgs_bp.route('/<task_id>', methods=['GET'])
|
@prgs_bp.route('/<task_id>', methods=['GET'])
|
||||||
def get_prg_file(task_id):
|
def get_prg_file(task_id):
|
||||||
@@ -35,236 +34,21 @@ def get_prg_file(task_id):
|
|||||||
Args:
|
Args:
|
||||||
task_id: Either a task UUID from Celery or a PRG filename from the old system
|
task_id: Either a task UUID from Celery or a PRG filename from the old system
|
||||||
"""
|
"""
|
||||||
try:
|
# Only support new task IDs
|
||||||
# First check if this is a task ID in the new system
|
task_info = get_task_info(task_id)
|
||||||
task_info = get_task_info(task_id)
|
if not task_info:
|
||||||
|
abort(404, "Task not found")
|
||||||
if task_info:
|
original_request = task_info.get("original_request", {})
|
||||||
# This is a task ID in the new system
|
last_status = get_last_task_status(task_id)
|
||||||
original_request = task_info.get("original_request", {})
|
status_count = len(get_task_status(task_id))
|
||||||
|
response = {
|
||||||
# Get the latest status update for this task
|
"original_url": original_request.get("original_url", ""),
|
||||||
last_status = get_last_task_status(task_id)
|
"last_line": last_status,
|
||||||
logger.debug(f"API: Got last_status for {task_id}: {json.dumps(last_status) if last_status else None}")
|
"timestamp": time.time(),
|
||||||
|
"task_id": task_id,
|
||||||
# Get all status updates for debugging
|
"status_count": status_count
|
||||||
all_statuses = get_task_status(task_id)
|
}
|
||||||
status_count = len(all_statuses)
|
return jsonify(response)
|
||||||
logger.debug(f"API: Task {task_id} has {status_count} status updates")
|
|
||||||
|
|
||||||
# Prepare the response with basic info
|
|
||||||
response = {
|
|
||||||
"type": task_info.get("type", ""),
|
|
||||||
"name": task_info.get("name", ""),
|
|
||||||
"artist": task_info.get("artist", ""),
|
|
||||||
"last_line": last_status,
|
|
||||||
"original_request": original_request,
|
|
||||||
"display_title": original_request.get("display_title", task_info.get("name", "")),
|
|
||||||
"display_type": original_request.get("display_type", task_info.get("type", "")),
|
|
||||||
"display_artist": original_request.get("display_artist", task_info.get("artist", "")),
|
|
||||||
"status_count": status_count,
|
|
||||||
"task_id": task_id,
|
|
||||||
"timestamp": time.time()
|
|
||||||
}
|
|
||||||
|
|
||||||
# Handle different status types
|
|
||||||
if last_status:
|
|
||||||
status_type = last_status.get("status", "unknown")
|
|
||||||
|
|
||||||
# Set event type based on status (like in the previous SSE implementation)
|
|
||||||
event_type = "update"
|
|
||||||
if status_type in [ProgressState.COMPLETE, ProgressState.DONE]:
|
|
||||||
event_type = "complete"
|
|
||||||
elif status_type == ProgressState.TRACK_COMPLETE:
|
|
||||||
event_type = "track_complete"
|
|
||||||
elif status_type == ProgressState.ERROR:
|
|
||||||
event_type = "error"
|
|
||||||
elif status_type in [ProgressState.TRACK_PROGRESS, ProgressState.REAL_TIME]:
|
|
||||||
event_type = "progress"
|
|
||||||
|
|
||||||
response["event"] = event_type
|
|
||||||
|
|
||||||
# For terminal statuses (complete, error, cancelled)
|
|
||||||
if status_type in [ProgressState.COMPLETE, ProgressState.ERROR, ProgressState.CANCELLED]:
|
|
||||||
response["progress_message"] = last_status.get("message", f"Download {status_type}")
|
|
||||||
|
|
||||||
# For progress status with track information
|
|
||||||
elif status_type == "progress" and last_status.get("track"):
|
|
||||||
# Add explicit track progress fields to the top level for easy access
|
|
||||||
response["current_track"] = last_status.get("track", "")
|
|
||||||
response["track_number"] = last_status.get("parsed_current_track", 0)
|
|
||||||
response["total_tracks"] = last_status.get("parsed_total_tracks", 0)
|
|
||||||
response["progress_percent"] = last_status.get("overall_progress", 0)
|
|
||||||
response["album"] = last_status.get("album", "")
|
|
||||||
|
|
||||||
# Format a nice progress message for display
|
|
||||||
track_info = last_status.get("track", "")
|
|
||||||
current = last_status.get("parsed_current_track", 0)
|
|
||||||
total = last_status.get("parsed_total_tracks", 0)
|
|
||||||
progress = last_status.get("overall_progress", 0)
|
|
||||||
|
|
||||||
if current and total:
|
|
||||||
response["progress_message"] = f"Downloading track {current}/{total} ({progress}%): {track_info}"
|
|
||||||
elif track_info:
|
|
||||||
response["progress_message"] = f"Downloading: {track_info}"
|
|
||||||
|
|
||||||
# For real-time status messages
|
|
||||||
elif status_type == "real_time":
|
|
||||||
# Add real-time specific fields
|
|
||||||
response["current_song"] = last_status.get("song", "")
|
|
||||||
response["percent"] = last_status.get("percent", 0)
|
|
||||||
response["percentage"] = last_status.get("percentage", 0)
|
|
||||||
response["time_elapsed"] = last_status.get("time_elapsed", 0)
|
|
||||||
|
|
||||||
# Format a nice progress message for display
|
|
||||||
song = last_status.get("song", "")
|
|
||||||
percent = last_status.get("percent", 0)
|
|
||||||
if song:
|
|
||||||
response["progress_message"] = f"Downloading {song} ({percent}%)"
|
|
||||||
else:
|
|
||||||
response["progress_message"] = f"Downloading ({percent}%)"
|
|
||||||
|
|
||||||
# For initializing status
|
|
||||||
elif status_type == "initializing":
|
|
||||||
album = last_status.get("album", "")
|
|
||||||
if album:
|
|
||||||
response["progress_message"] = f"Initializing download for {album}"
|
|
||||||
else:
|
|
||||||
response["progress_message"] = "Initializing download..."
|
|
||||||
|
|
||||||
# For processing status (default)
|
|
||||||
elif status_type == "processing":
|
|
||||||
# Search for the most recent track progress in all statuses
|
|
||||||
has_progress = False
|
|
||||||
for status in reversed(all_statuses):
|
|
||||||
if status.get("status") == "progress" and status.get("track"):
|
|
||||||
# Use this track progress information
|
|
||||||
track_info = status.get("track", "")
|
|
||||||
current_raw = status.get("current_track", "")
|
|
||||||
response["current_track"] = track_info
|
|
||||||
|
|
||||||
# Try to parse track numbers if available
|
|
||||||
if isinstance(current_raw, str) and "/" in current_raw:
|
|
||||||
try:
|
|
||||||
parts = current_raw.split("/")
|
|
||||||
current = int(parts[0])
|
|
||||||
total = int(parts[1])
|
|
||||||
response["track_number"] = current
|
|
||||||
response["total_tracks"] = total
|
|
||||||
response["progress_percent"] = min(int((current / total) * 100), 100)
|
|
||||||
response["progress_message"] = f"Processing track {current}/{total}: {track_info}"
|
|
||||||
except (ValueError, IndexError):
|
|
||||||
response["progress_message"] = f"Processing: {track_info}"
|
|
||||||
else:
|
|
||||||
response["progress_message"] = f"Processing: {track_info}"
|
|
||||||
|
|
||||||
has_progress = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not has_progress:
|
|
||||||
# Just use the processing message
|
|
||||||
response["progress_message"] = last_status.get("message", "Processing download...")
|
|
||||||
|
|
||||||
# For other status types
|
|
||||||
else:
|
|
||||||
response["progress_message"] = last_status.get("message", f"Status: {status_type}")
|
|
||||||
|
|
||||||
return jsonify(response)
|
|
||||||
|
|
||||||
# If not found in new system, try the old PRG file system
|
|
||||||
# Security check to prevent path traversal attacks.
|
|
||||||
if '..' in task_id or '/' in task_id:
|
|
||||||
abort(400, "Invalid file request")
|
|
||||||
|
|
||||||
filepath = os.path.join(PRGS_DIR, task_id)
|
|
||||||
|
|
||||||
with open(filepath, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
lines = content.splitlines()
|
|
||||||
|
|
||||||
# If the file is empty, return default values.
|
|
||||||
if not lines:
|
|
||||||
return jsonify({
|
|
||||||
"type": "",
|
|
||||||
"name": "",
|
|
||||||
"artist": "",
|
|
||||||
"last_line": None,
|
|
||||||
"original_request": None,
|
|
||||||
"display_title": "",
|
|
||||||
"display_type": "",
|
|
||||||
"display_artist": "",
|
|
||||||
"task_id": task_id,
|
|
||||||
"event": "unknown"
|
|
||||||
})
|
|
||||||
|
|
||||||
# Attempt to extract the original request from the first line.
|
|
||||||
original_request = None
|
|
||||||
display_title = ""
|
|
||||||
display_type = ""
|
|
||||||
display_artist = ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
first_line = json.loads(lines[0])
|
|
||||||
if isinstance(first_line, dict):
|
|
||||||
if "original_request" in first_line:
|
|
||||||
original_request = first_line["original_request"]
|
|
||||||
else:
|
|
||||||
# The first line might be the original request itself
|
|
||||||
original_request = first_line
|
|
||||||
|
|
||||||
# Extract display information from the original request
|
|
||||||
if original_request:
|
|
||||||
display_title = original_request.get("display_title", original_request.get("name", ""))
|
|
||||||
display_type = original_request.get("display_type", original_request.get("type", ""))
|
|
||||||
display_artist = original_request.get("display_artist", original_request.get("artist", ""))
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error parsing first line of PRG file: {e}")
|
|
||||||
original_request = None
|
|
||||||
|
|
||||||
# For resource type and name, use the second line if available.
|
|
||||||
resource_type = ""
|
|
||||||
resource_name = ""
|
|
||||||
resource_artist = ""
|
|
||||||
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", "")
|
|
||||||
resource_name = second_line.get("name", "")
|
|
||||||
resource_artist = second_line.get("artist", "")
|
|
||||||
except Exception:
|
|
||||||
resource_type = ""
|
|
||||||
resource_name = ""
|
|
||||||
resource_artist = ""
|
|
||||||
else:
|
|
||||||
resource_type = ""
|
|
||||||
resource_name = ""
|
|
||||||
resource_artist = ""
|
|
||||||
|
|
||||||
# Get the last line from the file.
|
|
||||||
last_line_raw = lines[-1]
|
|
||||||
try:
|
|
||||||
last_line_parsed = json.loads(last_line_raw)
|
|
||||||
except Exception:
|
|
||||||
last_line_parsed = last_line_raw # Fallback to raw string if JSON parsing fails.
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"type": resource_type,
|
|
||||||
"name": resource_name,
|
|
||||||
"artist": resource_artist,
|
|
||||||
"last_line": last_line_parsed,
|
|
||||||
"original_request": original_request,
|
|
||||||
"display_title": display_title,
|
|
||||||
"display_type": display_type,
|
|
||||||
"display_artist": display_artist,
|
|
||||||
"task_id": task_id,
|
|
||||||
"event": "unknown", # Old files don't have event types
|
|
||||||
"timestamp": time.time()
|
|
||||||
})
|
|
||||||
except FileNotFoundError:
|
|
||||||
abort(404, "Task or file not found")
|
|
||||||
except Exception as e:
|
|
||||||
abort(500, f"An error occurred: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@prgs_bp.route('/delete/<task_id>', methods=['DELETE'])
|
@prgs_bp.route('/delete/<task_id>', methods=['DELETE'])
|
||||||
@@ -276,42 +60,15 @@ def delete_prg_file(task_id):
|
|||||||
Args:
|
Args:
|
||||||
task_id: Either a task UUID from Celery or a PRG filename from the old system
|
task_id: Either a task UUID from Celery or a PRG filename from the old system
|
||||||
"""
|
"""
|
||||||
try:
|
# Only support new task IDs
|
||||||
# First try to delete from Redis if it's a task ID
|
task_info = get_task_info(task_id)
|
||||||
task_info = get_task_info(task_id)
|
if not task_info:
|
||||||
|
abort(404, "Task not found")
|
||||||
if task_info:
|
cancel_task(task_id)
|
||||||
# This is a task ID in the new system - we should cancel it first
|
from routes.utils.celery_tasks import redis_client
|
||||||
# if it's still running, then clear its data from Redis
|
redis_client.delete(f"task:{task_id}:info")
|
||||||
cancel_result = cancel_task(task_id)
|
redis_client.delete(f"task:{task_id}:status")
|
||||||
|
return {'message': f'Task {task_id} deleted successfully'}, 200
|
||||||
# Use Redis connection to delete the task data
|
|
||||||
from routes.utils.celery_tasks import redis_client
|
|
||||||
|
|
||||||
# Delete task info and status
|
|
||||||
redis_client.delete(f"task:{task_id}:info")
|
|
||||||
redis_client.delete(f"task:{task_id}:status")
|
|
||||||
|
|
||||||
return {'message': f'Task {task_id} deleted successfully'}, 200
|
|
||||||
|
|
||||||
# If not found in Redis, try the old PRG file system
|
|
||||||
# Security checks to prevent path traversal and ensure correct file type.
|
|
||||||
if '..' in task_id or '/' in task_id:
|
|
||||||
abort(400, "Invalid file request")
|
|
||||||
if not task_id.endswith('.prg'):
|
|
||||||
abort(400, "Only .prg files can be deleted")
|
|
||||||
|
|
||||||
filepath = os.path.join(PRGS_DIR, task_id)
|
|
||||||
|
|
||||||
if not os.path.isfile(filepath):
|
|
||||||
abort(404, "File not found")
|
|
||||||
|
|
||||||
os.remove(filepath)
|
|
||||||
return {'message': f'File {task_id} deleted successfully'}, 200
|
|
||||||
except FileNotFoundError:
|
|
||||||
abort(404, "Task or file not found")
|
|
||||||
except Exception as e:
|
|
||||||
abort(500, f"An error occurred: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@prgs_bp.route('/list', methods=['GET'])
|
@prgs_bp.route('/list', methods=['GET'])
|
||||||
@@ -320,25 +77,10 @@ def list_prg_files():
|
|||||||
Retrieve a list of all tasks in the system.
|
Retrieve a list of all tasks in the system.
|
||||||
Combines results from both the old PRG file system and the new task ID based system.
|
Combines results from both the old PRG file system and the new task ID based system.
|
||||||
"""
|
"""
|
||||||
try:
|
# List only new system tasks
|
||||||
# Get tasks from the new system
|
tasks = get_all_tasks()
|
||||||
tasks = get_all_tasks()
|
task_ids = [task["task_id"] for task in tasks]
|
||||||
task_ids = [task["task_id"] for task in tasks]
|
return jsonify(task_ids)
|
||||||
|
|
||||||
# Get PRG files from the old system
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Combine both lists
|
|
||||||
all_ids = task_ids + prg_files
|
|
||||||
|
|
||||||
return jsonify(all_ids)
|
|
||||||
except Exception as e:
|
|
||||||
abort(500, f"An error occurred: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@prgs_bp.route('/retry/<task_id>', methods=['POST'])
|
@prgs_bp.route('/retry/<task_id>', methods=['POST'])
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import os
|
|||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
from routes.utils.celery_queue_manager import download_queue_manager
|
from routes.utils.celery_queue_manager import download_queue_manager
|
||||||
|
from urllib.parse import urlparse # for URL validation
|
||||||
|
|
||||||
track_bp = Blueprint('track', __name__)
|
track_bp = Blueprint('track', __name__)
|
||||||
|
|
||||||
@@ -16,19 +17,30 @@ def handle_download():
|
|||||||
# Validate required parameters
|
# Validate required parameters
|
||||||
if not url:
|
if not url:
|
||||||
return Response(
|
return Response(
|
||||||
json.dumps({"error": "Missing required parameter: url"}),
|
json.dumps({"error": "Missing required parameter: url", "original_url": url}),
|
||||||
|
status=400,
|
||||||
|
mimetype='application/json'
|
||||||
|
)
|
||||||
|
# Validate URL domain
|
||||||
|
parsed = urlparse(url)
|
||||||
|
host = parsed.netloc.lower()
|
||||||
|
if not (host.endswith('deezer.com') or host.endswith('open.spotify.com') or host.endswith('spotify.com')):
|
||||||
|
return Response(
|
||||||
|
json.dumps({"error": f"Invalid Link {url} :(", "original_url": url}),
|
||||||
status=400,
|
status=400,
|
||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add the task to the queue with only essential parameters
|
# Add the task to the queue with only essential parameters
|
||||||
# The queue manager will now handle all config parameters
|
# The queue manager will now handle all config parameters
|
||||||
|
orig_params = request.args.to_dict()
|
||||||
|
orig_params["original_url"] = request.url
|
||||||
task_id = download_queue_manager.add_task({
|
task_id = download_queue_manager.add_task({
|
||||||
"download_type": "track",
|
"download_type": "track",
|
||||||
"url": url,
|
"url": url,
|
||||||
"name": name,
|
"name": name,
|
||||||
"artist": artist,
|
"artist": artist,
|
||||||
"orig_request": request.args.to_dict()
|
"orig_request": orig_params
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from deezspot.deezloader import DeeLogin
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def download_album(
|
def download_album(
|
||||||
service,
|
|
||||||
url,
|
url,
|
||||||
main,
|
main,
|
||||||
fallback=None,
|
fallback=None,
|
||||||
@@ -22,8 +21,24 @@ def download_album(
|
|||||||
progress_callback=None
|
progress_callback=None
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
# DEBUG: Print parameters
|
# Detect URL source (Spotify or Deezer) from URL
|
||||||
print(f"DEBUG: album.py received - service={service}, main={main}, fallback={fallback}")
|
is_spotify_url = 'open.spotify.com' in url.lower()
|
||||||
|
is_deezer_url = 'deezer.com' in url.lower()
|
||||||
|
|
||||||
|
# Determine service exclusively from URL
|
||||||
|
if is_spotify_url:
|
||||||
|
service = 'spotify'
|
||||||
|
elif is_deezer_url:
|
||||||
|
service = 'deezer'
|
||||||
|
else:
|
||||||
|
# If URL can't be detected, raise an error
|
||||||
|
error_msg = "Invalid URL: Must be from open.spotify.com or deezer.com"
|
||||||
|
print(f"ERROR: {error_msg}")
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
|
print(f"DEBUG: album.py - URL detection: is_spotify_url={is_spotify_url}, is_deezer_url={is_deezer_url}")
|
||||||
|
print(f"DEBUG: album.py - Service determined from URL: {service}")
|
||||||
|
print(f"DEBUG: album.py - Credentials: main={main}, fallback={fallback}")
|
||||||
|
|
||||||
# Load Spotify client credentials if available
|
# Load Spotify client credentials if available
|
||||||
spotify_client_id = None
|
spotify_client_id = None
|
||||||
@@ -49,6 +64,8 @@ def download_album(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading Spotify search credentials: {e}")
|
print(f"Error loading Spotify search credentials: {e}")
|
||||||
|
|
||||||
|
# For Spotify URLs: check if fallback is enabled, if so use the fallback logic,
|
||||||
|
# otherwise download directly from Spotify
|
||||||
if service == 'spotify':
|
if service == 'spotify':
|
||||||
if fallback:
|
if fallback:
|
||||||
if quality is None:
|
if quality is None:
|
||||||
@@ -186,6 +203,7 @@ def download_album(
|
|||||||
max_retries=max_retries
|
max_retries=max_retries
|
||||||
)
|
)
|
||||||
print(f"DEBUG: Album download completed successfully using Spotify main")
|
print(f"DEBUG: Album download completed successfully using Spotify main")
|
||||||
|
# For Deezer URLs: download directly from Deezer
|
||||||
elif service == 'deezer':
|
elif service == 'deezer':
|
||||||
if quality is None:
|
if quality is None:
|
||||||
quality = 'FLAC'
|
quality = 'FLAC'
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import traceback
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
from flask import Blueprint, Response, request, url_for
|
||||||
from routes.utils.celery_queue_manager import download_queue_manager, get_config_params
|
from routes.utils.celery_queue_manager import download_queue_manager, get_config_params
|
||||||
from routes.utils.get_info import get_spotify_info
|
from routes.utils.get_info import get_spotify_info
|
||||||
|
|
||||||
@@ -87,6 +88,16 @@ def download_artist_albums(url, album_type="album,single,compilation", request_a
|
|||||||
|
|
||||||
logger.info(f"Fetching artist info for ID: {artist_id}")
|
logger.info(f"Fetching artist info for ID: {artist_id}")
|
||||||
|
|
||||||
|
# Detect URL source (only Spotify is supported for artists)
|
||||||
|
is_spotify_url = 'open.spotify.com' in url.lower()
|
||||||
|
is_deezer_url = 'deezer.com' in url.lower()
|
||||||
|
|
||||||
|
# Artist functionality only works with Spotify URLs currently
|
||||||
|
if not is_spotify_url:
|
||||||
|
error_msg = "Invalid URL: Artist functionality only supports open.spotify.com URLs"
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
# Get artist info with albums
|
# Get artist info with albums
|
||||||
artist_data = get_spotify_info(artist_id, "artist")
|
artist_data = get_spotify_info(artist_id, "artist")
|
||||||
|
|
||||||
@@ -152,17 +163,18 @@ def download_artist_albums(url, album_type="album,single,compilation", request_a
|
|||||||
"name": album_name,
|
"name": album_name,
|
||||||
"artist": album_artist,
|
"artist": album_artist,
|
||||||
"type": "album",
|
"type": "album",
|
||||||
"service": "spotify",
|
# URL source will be automatically detected in the download functions
|
||||||
# Add reference to parent artist request if needed
|
|
||||||
"parent_artist_url": url,
|
"parent_artist_url": url,
|
||||||
"parent_request_type": "artist"
|
"parent_request_type": "artist"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Include original download URL for this album task
|
||||||
|
album_request_args["original_url"] = url_for('album.handle_download', url=album_url, _external=True)
|
||||||
|
|
||||||
# Create task for this album
|
# Create task for this album
|
||||||
task_data = {
|
task_data = {
|
||||||
"download_type": "album",
|
"download_type": "album",
|
||||||
"type": "album", # Type for the download task
|
"type": "album", # Type for the download task
|
||||||
"service": "spotify", # Default to Spotify since we're using Spotify API
|
|
||||||
"url": album_url, # Important: use the album URL, not artist URL
|
"url": album_url, # Important: use the album URL, not artist URL
|
||||||
"retry_url": album_url, # Use album URL for retry logic, not artist URL
|
"retry_url": album_url, # Use album URL for retry logic, not artist URL
|
||||||
"name": album_name,
|
"name": album_name,
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ logger = logging.getLogger(__name__)
|
|||||||
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
|
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
|
||||||
REDIS_PORT = os.getenv('REDIS_PORT', '6379')
|
REDIS_PORT = os.getenv('REDIS_PORT', '6379')
|
||||||
REDIS_DB = os.getenv('REDIS_DB', '0')
|
REDIS_DB = os.getenv('REDIS_DB', '0')
|
||||||
REDIS_URL = os.getenv('REDIS_URL', f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}")
|
# Optional Redis password
|
||||||
|
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD', '')
|
||||||
|
# Build default URL with password if provided
|
||||||
|
_password_part = f":{REDIS_PASSWORD}@" if REDIS_PASSWORD else ""
|
||||||
|
default_redis_url = f"redis://{_password_part}{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}"
|
||||||
|
REDIS_URL = os.getenv('REDIS_URL', default_redis_url)
|
||||||
REDIS_BACKEND = os.getenv('REDIS_BACKEND', REDIS_URL)
|
REDIS_BACKEND = os.getenv('REDIS_BACKEND', REDIS_URL)
|
||||||
|
|
||||||
# Log Redis connection details
|
# Log Redis connection details
|
||||||
|
|||||||
@@ -120,9 +120,6 @@ class CeleryDownloadQueueManager:
|
|||||||
# Extract original request or use empty dict
|
# Extract original request or use empty dict
|
||||||
original_request = task.get("orig_request", task.get("original_request", {}))
|
original_request = task.get("orig_request", task.get("original_request", {}))
|
||||||
|
|
||||||
# Determine service (spotify or deezer) from config or request
|
|
||||||
service = original_request.get("service", config_params.get("service", "spotify"))
|
|
||||||
|
|
||||||
# Debug retry_url if present
|
# Debug retry_url if present
|
||||||
if "retry_url" in task:
|
if "retry_url" in task:
|
||||||
logger.debug(f"Task has retry_url: {task['retry_url']}")
|
logger.debug(f"Task has retry_url: {task['retry_url']}")
|
||||||
@@ -133,21 +130,20 @@ class CeleryDownloadQueueManager:
|
|||||||
"type": task.get("type", download_type),
|
"type": task.get("type", download_type),
|
||||||
"name": task.get("name", ""),
|
"name": task.get("name", ""),
|
||||||
"artist": task.get("artist", ""),
|
"artist": task.get("artist", ""),
|
||||||
"service": service,
|
|
||||||
"url": task.get("url", ""),
|
"url": task.get("url", ""),
|
||||||
|
|
||||||
# Preserve retry_url if present
|
# Preserve retry_url if present
|
||||||
"retry_url": task.get("retry_url", ""),
|
"retry_url": task.get("retry_url", ""),
|
||||||
|
|
||||||
# Use config values but allow override from request
|
# Use main account from config
|
||||||
"main": original_request.get("main",
|
"main": original_request.get("main", config_params['deezer']),
|
||||||
config_params['spotify'] if service == 'spotify' else config_params['deezer']),
|
|
||||||
|
|
||||||
|
# Set fallback if enabled in config
|
||||||
"fallback": original_request.get("fallback",
|
"fallback": original_request.get("fallback",
|
||||||
config_params['spotify'] if config_params['fallback'] and service == 'spotify' else None),
|
config_params['spotify'] if config_params['fallback'] else None),
|
||||||
|
|
||||||
"quality": original_request.get("quality",
|
# Use default quality settings
|
||||||
config_params['spotifyQuality'] if service == 'spotify' else config_params['deezerQuality']),
|
"quality": original_request.get("quality", config_params['deezerQuality']),
|
||||||
|
|
||||||
"fall_quality": original_request.get("fall_quality", config_params['spotifyQuality']),
|
"fall_quality": original_request.get("fall_quality", config_params['spotifyQuality']),
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from celery.exceptions import Retry
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Setup Redis and Celery
|
# Setup Redis and Celery
|
||||||
from routes.utils.celery_config import REDIS_URL, REDIS_BACKEND, get_config_params
|
from routes.utils.celery_config import REDIS_URL, REDIS_BACKEND, REDIS_PASSWORD, get_config_params
|
||||||
|
|
||||||
# Initialize Celery app
|
# Initialize Celery app
|
||||||
celery_app = Celery('download_tasks',
|
celery_app = Celery('download_tasks',
|
||||||
@@ -893,12 +893,11 @@ def download_track(self, **task_data):
|
|||||||
custom_track_format = task_data.get("custom_track_format", config_params.get("customTrackFormat", "%tracknum%. %music%"))
|
custom_track_format = task_data.get("custom_track_format", config_params.get("customTrackFormat", "%tracknum%. %music%"))
|
||||||
pad_tracks = task_data.get("pad_tracks", config_params.get("tracknum_padding", True))
|
pad_tracks = task_data.get("pad_tracks", config_params.get("tracknum_padding", True))
|
||||||
|
|
||||||
# Execute the download
|
# Execute the download - service is now determined from URL
|
||||||
download_track_func(
|
download_track_func(
|
||||||
service=service,
|
|
||||||
url=url,
|
url=url,
|
||||||
main=main,
|
main=main,
|
||||||
fallback=fallback,
|
fallback=fallback if fallback_enabled else None,
|
||||||
quality=quality,
|
quality=quality,
|
||||||
fall_quality=fall_quality,
|
fall_quality=fall_quality,
|
||||||
real_time=real_time,
|
real_time=real_time,
|
||||||
@@ -961,12 +960,11 @@ def download_album(self, **task_data):
|
|||||||
custom_track_format = task_data.get("custom_track_format", config_params.get("customTrackFormat", "%tracknum%. %music%"))
|
custom_track_format = task_data.get("custom_track_format", config_params.get("customTrackFormat", "%tracknum%. %music%"))
|
||||||
pad_tracks = task_data.get("pad_tracks", config_params.get("tracknum_padding", True))
|
pad_tracks = task_data.get("pad_tracks", config_params.get("tracknum_padding", True))
|
||||||
|
|
||||||
# Execute the download
|
# Execute the download - service is now determined from URL
|
||||||
download_album_func(
|
download_album_func(
|
||||||
service=service,
|
|
||||||
url=url,
|
url=url,
|
||||||
main=main,
|
main=main,
|
||||||
fallback=fallback,
|
fallback=fallback if fallback_enabled else None,
|
||||||
quality=quality,
|
quality=quality,
|
||||||
fall_quality=fall_quality,
|
fall_quality=fall_quality,
|
||||||
real_time=real_time,
|
real_time=real_time,
|
||||||
@@ -1034,12 +1032,11 @@ def download_playlist(self, **task_data):
|
|||||||
retry_delay_increase = task_data.get("retry_delay_increase", config_params.get("retry_delay_increase", 5))
|
retry_delay_increase = task_data.get("retry_delay_increase", config_params.get("retry_delay_increase", 5))
|
||||||
max_retries = task_data.get("max_retries", config_params.get("maxRetries", 3))
|
max_retries = task_data.get("max_retries", config_params.get("maxRetries", 3))
|
||||||
|
|
||||||
# Execute the download
|
# Execute the download - service is now determined from URL
|
||||||
download_playlist_func(
|
download_playlist_func(
|
||||||
service=service,
|
|
||||||
url=url,
|
url=url,
|
||||||
main=main,
|
main=main,
|
||||||
fallback=fallback,
|
fallback=fallback if fallback_enabled else None,
|
||||||
quality=quality,
|
quality=quality,
|
||||||
fall_quality=fall_quality,
|
fall_quality=fall_quality,
|
||||||
real_time=real_time,
|
real_time=real_time,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from deezspot.deezloader import DeeLogin
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def download_playlist(
|
def download_playlist(
|
||||||
service,
|
|
||||||
url,
|
url,
|
||||||
main,
|
main,
|
||||||
fallback=None,
|
fallback=None,
|
||||||
@@ -22,8 +21,24 @@ def download_playlist(
|
|||||||
progress_callback=None,
|
progress_callback=None,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
# DEBUG: Print parameters
|
# Detect URL source (Spotify or Deezer) from URL
|
||||||
print(f"DEBUG: playlist.py received - service={service}, main={main}, fallback={fallback}")
|
is_spotify_url = 'open.spotify.com' in url.lower()
|
||||||
|
is_deezer_url = 'deezer.com' in url.lower()
|
||||||
|
|
||||||
|
# Determine service exclusively from URL
|
||||||
|
if is_spotify_url:
|
||||||
|
service = 'spotify'
|
||||||
|
elif is_deezer_url:
|
||||||
|
service = 'deezer'
|
||||||
|
else:
|
||||||
|
# If URL can't be detected, raise an error
|
||||||
|
error_msg = "Invalid URL: Must be from open.spotify.com or deezer.com"
|
||||||
|
print(f"ERROR: {error_msg}")
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
|
print(f"DEBUG: playlist.py - URL detection: is_spotify_url={is_spotify_url}, is_deezer_url={is_deezer_url}")
|
||||||
|
print(f"DEBUG: playlist.py - Service determined from URL: {service}")
|
||||||
|
print(f"DEBUG: playlist.py - Credentials: main={main}, fallback={fallback}")
|
||||||
|
|
||||||
# Load Spotify client credentials if available
|
# Load Spotify client credentials if available
|
||||||
spotify_client_id = None
|
spotify_client_id = None
|
||||||
@@ -49,6 +64,8 @@ def download_playlist(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading Spotify search credentials: {e}")
|
print(f"Error loading Spotify search credentials: {e}")
|
||||||
|
|
||||||
|
# For Spotify URLs: check if fallback is enabled, if so use the fallback logic,
|
||||||
|
# otherwise download directly from Spotify
|
||||||
if service == 'spotify':
|
if service == 'spotify':
|
||||||
if fallback:
|
if fallback:
|
||||||
if quality is None:
|
if quality is None:
|
||||||
@@ -181,6 +198,7 @@ def download_playlist(
|
|||||||
max_retries=max_retries
|
max_retries=max_retries
|
||||||
)
|
)
|
||||||
print(f"DEBUG: Playlist download completed successfully using Spotify main")
|
print(f"DEBUG: Playlist download completed successfully using Spotify main")
|
||||||
|
# For Deezer URLs: download directly from Deezer
|
||||||
elif service == 'deezer':
|
elif service == 'deezer':
|
||||||
if quality is None:
|
if quality is None:
|
||||||
quality = 'FLAC'
|
quality = 'FLAC'
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from deezspot.deezloader import DeeLogin
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def download_track(
|
def download_track(
|
||||||
service,
|
|
||||||
url,
|
url,
|
||||||
main,
|
main,
|
||||||
fallback=None,
|
fallback=None,
|
||||||
@@ -22,8 +21,24 @@ def download_track(
|
|||||||
progress_callback=None
|
progress_callback=None
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
# DEBUG: Print parameters
|
# Detect URL source (Spotify or Deezer) from URL
|
||||||
print(f"DEBUG: track.py received - service={service}, main={main}, fallback={fallback}")
|
is_spotify_url = 'open.spotify.com' in url.lower()
|
||||||
|
is_deezer_url = 'deezer.com' in url.lower()
|
||||||
|
|
||||||
|
# Determine service exclusively from URL
|
||||||
|
if is_spotify_url:
|
||||||
|
service = 'spotify'
|
||||||
|
elif is_deezer_url:
|
||||||
|
service = 'deezer'
|
||||||
|
else:
|
||||||
|
# If URL can't be detected, raise an error
|
||||||
|
error_msg = "Invalid URL: Must be from open.spotify.com or deezer.com"
|
||||||
|
print(f"ERROR: {error_msg}")
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
|
print(f"DEBUG: track.py - URL detection: is_spotify_url={is_spotify_url}, is_deezer_url={is_deezer_url}")
|
||||||
|
print(f"DEBUG: track.py - Service determined from URL: {service}")
|
||||||
|
print(f"DEBUG: track.py - Credentials: main={main}, fallback={fallback}")
|
||||||
|
|
||||||
# Load Spotify client credentials if available
|
# Load Spotify client credentials if available
|
||||||
spotify_client_id = None
|
spotify_client_id = None
|
||||||
@@ -49,6 +64,8 @@ def download_track(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading Spotify search credentials: {e}")
|
print(f"Error loading Spotify search credentials: {e}")
|
||||||
|
|
||||||
|
# For Spotify URLs: check if fallback is enabled, if so use the fallback logic,
|
||||||
|
# otherwise download directly from Spotify
|
||||||
if service == 'spotify':
|
if service == 'spotify':
|
||||||
if fallback:
|
if fallback:
|
||||||
if quality is None:
|
if quality is None:
|
||||||
@@ -166,6 +183,7 @@ def download_track(
|
|||||||
retry_delay_increase=retry_delay_increase,
|
retry_delay_increase=retry_delay_increase,
|
||||||
max_retries=max_retries
|
max_retries=max_retries
|
||||||
)
|
)
|
||||||
|
# For Deezer URLs: download directly from Deezer
|
||||||
elif service == 'deezer':
|
elif service == 'deezer':
|
||||||
if quality is None:
|
if quality is None:
|
||||||
quality = 'FLAC'
|
quality = 'FLAC'
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -310,6 +310,96 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Overall progress container for albums and playlists */
|
||||||
|
.overall-progress-container {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
position: relative; /* Positioning context for z-index */
|
||||||
|
z-index: 2; /* Ensure overall progress appears above track progress */
|
||||||
|
}
|
||||||
|
|
||||||
|
.overall-progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #b3b3b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overall-progress-label {
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overall-progress-count {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1DB954;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overall-progress-bar-container {
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overall-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #4a90e2, #7a67ee); /* Changed to blue-purple gradient */
|
||||||
|
width: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overall-progress-bar.complete {
|
||||||
|
background: #4a90e2; /* Changed to solid blue for completed overall progress */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Track progress bar container */
|
||||||
|
.track-progress-bar-container {
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1; /* Ensure it's below the overall progress */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Track progress bar */
|
||||||
|
.track-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: #1DB954; /* Keep green for track-level progress */
|
||||||
|
width: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
box-shadow: 0 0 3px rgba(29, 185, 84, 0.5); /* Add subtle glow to differentiate */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Complete state for track progress */
|
||||||
|
/* Real-time progress style */
|
||||||
|
.track-progress-bar.real-time {
|
||||||
|
background: #1DB954; /* Vivid green for real-time progress */
|
||||||
|
background: #1DB954;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulsing animation for indeterminate progress */
|
||||||
|
.track-progress-bar.progress-pulse {
|
||||||
|
background: linear-gradient(90deg, #1DB954 0%, #2cd267 50%, #1DB954 100%); /* Keep in green family */
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: progress-pulse-slide 1.5s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-pulse-slide {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Progress percentage text */
|
/* Progress percentage text */
|
||||||
.progress-percent {
|
.progress-percent {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|||||||
@@ -42,6 +42,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore last search type if no URL override
|
||||||
|
const savedType = localStorage.getItem('lastSearchType');
|
||||||
|
if (savedType && ['track','album','playlist','artist'].includes(savedType)) {
|
||||||
|
searchType.value = savedType;
|
||||||
|
}
|
||||||
|
// Save last selection on change
|
||||||
|
if (searchType) {
|
||||||
|
searchType.addEventListener('change', () => {
|
||||||
|
localStorage.setItem('lastSearchType', searchType.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check for URL parameters
|
// Check for URL parameters
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const query = urlParams.get('q');
|
const query = urlParams.get('q');
|
||||||
@@ -341,7 +353,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
* Extracts details from a Spotify URL
|
* Extracts details from a Spotify URL
|
||||||
*/
|
*/
|
||||||
function getSpotifyResourceDetails(url) {
|
function getSpotifyResourceDetails(url) {
|
||||||
const regex = /spotify\.com\/(track|album|playlist|artist)\/([a-zA-Z0-9]+)/;
|
// Allow optional path segments (e.g. intl-fr) before resource type
|
||||||
|
const regex = /spotify\.com\/(?:[^\/]+\/)??(track|album|playlist|artist)\/([a-zA-Z0-9]+)/i;
|
||||||
const match = url.match(regex);
|
const match = url.match(regex);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
|
|||||||
1665
static/js/queue.js
1665
static/js/queue.js
File diff suppressed because it is too large
Load Diff
@@ -116,7 +116,7 @@
|
|||||||
<option value="%music%">%music% - Track title</option>
|
<option value="%music%">%music% - Track title</option>
|
||||||
<option value="%artist%">%artist% - Track artist</option>
|
<option value="%artist%">%artist% - Track artist</option>
|
||||||
<option value="%album%">%album% - Album name</option>
|
<option value="%album%">%album% - Album name</option>
|
||||||
<option value="%album_artist%">%album_artist% - Album artist</option>
|
<option value="%ar_album%">%ar_album% - Album artist</option>
|
||||||
<option value="%tracknum%">%tracknum% - Track number</option>
|
<option value="%tracknum%">%tracknum% - Track number</option>
|
||||||
<option value="%year%">%year% - Year of release</option>
|
<option value="%year%">%year% - Year of release</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
<option value="%music%">%music% - Track title</option>
|
<option value="%music%">%music% - Track title</option>
|
||||||
<option value="%artist%">%artist% - Track artist</option>
|
<option value="%artist%">%artist% - Track artist</option>
|
||||||
<option value="%album%">%album% - Album name</option>
|
<option value="%album%">%album% - Album name</option>
|
||||||
<option value="%album_artist%">%album_artist% - Album artist</option>
|
<option value="%ar_album%">%ar_album% - Album artist</option>
|
||||||
<option value="%tracknum%">%tracknum% - Track number</option>
|
<option value="%tracknum%">%tracknum% - Track number</option>
|
||||||
<option value="%year%">%year% - Year of release</option>
|
<option value="%year%">%year% - Year of release</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user