added artist functionality
This commit is contained in:
229
routes/artist.py
Normal file
229
routes/artist.py
Normal file
@@ -0,0 +1,229 @@
|
||||
#!/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)) + '.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'):
|
||||
if line.startswith('{'):
|
||||
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):
|
||||
"""
|
||||
This function wraps the call to download_artist_albums and writes JSON status to the prg file.
|
||||
"""
|
||||
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
|
||||
|
||||
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,
|
||||
)
|
||||
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)
|
||||
"""
|
||||
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']
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
)
|
||||
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'
|
||||
)
|
||||
112
routes/utils/artist.py
Normal file
112
routes/utils/artist.py
Normal file
@@ -0,0 +1,112 @@
|
||||
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'):
|
||||
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', [])
|
||||
# Attempt to extract the artist name from the discography; fallback to artist_url if not available.
|
||||
artist_name = discography.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", "total_albums": len(albums)})
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
})
|
||||
Reference in New Issue
Block a user