from datetime import datetime from pathlib import Path from typing import List, Literal from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel from api.db.section import list_sections from api.db.video import ( delete_video, get_video, list_videos, set_video_section, ) router = APIRouter(prefix="/videos", tags=["videos"]) _PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent class VideoOut(BaseModel): uuid: str source_url: str section_id: int | None title: str video_path: str text_full_path: str text_summary_path: str transcription_method: str created_at: datetime class SectionPatch(BaseModel): section_id: int | None def _to_out(v) -> VideoOut: return VideoOut( uuid=v.uuid, source_url=v.source_url, section_id=v.section_id, title=v.title, video_path=v.video_path, text_full_path=v.text_full_path, text_summary_path=v.text_summary_path, transcription_method=v.transcription_method, created_at=v.created_at, ) async def _descendant_ids(section_id: int) -> list[int]: """Раздел + все его потомки (фильтр по разделу захватывает подразделы).""" sections = await list_sections() children: dict[int | None, list[int]] = {} for s in sections: children.setdefault(s["parent_id"], []).append(s["id"]) ids, stack = [], [section_id] while stack: cur = stack.pop() ids.append(cur) stack.extend(children.get(cur, [])) return ids @router.get("/", response_model=List[VideoOut]) async def list_all( q: str | None = Query(default=None, description="Поиск по названию/URL"), section_id: int | None = Query(default=None, description="Раздел (включая подразделы)"), method: str | None = Query(default=None, description="Префикс метода: subtitles | vosk"), ): section_ids = await _descendant_ids(section_id) if section_id is not None else None return [ _to_out(v) for v in await list_videos(q=q, section_ids=section_ids, method=method) ] @router.get("/{video_uuid}", response_model=VideoOut) async def get_one(video_uuid: str): v = await get_video(video_uuid) if not v: raise HTTPException(status_code=404, detail="Video not found") return _to_out(v) @router.get("/{video_uuid}/text") async def get_text(video_uuid: str, kind: Literal["full", "summary"] = "full"): v = await get_video(video_uuid) if not v: raise HTTPException(status_code=404, detail="Video not found") rel = v.text_full_path if kind == "full" else v.text_summary_path if not rel: raise HTTPException(status_code=404, detail="Text file is not recorded") p = (_PROJECT_ROOT / rel).resolve() try: p.relative_to(_PROJECT_ROOT) except ValueError: raise HTTPException(status_code=400, detail="Bad text path") if not p.exists(): raise HTTPException(status_code=404, detail=f"File missing on disk: {rel}") return {"uuid": video_uuid, "kind": kind, "text": p.read_text(encoding="utf-8", errors="ignore")} @router.patch("/{video_uuid}", response_model=VideoOut) async def patch_section(video_uuid: str, body: SectionPatch): v = await set_video_section(video_uuid, body.section_id) if not v: raise HTTPException(status_code=404, detail="Video not found") return _to_out(v) @router.delete("/{video_uuid}") async def delete_one(video_uuid: str, delete_files: bool = True): v = await get_video(video_uuid) if not v: raise HTTPException(status_code=404, detail="Video not found") if delete_files: for rel in (v.video_path, v.text_full_path, v.text_summary_path): if not rel: continue p = (_PROJECT_ROOT / rel).resolve() try: p.relative_to(_PROJECT_ROOT) except ValueError: continue p.unlink(missing_ok=True) # сопутствующие субтитры .vtt рядом с видео for p in (_PROJECT_ROOT / "data" / "videos").glob(f"{video_uuid}*.vtt"): p.unlink(missing_ok=True) await delete_video(video_uuid) return {"ok": True}