2026-07-03 01:06:35 +00:00

159 lines
5.0 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from typing import Optional
from ..db.database import get_db
from ..models.song import Song
from ..schemas.song import SongResponse, ScanResult
from ..schemas.common import PaginatedResponse
from ..services.audio import (
scan_directory, extract_metadata, delete_song as delete_song_service,
get_stream_path, transcode_to_ogg, SUPPORTED_FORMATS
)
import uuid
import os
import aiofiles
router = APIRouter(prefix="/api/songs", tags=["songs"])
@router.get("", response_model=PaginatedResponse[SongResponse])
def list_songs(
page: int = Query(1, ge=1),
per_page: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
):
offset = (page - 1) * per_page
total = db.query(Song).count()
songs = db.query(Song).offset(offset).limit(per_page).all()
return PaginatedResponse(
items=[SongResponse.model_validate(s) for s in songs],
total=total,
page=page,
per_page=per_page,
total_pages=max(1, (total + per_page - 1) // per_page),
)
@router.get("/{song_id}", response_model=SongResponse)
def get_song(song_id: str, db: Session = Depends(get_db)):
song = db.query(Song).filter(Song.id == song_id).first()
if not song:
raise HTTPException(status_code=404, detail="Song not found")
return SongResponse.model_validate(song)
@router.get("/{song_id}/stream")
async def stream_song(
song_id: str,
range: Optional[str] = None,
db: Session = Depends(get_db),
):
song = db.query(Song).filter(Song.id == song_id).first()
if not song:
raise HTTPException(status_code=404, detail="Song not found")
file_path = get_stream_path(song)
if not file_path:
raise HTTPException(status_code=404, detail="Audio file not found")
file_size = os.path.getsize(file_path)
start = 0
end = file_size - 1
status_code = 200
chunk_size = 1024 * 1024
if range:
try:
range_str = range.replace("bytes=", "")
start = int(range_str.split("-")[0])
end_part = range_str.split("-")[1]
end = int(end_part) if end_part else file_size - 1
status_code = 206
except (ValueError, IndexError):
pass
headers = {
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Accept-Ranges": "bytes",
"Content-Length": str(end - start + 1),
}
import mimetypes
content_type = mimetypes.guess_type(file_path)[0] or "audio/octet-stream"
async def stream():
async with aiofiles.open(file_path, mode="rb") as f:
await f.seek(start)
remaining = end - start + 1
while remaining > 0:
chunk = await f.read(min(chunk_size, remaining))
if not chunk:
break
remaining -= len(chunk)
yield chunk
return StreamingResponse(
stream(),
status_code=status_code,
headers=headers,
media_type=content_type,
)
@router.post("/upload")
async def upload_song(file: UploadFile = File(...), db: Session = Depends(get_db)):
upload_dir = os.getenv("UPLOAD_DIR", "./uploads")
os.makedirs(upload_dir, exist_ok=True)
ext = os.path.splitext(file.filename)[1].lower()
if ext not in SUPPORTED_FORMATS:
raise HTTPException(status_code=400, detail=f"Unsupported format: {ext}")
file_id = str(uuid.uuid4())
saved_filename = f"{file_id}_{uuid.uuid4().hex[:8]}{ext}"
file_path = os.path.join(upload_dir, saved_filename)
async with aiofiles.open(file_path, "wb") as f:
content = await file.read()
await f.write(content)
metadata = extract_metadata(file_path)
transcoded_path = transcode_to_ogg(file_path, upload_dir)
song = Song(
id=file_id,
title=metadata.get("title") or file.filename.replace(ext, ""),
artist=metadata.get("artist") or "Unknown",
album=metadata.get("album"),
duration_sec=metadata.get("duration", 0),
genre=metadata.get("genre"),
file_path=file_path,
transcoded_path=transcoded_path,
album_art_path=metadata.get("album_art_path"),
file_format=ext.replace(".", ""),
file_size_bytes=len(content),
)
db.add(song)
db.commit()
db.refresh(song)
return SongResponse.model_validate(song)
@router.post("/scan", response_model=ScanResult)
def scan_songs(directory: str = Query(None), db: Session = Depends(get_db)):
if not directory:
directory = os.getenv("MUSIC_DIR", "./music")
return scan_directory(directory, db)
@router.delete("/{song_id}")
def delete_song(song_id: str, db: Session = Depends(get_db)):
song = db.query(Song).filter(Song.id == song_id).first()
if not song:
raise HTTPException(status_code=404, detail="Song not found")
delete_song_service(song)
db.delete(song)
db.commit()
return {"message": "Song deleted"}