Фоновые задачи: плейлисты, длинные лекции, вкладка настроек

- таблица 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:
jze9
2026-07-14 15:01:11 +05:00
parent bf854f2d6e
commit e87f6cd215
18 changed files with 1059 additions and 292 deletions

53
api/db/job.py Normal file
View File

@@ -0,0 +1,53 @@
from sqlalchemy import select, update
from api.db.connection import SessionLocal
from api.models.job import Job
async def create_job(job_id: str, url: str) -> Job:
async with SessionLocal() as session:
j = Job(id=job_id, url=url, status="queued")
session.add(j)
await session.commit()
await session.refresh(j)
return j
async def get_job(job_id: str) -> Job | None:
async with SessionLocal() as session:
return await session.get(Job, job_id)
async def list_jobs(limit: int = 50) -> list[Job]:
async with SessionLocal() as session:
res = await session.execute(
select(Job).order_by(Job.created_at.desc()).limit(limit)
)
return list(res.scalars().all())
async def update_job(job_id: str, **fields) -> None:
async with SessionLocal() as session:
await session.execute(update(Job).where(Job.id == job_id).values(**fields))
await session.commit()
async def delete_job(job_id: str) -> bool:
async with SessionLocal() as session:
j = await session.get(Job, job_id)
if j is None:
return False
await session.delete(j)
await session.commit()
return True
async def mark_interrupted() -> None:
"""На старте приложения: задачи, шедшие до перезапуска, отмечаем прерванными."""
async with SessionLocal() as session:
await session.execute(
update(Job)
.where(Job.status.in_(("queued", "running")))
.values(status="interrupted", current_item="")
)
await session.commit()