diff --git a/app/song/api/routers/v1/song.py b/app/song/api/routers/v1/song.py index fb458da..65c50a7 100644 --- a/app/song/api/routers/v1/song.py +++ b/app/song/api/routers/v1/song.py @@ -32,7 +32,7 @@ from app.song.schemas.song_schema import ( PollingSongResponse, SongListItem, ) -from app.song.worker.song_task import download_and_upload_song_to_blob +from app.song.worker.song_task import download_and_upload_song_by_suno_task_id from app.utils.pagination import PaginatedResponse from app.utils.suno import SunoService @@ -243,7 +243,7 @@ async def get_song_status( audio_url = first_clip.audio_url if audio_url: - # suno_task_id로 Song 조회하여 task_id 가져오기 (여러 개 있을 경우 가장 최근 것 선택) + # suno_task_id로 Song 조회하여 store_name 가져오기 song_result = await session.execute( select(Song) .where(Song.suno_task_id == suno_task_id) @@ -253,7 +253,7 @@ async def get_song_status( song = song_result.scalar_one_or_none() if song: - # task_id로 Project 조회하여 store_name 가져오기 + # project_id로 Project 조회하여 store_name 가져오기 project_result = await session.execute( select(Project).where(Project.id == song.project_id) ) @@ -261,11 +261,11 @@ async def get_song_status( store_name = project.store_name if project else "song" - # 백그라운드 태스크로 MP3 다운로드 및 Blob 업로드, DB 업데이트 - print(f"[get_song_status] Background task args - task_id: {song.task_id}, audio_url: {audio_url}, store_name: {store_name}") + # 백그라운드 태스크로 MP3 다운로드 및 Blob 업로드, DB 업데이트 (suno_task_id 사용) + print(f"[get_song_status] Background task args - suno_task_id: {suno_task_id}, audio_url: {audio_url}, store_name: {store_name}") background_tasks.add_task( - download_and_upload_song_to_blob, - task_id=song.task_id, + download_and_upload_song_by_suno_task_id, + suno_task_id=suno_task_id, audio_url=audio_url, store_name=store_name, ) diff --git a/app/song/worker/song_task.py b/app/song/worker/song_task.py index 3fd62bd..087b013 100644 --- a/app/song/worker/song_task.py +++ b/app/song/worker/song_task.py @@ -175,7 +175,6 @@ async def download_and_upload_song_to_blob( print(f"[download_and_upload_song_to_blob] EXCEPTION - task_id: {task_id}, error: {e}") # 실패 시 Song 테이블 업데이트 async with AsyncSessionLocal() as session: - # 여러 개 있을 경우 가장 최근 것 선택 result = await session.execute( select(Song) .where(Song.task_id == task_id) @@ -205,3 +204,126 @@ async def download_and_upload_song_to_blob( temp_dir.rmdir() except Exception: pass # 디렉토리가 비어있지 않으면 무시 + + +async def download_and_upload_song_by_suno_task_id( + suno_task_id: str, + audio_url: str, + store_name: str, +) -> None: + """suno_task_id로 Song을 조회하여 노래를 다운로드하고 Azure Blob Storage에 업로드한 뒤 Song 테이블을 업데이트합니다. + + Args: + suno_task_id: Suno API 작업 ID + audio_url: 다운로드할 오디오 URL + store_name: 저장할 파일명에 사용할 업체명 + """ + print(f"[download_and_upload_song_by_suno_task_id] START - suno_task_id: {suno_task_id}, store_name: {store_name}") + temp_file_path: Path | None = None + task_id: str | None = None + + try: + # suno_task_id로 Song 조회하여 task_id 가져오기 + async with AsyncSessionLocal() as session: + result = await session.execute( + select(Song) + .where(Song.suno_task_id == suno_task_id) + .order_by(Song.created_at.desc()) + .limit(1) + ) + song = result.scalar_one_or_none() + + if not song: + print(f"[download_and_upload_song_by_suno_task_id] Song NOT FOUND - suno_task_id: {suno_task_id}") + return + + task_id = song.task_id + print(f"[download_and_upload_song_by_suno_task_id] Song found - suno_task_id: {suno_task_id}, task_id: {task_id}") + + # 파일명에 사용할 수 없는 문자 제거 + safe_store_name = "".join( + c for c in store_name if c.isalnum() or c in (" ", "_", "-") + ).strip() + safe_store_name = safe_store_name or "song" + file_name = f"{safe_store_name}.mp3" + + # 임시 저장 경로 생성 + temp_dir = Path("media") / "temp" / task_id + temp_dir.mkdir(parents=True, exist_ok=True) + temp_file_path = temp_dir / file_name + print(f"[download_and_upload_song_by_suno_task_id] Temp directory created - path: {temp_file_path}") + + # 오디오 파일 다운로드 + print(f"[download_and_upload_song_by_suno_task_id] Downloading audio - suno_task_id: {suno_task_id}, url: {audio_url}") + async with httpx.AsyncClient() as client: + response = await client.get(audio_url, timeout=60.0) + response.raise_for_status() + + async with aiofiles.open(str(temp_file_path), "wb") as f: + await f.write(response.content) + print(f"[download_and_upload_song_by_suno_task_id] File downloaded - suno_task_id: {suno_task_id}, path: {temp_file_path}") + + # Azure Blob Storage에 업로드 + uploader = AzureBlobUploader(task_id=task_id) + upload_success = await uploader.upload_music(file_path=str(temp_file_path)) + + if not upload_success: + raise Exception("Azure Blob Storage 업로드 실패") + + # SAS 토큰이 제외된 public_url 사용 + blob_url = uploader.public_url + print(f"[download_and_upload_song_by_suno_task_id] Uploaded to Blob - suno_task_id: {suno_task_id}, url: {blob_url}") + + # Song 테이블 업데이트 (새 세션 사용) + async with AsyncSessionLocal() as session: + result = await session.execute( + select(Song) + .where(Song.suno_task_id == suno_task_id) + .order_by(Song.created_at.desc()) + .limit(1) + ) + song = result.scalar_one_or_none() + + if song: + song.status = "completed" + song.song_result_url = blob_url + await session.commit() + print(f"[download_and_upload_song_by_suno_task_id] SUCCESS - suno_task_id: {suno_task_id}, status: completed") + else: + print(f"[download_and_upload_song_by_suno_task_id] Song NOT FOUND in DB - suno_task_id: {suno_task_id}") + + except Exception as e: + print(f"[download_and_upload_song_by_suno_task_id] EXCEPTION - suno_task_id: {suno_task_id}, error: {e}") + # 실패 시 Song 테이블 업데이트 + if task_id: + async with AsyncSessionLocal() as session: + result = await session.execute( + select(Song) + .where(Song.suno_task_id == suno_task_id) + .order_by(Song.created_at.desc()) + .limit(1) + ) + song = result.scalar_one_or_none() + + if song: + song.status = "failed" + await session.commit() + print(f"[download_and_upload_song_by_suno_task_id] FAILED - suno_task_id: {suno_task_id}, status updated to failed") + + finally: + # 임시 파일 삭제 + if temp_file_path and temp_file_path.exists(): + try: + temp_file_path.unlink() + print(f"[download_and_upload_song_by_suno_task_id] Temp file deleted - path: {temp_file_path}") + except Exception as e: + print(f"[download_and_upload_song_by_suno_task_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 # 디렉토리가 비어있지 않으면 무시