from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, or_ from typing import List, Optional import os import shutil from pathlib import Path from app.db.session import get_db from app.models.models import Music from app.schemas.schemas import Music as MusicSchema, MusicCreate, MusicUpdate, MusicDetailInfo from app.services.downloader import music_downloader from app.core.config import settings router = APIRouter() @router.get("/", response_model=List[MusicSchema]) async def get_all_music( skip: int = 0, limit: int = 100, include_missing: bool = True, db: AsyncSession = Depends(get_db) ): """Get all music files""" query = select(Music) # Filter out missing files if requested if not include_missing: query = query.where(Music.file_exists == True) result = await db.execute( query.offset(skip).limit(limit) ) music_list = result.scalars().all() return music_list @router.get("/search", response_model=List[MusicSchema]) async def search_music( q: str = Query(..., min_length=1), db: AsyncSession = Depends(get_db) ): """Search music by name or artist""" result = await db.execute( select(Music).where( or_( Music.title.contains(q), Music.artist.contains(q) ) ) ) music_list = result.scalars().all() return music_list @router.get("/{music_id}", response_model=MusicSchema) async def get_music(music_id: int, db: AsyncSession = Depends(get_db)): """Get specific music by ID""" result = await db.execute( select(Music).where(Music.id == music_id) ) music = result.scalar_one_or_none() if not music: raise HTTPException(status_code=404, detail="Music not found") return music @router.get("/{music_id}/info", response_model=MusicDetailInfo) async def get_music_detail_info(music_id: int, db: AsyncSession = Depends(get_db)): """Get detailed information about a music file""" result = await db.execute( select(Music).where(Music.id == music_id) ) music = result.scalar_one_or_none() if not music: raise HTTPException(status_code=404, detail="Music not found") return music @router.put("/{music_id}", response_model=MusicSchema) async def update_music( music_id: int, music_update: MusicUpdate, db: AsyncSession = Depends(get_db) ): """Update music metadata""" result = await db.execute( select(Music).where(Music.id == music_id) ) music = result.scalar_one_or_none() if not music: raise HTTPException(status_code=404, detail="Music not found") for field, value in music_update.model_dump(exclude_unset=True).items(): setattr(music, field, value) await db.commit() await db.refresh(music) return music @router.delete("/{music_id}") async def delete_music(music_id: int, db: AsyncSession = Depends(get_db)): """Delete music file and database record""" result = await db.execute( select(Music).where(Music.id == music_id) ) music = result.scalar_one_or_none() if not music: raise HTTPException(status_code=404, detail="Music not found") # Delete physical file file_path = os.path.join(settings.MUSIC_DIR, music.file_path) if os.path.exists(file_path): os.remove(file_path) await db.delete(music) await db.commit() return {"message": "Music deleted successfully"} @router.post("/upload", response_model=MusicSchema) async def upload_music( file: UploadFile = File(...), db: AsyncSession = Depends(get_db) ): """Upload a music file""" # Validate file extension allowed_extensions = ['.mp3', '.m4a', '.flac', '.wav', '.ogg'] file_ext = os.path.splitext(file.filename)[1].lower() if file_ext not in allowed_extensions: raise HTTPException( status_code=400, detail=f"File type not supported. Allowed: {allowed_extensions}" ) # Save file file_path = os.path.join(settings.UPLOAD_DIR, file.filename) with open(file_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer) # Extract metadata metadata = await music_downloader.get_music_metadata(file_path) # Get file format file_extension = os.path.splitext(file.filename)[1][1:] # Create database record db_music = Music( title=metadata.get("title", file.filename), artist=metadata.get("artist", "Unknown"), album=metadata.get("album", ""), duration=metadata.get("duration", 0), file_path=os.path.join("uploads", file.filename), file_size=os.path.getsize(file_path), file_format=file_extension, file_location=file_path, file_exists=True, source_type="upload" ) db.add(db_music) await db.commit() await db.refresh(db_music) return db_music @router.get("/artist/{artist_name}", response_model=List[MusicSchema]) async def get_music_by_artist( artist_name: str, db: AsyncSession = Depends(get_db) ): """Get all music by a specific artist""" result = await db.execute( select(Music).where(Music.artist == artist_name) ) music_list = result.scalars().all() return music_list @router.post("/scan") async def scan_music_directory(db: AsyncSession = Depends(get_db)): """Scan music directory and add new files to database""" music_dir = Path(settings.MUSIC_DIR) added_count = 0 for file_path in music_dir.rglob("*"): if file_path.is_file() and file_path.suffix.lower() in ['.mp3', '.m4a', '.flac', '.wav']: relative_path = str(file_path.relative_to(music_dir)) # Check if already in database result = await db.execute( select(Music).where(Music.file_path == relative_path) ) existing = result.scalar_one_or_none() if not existing: # Add to database metadata = await music_downloader.get_music_metadata(str(file_path)) db_music = Music( title=metadata.get("title", file_path.name), artist=metadata.get("artist", "Unknown"), album=metadata.get("album", ""), duration=metadata.get("duration", 0), file_path=relative_path, file_size=file_path.stat().st_size, file_format=file_path.suffix[1:], # Extension without dot file_location=str(file_path), file_exists=True, source_type="local" ) db.add(db_music) added_count += 1 await db.commit() return {"message": f"Scan complete. Added {added_count} new files."} @router.get("/file/{music_id}") async def serve_music_file( music_id: int, db: AsyncSession = Depends(get_db) ): """Serve music file - handles both regular and local music directory files""" # Get music record result = await db.execute(select(Music).where(Music.id == music_id)) music = result.scalar_one_or_none() if not music: raise HTTPException(status_code=404, detail="Music not found") # Determine file path if music.file_location and os.path.isabs(music.file_location): # Use absolute path from file_location (local music dir) file_path = music.file_location else: # Use relative path from MUSIC_DIR (downloaded music) file_path = os.path.join(settings.MUSIC_DIR, music.file_path) # Check if file exists if not os.path.exists(file_path): raise HTTPException(status_code=404, detail="File not found on disk") # Return file return FileResponse( path=file_path, media_type=f"audio/{music.file_format or 'mpeg'}", filename=os.path.basename(file_path) )