296 lines
12 KiB
Python
296 lines
12 KiB
Python
"""
|
|
Video Background Tasks
|
|
|
|
영상 생성 관련 백그라운드 태스크를 정의합니다.
|
|
"""
|
|
|
|
import traceback
|
|
from pathlib import Path
|
|
|
|
import aiofiles
|
|
import httpx
|
|
from sqlalchemy import select
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
from app.database.session import BackgroundSessionLocal
|
|
from app.video.models import Video
|
|
from app.utils.upload_blob_as_request import AzureBlobUploader
|
|
from app.utils.logger import get_logger
|
|
|
|
# 로거 설정
|
|
logger = get_logger("video")
|
|
|
|
# HTTP 요청 설정
|
|
REQUEST_TIMEOUT = 300.0 # 초 (영상은 용량이 크므로 5분)
|
|
|
|
|
|
async def _update_video_status(
|
|
task_id: str,
|
|
status: str,
|
|
video_url: str | None = None,
|
|
creatomate_render_id: str | None = None,
|
|
) -> bool:
|
|
"""Video 테이블의 상태를 업데이트합니다.
|
|
|
|
Args:
|
|
task_id: 프로젝트 task_id
|
|
status: 변경할 상태 ("processing", "completed", "failed")
|
|
video_url: 영상 URL
|
|
creatomate_render_id: Creatomate render ID (선택)
|
|
|
|
Returns:
|
|
bool: 업데이트 성공 여부
|
|
"""
|
|
try:
|
|
async with BackgroundSessionLocal() as session:
|
|
if creatomate_render_id:
|
|
query_result = await session.execute(
|
|
select(Video)
|
|
.where(Video.creatomate_render_id == creatomate_render_id)
|
|
.order_by(Video.created_at.desc())
|
|
.limit(1)
|
|
)
|
|
else:
|
|
query_result = await session.execute(
|
|
select(Video)
|
|
.where(Video.task_id == task_id)
|
|
.order_by(Video.created_at.desc())
|
|
.limit(1)
|
|
)
|
|
|
|
video = query_result.scalar_one_or_none()
|
|
|
|
if video:
|
|
video.status = status
|
|
if video_url is not None:
|
|
video.result_movie_url = video_url
|
|
await session.commit()
|
|
logger.info(f"[Video] Status updated - task_id: {task_id}, status: {status}")
|
|
return True
|
|
else:
|
|
logger.warning(f"[Video] NOT FOUND in DB - task_id: {task_id}")
|
|
return False
|
|
|
|
except SQLAlchemyError as e:
|
|
logger.error(f"[Video] DB Error while updating status - task_id: {task_id}, error: {e}")
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"[Video] Unexpected error while updating status - task_id: {task_id}, error: {e}")
|
|
return False
|
|
|
|
|
|
async def _download_video(url: str, task_id: str) -> bytes:
|
|
"""URL에서 영상을 다운로드합니다.
|
|
|
|
Args:
|
|
url: 다운로드할 URL
|
|
task_id: 로그용 task_id
|
|
|
|
Returns:
|
|
bytes: 다운로드한 파일 내용
|
|
|
|
Raises:
|
|
httpx.HTTPError: 다운로드 실패 시
|
|
"""
|
|
logger.info(f"[VideoDownload] Downloading - task_id: {task_id}")
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.get(url, timeout=REQUEST_TIMEOUT)
|
|
response.raise_for_status()
|
|
|
|
logger.info(f"[VideoDownload] SUCCESS - task_id: {task_id}, size: {len(response.content)} bytes")
|
|
return response.content
|
|
|
|
|
|
async def download_and_upload_video_to_blob(
|
|
task_id: str,
|
|
video_url: str,
|
|
creatomate_render_id: str,
|
|
user_uuid: str,
|
|
) -> None:
|
|
"""백그라운드에서 영상을 다운로드하고 Azure Blob Storage에 업로드한 뒤 Video 테이블을 업데이트합니다.
|
|
|
|
파일명은 creatomate_render_id를 사용하여 고유성을 보장합니다.
|
|
|
|
Args:
|
|
task_id: 프로젝트 task_id
|
|
video_url: 다운로드할 영상 URL
|
|
creatomate_render_id: Creatomate API 렌더 ID (파일명 및 Video 식별용)
|
|
user_uuid: 사용자 UUID (Azure Blob Storage 경로에 사용)
|
|
"""
|
|
logger.info(f"[download_and_upload_video_to_blob] START - task_id: {task_id}, creatomate_render_id: {creatomate_render_id}")
|
|
temp_file_path: Path | None = None
|
|
|
|
try:
|
|
# creatomate_render_id를 파일명으로 사용 (고유 ID이므로 sanitize 불필요)
|
|
file_name = f"{creatomate_render_id}.mp4"
|
|
|
|
# 임시 저장 경로 생성
|
|
temp_dir = Path("media") / "temp" / task_id
|
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
temp_file_path = temp_dir / file_name
|
|
logger.debug(f"[download_and_upload_video_to_blob] Temp directory created - path: {temp_file_path}")
|
|
|
|
# 영상 파일 다운로드
|
|
logger.info(f"[download_and_upload_video_to_blob] Downloading video - task_id: {task_id}, url: {video_url}")
|
|
|
|
content = await _download_video(video_url, task_id)
|
|
|
|
async with aiofiles.open(str(temp_file_path), "wb") as f:
|
|
await f.write(content)
|
|
|
|
logger.info(f"[download_and_upload_video_to_blob] File downloaded - task_id: {task_id}, path: {temp_file_path}")
|
|
|
|
# Azure Blob Storage에 업로드
|
|
uploader = AzureBlobUploader(user_uuid=user_uuid, task_id=task_id)
|
|
upload_success = await uploader.upload_video(file_path=str(temp_file_path))
|
|
|
|
if not upload_success:
|
|
raise Exception("Azure Blob Storage 업로드 실패")
|
|
|
|
# SAS 토큰이 제외된 public_url 사용
|
|
blob_url = uploader.public_url
|
|
logger.info(f"[download_and_upload_video_to_blob] Uploaded to Blob - task_id: {task_id}, url: {blob_url}")
|
|
|
|
# Video 테이블 업데이트 (creatomate_render_id로 특정 Video 식별)
|
|
await _update_video_status(task_id, "completed", blob_url, creatomate_render_id)
|
|
logger.info(f"[download_and_upload_video_to_blob] SUCCESS - task_id: {task_id}, creatomate_render_id: {creatomate_render_id}")
|
|
|
|
except httpx.HTTPError as e:
|
|
logger.error(f"[download_and_upload_video_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}", exc_info=True)
|
|
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id)
|
|
|
|
except SQLAlchemyError as e:
|
|
logger.error(f"[download_and_upload_video_to_blob] DB ERROR - task_id: {task_id}, error: {e}", exc_info=True)
|
|
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id)
|
|
|
|
except Exception as e:
|
|
logger.error(f"[download_and_upload_video_to_blob] EXCEPTION - task_id: {task_id}, error: {e}", exc_info=True)
|
|
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id)
|
|
|
|
finally:
|
|
# 임시 파일 삭제
|
|
if temp_file_path and temp_file_path.exists():
|
|
try:
|
|
temp_file_path.unlink()
|
|
logger.debug(f"[download_and_upload_video_to_blob] Temp file deleted - path: {temp_file_path}")
|
|
except Exception as e:
|
|
logger.warning(f"[download_and_upload_video_to_blob] Failed to delete temp file: {e}")
|
|
|
|
# 임시 디렉토리 삭제 시도
|
|
temp_dir = Path("media") / "temp" / task_id
|
|
if temp_dir.exists():
|
|
try:
|
|
temp_dir.rmdir()
|
|
except Exception:
|
|
pass # 디렉토리가 비어있지 않으면 무시
|
|
|
|
|
|
async def download_and_upload_video_by_creatomate_render_id(
|
|
creatomate_render_id: str,
|
|
video_url: str,
|
|
user_uuid: str,
|
|
) -> None:
|
|
"""creatomate_render_id로 Video를 조회하여 영상을 다운로드하고 Azure Blob Storage에 업로드한 뒤 Video 테이블을 업데이트합니다.
|
|
|
|
파일명은 creatomate_render_id를 사용하여 고유성을 보장합니다.
|
|
|
|
Args:
|
|
creatomate_render_id: Creatomate API 렌더 ID (파일명으로도 사용)
|
|
video_url: 다운로드할 영상 URL
|
|
user_uuid: 사용자 UUID (Azure Blob Storage 경로에 사용)
|
|
"""
|
|
logger.info(f"[download_and_upload_video_by_creatomate_render_id] START - creatomate_render_id: {creatomate_render_id}")
|
|
temp_file_path: Path | None = None
|
|
task_id: str | None = None
|
|
|
|
try:
|
|
# creatomate_render_id로 Video 조회하여 task_id 가져오기
|
|
async with BackgroundSessionLocal() as session:
|
|
result = await session.execute(
|
|
select(Video)
|
|
.where(Video.creatomate_render_id == creatomate_render_id)
|
|
.order_by(Video.created_at.desc())
|
|
.limit(1)
|
|
)
|
|
video = result.scalar_one_or_none()
|
|
|
|
if not video:
|
|
logger.warning(f"[download_and_upload_video_by_creatomate_render_id] Video NOT FOUND - creatomate_render_id: {creatomate_render_id}")
|
|
return
|
|
|
|
task_id = video.task_id
|
|
logger.info(f"[download_and_upload_video_by_creatomate_render_id] Video found - creatomate_render_id: {creatomate_render_id}, task_id: {task_id}")
|
|
|
|
# creatomate_render_id를 파일명으로 사용 (고유 ID이므로 sanitize 불필요)
|
|
file_name = f"{creatomate_render_id}.mp4"
|
|
|
|
# 임시 저장 경로 생성
|
|
temp_dir = Path("media") / "temp" / task_id
|
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
temp_file_path = temp_dir / file_name
|
|
logger.debug(f"[download_and_upload_video_by_creatomate_render_id] Temp directory created - path: {temp_file_path}")
|
|
|
|
# 영상 파일 다운로드
|
|
logger.info(f"[download_and_upload_video_by_creatomate_render_id] Downloading video - creatomate_render_id: {creatomate_render_id}, url: {video_url}")
|
|
|
|
content = await _download_video(video_url, task_id)
|
|
|
|
async with aiofiles.open(str(temp_file_path), "wb") as f:
|
|
await f.write(content)
|
|
|
|
logger.info(f"[download_and_upload_video_by_creatomate_render_id] File downloaded - creatomate_render_id: {creatomate_render_id}, path: {temp_file_path}")
|
|
|
|
# Azure Blob Storage에 업로드
|
|
uploader = AzureBlobUploader(user_uuid=user_uuid, task_id=task_id)
|
|
upload_success = await uploader.upload_video(file_path=str(temp_file_path))
|
|
|
|
if not upload_success:
|
|
raise Exception("Azure Blob Storage 업로드 실패")
|
|
|
|
# SAS 토큰이 제외된 public_url 사용
|
|
blob_url = uploader.public_url
|
|
logger.info(f"[download_and_upload_video_by_creatomate_render_id] Uploaded to Blob - creatomate_render_id: {creatomate_render_id}, url: {blob_url}")
|
|
|
|
# Video 테이블 업데이트
|
|
await _update_video_status(
|
|
task_id=task_id,
|
|
status="completed",
|
|
video_url=blob_url,
|
|
creatomate_render_id=creatomate_render_id,
|
|
)
|
|
logger.info(f"[download_and_upload_video_by_creatomate_render_id] SUCCESS - creatomate_render_id: {creatomate_render_id}")
|
|
|
|
except httpx.HTTPError as e:
|
|
logger.error(f"[download_and_upload_video_by_creatomate_render_id] DOWNLOAD ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}", exc_info=True)
|
|
if task_id:
|
|
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id)
|
|
|
|
except SQLAlchemyError as e:
|
|
logger.error(f"[download_and_upload_video_by_creatomate_render_id] DB ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}", exc_info=True)
|
|
if task_id:
|
|
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id)
|
|
|
|
except Exception as e:
|
|
logger.error(f"[download_and_upload_video_by_creatomate_render_id] EXCEPTION - creatomate_render_id: {creatomate_render_id}, error: {e}", exc_info=True)
|
|
if task_id:
|
|
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id)
|
|
|
|
finally:
|
|
# 임시 파일 삭제
|
|
if temp_file_path and temp_file_path.exists():
|
|
try:
|
|
temp_file_path.unlink()
|
|
logger.debug(f"[download_and_upload_video_by_creatomate_render_id] Temp file deleted - path: {temp_file_path}")
|
|
except Exception as e:
|
|
logger.warning(f"[download_and_upload_video_by_creatomate_render_id] Failed to delete temp file: {e}")
|
|
|
|
# 임시 디렉토리 삭제 시도
|
|
if task_id:
|
|
temp_dir = Path("media") / "temp" / task_id
|
|
if temp_dir.exists():
|
|
try:
|
|
temp_dir.rmdir()
|
|
except Exception:
|
|
pass # 디렉토리가 비어있지 않으면 무시
|