import queue import logging import threading from flask import Flask, render_template, request, jsonify, send_from_directory from flask_socketio import SocketIO, emit from app.services.config_service import ConfigService from app.services.spotify_service import SpotifyService from app.services.download_service import DownloadService from app.services.playlist_manager import PlaylistManager # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) class SpotSpotApp: def __init__(self, config=None): # Initialize services if config: self.config = config else: self.config = ConfigService() # Initialize Flask app self.app = Flask(__name__, static_folder='static', template_folder='templates') self.app.secret_key = self.config.secret_key # Initialize Socket.IO with CORS self.socketio = SocketIO( self.app, cors_allowed_origins="*", logger=self.config.debug, engineio_logger=self.config.debug, async_mode='threading', ping_timeout=60, ping_interval=25 ) # Initialize data structures self.download_queue = queue.Queue() self.download_history = {} self.active_downloads = {} self.spotify_service = SpotifyService(self.config) self.playlist_manager = PlaylistManager(self.config) self.download_service = DownloadService( config=self.config, playlist_manager=self.playlist_manager, socketio=self.socketio, download_queue=self.download_queue, download_history=self.download_history ) # Setup routes and socket events self.setup_routes() self.setup_socket_events() def setup_routes(self): """Setup Flask routes.""" @self.app.route('/') def index(): return render_template('index.html') @self.app.route('/static/') def static_files(filename): return send_from_directory(self.app.static_folder, filename) # REST endpoint for the call in index.html @self.app.route('/api/search') def api_search(): """Search Spotify and return raw JSON for the front-end. Query params: q: search term (required) type: track | album | playlist | artist (optional, default track) """ query = request.args.get('q', '').strip() search_type = request.args.get('type', 'track').strip() or 'track' if not query: return jsonify({'error': 'Query parameter "q" is required'}), 400 try: results = self.spotify_service.perform_spotify_search({'query': query, 'type': search_type}) return jsonify(results) except Exception as exc: # noqa: BLE001 logger.exception("Search failed") return jsonify({'error': str(exc)}), 500 def setup_socket_events(self): """Setup Socket.IO event handlers.""" @self.socketio.on('connect') def handle_connect(): logger.info('Client connected') emit('connection_status', {'status': 'connected'}) @self.socketio.on('disconnect') def handle_disconnect(): logger.info('Client disconnected') @self.socketio.on('search') def handle_search(search_req): """Handle search requests from the client.""" try: logger.info(f'Search request: {search_req}') if not search_req.get('query'): emit('toast', { 'title': 'Search Error', 'message': 'Search query cannot be empty', 'type': 'error' }) return # Perform search using Spotify service results = self.spotify_service.perform_spotify_search(search_req) if not results: emit('search_error', { 'message': 'No results found or Spotify search failed', 'query': search_req.get('query'), 'type': search_req.get('type', 'track') }) return emit('search_results', { 'results': results, 'query': search_req.get('query'), 'type': search_req.get('type', 'track') }) except Exception as e: logger.error(f'Search error: {str(e)}') emit('toast', { 'title': 'Search Error', 'message': str(e), 'type': 'error' }) @self.socketio.on('download') @self.socketio.on('download_item') def handle_download(download_req): """Handle download requests from the client.""" try: logger.info(f'Download request: {download_req}') if not download_req.get('url'): emit('toast', { 'title': 'Download Error', 'message': 'URL is required', 'type': 'error' }) return # Add to download queue self.download_service.add_item_to_queue(download_req) emit('toast', { 'title': 'Download Started', 'message': 'Your download has been added to the queue', 'type': 'success' }) except Exception as e: logger.error(f'Download error: {str(e)}') emit('toast', { 'title': 'Download Error', 'message': str(e), 'type': 'error' }) @self.socketio.on('get_status') def handle_get_status(): """Send current download status to the client.""" emit('update_status', { 'queue': list(self.download_queue.queue), 'history': list(self.download_history.values()), 'active': self.active_downloads }) def run(self, host='0.0.0.0', port=6544, debug=True): """Run the application.""" logger.info(f'Starting SpotSpot server on {host}:{port}') self.socketio.run(self.app, host=host, port=port, debug=debug, use_reloader=True) # Create and run the application if __name__ == '__main__': app = SpotSpotApp() app.run()