276 lines
9.9 KiB
Python
276 lines
9.9 KiB
Python
from __future__ import annotations
|
|
|
|
import math
|
|
import uuid
|
|
from os import path
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from cpv3.infrastructure.auth import get_current_user
|
|
from cpv3.infrastructure.deps import get_storage
|
|
from cpv3.infrastructure.storage.base import StorageService
|
|
from cpv3.infrastructure.storage.utils import get_user_folder
|
|
from cpv3.db.session import get_db
|
|
from cpv3.modules.media.schemas import (
|
|
ArtifactMediaFileCreate,
|
|
ArtifactMediaFileRead,
|
|
ArtifactMediaFileUpdate,
|
|
FrameItem,
|
|
FrameRangeResponse,
|
|
MediaConverterParams,
|
|
MediaFileCreate,
|
|
MediaFileRead,
|
|
MediaFileUpdate,
|
|
MediaProbeSchema,
|
|
MediaSilencerParams,
|
|
)
|
|
from cpv3.modules.media.service import (
|
|
convert_to_mp4,
|
|
get_frames_folder,
|
|
probe_media,
|
|
read_frames_metadata,
|
|
remove_silence,
|
|
)
|
|
from cpv3.modules.media.repository import ArtifactRepository, MediaFileRepository
|
|
from cpv3.modules.files.schemas import FileInfoResponse
|
|
from cpv3.modules.users.models import User
|
|
|
|
media_router = APIRouter(prefix="/api/media", tags=["media"])
|
|
mediafiles_router = APIRouter(prefix="/api/media", tags=["mediafiles"])
|
|
artifacts_router = APIRouter(prefix="/api/media", tags=["artifacts"])
|
|
|
|
|
|
@media_router.get("/get_meta/", response_model=MediaProbeSchema)
|
|
async def get_meta(
|
|
file_path: str = Query(...),
|
|
current_user: User = Depends(get_current_user),
|
|
storage: StorageService = Depends(get_storage),
|
|
) -> MediaProbeSchema:
|
|
_ = current_user
|
|
return await probe_media(storage, file_key=file_path)
|
|
|
|
|
|
@media_router.post("/silence_remove", response_model=FileInfoResponse)
|
|
async def silence_remove(
|
|
body: MediaSilencerParams,
|
|
current_user: User = Depends(get_current_user),
|
|
storage: StorageService = Depends(get_storage),
|
|
) -> FileInfoResponse:
|
|
user_folder = get_user_folder(current_user)
|
|
resolved_folder = f"{user_folder}/{body.folder}" if body.folder else f"{user_folder}/output_files"
|
|
|
|
info = await remove_silence(
|
|
storage,
|
|
file_key=body.file_path,
|
|
out_folder=resolved_folder,
|
|
min_silence_duration_ms=body.min_silence_duration_ms,
|
|
silence_threshold_db=body.silence_threshold_db,
|
|
padding_ms=body.padding_ms,
|
|
)
|
|
|
|
return FileInfoResponse(
|
|
file_path=info.file_path,
|
|
file_url=info.file_url,
|
|
file_size=info.file_size,
|
|
filename=info.filename,
|
|
)
|
|
|
|
|
|
@media_router.post("/convert", response_model=FileInfoResponse)
|
|
async def convert(
|
|
body: MediaConverterParams,
|
|
current_user: User = Depends(get_current_user),
|
|
storage: StorageService = Depends(get_storage),
|
|
) -> FileInfoResponse:
|
|
user_folder = get_user_folder(current_user)
|
|
resolved_folder = f"{user_folder}/{body.folder}" if body.folder else f"{user_folder}/output_files"
|
|
|
|
info = await convert_to_mp4(storage, file_key=body.file_path, out_folder=resolved_folder)
|
|
return FileInfoResponse(
|
|
file_path=info.file_path,
|
|
file_url=info.file_url,
|
|
file_size=info.file_size,
|
|
filename=info.filename,
|
|
)
|
|
|
|
|
|
@media_router.get("/frames/", response_model=FrameRangeResponse)
|
|
async def get_frames(
|
|
file_key: str = Query(..., description="S3 key of the source video"),
|
|
start: float = Query(0.0, ge=0, description="Start time in seconds"),
|
|
end: float = Query(..., gt=0, description="End time in seconds"),
|
|
current_user: User = Depends(get_current_user),
|
|
storage: StorageService = Depends(get_storage),
|
|
) -> FrameRangeResponse:
|
|
"""Return presigned URLs for extracted frames within a time range."""
|
|
user_folder = get_user_folder(current_user)
|
|
frames_folder = get_frames_folder(user_folder, file_key)
|
|
|
|
metadata = await read_frames_metadata(storage, frames_folder=frames_folder)
|
|
if metadata is None:
|
|
return FrameRangeResponse(interval=1.0, frames=[])
|
|
|
|
interval = metadata.interval
|
|
first_index = max(1, math.floor(start / interval) + 1)
|
|
last_index = min(metadata.frame_count, math.ceil(end / interval) + 1)
|
|
|
|
frames: list[FrameItem] = []
|
|
for i in range(first_index, last_index + 1):
|
|
key = path.join(frames_folder, f"{i:06d}.jpg")
|
|
timestamp = (i - 1) * interval
|
|
url = await storage.url(key)
|
|
frames.append(FrameItem(timestamp=timestamp, url=url))
|
|
|
|
return FrameRangeResponse(interval=interval, frames=frames)
|
|
|
|
|
|
@mediafiles_router.get("/mediafiles/", response_model=list[MediaFileRead])
|
|
async def list_mediafiles(
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> list[MediaFileRead]:
|
|
repo = MediaFileRepository(db)
|
|
items = await repo.list_all(requester=current_user)
|
|
return [MediaFileRead.model_validate(m) for m in items]
|
|
|
|
|
|
@mediafiles_router.post(
|
|
"/mediafiles/", response_model=MediaFileRead, status_code=status.HTTP_201_CREATED
|
|
)
|
|
async def create_mediafile(
|
|
body: MediaFileCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> MediaFileRead:
|
|
repo = MediaFileRepository(db)
|
|
media_file = await repo.create(requester=current_user, data=body)
|
|
return MediaFileRead.model_validate(media_file)
|
|
|
|
|
|
@mediafiles_router.get("/mediafiles/{media_file_id}/", response_model=MediaFileRead)
|
|
async def retrieve_mediafile(
|
|
media_file_id: uuid.UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> MediaFileRead:
|
|
repo = MediaFileRepository(db)
|
|
media_file = await repo.get_by_id(media_file_id)
|
|
if media_file is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено")
|
|
|
|
if not current_user.is_staff and media_file.owner_id != current_user.id:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Доступ запрещён")
|
|
|
|
return MediaFileRead.model_validate(media_file)
|
|
|
|
|
|
@mediafiles_router.patch("/mediafiles/{media_file_id}/", response_model=MediaFileRead)
|
|
async def patch_mediafile(
|
|
media_file_id: uuid.UUID,
|
|
body: MediaFileUpdate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> MediaFileRead:
|
|
repo = MediaFileRepository(db)
|
|
media_file = await repo.get_by_id(media_file_id)
|
|
if media_file is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено")
|
|
|
|
if not current_user.is_staff and media_file.owner_id != current_user.id:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Доступ запрещён")
|
|
|
|
media_file = await repo.update(media_file, body)
|
|
return MediaFileRead.model_validate(media_file)
|
|
|
|
|
|
@mediafiles_router.delete("/mediafiles/{media_file_id}/", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_mediafile(
|
|
media_file_id: uuid.UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> Response:
|
|
repo = MediaFileRepository(db)
|
|
media_file = await repo.get_by_id(media_file_id)
|
|
if media_file is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено")
|
|
|
|
if not current_user.is_staff and media_file.owner_id != current_user.id:
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Доступ запрещён")
|
|
|
|
await repo.mark_deleted(media_file)
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
@artifacts_router.get("/artifacts/", response_model=list[ArtifactMediaFileRead])
|
|
async def list_artifact_mediafiles(
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> list[ArtifactMediaFileRead]:
|
|
_ = current_user
|
|
repo = ArtifactRepository(db)
|
|
items = await repo.list_all()
|
|
return [ArtifactMediaFileRead.model_validate(a) for a in items]
|
|
|
|
|
|
@artifacts_router.post(
|
|
"/artifacts/", response_model=ArtifactMediaFileRead, status_code=status.HTTP_201_CREATED
|
|
)
|
|
async def create_artifact_mediafile(
|
|
body: ArtifactMediaFileCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> ArtifactMediaFileRead:
|
|
_ = current_user
|
|
repo = ArtifactRepository(db)
|
|
artifact = await repo.create(body)
|
|
return ArtifactMediaFileRead.model_validate(artifact)
|
|
|
|
|
|
@artifacts_router.get("/artifacts/{artifact_id}/", response_model=ArtifactMediaFileRead)
|
|
async def retrieve_artifact_mediafile(
|
|
artifact_id: uuid.UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> ArtifactMediaFileRead:
|
|
_ = current_user
|
|
repo = ArtifactRepository(db)
|
|
artifact = await repo.get_by_id(artifact_id)
|
|
if artifact is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено")
|
|
|
|
return ArtifactMediaFileRead.model_validate(artifact)
|
|
|
|
|
|
@artifacts_router.patch("/artifacts/{artifact_id}/", response_model=ArtifactMediaFileRead)
|
|
async def patch_artifact_mediafile(
|
|
artifact_id: uuid.UUID,
|
|
body: ArtifactMediaFileUpdate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> ArtifactMediaFileRead:
|
|
_ = current_user
|
|
repo = ArtifactRepository(db)
|
|
artifact = await repo.get_by_id(artifact_id)
|
|
if artifact is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено")
|
|
|
|
artifact = await repo.update(artifact, body)
|
|
return ArtifactMediaFileRead.model_validate(artifact)
|
|
|
|
|
|
@artifacts_router.delete("/artifacts/{artifact_id}/", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_artifact_mediafile(
|
|
artifact_id: uuid.UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> Response:
|
|
_ = current_user
|
|
repo = ArtifactRepository(db)
|
|
artifact = await repo.get_by_id(artifact_id)
|
|
if artifact is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Не найдено")
|
|
|
|
await repo.mark_deleted(artifact)
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|