From c6204ada0059eefd84be66d7d8d8c2714f9f937b Mon Sep 17 00:00:00 2001 From: "cool.gitter.choco" Date: Sat, 15 Mar 2025 10:23:41 -0600 Subject: [PATCH] =?UTF-8?q?si=20100=20a=C3=B1os=20vivo,=20100=20a=C3=B1os?= =?UTF-8?q?=20chingas=20a=20tu=20madre=20spotify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + app.py | 23 ++-- requirements.txt | 4 +- routes/album.py | 20 ++- routes/artist.py | 20 ++- routes/credentials.py | 70 ++++++++-- routes/playlist.py | 20 ++- routes/search.py | 41 +++++- routes/track.py | 20 ++- routes/utils/album.py | 46 ++++++- routes/utils/artist.py | 25 +++- routes/utils/credentials.py | 162 ++++++++++++++++------ routes/utils/get_info.py | 27 +++- routes/utils/playlist.py | 44 +++++- routes/utils/search.py | 41 +++++- routes/utils/track.py | 48 ++++++- static/css/config/config.css | 71 +++++++++- static/js/album.js | 13 +- static/js/artist.js | 13 +- static/js/config.js | 259 ++++++++++++++++++++++++++++++----- static/js/main.js | 12 +- static/js/playlist.js | 16 ++- static/js/track.js | 14 +- templates/config.html | 5 +- 24 files changed, 869 insertions(+), 146 deletions(-) diff --git a/.gitignore b/.gitignore index 33ebb82..0914cfb 100755 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ routes/utils/__pycache__/credentials.cpython-312.pyc routes/utils/__pycache__/search.cpython-312.pyc search_test.py config/main.json +.cache diff --git a/app.py b/app.py index 31eccc0..a8f07bc 100755 --- a/app.py +++ b/app.py @@ -106,12 +106,19 @@ def create_app(): return app +app = create_app() + if __name__ == '__main__': - # Configure waitress logger - logger = logging.getLogger('waitress') - logger.setLevel(logging.INFO) - - app = create_app() - logging.info("Starting Flask server on port 7171") - from waitress import serve - serve(app, host='0.0.0.0', port=7171) + import os + + DEBUG = os.getenv("FLASK_DEBUG", "0") == "1" + + if DEBUG: + logging.info("Starting Flask in DEBUG mode on port 7171") + app.run(debug=True, host='0.0.0.0', port=7171) # Use Flask's built-in server + else: + logger = logging.getLogger('waitress') + logger.setLevel(logging.INFO) + logging.info("Starting Flask server with Waitress on port 7171") + serve(app, host='0.0.0.0', port=7171) # Use Waitress for production + diff --git a/requirements.txt b/requirements.txt index 187a6d3..dcfaf5d 100755 --- a/requirements.txt +++ b/requirements.txt @@ -29,8 +29,8 @@ PyYAML==6.0.2 redis==5.2.1 requests==2.30.0 sniffio==1.3.1 -spotipy==2.25.0 -spotipy_anon==1.3 +spotipy +spotipy_anon starlette==0.45.3 tqdm==4.67.1 typing_extensions==4.12.2 diff --git a/routes/album.py b/routes/album.py index 24bef6e..d72fc38 100755 --- a/routes/album.py +++ b/routes/album.py @@ -147,6 +147,8 @@ def get_album_info(): Expects a query parameter 'id' that contains the Spotify album ID. """ spotify_id = request.args.get('id') + main = request.args.get('main', '') + if not spotify_id: return Response( json.dumps({"error": "Missing parameter: id"}), @@ -154,10 +156,26 @@ def get_album_info(): mimetype='application/json' ) + # If main parameter is not provided in the request, get it from config + if not main: + from routes.config import get_config + config = get_config() + if config and 'spotify' in config: + main = config['spotify'] + print(f"Using main from config for album info: {main}") + + # Validate main parameter + if not main: + return Response( + json.dumps({"error": "Missing parameter: main (Spotify account)"}), + status=400, + mimetype='application/json' + ) + try: # Import and use the get_spotify_info function from the utility module. from routes.utils.get_info import get_spotify_info - album_info = get_spotify_info(spotify_id, "album") + album_info = get_spotify_info(spotify_id, "album", main=main) return Response( json.dumps(album_info), status=200, diff --git a/routes/artist.py b/routes/artist.py index 9cf45f6..e5764e5 100644 --- a/routes/artist.py +++ b/routes/artist.py @@ -169,6 +169,8 @@ def get_artist_info(): Expects a query parameter 'id' with the Spotify artist ID. """ spotify_id = request.args.get('id') + main = request.args.get('main', '') + if not spotify_id: return Response( json.dumps({"error": "Missing parameter: id"}), @@ -176,9 +178,25 @@ def get_artist_info(): mimetype='application/json' ) + # If main parameter is not provided in the request, get it from config + if not main: + from routes.config import get_config + config = get_config() + if config and 'spotify' in config: + main = config['spotify'] + print(f"Using main from config for artist info: {main}") + + # Validate main parameter + if not main: + return Response( + json.dumps({"error": "Missing parameter: main (Spotify account)"}), + status=400, + mimetype='application/json' + ) + try: from routes.utils.get_info import get_spotify_info - artist_info = get_spotify_info(spotify_id, "artist") + artist_info = get_spotify_info(spotify_id, "artist", main=main) return Response( json.dumps(artist_info), status=200, diff --git a/routes/credentials.py b/routes/credentials.py index 1b24ffd..0461de6 100755 --- a/routes/credentials.py +++ b/routes/credentials.py @@ -6,6 +6,7 @@ from routes.utils.credentials import ( delete_credential, edit_credential ) +from pathlib import Path credentials_bp = Blueprint('credentials', __name__) @@ -21,22 +22,27 @@ def handle_list_credentials(service): @credentials_bp.route('//', methods=['GET', 'POST', 'PUT', 'DELETE']) def handle_single_credential(service, name): try: + # Get credential type from query parameters, default to 'credentials' + cred_type = request.args.get('type', 'credentials') + if cred_type not in ['credentials', 'search']: + return jsonify({"error": "Invalid credential type. Must be 'credentials' or 'search'"}), 400 + if request.method == 'GET': - return jsonify(get_credential(service, name)) + return jsonify(get_credential(service, name, cred_type)) elif request.method == 'POST': data = request.get_json() - create_credential(service, name, data) - return jsonify({"message": "Credential created successfully"}), 201 + create_credential(service, name, data, cred_type) + return jsonify({"message": f"{cred_type.capitalize()} credential created successfully"}), 201 elif request.method == 'PUT': data = request.get_json() - edit_credential(service, name, data) - return jsonify({"message": "Credential updated successfully"}) + edit_credential(service, name, data, cred_type) + return jsonify({"message": f"{cred_type.capitalize()} credential updated successfully"}) elif request.method == 'DELETE': - delete_credential(service, name) - return jsonify({"message": "Credential deleted successfully"}) + delete_credential(service, name, cred_type if cred_type != 'credentials' else None) + return jsonify({"message": f"{cred_type.capitalize()} credential deleted successfully"}) except (ValueError, FileNotFoundError, FileExistsError) as e: status_code = 400 @@ -48,15 +54,59 @@ def handle_single_credential(service, name): except Exception as e: return jsonify({"error": str(e)}), 500 +@credentials_bp.route('/search//', methods=['GET', 'POST', 'PUT']) +def handle_search_credential(service, name): + """Special route specifically for search credentials""" + try: + if request.method == 'GET': + return jsonify(get_credential(service, name, 'search')) + + elif request.method in ['POST', 'PUT']: + data = request.get_json() + + # Validate required fields + if not data.get('client_id') or not data.get('client_secret'): + return jsonify({"error": "Both client_id and client_secret are required"}), 400 + + # For POST, first check if the credentials directory exists + if request.method == 'POST' and not any(Path(f'./creds/{service}/{name}').glob('*.json')): + return jsonify({"error": f"Account '{name}' doesn't exist. Create it first."}), 404 + + # Create or update search credentials + method_func = create_credential if request.method == 'POST' else edit_credential + method_func(service, name, data, 'search') + + action = "created" if request.method == 'POST' else "updated" + return jsonify({"message": f"Search credentials {action} successfully"}) + + except (ValueError, FileNotFoundError) as e: + status_code = 400 if isinstance(e, ValueError) else 404 + return jsonify({"error": str(e)}), status_code + except Exception as e: + return jsonify({"error": str(e)}), 500 + @credentials_bp.route('/all/', methods=['GET']) def handle_all_credentials(service): try: credentials = [] for name in list_credentials(service): - credentials.append({ + # For each credential, get both the main credentials and search credentials if they exist + cred_data = { "name": name, - "data": get_credential(service, name) - }) + "credentials": get_credential(service, name, 'credentials') + } + + # For Spotify accounts, also try to get search credentials + if service == 'spotify': + try: + search_creds = get_credential(service, name, 'search') + if search_creds: # Only add if not empty + cred_data["search"] = search_creds + except: + pass # Ignore errors if search.json doesn't exist + + credentials.append(cred_data) + return jsonify(credentials) except (ValueError, FileNotFoundError) as e: status_code = 400 if isinstance(e, ValueError) else 404 diff --git a/routes/playlist.py b/routes/playlist.py index 1cd50a5..4b65152 100755 --- a/routes/playlist.py +++ b/routes/playlist.py @@ -96,6 +96,8 @@ def get_playlist_info(): Expects a query parameter 'id' that contains the Spotify playlist ID. """ spotify_id = request.args.get('id') + main = request.args.get('main', '') + if not spotify_id: return Response( json.dumps({"error": "Missing parameter: id"}), @@ -103,10 +105,26 @@ def get_playlist_info(): mimetype='application/json' ) + # If main parameter is not provided in the request, get it from config + if not main: + from routes.config import get_config + config = get_config() + if config and 'spotify' in config: + main = config['spotify'] + print(f"Using main from config for playlist info: {main}") + + # Validate main parameter + if not main: + return Response( + json.dumps({"error": "Missing parameter: main (Spotify account)"}), + status=400, + mimetype='application/json' + ) + try: # Import and use the get_spotify_info function from the utility module. from routes.utils.get_info import get_spotify_info - playlist_info = get_spotify_info(spotify_id, "playlist") + playlist_info = get_spotify_info(spotify_id, "playlist", main=main) return Response( json.dumps(playlist_info), status=200, diff --git a/routes/search.py b/routes/search.py index e7756a1..7e90635 100755 --- a/routes/search.py +++ b/routes/search.py @@ -1,5 +1,7 @@ from flask import Blueprint, jsonify, request +import logging from routes.utils.search import search # Corrected import +from routes.config import get_config # Import get_config function search_bp = Blueprint('search', __name__) @@ -10,6 +12,16 @@ def handle_search(): query = request.args.get('q', '') search_type = request.args.get('search_type', '') limit = int(request.args.get('limit', 10)) + main = request.args.get('main', '') # Get the main parameter for account selection + + # If main parameter is not provided in the request, get it from config + if not main: + config = get_config() + if config and 'spotify' in config: + main = config['spotify'] + print(f"Using main from config: {main}") + + print(f"Search request: query={query}, type={search_type}, limit={limit}, main={main}") # Validate parameters if not query: @@ -23,15 +35,38 @@ def handle_search(): raw_results = search( query=query, search_type=search_type, # Fixed parameter name - limit=limit + limit=limit, + main=main # Pass the main parameter ) + print(f"Search response keys: {raw_results.keys() if raw_results else 'None'}") + + # Extract items from the appropriate section of the response based on search_type + items = [] + if raw_results and search_type + 's' in raw_results: + # Handle plural form (e.g., 'tracks' instead of 'track') + type_key = search_type + 's' + print(f"Using type key: {type_key}") + items = raw_results[type_key].get('items', []) + elif raw_results and search_type in raw_results: + # Handle singular form + print(f"Using type key: {search_type}") + items = raw_results[search_type].get('items', []) + + print(f"Found {len(items)} items") + + # Return both the items array and the full data for debugging return jsonify({ - 'data': raw_results, + 'items': items, + 'data': raw_results, # Include full data for debugging 'error': None }) except ValueError as e: + print(f"ValueError in search: {str(e)}") return jsonify({'error': str(e)}), 400 except Exception as e: - return jsonify({'error': 'Internal server error'}), 500 \ No newline at end of file + import traceback + print(f"Exception in search: {str(e)}") + print(traceback.format_exc()) + return jsonify({'error': f'Internal server error: {str(e)}'}), 500 \ No newline at end of file diff --git a/routes/track.py b/routes/track.py index b86f296..32f6453 100755 --- a/routes/track.py +++ b/routes/track.py @@ -149,6 +149,8 @@ def get_track_info(): Expects a query parameter 'id' that contains the Spotify track ID. """ spotify_id = request.args.get('id') + main = request.args.get('main', '') + if not spotify_id: return Response( json.dumps({"error": "Missing parameter: id"}), @@ -156,10 +158,26 @@ def get_track_info(): mimetype='application/json' ) + # If main parameter is not provided in the request, get it from config + if not main: + from routes.config import get_config + config = get_config() + if config and 'spotify' in config: + main = config['spotify'] + print(f"Using main from config for track info: {main}") + + # Validate main parameter + if not main: + return Response( + json.dumps({"error": "Missing parameter: main (Spotify account)"}), + status=400, + mimetype='application/json' + ) + try: # Import and use the get_spotify_info function from the utility module. from routes.utils.get_info import get_spotify_info - track_info = get_spotify_info(spotify_id, "track") + track_info = get_spotify_info(spotify_id, "track", main=main) return Response( json.dumps(track_info), status=200, diff --git a/routes/utils/album.py b/routes/utils/album.py index a64b431..af8a804 100755 --- a/routes/utils/album.py +++ b/routes/utils/album.py @@ -3,6 +3,7 @@ import json import traceback from deezspot.spotloader import SpoLogin from deezspot.deezloader import DeeLogin +from pathlib import Path def download_album( service, @@ -16,6 +17,19 @@ def download_album( custom_track_format="%tracknum%. %music% - %artist%" ): try: + # Load Spotify client credentials if available + spotify_client_id = None + spotify_client_secret = None + search_creds_path = Path(f'./creds/spotify/{main}/search.json') + if search_creds_path.exists(): + try: + with open(search_creds_path, 'r') as f: + search_creds = json.load(f) + spotify_client_id = search_creds.get('client_id') + spotify_client_secret = search_creds.get('client_secret') + except Exception as e: + print(f"Error loading Spotify search credentials: {e}") + if service == 'spotify': if fallback: if quality is None: @@ -29,9 +43,11 @@ def download_album( 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 + # Initialize DeeLogin with Deezer credentials and Spotify client credentials dl = DeeLogin( arl=deezer_creds.get('arl', ''), + spotify_client_id=spotify_client_id, + spotify_client_secret=spotify_client_secret ) # Download using download_albumspo; pass real_time_dl accordingly and the custom formatting dl.download_albumspo( @@ -51,7 +67,25 @@ def download_album( 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) + + # Check for Spotify client credentials in fallback account + fallback_client_id = spotify_client_id + fallback_client_secret = spotify_client_secret + fallback_search_path = Path(f'./creds/spotify/{fallback}/search.json') + if fallback_search_path.exists(): + try: + with open(fallback_search_path, 'r') as f: + fallback_search_creds = json.load(f) + fallback_client_id = fallback_search_creds.get('client_id') + fallback_client_secret = fallback_search_creds.get('client_secret') + except Exception as e: + print(f"Error loading fallback Spotify search credentials: {e}") + + spo = SpoLogin( + credentials_path=spo_creds_path, + spotify_client_id=fallback_client_id, + spotify_client_secret=fallback_client_secret + ) spo.download_album( link_album=url, output_dir="./downloads", @@ -77,7 +111,11 @@ def download_album( quality = 'HIGH' 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 = SpoLogin( + credentials_path=credentials_path, + spotify_client_id=spotify_client_id, + spotify_client_secret=spotify_client_secret + ) spo.download_album( link_album=url, output_dir="./downloads", @@ -101,6 +139,8 @@ def download_album( creds = json.load(f) dl = DeeLogin( arl=creds.get('arl', ''), + spotify_client_id=spotify_client_id, + spotify_client_secret=spotify_client_secret ) dl.download_albumdee( link_album=url, diff --git a/routes/utils/artist.py b/routes/utils/artist.py index b441584..8a3af38 100644 --- a/routes/utils/artist.py +++ b/routes/utils/artist.py @@ -1,5 +1,6 @@ import json import traceback +from pathlib import Path from deezspot.easy_spoty import Spo from deezspot.libutils.utils import get_ids, link_is_valid @@ -11,7 +12,7 @@ def log_json(message_dict): print(json.dumps(message_dict)) -def get_artist_discography(url, album_type='album,single,compilation,appears_on'): +def get_artist_discography(url, main, album_type='album,single,compilation,appears_on'): """ Validate the URL, extract the artist ID, and retrieve the discography. """ @@ -21,6 +22,26 @@ def get_artist_discography(url, album_type='album,single,compilation,appears_on' # This will raise an exception if the link is invalid. link_is_valid(link=url) + + # Initialize Spotify API with credentials + spotify_client_id = None + spotify_client_secret = None + search_creds_path = Path(f'./creds/spotify/{main}/search.json') + if search_creds_path.exists(): + try: + with open(search_creds_path, 'r') as f: + search_creds = json.load(f) + spotify_client_id = search_creds.get('client_id') + spotify_client_secret = search_creds.get('client_secret') + except Exception as e: + log_json({"status": "error", "message": f"Error loading Spotify search credentials: {e}"}) + raise + + # Initialize the Spotify client with credentials + if spotify_client_id and spotify_client_secret: + Spo.__init__(spotify_client_id, spotify_client_secret) + else: + raise ValueError("No Spotify credentials found") try: artist_id = get_ids(url) @@ -50,7 +71,7 @@ def download_artist_albums(service, url, main, fallback=None, quality=None, album PRG filenames. """ try: - discography = get_artist_discography(url, album_type=album_type) + discography = get_artist_discography(url, main, album_type=album_type) except Exception as e: log_json({"status": "error", "message": f"Error retrieving artist discography: {e}"}) raise diff --git a/routes/utils/credentials.py b/routes/utils/credentials.py index 0b16281..1d7c97b 100755 --- a/routes/utils/credentials.py +++ b/routes/utils/credentials.py @@ -2,28 +2,39 @@ import json from pathlib import Path import shutil -def get_credential(service, name): +def get_credential(service, name, cred_type='credentials'): """ Retrieves existing credential contents by name. Args: service (str): 'spotify' or 'deezer' name (str): Custom name of the credential to retrieve + cred_type (str): 'credentials' or 'search' - type of credential file to read Returns: dict: Credential data as dictionary Raises: FileNotFoundError: If the credential doesn't exist - ValueError: For invalid service name + ValueError: For invalid service name or cred_type """ if service not in ['spotify', 'deezer']: raise ValueError("Service must be 'spotify' or 'deezer'") + if cred_type not in ['credentials', 'search']: + raise ValueError("Credential type must be 'credentials' or 'search'") + + # For Deezer, only credentials.json is supported + if service == 'deezer' and cred_type == 'search': + raise ValueError("Search credentials are only supported for Spotify") + creds_dir = Path('./creds') / service / name - file_path = creds_dir / 'credentials.json' + file_path = creds_dir / f'{cred_type}.json' if not file_path.exists(): + if cred_type == 'search': + # Return empty dict if search.json doesn't exist + return {} raise FileNotFoundError(f"Credential '{name}' not found for {service}") with open(file_path, 'r') as f: @@ -52,7 +63,7 @@ def list_credentials(service): return [d.name for d in service_dir.iterdir() if d.is_dir()] -def create_credential(service, name, data): +def create_credential(service, name, data, cred_type='credentials'): """ Creates a new credential file for the specified service. @@ -60,63 +71,104 @@ def create_credential(service, name, data): service (str): 'spotify' or 'deezer' name (str): Custom name for the credential data (dict): Dictionary containing the credential data + cred_type (str): 'credentials' or 'search' - type of credential file to create Raises: ValueError: If service is invalid, data has invalid fields, or missing required fields - FileExistsError: If the credential directory already exists + FileExistsError: If the credential directory already exists (for credentials.json) """ if service not in ['spotify', 'deezer']: raise ValueError("Service must be 'spotify' or 'deezer'") + if cred_type not in ['credentials', 'search']: + raise ValueError("Credential type must be 'credentials' or 'search'") + + # For Deezer, only credentials.json is supported + if service == 'deezer' and cred_type == 'search': + raise ValueError("Search credentials are only supported for Spotify") + # Validate data structure required_fields = [] allowed_fields = [] - if service == 'spotify': - required_fields = ['username', 'credentials'] - allowed_fields = required_fields + ['type'] - data['type'] = 'AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS' - else: - required_fields = ['arl'] + + if cred_type == 'credentials': + if service == 'spotify': + required_fields = ['username', 'credentials'] + allowed_fields = required_fields + ['type'] + data['type'] = 'AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS' + else: + required_fields = ['arl'] + allowed_fields = required_fields.copy() + # Check for extra fields + extra_fields = set(data.keys()) - set(allowed_fields) + if extra_fields: + raise ValueError(f"Deezer credentials can only contain 'arl'. Extra fields found: {', '.join(extra_fields)}") + elif cred_type == 'search': + required_fields = ['client_id', 'client_secret'] allowed_fields = required_fields.copy() # Check for extra fields extra_fields = set(data.keys()) - set(allowed_fields) if extra_fields: - raise ValueError(f"Deezer credentials can only contain 'arl'. Extra fields found: {', '.join(extra_fields)}") + raise ValueError(f"Search credentials can only contain 'client_id' and 'client_secret'. Extra fields found: {', '.join(extra_fields)}") for field in required_fields: if field not in data: - raise ValueError(f"Missing required field for {service}: {field}") + raise ValueError(f"Missing required field for {cred_type}: {field}") # Create directory creds_dir = Path('./creds') / service / name - try: - creds_dir.mkdir(parents=True, exist_ok=False) - except FileExistsError: - raise FileExistsError(f"Credential '{name}' already exists for {service}") + if cred_type == 'credentials': + try: + creds_dir.mkdir(parents=True, exist_ok=False) + except FileExistsError: + raise FileExistsError(f"Credential '{name}' already exists for {service}") + else: + # For search.json, ensure the directory exists (it should if credentials.json exists) + if not creds_dir.exists(): + raise FileNotFoundError(f"Credential '{name}' not found for {service}") # Write credentials file - file_path = creds_dir / 'credentials.json' + file_path = creds_dir / f'{cred_type}.json' with open(file_path, 'w') as f: json.dump(data, f, indent=4) -def delete_credential(service, name): +def delete_credential(service, name, cred_type=None): """ - Deletes an existing credential directory. + Deletes an existing credential directory or specific credential file. Args: service (str): 'spotify' or 'deezer' name (str): Name of the credential to delete + cred_type (str, optional): If specified ('credentials' or 'search'), only deletes + that specific file. If None, deletes the whole directory. Raises: - FileNotFoundError: If the credential directory does not exist + FileNotFoundError: If the credential directory or specified file does not exist """ creds_dir = Path('./creds') / service / name - if not creds_dir.exists(): - raise FileNotFoundError(f"Credential '{name}' not found for {service}") - shutil.rmtree(creds_dir) + if cred_type: + if cred_type not in ['credentials', 'search']: + raise ValueError("Credential type must be 'credentials' or 'search'") + + file_path = creds_dir / f'{cred_type}.json' + if not file_path.exists(): + raise FileNotFoundError(f"{cred_type.capitalize()} credential '{name}' not found for {service}") + + # Delete just the specific file + file_path.unlink() + + # If it was credentials.json and no other credential files remain, also delete the directory + if cred_type == 'credentials' and not any(creds_dir.iterdir()): + creds_dir.rmdir() + else: + # Delete the entire directory + if not creds_dir.exists(): + raise FileNotFoundError(f"Credential '{name}' not found for {service}") + + shutil.rmtree(creds_dir) -def edit_credential(service, name, new_data): +def edit_credential(service, name, new_data, cred_type='credentials'): """ Edits an existing credential file. @@ -124,6 +176,7 @@ def edit_credential(service, name, new_data): service (str): 'spotify' or 'deezer' name (str): Name of the credential to edit new_data (dict): Dictionary containing fields to update + cred_type (str): 'credentials' or 'search' - type of credential file to edit Raises: FileNotFoundError: If the credential does not exist @@ -132,46 +185,67 @@ def edit_credential(service, name, new_data): if service not in ['spotify', 'deezer']: raise ValueError("Service must be 'spotify' or 'deezer'") - # Load existing data - creds_dir = Path('./creds') / service / name - file_path = creds_dir / 'credentials.json' - if not file_path.exists(): - raise FileNotFoundError(f"Credential '{name}' not found for {service}") + if cred_type not in ['credentials', 'search']: + raise ValueError("Credential type must be 'credentials' or 'search'") - with open(file_path, 'r') as f: - data = json.load(f) + # For Deezer, only credentials.json is supported + if service == 'deezer' and cred_type == 'search': + raise ValueError("Search credentials are only supported for Spotify") + + # Get file path + creds_dir = Path('./creds') / service / name + file_path = creds_dir / f'{cred_type}.json' + + # For search.json, create if it doesn't exist + if cred_type == 'search' and not file_path.exists(): + if not creds_dir.exists(): + raise FileNotFoundError(f"Credential '{name}' not found for {service}") + data = {} + else: + # Load existing data + if not file_path.exists(): + raise FileNotFoundError(f"{cred_type.capitalize()} credential '{name}' not found for {service}") + + with open(file_path, 'r') as f: + data = json.load(f) # Validate new_data fields allowed_fields = [] - if service == 'spotify': - allowed_fields = ['username', 'credentials'] - else: - allowed_fields = ['arl'] + if cred_type == 'credentials': + if service == 'spotify': + allowed_fields = ['username', 'credentials'] + else: + allowed_fields = ['arl'] + else: # search.json + allowed_fields = ['client_id', 'client_secret'] for key in new_data.keys(): if key not in allowed_fields: - raise ValueError(f"Invalid field '{key}' for {service} credentials") + raise ValueError(f"Invalid field '{key}' for {cred_type} credentials") # Update data data.update(new_data) # For Deezer: Strip all fields except 'arl' - if service == 'deezer': + if service == 'deezer' and cred_type == 'credentials': if 'arl' not in data: raise ValueError("Missing 'arl' field for Deezer credential") data = {'arl': data['arl']} # Ensure required fields are present required_fields = [] - if service == 'spotify': - required_fields = ['username', 'credentials', 'type'] - data['type'] = 'AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS' - else: - required_fields = ['arl'] + if cred_type == 'credentials': + if service == 'spotify': + required_fields = ['username', 'credentials', 'type'] + data['type'] = 'AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS' + else: + required_fields = ['arl'] + else: # search.json + required_fields = ['client_id', 'client_secret'] for field in required_fields: if field not in data: - raise ValueError(f"Missing required field '{field}' after update for {service}") + raise ValueError(f"Missing required field '{field}' after update for {cred_type}") # Save updated data with open(file_path, 'w') as f: diff --git a/routes/utils/get_info.py b/routes/utils/get_info.py index 86543ec..9375dd5 100644 --- a/routes/utils/get_info.py +++ b/routes/utils/get_info.py @@ -1,10 +1,31 @@ #!/usr/bin/python3 from deezspot.easy_spoty import Spo +import json +from pathlib import Path -Spo() - -def get_spotify_info(spotify_id, spotify_type): +def get_spotify_info(spotify_id, spotify_type, main=None): + client_id = None + client_secret = None + if spotify_id: + search_creds_path = Path(f'./creds/spotify/{main}/search.json') + print(search_creds_path) + if search_creds_path.exists(): + try: + with open(search_creds_path, 'r') as f: + search_creds = json.load(f) + client_id = search_creds.get('client_id') + print(client_id) + client_secret = search_creds.get('client_secret') + print(client_secret) + except Exception as e: + print(f"Error loading search credentials: {e}") + + # Initialize the Spotify client with credentials (if available) + if client_id and client_secret: + Spo.__init__(client_id, client_secret) + else: + raise ValueError("No Spotify credentials found") if spotify_type == "track": return Spo.get_track(spotify_id) elif spotify_type == "album": diff --git a/routes/utils/playlist.py b/routes/utils/playlist.py index 659309f..5f0abab 100755 --- a/routes/utils/playlist.py +++ b/routes/utils/playlist.py @@ -3,6 +3,7 @@ import json import traceback from deezspot.spotloader import SpoLogin from deezspot.deezloader import DeeLogin +from pathlib import Path def download_playlist( service, @@ -16,6 +17,19 @@ def download_playlist( custom_track_format="%tracknum%. %music% - %artist%" ): try: + # Load Spotify client credentials if available + spotify_client_id = None + spotify_client_secret = None + search_creds_path = Path(f'./creds/spotify/{main}/search.json') + if search_creds_path.exists(): + try: + with open(search_creds_path, 'r') as f: + search_creds = json.load(f) + spotify_client_id = search_creds.get('client_id') + spotify_client_secret = search_creds.get('client_secret') + except Exception as e: + print(f"Error loading Spotify search credentials: {e}") + if service == 'spotify': if fallback: if quality is None: @@ -32,6 +46,8 @@ def download_playlist( # Initialize DeeLogin with Deezer credentials dl = DeeLogin( arl=deezer_creds.get('arl', ''), + spotify_client_id=spotify_client_id, + spotify_client_secret=spotify_client_secret ) # Download using download_playlistspo; pass the custom formatting parameters. dl.download_playlistspo( @@ -51,7 +67,25 @@ def download_playlist( 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) + + # Check for Spotify client credentials in fallback account + fallback_client_id = spotify_client_id + fallback_client_secret = spotify_client_secret + fallback_search_path = Path(f'./creds/spotify/{fallback}/search.json') + if fallback_search_path.exists(): + try: + with open(fallback_search_path, 'r') as f: + fallback_search_creds = json.load(f) + fallback_client_id = fallback_search_creds.get('client_id') + fallback_client_secret = fallback_search_creds.get('client_secret') + except Exception as e: + print(f"Error loading fallback Spotify search credentials: {e}") + + spo = SpoLogin( + credentials_path=spo_creds_path, + spotify_client_id=fallback_client_id, + spotify_client_secret=fallback_client_secret + ) spo.download_playlist( link_playlist=url, output_dir="./downloads", @@ -77,7 +111,11 @@ def download_playlist( quality = 'HIGH' 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 = SpoLogin( + credentials_path=credentials_path, + spotify_client_id=spotify_client_id, + spotify_client_secret=spotify_client_secret + ) spo.download_playlist( link_playlist=url, output_dir="./downloads", @@ -101,6 +139,8 @@ def download_playlist( creds = json.load(f) dl = DeeLogin( arl=creds.get('arl', ''), + spotify_client_id=spotify_client_id, + spotify_client_secret=spotify_client_secret ) dl.download_playlistdee( link_playlist=url, diff --git a/routes/utils/search.py b/routes/utils/search.py index f87bef4..7d1072f 100755 --- a/routes/utils/search.py +++ b/routes/utils/search.py @@ -1,13 +1,44 @@ from deezspot.easy_spoty import Spo +import json +from pathlib import Path def search( query: str, search_type: str, - limit: int = 3 + limit: int = 3, + main: str = None ) -> dict: - # Initialize the Spotify client - Spo.__init__() + # If main account is specified, load client ID and secret from the account's search.json + client_id = None + client_secret = None + + if main: + search_creds_path = Path(f'./creds/spotify/{main}/search.json') + print(search_creds_path) + if search_creds_path.exists(): + try: + with open(search_creds_path, 'r') as f: + search_creds = json.load(f) + client_id = search_creds.get('client_id') + print(client_id) + client_secret = search_creds.get('client_secret') + print(client_secret) + except Exception as e: + print(f"Error loading search credentials: {e}") + + # Initialize the Spotify client with credentials (if available) + if client_id and client_secret: + Spo.__init__(client_id, client_secret) + + # Perform the Spotify search + # Note: We don't need to pass client_id and client_secret again in the search method + # as they've already been set during initialization + spotify_response = Spo.search( + query=query, + search_type=search_type, + limit=limit, + client_id=client_id, + client_secret=client_secret + ) - # Perform the Spotify search and return the raw response - spotify_response = Spo.search(query=query, search_type=search_type, limit=limit) return spotify_response diff --git a/routes/utils/track.py b/routes/utils/track.py index 0510ee7..6426553 100755 --- a/routes/utils/track.py +++ b/routes/utils/track.py @@ -3,6 +3,7 @@ import json import traceback from deezspot.spotloader import SpoLogin from deezspot.deezloader import DeeLogin +from pathlib import Path def download_track( service, @@ -16,6 +17,19 @@ def download_track( custom_track_format="%tracknum%. %music% - %artist%" ): try: + # Load Spotify client credentials if available + spotify_client_id = None + spotify_client_secret = None + search_creds_path = Path(f'./creds/spotify/{main}/search.json') + if search_creds_path.exists(): + try: + with open(search_creds_path, 'r') as f: + search_creds = json.load(f) + spotify_client_id = search_creds.get('client_id') + spotify_client_secret = search_creds.get('client_secret') + except Exception as e: + print(f"Error loading Spotify search credentials: {e}") + if service == 'spotify': if fallback: if quality is None: @@ -29,7 +43,9 @@ def download_track( with open(deezer_creds_path, 'r') as f: deezer_creds = json.load(f) dl = DeeLogin( - arl=deezer_creds.get('arl', '') + arl=deezer_creds.get('arl', ''), + spotify_client_id=spotify_client_id, + spotify_client_secret=spotify_client_secret ) dl.download_trackspo( link_track=url, @@ -46,7 +62,25 @@ def download_track( # If the first attempt fails, use the fallback Spotify credentials 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) + + # Check for Spotify client credentials in fallback account + fallback_client_id = spotify_client_id + fallback_client_secret = spotify_client_secret + fallback_search_path = Path(f'./creds/spotify/{fallback}/search.json') + if fallback_search_path.exists(): + try: + with open(fallback_search_path, 'r') as f: + fallback_search_creds = json.load(f) + fallback_client_id = fallback_search_creds.get('client_id') + fallback_client_secret = fallback_search_creds.get('client_secret') + except Exception as e: + print(f"Error loading fallback Spotify search credentials: {e}") + + spo = SpoLogin( + credentials_path=spo_creds_path, + spotify_client_id=fallback_client_id, + spotify_client_secret=fallback_client_secret + ) spo.download_track( link_track=url, output_dir="./downloads", @@ -65,7 +99,11 @@ def download_track( quality = 'HIGH' 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 = SpoLogin( + credentials_path=credentials_path, + spotify_client_id=spotify_client_id, + spotify_client_secret=spotify_client_secret + ) spo.download_track( link_track=url, output_dir="./downloads", @@ -87,7 +125,9 @@ def download_track( with open(creds_path, 'r') as f: creds = json.load(f) dl = DeeLogin( - arl=creds.get('arl', '') + arl=creds.get('arl', ''), + spotify_client_id=spotify_client_id, + spotify_client_secret=spotify_client_secret ) dl.download_trackdee( link_track=url, diff --git a/static/css/config/config.css b/static/css/config/config.css index bf17c6b..e6a95b4 100644 --- a/static/css/config/config.css +++ b/static/css/config/config.css @@ -224,6 +224,16 @@ input:checked + .slider:before { transform: translateY(-2px); } +/* No Credentials Message */ +.no-credentials { + padding: 1.5rem; + background: #2a2a2a; + border-radius: 8px; + margin-bottom: 1.5rem; + text-align: center; + color: #b3b3b3; +} + /* Credentials List */ .credentials-list { margin-bottom: 2rem; @@ -244,14 +254,49 @@ input:checked + .slider:before { background: #3a3a3a; } +/* New styling for credential info and actions */ +.credential-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.credential-name { + font-weight: 600; + font-size: 1rem; +} + +.search-credentials-status { + font-size: 0.8rem; + padding: 0.2rem 0.5rem; + border-radius: 12px; + display: inline-block; + width: fit-content; +} + +.search-credentials-status.has-api { + background: rgba(29, 185, 84, 0.2); + color: #1db954; +} + +.search-credentials-status.no-api { + background: rgba(255, 85, 85, 0.2); + color: #ff5555; +} + +.credential-actions { + display: flex; + gap: 0.5rem; +} + .credential-actions button { - margin-left: 0.5rem; - padding: 0.4rem 0.8rem; + padding: 0.5rem 0.8rem; border: none; - border-radius: 4px; + border-radius: 6px; cursor: pointer; - font-size: 0.9rem; + font-size: 0.8rem; transition: opacity 0.3s ease, transform 0.2s ease; + white-space: nowrap; } .edit-btn { @@ -259,6 +304,11 @@ input:checked + .slider:before { color: #ffffff; } +.edit-search-btn { + background: #2d6db5; + color: #ffffff; +} + .delete-btn { background: #ff5555; color: #ffffff; @@ -282,7 +332,7 @@ input:checked + .slider:before { transform: translateY(-2px); } -#serviceFields { +#serviceFields, #searchFields { margin: 1.5rem 0; } @@ -334,6 +384,7 @@ input:checked + .slider:before { margin-top: 1rem; text-align: center; font-size: 0.9rem; + min-height: 1.2rem; } /* MOBILE RESPONSIVENESS */ @@ -382,7 +433,15 @@ input:checked + .slider:before { .credential-actions { width: 100%; display: flex; - justify-content: flex-end; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.5rem; + } + + .credential-actions button { + flex: 1; + text-align: center; + padding: 0.7rem 0.5rem; } /* Adjust toggle switch size for better touch support */ diff --git a/static/js/album.js b/static/js/album.js index a1abac1..090ede4 100644 --- a/static/js/album.js +++ b/static/js/album.js @@ -9,7 +9,18 @@ document.addEventListener('DOMContentLoaded', () => { return; } - fetch(`/api/album/info?id=${encodeURIComponent(albumId)}`) + // Fetch the config to get active Spotify account first + fetch('/api/config') + .then(response => { + if (!response.ok) throw new Error('Failed to fetch config'); + return response.json(); + }) + .then(config => { + const mainAccount = config.spotify || ''; + + // Then fetch album info with the main parameter + return fetch(`/api/album/info?id=${encodeURIComponent(albumId)}&main=${mainAccount}`); + }) .then(response => { if (!response.ok) throw new Error('Network response was not ok'); return response.json(); diff --git a/static/js/artist.js b/static/js/artist.js index eb5c7b4..fe76927 100644 --- a/static/js/artist.js +++ b/static/js/artist.js @@ -10,7 +10,18 @@ document.addEventListener('DOMContentLoaded', () => { return; } - fetch(`/api/artist/info?id=${encodeURIComponent(artistId)}`) + // Fetch the config to get active Spotify account first + fetch('/api/config') + .then(response => { + if (!response.ok) throw new Error('Failed to fetch config'); + return response.json(); + }) + .then(config => { + const mainAccount = config.spotify || ''; + + // Then fetch artist info with the main parameter + return fetch(`/api/artist/info?id=${encodeURIComponent(artistId)}&main=${mainAccount}`); + }) .then(response => { if (!response.ok) throw new Error('Network response was not ok'); return response.json(); diff --git a/static/js/config.js b/static/js/config.js index 23521d2..2ff2726 100644 --- a/static/js/config.js +++ b/static/js/config.js @@ -9,6 +9,15 @@ const serviceConfig = { validator: (data) => ({ username: data.username, credentials: data.credentials + }), + // Adding search credentials fields + searchFields: [ + { id: 'client_id', label: 'Client ID', type: 'text' }, + { id: 'client_secret', label: 'Client Secret', type: 'password' } + ], + searchValidator: (data) => ({ + client_id: data.client_id, + client_secret: data.client_secret }) }, deezer: { @@ -23,6 +32,7 @@ const serviceConfig = { let currentService = 'spotify'; let currentCredential = null; +let isEditingSearch = false; // Global variables to hold the active accounts from the config response. let activeSpotifyAccount = ''; @@ -88,7 +98,7 @@ function setupEventListeners() { document.getElementById('customDirFormat').addEventListener('change', saveConfig); document.getElementById('customTrackFormat').addEventListener('change', saveConfig); - // New: Max concurrent downloads change listener + // Max concurrent downloads change listener document.getElementById('maxConcurrentDownloads').addEventListener('change', saveConfig); } @@ -148,8 +158,13 @@ async function updateAccountSelectors() { async function loadCredentials(service) { try { - const response = await fetch(`/api/credentials/${service}`); - renderCredentialsList(service, await response.json()); + const response = await fetch(`/api/credentials/all/${service}`); + if (!response.ok) { + throw new Error(`Failed to load credentials: ${response.statusText}`); + } + + const credentials = await response.json(); + renderCredentialsList(service, credentials); } catch (error) { showConfigError(error.message); } @@ -157,25 +172,57 @@ async function loadCredentials(service) { function renderCredentialsList(service, credentials) { const list = document.querySelector('.credentials-list'); - list.innerHTML = credentials - .map(name => - `
- ${name} -
- - -
-
` - ) - .join(''); + list.innerHTML = ''; + if (!credentials.length) { + list.innerHTML = '
No accounts found. Add a new account below.
'; + return; + } + + credentials.forEach(credData => { + const credItem = document.createElement('div'); + credItem.className = 'credential-item'; + + const hasSearchCreds = credData.search && Object.keys(credData.search).length > 0; + + credItem.innerHTML = ` +
+ ${credData.name} + ${service === 'spotify' ? + `
+ ${hasSearchCreds ? 'API Configured' : 'No API Credentials'} +
` : ''} +
+
+ + ${service === 'spotify' ? + `` : ''} + +
+ `; + + list.appendChild(credItem); + }); + + // Set up event handlers list.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', handleDeleteCredential); }); list.querySelectorAll('.edit-btn').forEach(btn => { - btn.addEventListener('click', handleEditCredential); + btn.addEventListener('click', (e) => { + isEditingSearch = false; + handleEditCredential(e); + }); }); + + if (service === 'spotify') { + list.querySelectorAll('.edit-search-btn').forEach(btn => { + btn.addEventListener('click', handleEditSearchCredential); + }); + } } async function handleDeleteCredential(e) { @@ -187,6 +234,10 @@ async function handleDeleteCredential(e) { throw new Error('Missing credential information'); } + if (!confirm(`Are you sure you want to delete the ${name} account?`)) { + return; + } + const response = await fetch(`/api/credentials/${service}/${name}`, { method: 'DELETE' }); @@ -223,31 +274,137 @@ async function handleEditCredential(e) { await new Promise(resolve => setTimeout(resolve, 50)); const response = await fetch(`/api/credentials/${service}/${name}`); + if (!response.ok) { + throw new Error(`Failed to load credential: ${response.statusText}`); + } + const data = await response.json(); currentCredential = name; document.getElementById('credentialName').value = name; document.getElementById('credentialName').disabled = true; + document.getElementById('formTitle').textContent = `Edit ${service.charAt(0).toUpperCase() + service.slice(1)} Account`; + document.getElementById('submitCredentialBtn').textContent = 'Update Account'; + + // Show regular fields populateFormFields(service, data); + toggleSearchFieldsVisibility(false); } catch (error) { showConfigError(error.message); } } +async function handleEditSearchCredential(e) { + const service = e.target.dataset.service; + const name = e.target.dataset.name; + + try { + if (service !== 'spotify') { + throw new Error('Search credentials are only available for Spotify'); + } + + document.querySelector(`[data-service="${service}"]`).click(); + await new Promise(resolve => setTimeout(resolve, 50)); + + isEditingSearch = true; + currentCredential = name; + document.getElementById('credentialName').value = name; + document.getElementById('credentialName').disabled = true; + document.getElementById('formTitle').textContent = `Spotify API Credentials for ${name}`; + document.getElementById('submitCredentialBtn').textContent = 'Save API Credentials'; + + // Try to load existing search credentials + try { + const searchResponse = await fetch(`/api/credentials/${service}/${name}?type=search`); + if (searchResponse.ok) { + const searchData = await searchResponse.json(); + // Populate search fields + serviceConfig[service].searchFields.forEach(field => { + const element = document.getElementById(field.id); + if (element) element.value = searchData[field.id] || ''; + }); + } else { + // Clear search fields if no existing search credentials + serviceConfig[service].searchFields.forEach(field => { + const element = document.getElementById(field.id); + if (element) element.value = ''; + }); + } + } catch (error) { + // Clear search fields if there was an error + serviceConfig[service].searchFields.forEach(field => { + const element = document.getElementById(field.id); + if (element) element.value = ''; + }); + } + + // Hide regular account fields, show search fields + toggleSearchFieldsVisibility(true); + } catch (error) { + showConfigError(error.message); + } +} + +function toggleSearchFieldsVisibility(showSearchFields) { + const serviceFieldsDiv = document.getElementById('serviceFields'); + const searchFieldsDiv = document.getElementById('searchFields'); + + if (showSearchFields) { + serviceFieldsDiv.style.display = 'none'; + searchFieldsDiv.style.display = 'block'; + } else { + serviceFieldsDiv.style.display = 'block'; + searchFieldsDiv.style.display = 'none'; + } +} + function updateFormFields() { - const serviceFields = document.getElementById('serviceFields'); - serviceFields.innerHTML = serviceConfig[currentService].fields - .map(field => - `
- - -
` - ) - .join(''); + const serviceFieldsDiv = document.getElementById('serviceFields'); + const searchFieldsDiv = document.getElementById('searchFields'); + + // Clear any existing fields + serviceFieldsDiv.innerHTML = ''; + searchFieldsDiv.innerHTML = ''; + + // Add regular account fields + serviceConfig[currentService].fields.forEach(field => { + const fieldDiv = document.createElement('div'); + fieldDiv.className = 'form-group'; + fieldDiv.innerHTML = ` + + + `; + serviceFieldsDiv.appendChild(fieldDiv); + }); + + // Add search fields for Spotify + if (currentService === 'spotify' && serviceConfig[currentService].searchFields) { + serviceConfig[currentService].searchFields.forEach(field => { + const fieldDiv = document.createElement('div'); + fieldDiv.className = 'form-group'; + fieldDiv.innerHTML = ` + + + `; + searchFieldsDiv.appendChild(fieldDiv); + }); + } + + // Reset form title and button text + document.getElementById('formTitle').textContent = `Add New ${currentService.charAt(0).toUpperCase() + currentService.slice(1)} Account`; + document.getElementById('submitCredentialBtn').textContent = 'Save Account'; + + // Initially show regular fields, hide search fields + toggleSearchFieldsVisibility(false); + isEditingSearch = false; } function populateFormFields(service, data) { @@ -268,16 +425,35 @@ async function handleCredentialSubmit(e) { throw new Error('Credential name is required'); } - const formData = {}; - serviceConfig[service].fields.forEach(field => { - formData[field.id] = document.getElementById(field.id).value.trim(); - }); - - const data = serviceConfig[service].validator(formData); const endpointName = currentCredential || name; - const method = currentCredential ? 'PUT' : 'POST'; + let method, data, endpoint; - const response = await fetch(`/api/credentials/${service}/${endpointName}`, { + if (isEditingSearch && service === 'spotify') { + // Handle search credentials + const formData = {}; + serviceConfig[service].searchFields.forEach(field => { + formData[field.id] = document.getElementById(field.id).value.trim(); + }); + + data = serviceConfig[service].searchValidator(formData); + endpoint = `/api/credentials/${service}/${endpointName}?type=search`; + + // Check if search credentials already exist for this account + const checkResponse = await fetch(endpoint); + method = checkResponse.ok ? 'PUT' : 'POST'; + } else { + // Handle regular account credentials + const formData = {}; + serviceConfig[service].fields.forEach(field => { + formData[field.id] = document.getElementById(field.id).value.trim(); + }); + + data = serviceConfig[service].validator(formData); + endpoint = `/api/credentials/${service}/${endpointName}`; + method = currentCredential ? 'PUT' : 'POST'; + } + + const response = await fetch(endpoint, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) @@ -299,10 +475,19 @@ async function handleCredentialSubmit(e) { function resetForm() { currentCredential = null; + isEditingSearch = false; const nameInput = document.getElementById('credentialName'); nameInput.value = ''; nameInput.disabled = false; document.getElementById('credentialForm').reset(); + + // Reset form title and button text + const service = currentService.charAt(0).toUpperCase() + currentService.slice(1); + document.getElementById('formTitle').textContent = `Add New ${service} Account`; + document.getElementById('submitCredentialBtn').textContent = 'Save Account'; + + // Show regular account fields, hide search fields + toggleSearchFieldsVisibility(false); } async function saveConfig() { @@ -369,5 +554,5 @@ async function loadConfig() { function showConfigError(message) { const errorDiv = document.getElementById('configError'); errorDiv.textContent = message; - setTimeout(() => (errorDiv.textContent = ''), 3000); + setTimeout(() => (errorDiv.textContent = ''), 5000); } diff --git a/static/js/main.js b/static/js/main.js index cf50055..5cf7b0a 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -58,7 +58,13 @@ async function performSearch() { resultsContainer.innerHTML = '
Searching...
'; try { - const response = await fetch(`/api/search?q=${encodeURIComponent(query)}&search_type=${searchType}&limit=50`); + // Fetch config to get active Spotify account + const configResponse = await fetch('/api/config'); + const config = await configResponse.json(); + const mainAccount = config.spotify || ''; + + // Add the main parameter to the search API call + const response = await fetch(`/api/search?q=${encodeURIComponent(query)}&search_type=${searchType}&limit=50&main=${mainAccount}`); const data = await response.json(); if (data.error) throw new Error(data.error); @@ -81,7 +87,7 @@ async function performSearch() { /** * Attaches event listeners to all download buttons (both standard and small versions). * Instead of using the NodeList index (which can be off when multiple buttons are in one card), - * we look up the closest result card’s data-index to get the correct item. + * we look up the closest result card's data-index to get the correct item. */ function attachDownloadListeners(items) { document.querySelectorAll('.download-btn, .download-btn-small').forEach((btn) => { @@ -294,7 +300,7 @@ function createResultCard(item, type, index) {
${title}
- + + +