Фоновые задачи: плейлисты, длинные лекции, вкладка настроек
- таблица jobs + очередь с воркером: POST /jobs сразу отвечает, элементы
плейлиста обрабатываются последовательно, есть отмена и живой прогресс
(скачивание %, минуты распознанного Vosk-аудио)
- expand_url: плейлист раскрывается в список видео
- keep_video=false теперь качает только аудио (для 4-5ч лекций)
- настройки (LLM-ключ, модель, длина выжимки, хранение видео, путь Vosk)
хранятся в Postgres: переживают перезапуск и перезагрузку страницы
- длина выжимки прокидывается в LLM-промпт ({n} предложений)
- UI: вкладки «Главная»/«Настройки», карточка задач с автообновлением
каждые 3 сек, прогресс-бар, отмена, библиотека обновляется по мере
готовности элементов
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
112
api/route/jobs.py
Normal file
112
api/route/jobs.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Фоновые задачи: видео или плейлист любой длины.
|
||||
|
||||
POST ставит задачу в очередь и сразу возвращает id; прогресс — в GET /jobs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid as _uuid
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from api.db.job import create_job, delete_job, get_job, list_jobs
|
||||
from api.lib import jobqueue
|
||||
|
||||
|
||||
router = APIRouter(prefix="/jobs", tags=["jobs"])
|
||||
|
||||
|
||||
class JobIn(BaseModel):
|
||||
url: str
|
||||
section_id: int | None = None
|
||||
# None → берутся из сохранённых настроек
|
||||
keep_video: bool | None = None
|
||||
summary_max_sentences: int | None = None
|
||||
model_path: str | None = None
|
||||
|
||||
|
||||
class JobOut(BaseModel):
|
||||
id: str
|
||||
url: str
|
||||
kind: str
|
||||
title: str
|
||||
status: str
|
||||
total_items: int
|
||||
done_items: int
|
||||
failed_items: int
|
||||
current_item: str
|
||||
live: str
|
||||
error: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
def _to_out(j) -> JobOut:
|
||||
return JobOut(
|
||||
id=j.id,
|
||||
url=j.url,
|
||||
kind=j.kind,
|
||||
title=j.title,
|
||||
status=j.status,
|
||||
total_items=j.total_items,
|
||||
done_items=j.done_items,
|
||||
failed_items=j.failed_items,
|
||||
current_item=j.current_item,
|
||||
live=jobqueue.live_status(j.id) or j.current_item,
|
||||
error=j.error,
|
||||
created_at=j.created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/", response_model=JobOut)
|
||||
async def create(body: JobIn):
|
||||
url = body.url.strip()
|
||||
if not url:
|
||||
raise HTTPException(status_code=400, detail="URL is empty")
|
||||
job_id = _uuid.uuid4().hex
|
||||
j = await create_job(job_id, url)
|
||||
jobqueue.submit(
|
||||
job_id,
|
||||
{
|
||||
"section_id": body.section_id,
|
||||
"keep_video": body.keep_video,
|
||||
"summary_max_sentences": body.summary_max_sentences,
|
||||
"model_path": body.model_path,
|
||||
},
|
||||
)
|
||||
return _to_out(j)
|
||||
|
||||
|
||||
@router.get("/", response_model=list[JobOut])
|
||||
async def list_all(limit: int = 50):
|
||||
return [_to_out(j) for j in await list_jobs(limit)]
|
||||
|
||||
|
||||
@router.get("/{job_id}", response_model=JobOut)
|
||||
async def get_one(job_id: str):
|
||||
j = await get_job(job_id)
|
||||
if j is None:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
return _to_out(j)
|
||||
|
||||
|
||||
@router.post("/{job_id}/cancel")
|
||||
async def cancel(job_id: str):
|
||||
j = await get_job(job_id)
|
||||
if j is None:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
if j.status not in ("queued", "running"):
|
||||
raise HTTPException(status_code=400, detail=f"Job is {j.status}")
|
||||
jobqueue.request_cancel(job_id)
|
||||
return {"ok": True, "note": "остановится после текущего элемента"}
|
||||
|
||||
|
||||
@router.delete("/{job_id}")
|
||||
async def delete(job_id: str):
|
||||
j = await get_job(job_id)
|
||||
if j is None:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
if j.status in ("queued", "running"):
|
||||
raise HTTPException(status_code=400, detail="Сначала отмените задачу")
|
||||
await delete_job(job_id)
|
||||
return {"ok": True}
|
||||
Reference in New Issue
Block a user