# -*- coding: utf-8 -*- """썰박스 산출물 Blob 업로드. castad `AzureBlobUploader` 를 그대로 재사용한다 — 원본 썰박스의 `blob_client.py` 는 이식하지 않는다. 같은 Azure 계정을 쓰므로 업로더를 두 벌 둘 이유가 없다. 경로는 `{user_uuid}/{task_id}/video/{file}` 형태가 되는데, ADO2 콘텐츠와 섞이지 않도록 task_id 자리에 `ssulbox-{id}` 접두를 붙인다. """ from pathlib import Path from typing import Optional from app.utils.logger import get_logger from app.utils.upload_blob_as_request import AzureBlobUploader from app.utils.video_poster import extract_first_frame, generate_and_store_poster from config import azure_blob_settings, ssulbox_settings logger = get_logger("ssulbox") #: 설정되지 않았을 때의 플레이스홀더 (config.py 기본값) _PLACEHOLDER_SAS = {"", "your-sas-token", "none"} def blob_enabled() -> bool: """Blob 업로드가 가능한 상태인지. 비활성이면 로컬 파일을 그대로 서빙한다(개발 환경). """ token = (azure_blob_settings.AZURE_BLOB_SAS_TOKEN or "").strip() return token.lower() not in _PLACEHOLDER_SAS async def upload_ssul_video( mp4_path: Path, user_uuid: Optional[str], content_id: int ) -> Optional[str]: """완성 영상을 Blob 에 올리고 공개 URL 을 반환. 실패·비활성이면 None. Args: mp4_path: 로컬 mp4 경로 user_uuid: 소유자. 탈퇴로 NULL 이면 업로드하지 않는다 content_id: `ssul_content.id` Returns: SAS 토큰이 제외된 공개 URL, 또는 None """ if not blob_enabled(): logger.info("[upload_ssul_video] Blob 비활성 — 로컬 서빙") return None if not user_uuid: logger.warning(f"[upload_ssul_video] user_uuid 없음 id={content_id}") return None if not mp4_path.exists(): logger.error(f"[upload_ssul_video] 파일 없음 {mp4_path}") return None # ADO2 영상과 경로를 분리한다 task_id = f"{ssulbox_settings.SSULBOX_BLOB_PREFIX}-{content_id}" uploader = AzureBlobUploader(user_uuid=user_uuid, task_id=task_id) success = await uploader.upload_video(file_path=str(mp4_path)) if not success: logger.error(f"[upload_ssul_video] 업로드 실패 id={content_id}") return None logger.info(f"[upload_ssul_video] OK id={content_id} url={uploader.public_url}") return uploader.public_url def _ssul_task_id(content_id: int) -> str: return f"{ssulbox_settings.SSULBOX_BLOB_PREFIX}-{content_id}" async def generate_ssul_poster( mp4_path: Path, user_uuid: Optional[str], content_id: int ) -> Optional[str]: """영상 첫 프레임을 포스터로 만들고 URL을 반환합니다. 실패 시 None. Blob이 켜져 있으면 ADO2와 같은 업로더를 쓰고, 꺼져 있으면 로컬 mp4 옆에 jpg를 두어 `/ssul-videos/` 로 서빙한다. """ try: if blob_enabled() and user_uuid: url = await generate_and_store_poster( video_path=mp4_path, user_uuid=user_uuid, task_id=_ssul_task_id(content_id), file_stem=_ssul_task_id(content_id), ) if url: logger.info(f"[generate_ssul_poster] OK id={content_id} url={url}") return url logger.warning(f"[generate_ssul_poster] Blob 포스터 없음 id={content_id}") return None image_bytes = await extract_first_frame(mp4_path) if not image_bytes: return None poster_path = mp4_path.with_suffix(".jpg") poster_path.write_bytes(image_bytes) try: rel = poster_path.resolve().relative_to( ssulbox_settings.output_path.resolve() ) except ValueError: logger.warning( f"[generate_ssul_poster] output 밖 경로 id={content_id} path={poster_path}" ) return None url = f"/ssul-videos/{rel.as_posix()}" logger.info(f"[generate_ssul_poster] 로컬 서빙 id={content_id} url={url}") return url except Exception as e: logger.warning( f"[generate_ssul_poster] 실패 id={content_id} - {type(e).__name__}: {e}" ) return None