mirror of
https://git.victorphan.net/basketballcantho/ten-project.git
synced 2026-08-06 02:23:11 +07:00
hoàn thành tính năng mp3 player
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
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}",
|
||||
)
|
||||
Reference in New Issue
Block a user