- "backend/models.py" - "alembic/versions/026_add_share_token.py" - "backend/pipeline/stages.py" - "backend/routers/shorts.py" - "backend/routers/shorts_public.py" - "backend/main.py" GSD-Task: S01/T01
95 lines
2.9 KiB
Python
95 lines
2.9 KiB
Python
"""Public (unauthenticated) endpoint for sharing generated shorts.
|
|
|
|
Resolves a share_token to video metadata and a presigned download URL.
|
|
No auth dependency — anyone with the token can access the short.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from database import get_session
|
|
from models import GeneratedShort, HighlightCandidate, ShortStatus, SourceVideo
|
|
|
|
logger = logging.getLogger("chrysopedia.shorts_public")
|
|
|
|
router = APIRouter(prefix="/public/shorts", tags=["shorts-public"])
|
|
|
|
|
|
class PublicShortResponse(BaseModel):
|
|
"""Public metadata for a shared short — no internal IDs exposed."""
|
|
format_preset: str
|
|
width: int
|
|
height: int
|
|
duration_secs: float | None
|
|
creator_name: str
|
|
highlight_title: str
|
|
download_url: str
|
|
|
|
|
|
@router.get("/{share_token}", response_model=PublicShortResponse)
|
|
async def get_public_short(
|
|
share_token: str,
|
|
db: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Resolve a share token to short metadata and a presigned download URL."""
|
|
stmt = (
|
|
select(GeneratedShort)
|
|
.where(GeneratedShort.share_token == share_token)
|
|
.options(
|
|
selectinload(GeneratedShort.highlight_candidate)
|
|
.selectinload(HighlightCandidate.key_moment),
|
|
selectinload(GeneratedShort.highlight_candidate)
|
|
.selectinload(HighlightCandidate.source_video)
|
|
.selectinload(SourceVideo.creator),
|
|
)
|
|
)
|
|
result = await db.execute(stmt)
|
|
short = result.scalar_one_or_none()
|
|
|
|
if short is None:
|
|
raise HTTPException(status_code=404, detail="Short not found")
|
|
|
|
if short.status != ShortStatus.complete:
|
|
raise HTTPException(status_code=404, detail="Short not found")
|
|
|
|
if not short.minio_object_key:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Short is complete but has no storage key",
|
|
)
|
|
|
|
highlight = short.highlight_candidate
|
|
key_moment = highlight.key_moment
|
|
source_video = highlight.source_video
|
|
creator = source_video.creator
|
|
|
|
from minio_client import generate_download_url
|
|
|
|
try:
|
|
url = generate_download_url(short.minio_object_key)
|
|
except Exception as exc:
|
|
logger.error(
|
|
"Failed to generate download URL for share_token=%s: %s",
|
|
share_token, exc,
|
|
)
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="Failed to generate download URL",
|
|
) from exc
|
|
|
|
return PublicShortResponse(
|
|
format_preset=short.format_preset.value,
|
|
width=short.width,
|
|
height=short.height,
|
|
duration_secs=short.duration_secs,
|
|
creator_name=creator.name,
|
|
highlight_title=key_moment.title,
|
|
download_url=url,
|
|
)
|