mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-09 05:06:44 +10:00
* 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
178 lines
6.5 KiB
Python
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)
|