Files
you-music/backend/main.py
T
junv f7e4686816 Implement the following:* Search function should time out in 1 min if something goes wrong
* Add function tofull-size current playing song through a button in the bottom control panel
* Add an API also a feature in the home page, to allow user type a song name, then create a backend job to auto search and download song in the background, when pick the song from the results, put the result into priority candicdate if the name either has “official song” or “官方”, if the same exact song has been downloaded before, then just put the job as pending to confirm, and let user to confirm, once user confirmed, then the system can download the duplicate song, we need to persist the those jobs, if a job fails, then we mark it failed, and user can retry it later through UI, also add an summary about how to trigger this function through API
* Add API keys section in settings, so we need to evaluate api keys for public APIs
    * Now we only put the auto search and download song into the pubic API
* Add feature in search to search artist, which would return all the matched artists,
* Add sort feature to the artists and library page, like the one we have in playlist detail page
* By default sort the song in the library page by added at desc
2025-10-31 09:17:44 +11:00

178 lines
6.5 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, auto_download, api_keys
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"])
# 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)