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

257 lines
8.0 KiB
Python

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)
)