Add migration scripts for 3.0.6
This commit is contained in:
412
app.py
412
app.py
@@ -14,6 +14,14 @@ import redis
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# Run DB migrations as early as possible, before importing any routers that may touch DBs
|
||||
try:
|
||||
from routes.migrations import run_migrations_if_needed
|
||||
run_migrations_if_needed()
|
||||
logging.getLogger(__name__).info("Database migrations executed (if needed) early in startup.")
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).error(f"Database migration step failed early in startup: {e}", exc_info=True)
|
||||
|
||||
# Import route routers (to be created)
|
||||
from routes.auth.credentials import router as credentials_router
|
||||
from routes.auth.auth import router as auth_router
|
||||
@@ -41,246 +49,246 @@ import routes
|
||||
|
||||
# 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.mkdir(exist_ok=True)
|
||||
"""Configure application-wide logging with rotation"""
|
||||
# Create logs directory if it doesn't exist
|
||||
logs_dir = Path("logs")
|
||||
logs_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Set up log file paths
|
||||
main_log = logs_dir / "spotizerr.log"
|
||||
# Set up log file paths
|
||||
main_log = logs_dir / "spotizerr.log"
|
||||
|
||||
# Configure root logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
# Configure root logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Clear any existing handlers from the root logger
|
||||
if root_logger.hasHandlers():
|
||||
root_logger.handlers.clear()
|
||||
# Clear any existing handlers from the root logger
|
||||
if root_logger.hasHandlers():
|
||||
root_logger.handlers.clear()
|
||||
|
||||
# Log formatting
|
||||
log_format = logging.Formatter(
|
||||
"%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
# Log formatting
|
||||
log_format = logging.Formatter(
|
||||
"%(asctime)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"
|
||||
)
|
||||
file_handler.setFormatter(log_format)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
# 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"
|
||||
)
|
||||
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)
|
||||
# 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)
|
||||
# Add handlers to root logger
|
||||
root_logger.addHandler(file_handler)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# Set up specific loggers
|
||||
for logger_name in [
|
||||
"routes",
|
||||
"routes.utils",
|
||||
"routes.utils.celery_manager",
|
||||
"routes.utils.celery_tasks",
|
||||
"routes.utils.watch",
|
||||
]:
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.propagate = True # Propagate to root logger
|
||||
# Set up specific loggers
|
||||
for logger_name in [
|
||||
"routes",
|
||||
"routes.utils",
|
||||
"routes.utils.celery_manager",
|
||||
"routes.utils.celery_tasks",
|
||||
"routes.utils.watch",
|
||||
]:
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.propagate = True # Propagate to root logger
|
||||
|
||||
logging.info("Logging system initialized")
|
||||
logging.info("Logging system initialized")
|
||||
|
||||
|
||||
def check_redis_connection():
|
||||
"""Check if Redis is available and accessible"""
|
||||
if not REDIS_URL:
|
||||
logging.error("REDIS_URL is not configured. Please check your environment.")
|
||||
return False
|
||||
"""Check if Redis is available and accessible"""
|
||||
if not REDIS_URL:
|
||||
logging.error("REDIS_URL is not configured. Please check your environment.")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Parse Redis URL
|
||||
parsed_url = urlparse(REDIS_URL)
|
||||
host = parsed_url.hostname or "localhost"
|
||||
port = parsed_url.port or 6379
|
||||
try:
|
||||
# Parse Redis URL
|
||||
parsed_url = urlparse(REDIS_URL)
|
||||
host = parsed_url.hostname or "localhost"
|
||||
port = parsed_url.port or 6379
|
||||
|
||||
logging.info(f"Testing Redis connection to {host}:{port}...")
|
||||
logging.info(f"Testing Redis connection to {host}:{port}...")
|
||||
|
||||
# Test socket connection first
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
result = sock.connect_ex((host, port))
|
||||
sock.close()
|
||||
# Test socket connection first
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
result = sock.connect_ex((host, port))
|
||||
sock.close()
|
||||
|
||||
if result != 0:
|
||||
logging.error(f"Cannot connect to Redis at {host}:{port}")
|
||||
return False
|
||||
if result != 0:
|
||||
logging.error(f"Cannot connect to Redis at {host}:{port}")
|
||||
return False
|
||||
|
||||
# Test Redis client connection
|
||||
r = redis.from_url(REDIS_URL, socket_connect_timeout=5, socket_timeout=5)
|
||||
r.ping()
|
||||
logging.info("Redis connection successful")
|
||||
return True
|
||||
# Test Redis client connection
|
||||
r = redis.from_url(REDIS_URL, socket_connect_timeout=5, socket_timeout=5)
|
||||
r.ping()
|
||||
logging.info("Redis connection successful")
|
||||
return True
|
||||
|
||||
except redis.ConnectionError as e:
|
||||
logging.error(f"Redis connection error: {e}")
|
||||
return False
|
||||
except redis.TimeoutError as e:
|
||||
logging.error(f"Redis timeout error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error checking Redis connection: {e}")
|
||||
return False
|
||||
except redis.ConnectionError as e:
|
||||
logging.error(f"Redis connection error: {e}")
|
||||
return False
|
||||
except redis.TimeoutError as e:
|
||||
logging.error(f"Redis timeout error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error checking Redis connection: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Handle application startup and shutdown"""
|
||||
# Startup
|
||||
setup_logging()
|
||||
|
||||
# Check Redis connection
|
||||
if not check_redis_connection():
|
||||
logging.error("Failed to connect to Redis. Please ensure Redis is running and accessible.")
|
||||
# Don't exit, but warn - some functionality may not work
|
||||
|
||||
# Start Celery workers
|
||||
try:
|
||||
celery_manager.start()
|
||||
logging.info("Celery workers started successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to start Celery workers: {e}")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
try:
|
||||
celery_manager.stop()
|
||||
logging.info("Celery workers stopped")
|
||||
except Exception as e:
|
||||
logging.error(f"Error stopping Celery workers: {e}")
|
||||
"""Handle application startup and shutdown"""
|
||||
# Startup
|
||||
setup_logging()
|
||||
|
||||
# Check Redis connection
|
||||
if not check_redis_connection():
|
||||
logging.error("Failed to connect to Redis. Please ensure Redis is running and accessible.")
|
||||
# Don't exit, but warn - some functionality may not work
|
||||
|
||||
# Start Celery workers
|
||||
try:
|
||||
celery_manager.start()
|
||||
logging.info("Celery workers started successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to start Celery workers: {e}")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
try:
|
||||
celery_manager.stop()
|
||||
logging.info("Celery workers stopped")
|
||||
except Exception as e:
|
||||
logging.error(f"Error stopping Celery workers: {e}")
|
||||
|
||||
|
||||
def create_app():
|
||||
app = FastAPI(
|
||||
title="Spotizerr API",
|
||||
description="Music download service API",
|
||||
version="3.0.0",
|
||||
lifespan=lifespan,
|
||||
redirect_slashes=True # Enable automatic trailing slash redirects
|
||||
)
|
||||
app = FastAPI(
|
||||
title="Spotizerr API",
|
||||
description="Music download service API",
|
||||
version="3.0.0",
|
||||
lifespan=lifespan,
|
||||
redirect_slashes=True # Enable automatic trailing slash redirects
|
||||
)
|
||||
|
||||
# Set up CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
# Set up CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Add authentication middleware (only if auth is enabled)
|
||||
if AUTH_ENABLED:
|
||||
app.add_middleware(AuthMiddleware)
|
||||
logging.info("Authentication system enabled")
|
||||
else:
|
||||
logging.info("Authentication system disabled")
|
||||
# Add authentication middleware (only if auth is enabled)
|
||||
if AUTH_ENABLED:
|
||||
app.add_middleware(AuthMiddleware)
|
||||
logging.info("Authentication system enabled")
|
||||
else:
|
||||
logging.info("Authentication system disabled")
|
||||
|
||||
# Register routers with URL prefixes
|
||||
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
|
||||
|
||||
# Include SSO router if available
|
||||
try:
|
||||
from routes.auth.sso import router as sso_router
|
||||
app.include_router(sso_router, prefix="/api/auth", tags=["sso"])
|
||||
logging.info("SSO functionality enabled")
|
||||
except ImportError as e:
|
||||
logging.warning(f"SSO functionality not available: {e}")
|
||||
app.include_router(config_router, prefix="/api/config", tags=["config"])
|
||||
app.include_router(search_router, prefix="/api/search", tags=["search"])
|
||||
app.include_router(credentials_router, prefix="/api/credentials", tags=["credentials"])
|
||||
app.include_router(album_router, prefix="/api/album", tags=["album"])
|
||||
app.include_router(track_router, prefix="/api/track", tags=["track"])
|
||||
app.include_router(playlist_router, prefix="/api/playlist", tags=["playlist"])
|
||||
app.include_router(artist_router, prefix="/api/artist", tags=["artist"])
|
||||
app.include_router(prgs_router, prefix="/api/prgs", tags=["progress"])
|
||||
app.include_router(history_router, prefix="/api/history", tags=["history"])
|
||||
# Register routers with URL prefixes
|
||||
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
|
||||
|
||||
# Include SSO router if available
|
||||
try:
|
||||
from routes.auth.sso import router as sso_router
|
||||
app.include_router(sso_router, prefix="/api/auth", tags=["sso"])
|
||||
logging.info("SSO functionality enabled")
|
||||
except ImportError as e:
|
||||
logging.warning(f"SSO functionality not available: {e}")
|
||||
app.include_router(config_router, prefix="/api/config", tags=["config"])
|
||||
app.include_router(search_router, prefix="/api/search", tags=["search"])
|
||||
app.include_router(credentials_router, prefix="/api/credentials", tags=["credentials"])
|
||||
app.include_router(album_router, prefix="/api/album", tags=["album"])
|
||||
app.include_router(track_router, prefix="/api/track", tags=["track"])
|
||||
app.include_router(playlist_router, prefix="/api/playlist", tags=["playlist"])
|
||||
app.include_router(artist_router, prefix="/api/artist", tags=["artist"])
|
||||
app.include_router(prgs_router, prefix="/api/prgs", tags=["progress"])
|
||||
app.include_router(history_router, prefix="/api/history", tags=["history"])
|
||||
|
||||
# Add request logging middleware
|
||||
@app.middleware("http")
|
||||
async def log_requests(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
|
||||
# Log request
|
||||
logger = logging.getLogger("uvicorn.access")
|
||||
logger.debug(f"Request: {request.method} {request.url.path}")
|
||||
|
||||
try:
|
||||
response = await call_next(request)
|
||||
|
||||
# Log response
|
||||
duration = round((time.time() - start_time) * 1000, 2)
|
||||
logger.debug(f"Response: {response.status_code} | Duration: {duration}ms")
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
# Log errors
|
||||
logger.error(f"Server error: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal Server Error")
|
||||
# Add request logging middleware
|
||||
@app.middleware("http")
|
||||
async def log_requests(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
|
||||
# Log request
|
||||
logger = logging.getLogger("uvicorn.access")
|
||||
logger.debug(f"Request: {request.method} {request.url.path}")
|
||||
|
||||
try:
|
||||
response = await call_next(request)
|
||||
|
||||
# Log response
|
||||
duration = round((time.time() - start_time) * 1000, 2)
|
||||
logger.debug(f"Response: {response.status_code} | Duration: {duration}ms")
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
# Log errors
|
||||
logger.error(f"Server error: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal Server Error")
|
||||
|
||||
# Mount static files for React app
|
||||
if os.path.exists("spotizerr-ui/dist"):
|
||||
app.mount("/static", StaticFiles(directory="spotizerr-ui/dist"), name="static")
|
||||
|
||||
# Serve React App - catch-all route for SPA (but not for API routes)
|
||||
@app.get("/{full_path:path}")
|
||||
async def serve_react_app(full_path: str):
|
||||
"""Serve React app with fallback to index.html for SPA routing"""
|
||||
static_dir = "spotizerr-ui/dist"
|
||||
|
||||
# Don't serve React app for API routes (more specific check)
|
||||
if full_path.startswith("api") or full_path.startswith("api/"):
|
||||
raise HTTPException(status_code=404, detail="API endpoint not found")
|
||||
|
||||
# If it's a file that exists, serve it
|
||||
if full_path and os.path.exists(os.path.join(static_dir, full_path)):
|
||||
return FileResponse(os.path.join(static_dir, full_path))
|
||||
else:
|
||||
# Fallback to index.html for SPA routing
|
||||
return FileResponse(os.path.join(static_dir, "index.html"))
|
||||
else:
|
||||
logging.warning("React app build directory not found at spotizerr-ui/dist")
|
||||
# Mount static files for React app
|
||||
if os.path.exists("spotizerr-ui/dist"):
|
||||
app.mount("/static", StaticFiles(directory="spotizerr-ui/dist"), name="static")
|
||||
|
||||
# Serve React App - catch-all route for SPA (but not for API routes)
|
||||
@app.get("/{full_path:path}")
|
||||
async def serve_react_app(full_path: str):
|
||||
"""Serve React app with fallback to index.html for SPA routing"""
|
||||
static_dir = "spotizerr-ui/dist"
|
||||
|
||||
# Don't serve React app for API routes (more specific check)
|
||||
if full_path.startswith("api") or full_path.startswith("api/"):
|
||||
raise HTTPException(status_code=404, detail="API endpoint not found")
|
||||
|
||||
# If it's a file that exists, serve it
|
||||
if full_path and os.path.exists(os.path.join(static_dir, full_path)):
|
||||
return FileResponse(os.path.join(static_dir, full_path))
|
||||
else:
|
||||
# Fallback to index.html for SPA routing
|
||||
return FileResponse(os.path.join(static_dir, "index.html"))
|
||||
else:
|
||||
logging.warning("React app build directory not found at spotizerr-ui/dist")
|
||||
|
||||
return app
|
||||
return app
|
||||
|
||||
|
||||
def start_celery_workers():
|
||||
"""Start Celery workers with dynamic configuration"""
|
||||
# This function is now handled by the lifespan context manager
|
||||
# and the celery_manager.start() call
|
||||
pass
|
||||
"""Start Celery workers with dynamic configuration"""
|
||||
# This function is now handled by the lifespan context manager
|
||||
# and the celery_manager.start() call
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
import uvicorn
|
||||
|
||||
app = create_app()
|
||||
app = create_app()
|
||||
|
||||
# Use HOST environment variable if present, otherwise fall back to IPv4 wildcard
|
||||
host = os.getenv("HOST", "0.0.0.0")
|
||||
# Use HOST environment variable if present, otherwise fall back to IPv4 wildcard
|
||||
host = os.getenv("HOST", "0.0.0.0")
|
||||
|
||||
# Allow overriding port via PORT env var, with default 7171
|
||||
try:
|
||||
port = int(os.getenv("PORT", "7171"))
|
||||
except ValueError:
|
||||
port = 7171
|
||||
# Allow overriding port via PORT env var, with default 7171
|
||||
try:
|
||||
port = int(os.getenv("PORT", "7171"))
|
||||
except ValueError:
|
||||
port = 7171
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level="info",
|
||||
access_log=True
|
||||
)
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level="info",
|
||||
access_log=True
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user