mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-08 20:59:47 +10:00
257 lines
8.0 KiB
Python
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)
|
|
)
|