import logging import os import queue import subprocess import threading import eventlet import time from flask import Flask, request from flask_socketio import Sock# Configure SocketIO with proper settings socketio = SocketIO( app, cors_allowed_origins=["http://localhost:5144", "http://127.0.0.1:5144"], async_mode='eventlet', logger=True, engineio_logger=True, ping_timeout=60, ping_interval=25, always_connect=True ) # Initialize services after all class definitions config = Config() spotify_service = SpotifyService(config) download_manager = DownloadManager(socketio)from flask_cors import CORS from spotipy import Spotify from spotipy.oauth2 import SpotifyClientCredentials # Patch eventlet for better websocket performance eventlet.monkey_patch() logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # Configuration class Config: def __init__(self): self.client_id = "c1db641d73a745bb90911e56f1a2f0ba" self.client_secret = "ed8d9cfb99694e55a0682c2139f015ec" self.search_limit = 10 class SpotifyService: def __init__(self, config): self.config = config try: client_credentials_manager = SpotifyClientCredentials( client_id=self.config.client_id, client_secret=self.config.client_secret ) self.sp = Spotify(client_credentials_manager=client_credentials_manager) logging.info("Successfully initialized Spotify client") except Exception as e: logging.error(f"Failed to initialize Spotify client: {str(e)}") raise def perform_spotify_search(self, search_req): """Perform a Spotify search with comprehensive error handling and result formatting. Args: search_req: Dictionary containing: - query: Search string - type: Type of search ('track', 'album', 'artist', 'playlist', or 'all') - limit: Optional max results per type Returns: Dictionary of formatted search results by type """ try: query = search_req.get("query", "").strip() search_type = search_req.get("type", "track") limit = min(int(search_req.get("limit", 10)), 50) # Cap at 50 per type if not query: logging.warning("Empty search query received") return {} logging.info(f"Searching Spotify - Query: '{query}', Type: '{search_type}', Limit: {limit}") if search_type.lower() == 'all': search_types = ['track', 'album', 'artist', 'playlist'] results = {} for st in search_types: try: search_result = self.sp.search( q=query, type=st, limit=min(limit, 20) ) results[f"{st}s"] = search_result.get(f'{st}s', {}) except Exception as e: logging.error(f"Error searching for {st}: {str(e)}") results[f"{st}s"] = {"items": []} else: results = self.sp.search( q=query, type=search_type, limit=limit ) return self.parse_spotify_data(results) except Exception as e: logging.error(f"Search error: {str(e)}") return {} def parse_spotify_data(self, results): parsed_results = {"tracks": [], "albums": [], "artists": [], "playlists": []} try: # Parsing tracks if "tracks" in results: for item in results["tracks"]["items"]: parsed_results["tracks"].append({ "type": "track", "name": item["name"], "artist": item["artists"][0]["name"], "album": item["album"]["name"], "url": item["external_urls"]["spotify"], "image": item["album"]["images"][0]["url"] if item["album"]["images"] else None, "duration": item["duration_ms"], }) # Parsing albums if "albums" in results: for item in results["albums"]["items"]: parsed_results["albums"].append({ "type": "album", "name": item["name"], "artist": item["artists"][0]["name"], "url": item["external_urls"]["spotify"], "image": item["images"][0]["url"] if item["images"] else None, "total_tracks": item["total_tracks"], "release_date": item["release_date"], }) # Parsing artists if "artists" in results: for item in results["artists"]["items"]: parsed_results["artists"].append({ "type": "artist", "name": item["name"], "url": item["external_urls"]["spotify"], "image": item["images"][0]["url"] if item["images"] else None, "followers": item["followers"]["total"], }) # Parsing playlists if "playlists" in results: for item in results["playlists"]["items"]: parsed_results["playlists"].append({ "type": "playlist", "name": item["name"], "owner": item["owner"]["display_name"], "url": item["external_urls"]["spotify"], "image": item["images"][0]["url"] if item["images"] else None, "tracks_total": item["tracks"]["total"], }) return {k: v for k, v in parsed_results.items() if v} # Only return non-empty results except Exception as e: logging.error(f"Error parsing Spotify data: {str(e)}") return {} # Initialize Flask & SocketIO app = Flask(__name__) app.config['SECRET_KEY'] = 'secret!' app.config['JSON_SORT_KEYS'] = False app.config['CORS_HEADERS'] = 'Content-Type' # Enable CORS with specific settings CORS(app, resources={ r"/*": { "origins": ["http://localhost:5144", "http://127.0.0.1:5144"], "methods": ["GET", "POST"], "allow_headers": ["Content-Type"], "supports_credentials": True } }) # Configure SocketIO with proper settings socketio = SocketIO( app, cors_allowed_origins=["http://localhost:5144", "http://127.0.0.1:5144"], async_mode='eventlet', logger=True, engineio_logger=True, ping_timeout=60, ping_interval=25, always_connect=True, websocket_errors_to_ignore=None ) class DownloadManager: def __init__(self, socketio): self.socketio = socketio self.download_queue = queue.Queue() self.download_history = {} self.active_downloads = {} self.download_path = '/Volumes/Jellyfin Media/Music' # Start the download thread self.download_thread = threading.Thread(target=self._process_downloads, daemon=True) self.download_thread.start() logging.info("Download manager initialized") def _monitor_output(self, pipe, download_info, url): """Monitor process output and update progress.""" try: for line in iter(pipe.readline, ''): if not line: break line = line.strip().lower() # Update progress based on output if 'downloading' in line: download_info['status'] = 'Downloading...' download_info['progress'] = 50 elif 'processing' in line: download_info['status'] = 'Processing...' download_info['progress'] = 75 elif 'saving' in line: download_info['status'] = 'Saving...' download_info['progress'] = 90 self.download_history[url] = download_info self.socketio.emit('update_status', {'history': list(self.download_history.values())}) except Exception as e: logging.error(f"Error monitoring output: {str(e)}") def _process_downloads(self): """Process downloads from the queue.""" while True: try: url, download_info = self.download_queue.get() if download_info['status'] == 'Cancelled': self.download_queue.task_done() continue # Ensure download directory exists if not os.path.exists(self.download_path): os.makedirs(self.download_path) download_info['status'] = 'Preparing...' download_info['progress'] = 0 self.download_history[url] = download_info self.socketio.emit('update_status', {'history': list(self.download_history.values())}) try: # Enhanced spotdl command with metadata options command = [ 'spotdl', '--output', self.download_path, '--format', 'mp3', '--bitrate', '320k', '--embed-metadata', '--threads', '2', url ] process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1 ) self.active_downloads[url] = process # Track download progress stdout_thread = threading.Thread( target=self._monitor_output, args=(process.stdout, download_info, url), daemon=True ) stderr_thread = threading.Thread( target=self._monitor_output, args=(process.stderr, download_info, url), daemon=True ) stdout_thread.start() stderr_thread.start() # Wait for the process to complete process.wait() stdout_thread.join() stderr_thread.join() if process.returncode == 0: download_info['status'] = 'Complete' download_info['progress'] = 100 else: download_info['status'] = 'Failed' download_info['progress'] = 0 if process.stderr: error_output = process.stderr.read() logging.error(f"Download error: {error_output}") self.download_history[url] = download_info except Exception as e: logging.error(f"Download error: {str(e)}") download_info['status'] = 'Error' download_info['progress'] = 0 self.download_history[url] = download_info self.socketio.emit('update_status', {'history': list(self.download_history.values())}) self.download_queue.task_done() except Exception as e: logging.error(f"Queue processing error: {str(e)}") @socketio.on('connect') def handle_connect(): logging.info('Client connected') @socketio.on('disconnect') def handle_disconnect(): logging.info('Client disconnected') @socketio.on('search') def handle_search(data): query = data.get('query', '').strip() search_type = data.get('type', 'track') limit = min(int(data.get('limit', 10)), 50) if not query: socketio.emit('toast', { 'title': 'Empty Search', 'body': 'Please enter a search query' }) return try: logging.info(f"Received search request - Query: '{query}', Type: '{search_type}', Limit: {limit}") results = spotify_service.perform_spotify_search({ 'query': query, 'type': search_type, 'limit': limit }) if not results: socketio.emit('toast', { 'title': 'No Results', 'body': f'No {search_type}s found for "{query}"' }) return logging.info(f"Search successful - Found results in categories: {list(results.keys())}") socketio.emit('search_results', { 'query': query, 'type': search_type, 'results': results }) except Exception as e: error_msg = f"Search error: {str(e)}" logging.error(error_msg, exc_info=True) socketio.emit('toast', { 'title': 'Search Error', 'body': 'An error occurred while searching. Please try again.' }) socketio.emit('search_error', { 'message': error_msg, 'query': query, 'type': search_type }) @socketio.on('download') def handle_download(data): url = data.get('url') item_type = data.get('type') item_name = data.get('name') item_artist = data.get('artist') if not url: socketio.emit('toast', { 'title': 'Download Error', 'body': 'No URL provided' }) return download_info = { 'name': item_name, 'type': item_type, 'artist': item_artist, 'url': url, 'status': 'Pending...', 'progress': 0 } download_manager.add_to_queue(download_info) @socketio.on('cancel_all') def handle_cancel_all(): download_manager.cancel_all() @socketio.on('cancel_active') def handle_cancel_active(): download_manager.cancel_active() if __name__ == '__main__': try: logging.info("Starting server on port 5143...") socketio.run(app, host='0.0.0.0', port=5143) except Exception as e: logging.error(f"Failed to start server: {str(e)}", exc_info=True)