mirror of
https://git.victorphan.net/basketballcantho/ten-project.git
synced 2026-08-05 14:23:11 +07:00
186 lines
6.7 KiB
Python
186 lines
6.7 KiB
Python
"""
|
|
Audio management endpoints.
|
|
|
|
POST /api/pdfs/{pdf_id}/audio — upload one or more MP3 files (admin/teacher only)
|
|
GET /api/pdfs/{pdf_id}/audio — list all audio tracks for a PDF (any authenticated user)
|
|
DELETE /api/pdfs/{pdf_id}/audio/{audio_id} — delete a single track (admin/teacher only)
|
|
GET /api/pdfs/{pdf_id}/audio/{audio_id}/file — stream the audio file
|
|
"""
|
|
import os
|
|
import re
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, Depends, Form, HTTPException, Query, UploadFile, status
|
|
from fastapi.responses import FileResponse
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ..database import get_db
|
|
from ..dependencies import get_current_user, require_role
|
|
from ..models import PDF, PDFAudio, UserRole
|
|
from ..schemas import AudioOut, AudioUploadResult
|
|
|
|
router = APIRouter(prefix="/pdfs", tags=["audio"])
|
|
|
|
UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", "/uploads"))
|
|
MAX_AUDIO_SIZE = 200 * 1024 * 1024 # 200 MB per file
|
|
ALLOWED_AUDIO_TYPES = {"audio/mpeg", "audio/mp3", "audio/mp4", "audio/x-m4a", "audio/ogg", "application/octet-stream"}
|
|
|
|
|
|
def _audio_dir(pdf_id: int) -> Path:
|
|
d = UPLOAD_DIR / "audio" / str(pdf_id)
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
return d
|
|
|
|
|
|
def _safe_track_name(raw: str) -> str:
|
|
"""Strip control characters; truncate to 255 chars."""
|
|
cleaned = re.sub(r"[\x00-\x1f\x7f]", "", raw).strip()
|
|
return cleaned[:255] or "Track"
|
|
|
|
|
|
# ── GET /api/pdfs/{pdf_id}/audio ──────────────────────────────────────────────
|
|
|
|
@router.get("/{pdf_id}/audio", response_model=list[AudioOut])
|
|
def list_audio(
|
|
pdf_id: int,
|
|
current_user=Depends(get_current_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
pdf = db.get(PDF, pdf_id)
|
|
if not pdf:
|
|
raise HTTPException(status_code=404, detail="PDF not found.")
|
|
tracks = (
|
|
db.query(PDFAudio)
|
|
.filter(PDFAudio.pdf_id == pdf_id)
|
|
.order_by(PDFAudio.sort_order, PDFAudio.id)
|
|
.all()
|
|
)
|
|
return tracks
|
|
|
|
|
|
# ── POST /api/pdfs/{pdf_id}/audio ────────────────────────────────────────────
|
|
|
|
@router.post(
|
|
"/{pdf_id}/audio",
|
|
response_model=list[AudioUploadResult],
|
|
status_code=status.HTTP_201_CREATED,
|
|
)
|
|
async def upload_audio(
|
|
pdf_id: int,
|
|
files: list[UploadFile],
|
|
track_names: list[str] = Form(default=[]),
|
|
page_numbers: list[int] = Form(default=[]),
|
|
sort_orders: list[int] = Form(default=[]),
|
|
current_user=Depends(require_role(UserRole.admin, UserRole.teacher)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Upload one or more audio files for a PDF.
|
|
|
|
Optional form fields (parallel arrays, indexed per file):
|
|
- track_names: display name for each track
|
|
- page_numbers: page the track belongs to (0 = whole PDF / unset)
|
|
- sort_orders: ordering integer (default 1)
|
|
"""
|
|
pdf = db.get(PDF, pdf_id)
|
|
if not pdf:
|
|
raise HTTPException(status_code=404, detail="PDF not found.")
|
|
|
|
results: list[AudioUploadResult] = []
|
|
|
|
for idx, file in enumerate(files):
|
|
fname = file.filename or f"audio_{idx + 1}"
|
|
|
|
content_type = (file.content_type or "").lower()
|
|
if content_type not in ALLOWED_AUDIO_TYPES:
|
|
# Accept based on extension if browser sends wrong MIME
|
|
ext = Path(fname).suffix.lower()
|
|
if ext not in {".mp3", ".m4a", ".ogg", ".wav", ".aac"}:
|
|
results.append(AudioUploadResult(filename=fname, success=False, error="Unsupported audio format."))
|
|
continue
|
|
|
|
body = await file.read()
|
|
if len(body) > MAX_AUDIO_SIZE:
|
|
results.append(AudioUploadResult(filename=fname, success=False, error="File exceeds 200 MB limit."))
|
|
continue
|
|
|
|
safe_name = f"{uuid.uuid4()}{Path(fname).suffix or '.mp3'}"
|
|
dest = _audio_dir(pdf_id) / safe_name
|
|
dest.write_bytes(body)
|
|
|
|
# Derive metadata from parallel arrays (fall back to sensible defaults)
|
|
raw_track_name = track_names[idx] if idx < len(track_names) else Path(fname).stem
|
|
track_name = _safe_track_name(raw_track_name) or Path(fname).stem or f"Track {idx + 1}"
|
|
page_num = page_numbers[idx] if idx < len(page_numbers) else None
|
|
if page_num == 0:
|
|
page_num = None
|
|
sort_ord = sort_orders[idx] if idx < len(sort_orders) else (idx + 1)
|
|
|
|
# Determine next sort order if not supplied
|
|
if not sort_orders:
|
|
existing_count = db.query(PDFAudio).filter(PDFAudio.pdf_id == pdf_id).count()
|
|
sort_ord = existing_count + idx + 1
|
|
|
|
audio = PDFAudio(
|
|
pdf_id=pdf_id,
|
|
file_path=str(dest),
|
|
track_name=track_name,
|
|
page_number=page_num,
|
|
sort_order=sort_ord,
|
|
)
|
|
db.add(audio)
|
|
db.commit()
|
|
db.refresh(audio)
|
|
|
|
results.append(AudioUploadResult(filename=fname, success=True, audio=AudioOut.model_validate(audio)))
|
|
|
|
return results
|
|
|
|
|
|
# ── DELETE /api/pdfs/{pdf_id}/audio/{audio_id} ───────────────────────────────
|
|
|
|
@router.delete("/{pdf_id}/audio/{audio_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_audio(
|
|
pdf_id: int,
|
|
audio_id: int,
|
|
current_user=Depends(require_role(UserRole.admin, UserRole.teacher)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
track = db.query(PDFAudio).filter(PDFAudio.id == audio_id, PDFAudio.pdf_id == pdf_id).first()
|
|
if not track:
|
|
raise HTTPException(status_code=404, detail="Audio track not found.")
|
|
file_path = Path(track.file_path)
|
|
db.delete(track)
|
|
db.commit()
|
|
if file_path.exists():
|
|
file_path.unlink(missing_ok=True)
|
|
|
|
|
|
# ── GET /api/pdfs/{pdf_id}/audio/{audio_id}/file ─────────────────────────────
|
|
|
|
@router.get("/{pdf_id}/audio/{audio_id}/file")
|
|
def serve_audio(
|
|
pdf_id: int,
|
|
audio_id: int,
|
|
current_user=Depends(get_current_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
track = db.query(PDFAudio).filter(PDFAudio.id == audio_id, PDFAudio.pdf_id == pdf_id).first()
|
|
if not track:
|
|
raise HTTPException(status_code=404, detail="Audio track not found.")
|
|
file_path = Path(track.file_path)
|
|
if not file_path.exists():
|
|
raise HTTPException(status_code=404, detail="Audio file not found on disk.")
|
|
|
|
# Determine media type from extension
|
|
ext = file_path.suffix.lower()
|
|
media_type_map = {".mp3": "audio/mpeg", ".m4a": "audio/mp4", ".ogg": "audio/ogg", ".wav": "audio/wav", ".aac": "audio/aac"}
|
|
media_type = media_type_map.get(ext, "audio/mpeg")
|
|
|
|
return FileResponse(
|
|
path=str(file_path),
|
|
media_type=media_type,
|
|
filename=f"{track.track_name}{ext}",
|
|
)
|