Files
you-music/backend/main.py
T
2025-10-30 22:35:02 +11:00

177 lines
6.2 KiB
Python

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
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}")
# Run database migrations
await run_migrations()
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"])
# 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)