98 lines
3.3 KiB
Python
98 lines
3.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""generator/output 스캔 → content 테이블 upsert.
|
|
|
|
디스크의 완성 mp4 와 DB 를 잇는다. 시작 시 + 잡 완료 시 호출.
|
|
"""
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from . import blob_client
|
|
from .config import OUTPUT_DIR, ENGINE_SCENARIO
|
|
from .database import SessionLocal
|
|
from .models import Content
|
|
|
|
_TITLE_RE = re.compile(r"\[제목\]\s*(.+)")
|
|
|
|
|
|
def _cleanup_dir(job_dir: Path) -> None:
|
|
"""블롭 업로드·DB 커밋 성공한 job 폴더의 로컬 원본 제거(디스크 절약).
|
|
|
|
안전장치: 반드시 OUTPUT_DIR 하위 경로일 때만 삭제.
|
|
"""
|
|
try:
|
|
job_dir = job_dir.resolve()
|
|
if OUTPUT_DIR.resolve() in job_dir.parents and job_dir.is_dir():
|
|
shutil.rmtree(job_dir, ignore_errors=True)
|
|
except Exception as e:
|
|
print(f"[cleanup] skip {job_dir}: {type(e).__name__}: {e}")
|
|
|
|
|
|
def _title(job_dir) -> str:
|
|
txt = job_dir / "narration.txt"
|
|
if txt.exists():
|
|
for line in txt.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
m = _TITLE_RE.match(line.strip())
|
|
if m:
|
|
return m.group(1).strip()
|
|
return job_dir.name
|
|
|
|
|
|
def _scan() -> list[dict]:
|
|
rows = []
|
|
if not OUTPUT_DIR.is_dir():
|
|
return rows
|
|
for engine, sc in ENGINE_SCENARIO.items():
|
|
d = OUTPUT_DIR / engine
|
|
if not d.is_dir():
|
|
continue
|
|
for job in d.iterdir():
|
|
if not job.is_dir():
|
|
continue
|
|
mp4s = sorted(job.glob("*.mp4"))
|
|
if not mp4s:
|
|
continue
|
|
rel = mp4s[0].relative_to(OUTPUT_DIR).as_posix()
|
|
rows.append({
|
|
"id": job.name, "sc": sc, "title": _title(job),
|
|
"mp4": mp4s[0], "local_url": f"/videos/{rel}",
|
|
})
|
|
return rows
|
|
|
|
|
|
async def sync_output(session: AsyncSession | None = None, owner_uuid: str | None = None) -> int:
|
|
"""새 output 폴더를 content 로 삽입. 삽입 개수 반환."""
|
|
own = session is None
|
|
session = session or SessionLocal()
|
|
try:
|
|
existing = set((await session.execute(select(Content.id))).scalars().all())
|
|
added = 0
|
|
uploaded_dirs: list = [] # 블롭 업로드 성공한 job 폴더 → 커밋 후 정리
|
|
for r in _scan():
|
|
if r["id"] in existing:
|
|
continue
|
|
# Azure Blob 활성 시 mp4 업로드 → 공개 URL 저장, 실패/비활성 시 로컬 /videos/ URL
|
|
video = r["local_url"]
|
|
if blob_client.enabled():
|
|
url = await blob_client.upload_video(r["mp4"], owner_uuid, r["id"])
|
|
if url:
|
|
video = url
|
|
uploaded_dirs.append(r["mp4"].parent)
|
|
session.add(Content(
|
|
id=r["id"], scenario=r["sc"], title=r["title"], video=video,
|
|
owner_uuid=owner_uuid,
|
|
))
|
|
added += 1
|
|
if added:
|
|
await session.commit()
|
|
# 커밋 성공 후에만 로컬 원본 삭제(블롭이 진실의 원천). 실패분은 /videos fallback 위해 유지.
|
|
for d in uploaded_dirs:
|
|
_cleanup_dir(d)
|
|
return added
|
|
finally:
|
|
if own:
|
|
await session.close()
|