import os from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from app.api import music, playlist, download, search, stream, artist, settings as settings_api, auto_download, api_keys, v1 from app.core.config import settings from app.db.session import init_db from app.services.scheduler import start_scheduler, stop_scheduler import subprocess import sys async def run_migrations(): """Run database migrations on startup""" try: print("🔄 Running database migrations...") # Get the backend directory backend_dir = Path(__file__).parent # Run alembic upgrade head result = subprocess.run( [sys.executable, "-m", "alembic", "upgrade", "head"], cwd=backend_dir, capture_output=True, text=True ) if result.returncode == 0: print("✅ Database migrations completed successfully") if result.stdout: for line in result.stdout.strip().split('\n'): if line.strip(): print(f" {line}") else: print("⚠️ Migration warnings:") if result.stderr: for line in result.stderr.strip().split('\n'): if line.strip(): print(f" {line}") except Exception as e: print(f"⚠️ Could not run migrations: {e}") print(" You may need to run migrations manually: ./migrate.sh upgrade") @asynccontextmanager async def lifespan(app: FastAPI): # Startup print("=" * 80) print("🎵 YouMusic Starting Up") print("=" * 80) print(f"📂 BASE_DIR: {settings.BASE_DIR}") print(f"📂 MUSIC_DIR: {settings.MUSIC_DIR}") print(f"📂 LOCAL_MUSIC_DIR: {settings.LOCAL_MUSIC_DIR or 'Not configured'}") print(f"📂 UPLOAD_DIR: {settings.UPLOAD_DIR}") print(f"📂 TEMP_DIR: {settings.TEMP_DIR}") print(f"📂 DATABASE: {settings.DATABASE_URL}") cache_dir = os.path.join(settings.BASE_DIR, "data", "cache") artist_cache_dir = os.path.join(cache_dir, "artists") artist_images_dir = os.path.join(cache_dir, "artist_images") print(f"📂 CACHE_DIR: {cache_dir}") print(f"📂 ARTIST_CACHE: {artist_cache_dir}") print(f"📂 ARTIST_IMAGES: {artist_images_dir}") print(f"🎬 FFMPEG: {settings.FFMPEG_LOCATION}") print(f"⏰ AUTO_SCAN: {settings.AUTO_SCAN_ENABLED}") print(f"⏱️ SCAN_INTERVAL: {settings.SCAN_INTERVAL}s ({settings.SCAN_INTERVAL // 3600}h)") print("=" * 80) os.makedirs(settings.MUSIC_DIR, exist_ok=True) os.makedirs(settings.UPLOAD_DIR, exist_ok=True) os.makedirs(cache_dir, exist_ok=True) os.makedirs(artist_cache_dir, exist_ok=True) os.makedirs(artist_images_dir, exist_ok=True) if settings.LOCAL_MUSIC_DIR: os.makedirs(settings.LOCAL_MUSIC_DIR, exist_ok=True) print(f"✅ LOCAL_MUSIC_DIR created: {settings.LOCAL_MUSIC_DIR}") # Note: Database migrations are run by start.sh before uvicorn starts # We just initialize the connection pool here await init_db() # Start scheduler start_scheduler() print("✅ All directories created and database initialized") print("=" * 80) yield # Shutdown stop_scheduler() print("🛑 Scheduler stopped") app = FastAPI( title="YouMusic API", description="A modern web music player with download capabilities", version="1.0.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Mount static files os.makedirs(settings.MUSIC_DIR, exist_ok=True) app.mount("/music", StaticFiles(directory=settings.MUSIC_DIR), name="music") # Mount cache directory for artist images cache_dir = os.path.join(settings.BASE_DIR, "data", "cache") os.makedirs(cache_dir, exist_ok=True) app.mount("/cache", StaticFiles(directory=cache_dir), name="cache") # Include routers app.include_router(music.router, prefix="/api/music", tags=["music"]) app.include_router(playlist.router, prefix="/api/playlists", tags=["playlists"]) app.include_router(download.router, prefix="/api/download", tags=["download"]) app.include_router(search.router, prefix="/api/search", tags=["search"]) app.include_router(stream.router, prefix="/api", tags=["stream"]) app.include_router(artist.router, prefix="/api/artists", tags=["artists"]) app.include_router(settings_api.router, prefix="/api/settings", tags=["settings"]) app.include_router(auto_download.router, prefix="/api/auto-download", tags=["auto-download"]) app.include_router(api_keys.router, prefix="/api/api-keys", tags=["api-keys"]) app.include_router(v1.router, prefix="/api/v1", tags=["v1-public"]) # Health check endpoint (must be before catch-all route) @app.get("/health") async def health(): return {"status": "healthy"} # Mount frontend static files (built React app) static_dir = Path(__file__).parent / "static" if static_dir.exists(): app.mount("/assets", StaticFiles(directory=str(static_dir / "assets")), name="assets") @app.get("/") async def serve_frontend(): """Serve the React frontend""" index_file = static_dir / "index.html" if index_file.exists(): return FileResponse(index_file) return {"message": "YouMusic API is running (frontend not built)"} @app.get("/{full_path:path}") async def serve_spa(full_path: str): """Serve React app for all non-API routes (SPA routing)""" # Skip API routes, health check, and static assets if full_path.startswith(("api/", "music/", "cache/", "assets/", "health", "docs", "redoc", "openapi.json")): return {"error": "Not found"} # For all other routes, serve index.html (React Router handles client-side routing) index_file = static_dir / "index.html" if index_file.exists(): return FileResponse(index_file) return {"error": "Frontend not found"} else: @app.get("/") async def root(): return {"message": "YouMusic API is running (frontend not built)"} @app.get("/health") async def health_no_frontend(): return {"status": "healthy"} if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)