diff --git a/app.py b/app.py index 5b3ccaf..3e17a4a 100755 --- a/app.py +++ b/app.py @@ -21,75 +21,77 @@ import socket from urllib.parse import urlparse # Import Celery configuration and manager -from routes.utils.celery_tasks import celery_app from routes.utils.celery_manager import celery_manager from routes.utils.celery_config import REDIS_URL +from routes.utils.history_manager import init_history_db + # Configure application-wide logging def setup_logging(): """Configure application-wide logging with rotation""" # Create logs directory if it doesn't exist - logs_dir = Path('logs') + logs_dir = Path("logs") logs_dir.mkdir(exist_ok=True) - + # Set up log file paths - main_log = logs_dir / 'spotizerr.log' - + main_log = logs_dir / "spotizerr.log" + # Configure root logger root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) - + # Clear any existing handlers from the root logger if root_logger.hasHandlers(): root_logger.handlers.clear() - + # Log formatting log_format = logging.Formatter( - '%(asctime)s [%(processName)s:%(threadName)s] [%(name)s] [%(levelname)s] - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' + "%(asctime)s [%(processName)s:%(threadName)s] [%(name)s] [%(levelname)s] - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", ) - + # File handler with rotation (10 MB max, keep 5 backups) file_handler = logging.handlers.RotatingFileHandler( - main_log, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8' + main_log, maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8" ) file_handler.setFormatter(log_format) file_handler.setLevel(logging.INFO) - + # Console handler for stderr console_handler = logging.StreamHandler(sys.stderr) console_handler.setFormatter(log_format) console_handler.setLevel(logging.INFO) - + # Add handlers to root logger root_logger.addHandler(file_handler) root_logger.addHandler(console_handler) - + # Set up specific loggers - for logger_name in ['werkzeug', 'celery', 'routes', 'flask', 'waitress']: + for logger_name in ["werkzeug", "celery", "routes", "flask", "waitress"]: module_logger = logging.getLogger(logger_name) module_logger.setLevel(logging.INFO) # Handlers are inherited from root logger - + # Enable propagation for all loggers - logging.getLogger('celery').propagate = True - + logging.getLogger("celery").propagate = True + # Notify successful setup root_logger.info("Logging system initialized") - + # Return the main file handler for permissions adjustment return file_handler + def check_redis_connection(): """Check if Redis is reachable and retry with exponential backoff if not""" max_retries = 5 retry_count = 0 retry_delay = 1 # start with 1 second - + # Extract host and port from REDIS_URL redis_host = "redis" # default - redis_port = 6379 # default - + redis_port = 6379 # default + # Parse from REDIS_URL if possible if REDIS_URL: # parse hostname and port (handles optional auth) @@ -101,10 +103,10 @@ def check_redis_connection(): redis_port = parsed.port except Exception: pass - + # Log Redis connection details logging.info(f"Checking Redis connection to {redis_host}:{redis_port}") - + while retry_count < max_retries: try: # First try socket connection to check if Redis port is open @@ -112,10 +114,12 @@ def check_redis_connection(): sock.settimeout(2) result = sock.connect_ex((redis_host, redis_port)) sock.close() - + if result != 0: - raise ConnectionError(f"Cannot connect to Redis at {redis_host}:{redis_port}") - + raise ConnectionError( + f"Cannot connect to Redis at {redis_host}:{redis_port}" + ) + # If socket connection successful, try Redis ping r = redis.Redis.from_url(REDIS_URL) r.ping() @@ -124,82 +128,90 @@ def check_redis_connection(): except Exception as e: retry_count += 1 if retry_count >= max_retries: - logging.error(f"Failed to connect to Redis after {max_retries} attempts: {e}") - logging.error(f"Make sure Redis is running at {redis_host}:{redis_port}") + logging.error( + f"Failed to connect to Redis after {max_retries} attempts: {e}" + ) + logging.error( + f"Make sure Redis is running at {redis_host}:{redis_port}" + ) return False - + logging.warning(f"Redis connection attempt {retry_count} failed: {e}") logging.info(f"Retrying in {retry_delay} seconds...") time.sleep(retry_delay) retry_delay *= 2 # exponential backoff - + return False + def create_app(): - app = Flask(__name__, template_folder='static/html') - + app = Flask(__name__, template_folder="static/html") + # Set up CORS CORS(app) + # Initialize databases + init_history_db() + # Register blueprints - app.register_blueprint(config_bp, url_prefix='/api') - 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') - app.register_blueprint(artist_bp, url_prefix='/api/artist') - app.register_blueprint(prgs_bp, url_prefix='/api/prgs') - app.register_blueprint(history_bp, url_prefix='/api/history') - + app.register_blueprint(config_bp, url_prefix="/api") + 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") + app.register_blueprint(artist_bp, url_prefix="/api/artist") + app.register_blueprint(prgs_bp, url_prefix="/api/prgs") + app.register_blueprint(history_bp, url_prefix="/api/history") + # Serve frontend - @app.route('/') + @app.route("/") def serve_index(): - return render_template('main.html') + return render_template("main.html") # Config page route - @app.route('/config') + @app.route("/config") def serve_config(): - return render_template('config.html') + return render_template("config.html") # New route: Serve watch.html under /watchlist - @app.route('/watchlist') + @app.route("/watchlist") def serve_watchlist(): - return render_template('watch.html') + return render_template("watch.html") # New route: Serve playlist.html under /playlist/ - @app.route('/playlist/') + @app.route("/playlist/") def serve_playlist(id): # The id parameter is captured, but you can use it as needed. - return render_template('playlist.html') + return render_template("playlist.html") - @app.route('/album/') + @app.route("/album/") def serve_album(id): # The id parameter is captured, but you can use it as needed. - return render_template('album.html') + return render_template("album.html") - @app.route('/track/') + @app.route("/track/") def serve_track(id): # The id parameter is captured, but you can use it as needed. - return render_template('track.html') - - @app.route('/artist/') + return render_template("track.html") + + @app.route("/artist/") def serve_artist(id): # The id parameter is captured, but you can use it as needed. - return render_template('artist.html') + return render_template("artist.html") - @app.route('/history') + @app.route("/history") def serve_history_page(): - return render_template('history.html') + return render_template("history.html") - @app.route('/static/') + @app.route("/static/") def serve_static(path): - return send_from_directory('static', path) + return send_from_directory("static", path) # Serve favicon.ico from the same directory as index.html (templates) - @app.route('/favicon.ico') + @app.route("/favicon.ico") def serve_favicon(): - return send_from_directory('static/html', 'favicon.ico') + return send_from_directory("static/html", "favicon.ico") # Add request logging middleware @app.before_request @@ -209,7 +221,7 @@ def create_app(): @app.after_request def log_response(response): - if hasattr(request, 'start_time'): + if hasattr(request, "start_time"): duration = round((time.time() - request.start_time) * 1000, 2) app.logger.debug(f"Response: {response.status} | Duration: {duration}ms") return response @@ -222,41 +234,45 @@ def create_app(): return app + def start_celery_workers(): """Start Celery workers with dynamic configuration""" logging.info("Starting Celery workers with dynamic configuration") celery_manager.start() - + # Register shutdown handler atexit.register(celery_manager.stop) -if __name__ == '__main__': + +if __name__ == "__main__": # Configure application logging log_handler = setup_logging() - + # Set file permissions for log files if needed try: os.chmod(log_handler.baseFilename, 0o666) except: logging.warning("Could not set permissions on log file") - + # Log application startup logging.info("=== Spotizerr Application Starting ===") - + # Check Redis connection before starting workers if check_redis_connection(): # Start Watch Manager from routes.utils.watch.manager import start_watch_manager + start_watch_manager() # Start Celery workers start_celery_workers() - + # Create and start Flask app app = create_app() logging.info("Starting Flask server on port 7171") from waitress import serve - serve(app, host='0.0.0.0', port=7171) + + serve(app, host="0.0.0.0", port=7171) else: logging.error("Cannot start application: Redis connection failed") sys.exit(1)