- таблица sections (дерево через parent_id), у видео section_id
- API: CRUD /sections, фильтры /videos (поиск, раздел с потомками, метод),
GET /videos/{id}/text, PATCH раздела, DELETE вместе с файлами
- pipeline: keep_video=false удаляет mp4 после расшифровки, section_id
- UI: панель разделов с вложенностью и счётчиками, поиск, фильтр по методу,
диалоги полного текста/выжимки, привязка к разделу, удаление,
переключатель «Сохранять видео на диске»
- compose: bind-mount ./web для правок без пересборки
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
72 lines
2.5 KiB
Python
72 lines
2.5 KiB
Python
import asyncio
|
|
import logging
|
|
|
|
import asyncpg
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from api.config import settings
|
|
from api.models.base import Base
|
|
import api.models.section # noqa: F401 - register model with Base.metadata
|
|
import api.models.video # noqa: F401 - register model with Base.metadata
|
|
|
|
|
|
logger = logging.getLogger("db")
|
|
|
|
engine = create_async_engine(settings.database_url, echo=False, future=True)
|
|
SessionLocal = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
|
|
|
|
|
|
async def wait_for_postgres(retries: int = 30, delay: float = 1.0) -> None:
|
|
"""Wait for the postgres server to accept connections (using the default 'postgres' db)."""
|
|
last_exc: Exception | None = None
|
|
for attempt in range(1, retries + 1):
|
|
try:
|
|
conn = await asyncpg.connect(
|
|
user=settings.DB_USER,
|
|
password=settings.DB_PASS,
|
|
database="postgres",
|
|
host=settings.DB_HOST,
|
|
port=int(settings.DB_PORT),
|
|
)
|
|
await conn.close()
|
|
return
|
|
except Exception as e:
|
|
last_exc = e
|
|
wait = min(delay * (2 ** (attempt - 1)), 5)
|
|
logger.info("Waiting for postgres (attempt %s/%s): %s", attempt, retries, e)
|
|
await asyncio.sleep(wait)
|
|
raise RuntimeError(f"Could not connect to postgres after {retries} attempts") from last_exc
|
|
|
|
|
|
async def _create_database_if_missing() -> None:
|
|
admin = await asyncpg.connect(
|
|
user=settings.DB_USER,
|
|
password=settings.DB_PASS,
|
|
database="postgres",
|
|
host=settings.DB_HOST,
|
|
port=int(settings.DB_PORT),
|
|
)
|
|
try:
|
|
exists = await admin.fetchval(
|
|
"SELECT 1 FROM pg_database WHERE datname=$1", settings.DB_NAME
|
|
)
|
|
if not exists:
|
|
await admin.execute(f'CREATE DATABASE "{settings.DB_NAME}"')
|
|
finally:
|
|
await admin.close()
|
|
|
|
|
|
async def init_db() -> None:
|
|
await _create_database_if_missing()
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
# create_all не добавляет колонки в уже существующие таблицы
|
|
await conn.execute(
|
|
text(
|
|
"ALTER TABLE videos ADD COLUMN IF NOT EXISTS section_id INTEGER "
|
|
"REFERENCES sections(id) ON DELETE SET NULL"
|
|
)
|
|
)
|