diff --git a/api/src/rehearsalhub/routers/versions.py b/api/src/rehearsalhub/routers/versions.py index a4264b0..bdc34a1 100644 --- a/api/src/rehearsalhub/routers/versions.py +++ b/api/src/rehearsalhub/routers/versions.py @@ -1,22 +1,61 @@ import uuid +from pathlib import Path from typing import Any -from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.responses import RedirectResponse +from fastapi import APIRouter, Depends, HTTPException, Query, Request, status +from fastapi.responses import Response +from fastapi.security.utils import get_authorization_scheme_param from sqlalchemy.ext.asyncio import AsyncSession from rehearsalhub.db.engine import get_session from rehearsalhub.db.models import Member from rehearsalhub.dependencies import get_current_member from rehearsalhub.repositories.audio_version import AudioVersionRepository +from rehearsalhub.repositories.member import MemberRepository from rehearsalhub.repositories.song import SongRepository from rehearsalhub.schemas.audio_version import AudioVersionCreate, AudioVersionRead +from rehearsalhub.services.auth import decode_token from rehearsalhub.services.band import BandService from rehearsalhub.services.song import SongService from rehearsalhub.storage.nextcloud import NextcloudClient router = APIRouter(tags=["versions"]) +_AUDIO_CONTENT_TYPES: dict[str, str] = { + ".mp3": "audio/mpeg", + ".wav": "audio/wav", + ".flac": "audio/flac", + ".ogg": "audio/ogg", + ".m4a": "audio/mp4", + ".aac": "audio/aac", + ".opus": "audio/ogg", + ".m3u8": "application/vnd.apple.mpegurl", +} + + +async def _member_from_request( + request: Request, + token: str | None = Query(None), + session: AsyncSession = Depends(get_session), +) -> Member: + """Resolve member from Authorization header or ?token= query param.""" + auth_header = request.headers.get("Authorization") + if auth_header: + scheme, param = get_authorization_scheme_param(auth_header) + if scheme.lower() == "bearer": + token = param + if not token: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token required") + try: + payload = decode_token(token) + member_id = uuid.UUID(payload["sub"]) + except Exception: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") + member = await MemberRepository(session).get_by_id(member_id) + if member is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Member not found") + return member + async def _get_version_and_assert_band_membership( version_id: uuid.UUID, @@ -110,11 +149,19 @@ async def get_waveform( async def stream_version( version_id: uuid.UUID, session: AsyncSession = Depends(get_session), - current_member: Member = Depends(get_current_member), + current_member: Member = Depends(_member_from_request), ): version, _ = await _get_version_and_assert_band_membership(version_id, session, current_member) - if not version.cdn_hls_base: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Stream not ready") + + # Prefer HLS playlist if transcoding finished, otherwise serve the raw file + if version.cdn_hls_base: + file_path = f"{version.cdn_hls_base}/playlist.m3u8" + elif version.nc_file_path: + file_path = version.nc_file_path + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No audio file") + storage = NextcloudClient() - url = await storage.get_direct_url(f"{version.cdn_hls_base}/playlist.m3u8") - return RedirectResponse(url=url, status_code=302) + data = await storage.download(file_path) + content_type = _AUDIO_CONTENT_TYPES.get(Path(file_path).suffix.lower(), "application/octet-stream") + return Response(content=data, media_type=content_type) diff --git a/web/src/hooks/useWaveform.ts b/web/src/hooks/useWaveform.ts index fb319a7..0023fcb 100644 --- a/web/src/hooks/useWaveform.ts +++ b/web/src/hooks/useWaveform.ts @@ -31,7 +31,9 @@ export function useWaveform( normalize: true, }); - ws.load(options.url); + const token = localStorage.getItem("rh_token"); + const audioUrl = token ? `${options.url}?token=${encodeURIComponent(token)}` : options.url; + ws.load(audioUrl); ws.on("ready", () => { setIsReady(true);