From 48db9a1606e832b80b4995ce9c5232ea26c611ca Mon Sep 17 00:00:00 2001 From: "cool.gitter.choco" Date: Sun, 26 Jan 2025 08:39:06 -0600 Subject: [PATCH] added spot fallback --- .gitignore | 6 +- app.py | 54 +++++- routes/album.py | 87 +++++++++ routes/playlist.py | 88 +++++++++ routes/search.py | 9 +- routes/track.py | 87 +++++++++ .../utils/__pycache__/search.cpython-312.pyc | Bin 9243 -> 608 bytes routes/utils/album.py | 104 ++++++---- routes/utils/playlist.py | 104 ++++++---- routes/utils/search.py | 179 +----------------- routes/utils/track.py | 84 +++++--- 11 files changed, 525 insertions(+), 277 deletions(-) create mode 100644 routes/album.py create mode 100644 routes/playlist.py create mode 100644 routes/track.py diff --git a/.gitignore b/.gitignore index 17c19b2..89520ff 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ /venv /downloads/ /creds/ -Test.py +/Test.py +/prgs/ +/flask_server.log +routes/__pycache__/ +routes/utils/__pycache__/ diff --git a/app.py b/app.py index 731705d..f256db3 100644 --- a/app.py +++ b/app.py @@ -1,16 +1,66 @@ -from flask import Flask +from flask import Flask, request from flask_cors import CORS from routes.search import search_bp from routes.credentials import credentials_bp +from routes.album import album_bp +from routes.track import track_bp +from routes.playlist import playlist_bp +import logging +from datetime import datetime +import time def create_app(): app = Flask(__name__) + + # Configure basic logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s', + handlers=[ + logging.FileHandler('flask_server.log'), + logging.StreamHandler() + ] + ) + + # Get Flask's logger + logger = logging.getLogger('werkzeug') + logger.setLevel(logging.INFO) + CORS(app) + + # Register blueprints 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') + + # Add request logging middleware + @app.before_request + def log_request(): + request.start_time = time.time() + logger.info(f"Request: {request.method} {request.path}") + + @app.after_request + def log_response(response): + duration = round((time.time() - request.start_time) * 1000, 2) + logger.info(f"Response: {response.status} | Duration: {duration}ms") + return response + + # Error logging + @app.errorhandler(Exception) + def handle_exception(e): + logger.error(f"Server error: {str(e)}", exc_info=True) + return "Internal Server Error", 500 + return app if __name__ == '__main__': - from waitress import serve + # Configure waitress logger + logger = logging.getLogger('waitress') + logger.setLevel(logging.INFO) + app = create_app() + logging.info("Starting Flask server on port 5000") + from waitress import serve serve(app, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/routes/album.py b/routes/album.py new file mode 100644 index 0000000..f6e5763 --- /dev/null +++ b/routes/album.py @@ -0,0 +1,87 @@ +from flask import Blueprint, Response, request +import json +import os +import random +import string +import sys +from threading import Thread +import traceback + +album_bp = Blueprint('album', __name__) + +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): + self.file.write(text) + self.file.flush() + + def flush(self): + self.file.flush() + +@album_bp.route('/download', methods=['GET']) +def handle_download(): + service = request.args.get('service') + url = request.args.get('url') + main = request.args.get('main') + fallback = request.args.get('fallback') # New fallback parameter + + if not all([service, url, main]): + return Response( + json.dumps({"error": "Missing parameters"}), + status=400, + mimetype='application/json' + ) + + filename = generate_random_filename() + prg_dir = './prgs' + os.makedirs(prg_dir, exist_ok=True) + prg_path = os.path.join(prg_dir, filename) + + def download_task(): + try: + from routes.utils.album import download_album + with open(prg_path, 'w') as f: + flushing_file = FlushingFileWrapper(f) + original_stdout = sys.stdout + sys.stdout = flushing_file + + try: + # Pass fallback parameter to download_album + download_album( + service=service, + url=url, + main=main, + fallback=fallback + ) + 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 + 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") + + Thread(target=download_task).start() + + return Response( + json.dumps({"prg_file": filename}), + status=202, + mimetype='application/json' + ) \ No newline at end of file diff --git a/routes/playlist.py b/routes/playlist.py new file mode 100644 index 0000000..17b0883 --- /dev/null +++ b/routes/playlist.py @@ -0,0 +1,88 @@ +from flask import Blueprint, Response, request +import json +import os +import random +import string +import sys +from threading import Thread +import traceback + +playlist_bp = Blueprint('playlist', __name__) + +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): + self.file.write(text) + self.file.flush() + + def flush(self): + self.file.flush() + +@playlist_bp.route('/download', methods=['GET']) +def handle_download(): + service = request.args.get('service') + url = request.args.get('url') + main = request.args.get('main') # Changed from 'account' + fallback = request.args.get('fallback') # New parameter + + # Validate required parameters (main instead of account) + if not all([service, url, main]): + return Response( + json.dumps({"error": "Missing parameters"}), + status=400, + mimetype='application/json' + ) + + filename = generate_random_filename() + prg_dir = './prgs' + os.makedirs(prg_dir, exist_ok=True) + prg_path = os.path.join(prg_dir, filename) + + def download_task(): + try: + from routes.utils.playlist import download_playlist + with open(prg_path, 'w') as f: + flushing_file = FlushingFileWrapper(f) + original_stdout = sys.stdout + sys.stdout = flushing_file + + try: + # Updated call with main/fallback parameters + download_playlist( + service=service, + url=url, + main=main, + fallback=fallback + ) + 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 + 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") + + Thread(target=download_task).start() + + return Response( + json.dumps({"prg_file": filename}), + status=202, + mimetype='application/json' + ) \ No newline at end of file diff --git a/routes/search.py b/routes/search.py index c05aa5f..7f8a536 100644 --- a/routes/search.py +++ b/routes/search.py @@ -1,5 +1,5 @@ from flask import Blueprint, jsonify, request -from routes.utils.search import search_and_combine +from routes.utils.search import search # Renamed import search_bp = Blueprint('search', __name__) @@ -9,7 +9,6 @@ def handle_search(): # Get query parameters query = request.args.get('q', '') search_type = request.args.get('type', 'track') - service = request.args.get('service', 'both') limit = int(request.args.get('limit', 10)) # Validate parameters @@ -21,16 +20,14 @@ def handle_search(): return jsonify({'error': 'Invalid search type'}), 400 # Perform the search - results = search_and_combine( + raw_results = search( query=query, search_type=search_type, - service=service, limit=limit ) return jsonify({ - 'results': results, - 'count': len(results), + 'data': raw_results, 'error': None }) diff --git a/routes/track.py b/routes/track.py new file mode 100644 index 0000000..91e76a2 --- /dev/null +++ b/routes/track.py @@ -0,0 +1,87 @@ +from flask import Blueprint, Response, request +import json +import os +import random +import string +import sys +from threading import Thread +import traceback + +track_bp = Blueprint('track', __name__) + +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): + self.file.write(text) + self.file.flush() + + def flush(self): + self.file.flush() + +@track_bp.route('/download', methods=['GET']) +def handle_download(): + service = request.args.get('service') + url = request.args.get('url') + main = request.args.get('main') + fallback = request.args.get('fallback') # New fallback parameter + + if not all([service, url, main]): + return Response( + json.dumps({"error": "Missing parameters"}), + status=400, + mimetype='application/json' + ) + + filename = generate_random_filename() + prg_dir = './prgs' + os.makedirs(prg_dir, exist_ok=True) + prg_path = os.path.join(prg_dir, filename) + + def download_task(): + try: + from routes.utils.track import download_track + with open(prg_path, 'w') as f: + flushing_file = FlushingFileWrapper(f) + original_stdout = sys.stdout + sys.stdout = flushing_file + + try: + # Pass all parameters including fallback + download_track( + service=service, + url=url, + main=main, + fallback=fallback + ) + 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 + 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") + + Thread(target=download_task).start() + + return Response( + json.dumps({"prg_file": filename}), + status=202, + mimetype='application/json' + ) \ No newline at end of file diff --git a/routes/utils/__pycache__/search.cpython-312.pyc b/routes/utils/__pycache__/search.cpython-312.pyc index d6ccf36c80440ae5c9fc836a86c7169e3b731288..b7afcbcab28abdb77d0aba7f3ee703621d08675b 100644 GIT binary patch literal 608 zcmZ8eJ!{-R5S`U(W!+bt*arz*ia?qO!l_FWlg^#F#;IIHG1|3H;ZC~jt{^(j5Q8Cg z8sA@tv44Png29c86vsj!kjkCHUFFKN_klwO_RX8!H}h7j>qesr7?bVO!;b{uk1bYF zS(52%BxB&fNe}vDj}WK;_qih+PI0+{awgmZpL-{4j}>b{<<_j-!LRzKr;PrQ>&EbY6iHwYyqg#3b+Wfe9y|E^WZ^GWSd bNiqL^(kvO~(u4YXX-`TYyy~oiY(DoM2VIBS literal 9243 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 K#Xt+Q;C}&Kq9$Pg diff --git a/routes/utils/album.py b/routes/utils/album.py index b98154f..83653c3 100644 --- a/routes/utils/album.py +++ b/routes/utils/album.py @@ -4,57 +4,95 @@ import traceback from deezspot.spotloader import SpoLogin from deezspot.deezloader import DeeLogin -def download_album(service, url, account): +def download_album(service, url, main, fallback=None): try: if service == 'spotify': - # Construct Spotify credentials path - creds_dir = os.path.join('./creds/spotify', account) - credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) - - # Initialize Spotify client - spo = SpoLogin(credentials_path=credentials_path) - - # Download Spotify album - spo.download_album( - link_album=url, - output_dir="./downloads/albums", - quality_download="NORMAL", - recursive_quality=True, - recursive_download=False, - not_interface=False, - method_save=1, - make_zip=True - ) - + if fallback: + # First attempt: use DeeLogin's download_albumspo with the 'main' (Deezer credentials) + try: + # Load Deezer credentials from 'main' under deezer directory + deezer_creds_dir = os.path.join('./creds/deezer', main) + deezer_creds_path = os.path.abspath(os.path.join(deezer_creds_dir, 'credentials.json')) + with open(deezer_creds_path, 'r') as f: + deezer_creds = json.load(f) + # Initialize DeeLogin with Deezer credentials + dl = DeeLogin( + arl=deezer_creds.get('arl', ''), + email=deezer_creds.get('email', ''), + password=deezer_creds.get('password', '') + ) + # Download using download_albumspo + dl.download_albumspo( + link_album=url, + output_dir="./downloads", + quality_download="FLAC", + recursive_quality=True, + recursive_download=False, + not_interface=False, + make_zip=True, + method_save=1 + ) + except Exception as e: + # If the first attempt fails, use the fallback Spotify main + print(f"Failed to download via Deezer fallback: {e}. Trying Spotify fallback main.") + # Load fallback Spotify credentials and attempt download + try: + spo_creds_dir = os.path.join('./creds/spotify', fallback) + spo_creds_path = os.path.abspath(os.path.join(spo_creds_dir, 'credentials.json')) + spo = SpoLogin(credentials_path=spo_creds_path) + spo.download_album( + link_album=url, + output_dir="./downloads", + quality_download="HIGH", + recursive_quality=True, + recursive_download=False, + not_interface=False, + method_save=1, + make_zip=False + ) + except Exception as e2: + # If fallback also fails, raise an error indicating both attempts failed + raise RuntimeError( + f"Both main (Deezer) and fallback (Spotify) attempts failed. " + f"Deezer error: {e}, Spotify error: {e2}" + ) from e2 + else: + # Original behavior: use Spotify main + creds_dir = os.path.join('./creds/spotify', main) + credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) + spo = SpoLogin(credentials_path=credentials_path) + spo.download_album( + link_album=url, + output_dir="./downloads", + quality_download="HIGH", + recursive_quality=True, + recursive_download=False, + not_interface=False, + method_save=1, + make_zip=False + ) elif service == 'deezer': - # Construct Deezer credentials path - creds_dir = os.path.join('./creds/deezer', account) + # Existing code remains the same, ignoring fallback + creds_dir = os.path.join('./creds/deezer', main) creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) - - # Load Deezer credentials with open(creds_path, 'r') as f: creds = json.load(f) - - # Initialize Deezer client dl = DeeLogin( arl=creds.get('arl', ''), email=creds.get('email', ''), password=creds.get('password', '') ) - - # Download Deezer album dl.download_albumdee( link_album=url, - output_dir="./downloads/albums", + output_dir="./downloads", quality_download="FLAC", recursive_quality=True, recursive_download=False, - method_save=1 + method_save=1, + make_zip=False ) - else: raise ValueError(f"Unsupported service: {service}") - except Exception as e: traceback.print_exc() - raise \ No newline at end of file + raise # Re-raise the exception after logging \ No newline at end of file diff --git a/routes/utils/playlist.py b/routes/utils/playlist.py index 3d05504..bb8c5ed 100644 --- a/routes/utils/playlist.py +++ b/routes/utils/playlist.py @@ -4,57 +4,95 @@ import traceback from deezspot.spotloader import SpoLogin from deezspot.deezloader import DeeLogin -def download_playlist(service, url, account): +def download_playlist(service, url, main, fallback=None): try: if service == 'spotify': - # Construct Spotify credentials path - creds_dir = os.path.join('./creds/spotify', account) - credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) - - # Initialize Spotify client - spo = SpoLogin(credentials_path=credentials_path) - - # Download Spotify playlist - spo.download_playlist( - link_playlist=url, - output_dir="./downloads/playlists", - quality_download="NORMAL", - recursive_quality=True, - recursive_download=False, - not_interface=False, - method_save=1, - make_zip=True - ) - + if fallback: + # First attempt: use DeeLogin's download_playlistspo with the 'main' (Deezer credentials) + try: + # Load Deezer credentials from 'main' under deezer directory + deezer_creds_dir = os.path.join('./creds/deezer', main) + deezer_creds_path = os.path.abspath(os.path.join(deezer_creds_dir, 'credentials.json')) + with open(deezer_creds_path, 'r') as f: + deezer_creds = json.load(f) + # Initialize DeeLogin with Deezer credentials + dl = DeeLogin( + arl=deezer_creds.get('arl', ''), + email=deezer_creds.get('email', ''), + password=deezer_creds.get('password', '') + ) + # Download using download_playlistspo + dl.download_playlistspo( + link_playlist=url, + output_dir="./downloads", + quality_download="FLAC", + recursive_quality=True, + recursive_download=False, + not_interface=False, + make_zip=True, + method_save=1 + ) + except Exception as e: + # If the first attempt fails, use the fallback Spotify main + print(f"Failed to download via Deezer fallback: {e}. Trying Spotify fallback main.") + # Load fallback Spotify credentials and attempt download + try: + spo_creds_dir = os.path.join('./creds/spotify', fallback) + spo_creds_path = os.path.abspath(os.path.join(spo_creds_dir, 'credentials.json')) + spo = SpoLogin(credentials_path=spo_creds_path) + spo.download_playlist( + link_playlist=url, + output_dir="./downloads", + quality_download="HIGH", + recursive_quality=True, + recursive_download=False, + not_interface=False, + method_save=1, + make_zip=False + ) + except Exception as e2: + # If fallback also fails, raise an error indicating both attempts failed + raise RuntimeError( + f"Both main (Deezer) and fallback (Spotify) attempts failed. " + f"Deezer error: {e}, Spotify error: {e2}" + ) from e2 + else: + # Original behavior: use Spotify main + creds_dir = os.path.join('./creds/spotify', main) + credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) + spo = SpoLogin(credentials_path=credentials_path) + spo.download_playlist( + link_playlist=url, + output_dir="./downloads", + quality_download="HIGH", + recursive_quality=True, + recursive_download=False, + not_interface=False, + method_save=1, + make_zip=False + ) elif service == 'deezer': - # Construct Deezer credentials path - creds_dir = os.path.join('./creds/deezer', account) + # Existing code for Deezer, using main as Deezer account + creds_dir = os.path.join('./creds/deezer', main) creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) - - # Load Deezer credentials with open(creds_path, 'r') as f: creds = json.load(f) - - # Initialize Deezer client dl = DeeLogin( arl=creds.get('arl', ''), email=creds.get('email', ''), password=creds.get('password', '') ) - - # Download Deezer playlist dl.download_playlistdee( link_playlist=url, - output_dir="./downloads/playlists", + output_dir="./downloads", quality_download="FLAC", recursive_quality=False, recursive_download=False, - method_save=1 + method_save=1, + make_zip=False ) - else: raise ValueError(f"Unsupported service: {service}") - except Exception as e: traceback.print_exc() - raise \ No newline at end of file + raise # Re-raise the exception after logging \ No newline at end of file diff --git a/routes/utils/search.py b/routes/utils/search.py index 4e051c5..2386640 100644 --- a/routes/utils/search.py +++ b/routes/utils/search.py @@ -1,178 +1,13 @@ from deezspot.easy_spoty import Spo -from deezspot.deezloader import API -import json -import difflib -from typing import List, Dict -def string_similarity(a: str, b: str) -> float: - return difflib.SequenceMatcher(None, a.lower(), b.lower()).ratio() - -def normalize_item(item: Dict, service: str, item_type: str) -> Dict: - normalized = { - "service": service, - "type": item_type - } - - if item_type == "track": - normalized.update({ - "id": item.get('id'), - "title": item.get('title') if service == "deezer" else item.get('name'), - "artists": [{"name": item['artist']['name']}] if service == "deezer" - else [{"name": a['name']} for a in item.get('artists', [])], - "album": { - "title": item['album']['title'] if service == "deezer" else item['album']['name'], - "id": item['album']['id'] if service == "deezer" else item['album'].get('id'), - }, - "duration": item.get('duration') if service == "deezer" else item.get('duration_ms'), - "url": item.get('link') if service == "deezer" else item.get('external_urls', {}).get('spotify'), - "isrc": item.get('isrc') if service == "deezer" else item.get('external_ids', {}).get('isrc') - }) - - elif item_type == "album": - normalized.update({ - "id": item.get('id'), - "title": item.get('title') if service == "deezer" else item.get('name'), - "artists": [{"name": item['artist']['name']}] if service == "deezer" - else [{"name": a['name']} for a in item.get('artists', [])], - "total_tracks": item.get('nb_tracks') if service == "deezer" else item.get('total_tracks'), - "release_date": item.get('release_date'), - "url": item.get('link') if service == "deezer" else item.get('external_urls', {}).get('spotify'), - "images": [ - {"url": item.get('cover_xl')}, - {"url": item.get('cover_big')}, - {"url": item.get('cover_medium')} - ] if service == "deezer" else item.get('images', []) - }) - - elif item_type == "artist": - normalized.update({ - "id": item.get('id'), - "name": item.get('name'), - "url": item.get('link') if service == "deezer" else item.get('external_urls', {}).get('spotify'), - "images": [ - {"url": item.get('picture_xl')}, - {"url": item.get('picture_big')}, - {"url": item.get('picture_medium')} - ] if service == "deezer" else item.get('images', []) - }) - - else: # For playlists, episodes, etc. - normalized.update({ - "id": item.get('id'), - "title": item.get('title') if service == "deezer" else item.get('name'), - "url": item.get('link') if service == "deezer" else item.get('external_urls', {}).get('spotify'), - "description": item.get('description'), - "owner": item.get('user', {}).get('name') if service == "deezer" else item.get('owner', {}).get('display_name') - }) - - return {k: v for k, v in normalized.items() if v is not None} - -def is_same_item(deezer_item: Dict, spotify_item: Dict, item_type: str) -> bool: - deezer_normalized = normalize_item(deezer_item, "deezer", item_type) - spotify_normalized = normalize_item(spotify_item, "spotify", item_type) - - if item_type == "track": - title_match = string_similarity(deezer_normalized['title'], spotify_normalized['title']) >= 0.8 - artist_match = string_similarity( - deezer_normalized['artists'][0]['name'], - spotify_normalized['artists'][0]['name'] - ) >= 0.8 - album_match = string_similarity( - deezer_normalized['album']['title'], - spotify_normalized['album']['title'] - ) >= 0.9 - return title_match and artist_match and album_match - - if item_type == "album": - title_match = string_similarity(deezer_normalized['title'], spotify_normalized['title']) >= 0.8 - artist_match = string_similarity( - deezer_normalized['artists'][0]['name'], - spotify_normalized['artists'][0]['name'] - ) >= 0.8 - tracks_match = deezer_normalized['total_tracks'] == spotify_normalized['total_tracks'] - return title_match and artist_match and tracks_match - - if item_type == "artist": - name_match = string_similarity(deezer_normalized['name'], spotify_normalized['name']) >= 0.85 - return name_match - - return False - -def process_results(deezer_results: Dict, spotify_results: Dict, search_type: str) -> List[Dict]: - combined = [] - processed_spotify_ids = set() - - for deezer_item in deezer_results.get('data', []): - match_found = False - normalized_deezer = normalize_item(deezer_item, "deezer", search_type) - - for spotify_item in spotify_results.get('items', []): - if is_same_item(deezer_item, spotify_item, search_type): - processed_spotify_ids.add(spotify_item['id']) - match_found = True - break - - combined.append(normalized_deezer) - - for spotify_item in spotify_results.get('items', []): - if spotify_item['id'] not in processed_spotify_ids: - combined.append(normalize_item(spotify_item, "spotify", search_type)) - - return combined - -def search_and_combine( +def search( query: str, search_type: str, - service: str = "both", limit: int = 3 -) -> List[Dict]: - if search_type == "playlist" and service == "both": - raise ValueError("Playlist search requires explicit service selection (deezer or spotify)") +) -> dict: + # Initialize the Spotify client + Spo.__init__() - if search_type == "episode" and service != "spotify": - raise ValueError("Episode search is only available for Spotify") - - deezer_data = [] - spotify_items = [] - - # Deezer search with limit - if service in ["both", "deezer"] and search_type != "episode": - deezer_api = API() - deezer_methods = { - 'track': deezer_api.search_track, - 'album': deezer_api.search_album, - 'artist': deezer_api.search_artist, - 'playlist': deezer_api.search_playlist - } - deezer_method = deezer_methods.get(search_type, deezer_api.search) - deezer_response = deezer_method(query, limit=limit) - deezer_data = deezer_response.get('data', [])[:limit] - - if service == "deezer": - return [normalize_item(item, "deezer", search_type) for item in deezer_data] - - # Spotify search with limit - if service in ["both", "spotify"]: - Spo.__init__() - spotify_response = Spo.search(query=query, search_type=search_type, limit=limit) - - if search_type == "episode": - spotify_items = spotify_response.get('episodes', {}).get('items', [])[:limit] - else: - spotify_items = spotify_response.get('tracks', {}).get('items', - spotify_response.get('albums', {}).get('items', - spotify_response.get('artists', {}).get('items', - spotify_response.get('playlists', {}).get('items', []))))[:limit] - - if service == "spotify": - return [normalize_item(item, "spotify", search_type) for item in spotify_items] - - # Combined results - if service == "both" and search_type != "playlist": - return process_results( - {"data": deezer_data}, - {"items": spotify_items}, - search_type - )[:limit] - - return [] \ No newline at end of file + # Perform the Spotify search and return the raw response + spotify_response = Spo.search(query=query, search_type=search_type, limit=limit) + return spotify_response \ No newline at end of file diff --git a/routes/utils/track.py b/routes/utils/track.py index 8b62eb7..f5398fb 100644 --- a/routes/utils/track.py +++ b/routes/utils/track.py @@ -4,56 +4,80 @@ import traceback from deezspot.spotloader import SpoLogin from deezspot.deezloader import DeeLogin -def download_track(service, url, account): +def download_track(service, url, main, fallback=None): try: if service == 'spotify': - # Construct Spotify credentials path - creds_dir = os.path.join('./creds/spotify', account) - credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) - - # Initialize Spotify client - spo = SpoLogin(credentials_path=credentials_path) - - # Download Spotify track - spo.download_track( - link_track=url, - output_dir="./downloads/tracks", - quality_download="NORMAL", - recursive_quality=False, - recursive_download=False, - not_interface=False, - method_save=1 - ) - + if fallback: + # First attempt: use Deezer's download_trackspo with 'main' (Deezer credentials) + try: + deezer_creds_dir = os.path.join('./creds/deezer', main) + deezer_creds_path = os.path.abspath(os.path.join(deezer_creds_dir, 'credentials.json')) + with open(deezer_creds_path, 'r') as f: + deezer_creds = json.load(f) + dl = DeeLogin( + arl=deezer_creds.get('arl', ''), + email=deezer_creds.get('email', ''), + password=deezer_creds.get('password', '') + ) + dl.download_trackspo( + link_track=url, + output_dir="./downloads", + quality_download="FLAC", + recursive_quality=False, + recursive_download=False, + not_interface=False, + method_save=1 + ) + except Exception as e: + # Fallback to Spotify credentials if Deezer fails + print(f"Failed to download via Deezer fallback: {e}. Trying Spotify fallback.") + spo_creds_dir = os.path.join('./creds/spotify', fallback) + spo_creds_path = os.path.abspath(os.path.join(spo_creds_dir, 'credentials.json')) + spo = SpoLogin(credentials_path=spo_creds_path) + spo.download_track( + link_track=url, + output_dir="./downloads", + quality_download="HIGH", + recursive_quality=False, + recursive_download=False, + not_interface=False, + method_save=1 + ) + else: + # Directly use Spotify main account + creds_dir = os.path.join('./creds/spotify', main) + credentials_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) + spo = SpoLogin(credentials_path=credentials_path) + spo.download_track( + link_track=url, + output_dir="./downloads", + quality_download="HIGH", + recursive_quality=False, + recursive_download=False, + not_interface=False, + method_save=1 + ) elif service == 'deezer': - # Construct Deezer credentials path - creds_dir = os.path.join('./creds/deezer', account) + # Deezer download logic remains unchanged + creds_dir = os.path.join('./creds/deezer', main) creds_path = os.path.abspath(os.path.join(creds_dir, 'credentials.json')) - - # Load Deezer credentials with open(creds_path, 'r') as f: creds = json.load(f) - - # Initialize Deezer client dl = DeeLogin( arl=creds.get('arl', ''), email=creds.get('email', ''), password=creds.get('password', '') ) - - # Download Deezer track dl.download_trackdee( link_track=url, - output_dir="./downloads/tracks", + output_dir="./downloads", quality_download="FLAC", recursive_quality=False, recursive_download=False, method_save=1 ) - else: raise ValueError(f"Unsupported service: {service}") - except Exception as e: traceback.print_exc() raise \ No newline at end of file