feat: update song model and related routers, schemas, worker
parent
ece201f92b
commit
ee6069e5d5
|
|
@ -379,7 +379,7 @@ print(response.json())
|
|||
200: {"description": "이미지 업로드 성공"},
|
||||
400: {"description": "이미지가 제공되지 않음", "model": ErrorResponse},
|
||||
},
|
||||
tags=["image"],
|
||||
tags=["Image"],
|
||||
)
|
||||
async def upload_images(
|
||||
images_json: Optional[str] = Form(
|
||||
|
|
|
|||
|
|
@ -418,9 +418,9 @@ async def get_lyric_status(
|
|||
|
||||
## 사용 예시
|
||||
```
|
||||
GET /lyrics # 기본 조회 (1페이지, 20개)
|
||||
GET /lyrics?page=2 # 2페이지 조회
|
||||
GET /lyrics?page=1&page_size=50 # 50개씩 조회
|
||||
GET /lyrics/ # 기본 조회 (1페이지, 20개)
|
||||
GET /lyrics/?page=2 # 2페이지 조회
|
||||
GET /lyrics/?page=1&page_size=50 # 50개씩 조회
|
||||
```
|
||||
|
||||
## 참고
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Song API Router
|
|||
app.include_router(router, prefix="/api/v1")
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
|
@ -32,6 +32,7 @@ from app.song.schemas.song_schema import (
|
|||
PollingSongResponse,
|
||||
SongListItem,
|
||||
)
|
||||
from app.song.worker.song_task import download_and_upload_song_by_suno_task_id
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.pagination import PaginatedResponse
|
||||
from app.utils.suno import SunoService
|
||||
|
|
@ -294,17 +295,17 @@ async def generate_song(
|
|||
|
||||
@router.get(
|
||||
"/status/{song_id}",
|
||||
summary="노래 생성 상태 조회",
|
||||
summary="노래 생성 상태 조회 (Suno API)",
|
||||
description="""
|
||||
Suno API를 통해 노래 생성 작업의 상태를 조회합니다.
|
||||
SUCCESS 상태인 경우 백그라운드에서 MP3 파일을 다운로드하고 Azure Blob Storage에 업로드한 뒤 Song 테이블을 업데이트합니다.
|
||||
SUCCESS 상태인 경우 백그라운드에서 MP3 파일을 다운로드하고 Azure Blob Storage에 업로드를 시작합니다.
|
||||
|
||||
## 경로 파라미터
|
||||
- **song_id**: 노래 생성 시 반환된 작업 ID (필수)
|
||||
- **song_id**: 노래 생성 시 반환된 Suno API 작업 ID (필수)
|
||||
|
||||
## 반환 정보
|
||||
- **success**: 조회 성공 여부
|
||||
- **status**: 작업 상태 (PENDING, processing, SUCCESS, failed)
|
||||
- **status**: Suno API 작업 상태
|
||||
- **message**: 상태 메시지
|
||||
|
||||
## 사용 예시
|
||||
|
|
@ -312,27 +313,28 @@ SUCCESS 상태인 경우 백그라운드에서 MP3 파일을 다운로드하고
|
|||
GET /song/status/abc123...
|
||||
```
|
||||
|
||||
## 상태 값
|
||||
- **PENDING**: 대기 중
|
||||
- **processing**: 생성 중
|
||||
- **SUCCESS**: 생성 완료
|
||||
- **failed**: 생성 실패
|
||||
## 상태 값 (Suno API 응답)
|
||||
- **PENDING**: Suno API 대기 중
|
||||
- **processing**: Suno API에서 노래 생성 중
|
||||
- **SUCCESS**: Suno API 노래 생성 완료 (백그라운드 Blob 업로드 시작)
|
||||
- **TEXT_SUCCESS**: Suno API 노래 생성 완료
|
||||
- **failed**: Suno API 노래 생성 실패
|
||||
- **error**: API 조회 오류
|
||||
|
||||
## 참고
|
||||
- 스트림 URL: 30-40초 내 생성
|
||||
- 다운로드 URL: 2-3분 내 생성
|
||||
- SUCCESS 시 백그라운드에서 MP3 다운로드 → Azure Blob Storage 업로드 → Song 테이블 업데이트 진행
|
||||
- 저장 경로: Azure Blob Storage ({BASE_URL}/{task_id}/song/{store_name}.mp3)
|
||||
- Song 테이블의 song_result_url에 Blob URL이 저장됩니다
|
||||
- 이 엔드포인트는 Suno API의 상태를 반환합니다
|
||||
- SUCCESS 응답 시 백그라운드에서 MP3 다운로드 → Azure Blob Storage 업로드가 시작됩니다
|
||||
- 최종 완료 상태는 `/song/download/{task_id}` 엔드포인트에서 확인하세요
|
||||
- Song 테이블 상태: processing → uploading → completed
|
||||
""",
|
||||
response_model=PollingSongResponse,
|
||||
responses={
|
||||
200: {"description": "상태 조회 성공"},
|
||||
500: {"description": "상태 조회 실패"},
|
||||
},
|
||||
)
|
||||
async def get_song_status(
|
||||
song_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> PollingSongResponse:
|
||||
"""song_id로 노래 생성 작업의 상태를 조회합니다.
|
||||
|
|
@ -345,10 +347,11 @@ async def get_song_status(
|
|||
try:
|
||||
suno_service = SunoService()
|
||||
result = await suno_service.get_task_status(song_id)
|
||||
logger.debug(f"[get_song_status] Suno API raw response - song_id: {song_id}, result: {result}")
|
||||
parsed_response = suno_service.parse_status_response(result)
|
||||
logger.info(f"[get_song_status] Suno API response - song_id: {song_id}, status: {parsed_response.status}")
|
||||
|
||||
# SUCCESS 상태인 경우 첫 번째 클립 정보를 DB에 직접 저장
|
||||
# SUCCESS 상태인 경우 백그라운드에서 MP3 다운로드 및 Blob 업로드 진행
|
||||
if parsed_response.status == "SUCCESS" and result:
|
||||
# result에서 직접 clips 데이터 추출
|
||||
data = result.get("data", {})
|
||||
|
|
@ -372,14 +375,33 @@ async def get_song_status(
|
|||
)
|
||||
song = song_result.scalar_one_or_none()
|
||||
|
||||
if song and song.status != "completed":
|
||||
# 첫 번째 클립의 audio_url과 duration을 직접 DB에 저장
|
||||
song.status = "completed"
|
||||
song.song_result_url = audio_url
|
||||
if clip_duration is not None:
|
||||
song.duration = clip_duration
|
||||
# processing 상태인 경우에만 백그라운드 태스크 실행 (중복 방지)
|
||||
if song and song.status == "processing":
|
||||
# store_name 조회
|
||||
project_result = await session.execute(
|
||||
select(Project).where(Project.id == song.project_id)
|
||||
)
|
||||
project = project_result.scalar_one_or_none()
|
||||
store_name = project.store_name if project else "song"
|
||||
|
||||
# 상태를 uploading으로 변경 (중복 호출 방지)
|
||||
song.status = "uploading"
|
||||
song.suno_audio_id = first_clip.get('id')
|
||||
await session.commit()
|
||||
logger.info(f"[get_song_status] Song updated - song_id: {song_id}, status: completed, song_result_url: {audio_url}, duration: {clip_duration}")
|
||||
logger.info(f"[get_song_status] Song status changed to uploading - song_id: {song_id}")
|
||||
|
||||
# 백그라운드 태스크로 MP3 다운로드 및 Blob 업로드 실행
|
||||
background_tasks.add_task(
|
||||
download_and_upload_song_by_suno_task_id,
|
||||
suno_task_id=song_id,
|
||||
audio_url=audio_url,
|
||||
store_name=store_name,
|
||||
duration=clip_duration,
|
||||
)
|
||||
logger.info(f"[get_song_status] Background task scheduled - song_id: {song_id}, store_name: {store_name}")
|
||||
|
||||
elif song and song.status == "uploading":
|
||||
logger.info(f"[get_song_status] SKIPPED - Song is already uploading, song_id: {song_id}")
|
||||
elif song and song.status == "completed":
|
||||
logger.info(f"[get_song_status] SKIPPED - Song already completed, song_id: {song_id}")
|
||||
|
||||
|
|
@ -400,7 +422,7 @@ async def get_song_status(
|
|||
|
||||
@router.get(
|
||||
"/download/{task_id}",
|
||||
summary="노래 생성 URL 조회",
|
||||
summary="노래 다운로드 상태 조회 (DB Polling)",
|
||||
description="""
|
||||
task_id를 기반으로 Song 테이블의 상태를 조회하고,
|
||||
completed인 경우 Project 정보와 노래 URL을 반환합니다.
|
||||
|
|
@ -410,31 +432,38 @@ completed인 경우 Project 정보와 노래 URL을 반환합니다.
|
|||
|
||||
## 반환 정보
|
||||
- **success**: 조회 성공 여부
|
||||
- **status**: 처리 상태 (processing, completed, failed, not_found)
|
||||
- **status**: DB 처리 상태 (processing, uploading, completed, failed, not_found, error)
|
||||
- **message**: 응답 메시지
|
||||
- **store_name**: 업체명
|
||||
- **region**: 지역명
|
||||
- **detail_region_info**: 상세 지역 정보
|
||||
- **store_name**: 업체명 (completed 시)
|
||||
- **region**: 지역명 (completed 시)
|
||||
- **detail_region_info**: 상세 지역 정보 (completed 시)
|
||||
- **task_id**: 작업 고유 식별자
|
||||
- **language**: 언어
|
||||
- **language**: 언어 (completed 시)
|
||||
- **song_result_url**: 노래 결과 URL (completed 시, Azure Blob Storage URL)
|
||||
- **created_at**: 생성 일시
|
||||
- **created_at**: 생성 일시 (completed 시)
|
||||
- **error_message**: 에러 메시지 (실패 시)
|
||||
|
||||
## 사용 예시
|
||||
```
|
||||
GET /song/download/019123ab-cdef-7890-abcd-ef1234567890
|
||||
```
|
||||
|
||||
## 상태 값 (DB 상태)
|
||||
- **processing**: Suno API에서 노래 생성 중
|
||||
- **uploading**: MP3 다운로드 및 Azure Blob 업로드 중
|
||||
- **completed**: 모든 작업 완료, Blob URL 사용 가능
|
||||
- **failed**: 노래 생성 또는 업로드 실패
|
||||
- **not_found**: task_id에 해당하는 Song 없음
|
||||
- **error**: 조회 중 오류 발생
|
||||
|
||||
## 참고
|
||||
- processing 상태인 경우 song_result_url은 null입니다.
|
||||
- completed 상태인 경우 Project 정보와 함께 song_result_url (Azure Blob URL)을 반환합니다.
|
||||
- 이 엔드포인트는 DB의 Song 테이블 상태를 반환합니다
|
||||
- completed 상태인 경우 Project 정보와 함께 song_result_url (Azure Blob URL)을 반환합니다
|
||||
- song_result_url 형식: {AZURE_BLOB_BASE_URL}/{task_id}/song/{store_name}.mp3
|
||||
""",
|
||||
response_model=DownloadSongResponse,
|
||||
responses={
|
||||
200: {"description": "조회 성공"},
|
||||
404: {"description": "Song을 찾을 수 없음"},
|
||||
500: {"description": "조회 실패"},
|
||||
200: {"description": "조회 성공 (모든 상태에서 200 반환)"},
|
||||
},
|
||||
)
|
||||
async def download_song(
|
||||
|
|
@ -474,6 +503,16 @@ async def download_song(
|
|||
task_id=task_id,
|
||||
)
|
||||
|
||||
# uploading 상태인 경우
|
||||
if song.status == "uploading":
|
||||
logger.info(f"[download_song] UPLOADING - task_id: {task_id}")
|
||||
return DownloadSongResponse(
|
||||
success=True,
|
||||
status="uploading",
|
||||
message="노래 파일을 업로드 중입니다.",
|
||||
task_id=task_id,
|
||||
)
|
||||
|
||||
# failed 상태인 경우
|
||||
if song.status == "failed":
|
||||
logger.warning(f"[download_song] FAILED - task_id: {task_id}")
|
||||
|
|
@ -546,7 +585,6 @@ GET /songs/?page=1&page_size=10
|
|||
response_model=PaginatedResponse[SongListItem],
|
||||
responses={
|
||||
200: {"description": "노래 목록 조회 성공"},
|
||||
500: {"description": "조회 실패"},
|
||||
},
|
||||
)
|
||||
async def get_songs(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database.session import Base
|
||||
|
|
@ -25,7 +25,7 @@ class Song(Base):
|
|||
lyric_id: 연결된 Lyric의 id (외래키)
|
||||
task_id: 노래 생성 작업의 고유 식별자 (UUID 형식)
|
||||
suno_task_id: Suno API 작업 고유 식별자 (선택)
|
||||
status: 처리 상태 (pending, processing, completed, failed 등)
|
||||
status: 처리 상태 (processing, uploading, completed, failed)
|
||||
song_prompt: 노래 생성에 사용된 프롬프트
|
||||
song_result_url: 생성 결과 URL (선택)
|
||||
language: 출력 언어
|
||||
|
|
@ -82,10 +82,16 @@ class Song(Base):
|
|||
comment="Suno API 작업 고유 식별자",
|
||||
)
|
||||
|
||||
suno_audio_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(64),
|
||||
nullable=True,
|
||||
comment="Suno 첫번째 노래의 고유 식별자",
|
||||
)
|
||||
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(50),
|
||||
nullable=False,
|
||||
comment="처리 상태 (processing, completed, failed)",
|
||||
comment="처리 상태 (processing, uploading, completed, failed)",
|
||||
)
|
||||
|
||||
song_prompt: Mapped[str] = mapped_column(
|
||||
|
|
@ -150,3 +156,92 @@ class Song(Base):
|
|||
f"status='{self.status}'"
|
||||
f")>"
|
||||
)
|
||||
|
||||
|
||||
class SongTimestamp(Base):
|
||||
"""
|
||||
노래 타임스탬프 테이블
|
||||
|
||||
노래의 가사별 시작/종료 시간 정보를 저장합니다.
|
||||
Suno API에서 반환된 타임스탬프 데이터를 기반으로 생성됩니다.
|
||||
|
||||
Attributes:
|
||||
id: 고유 식별자 (자동 증가)
|
||||
suno_audio_id: 가사의 원본 오디오 ID
|
||||
order_idx: 오디오 내에서 가사의 순서
|
||||
lyric_line: 가사 한 줄의 내용
|
||||
start_time: 가사 시작 시점 (초)
|
||||
end_time: 가사 종료 시점 (초)
|
||||
created_at: 생성 일시 (자동 설정)
|
||||
"""
|
||||
|
||||
__tablename__ = "song_timestamp"
|
||||
__table_args__ = (
|
||||
{
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_charset": "utf8mb4",
|
||||
"mysql_collate": "utf8mb4_unicode_ci",
|
||||
},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
primary_key=True,
|
||||
nullable=False,
|
||||
autoincrement=True,
|
||||
comment="고유 식별자",
|
||||
)
|
||||
|
||||
suno_audio_id: Mapped[str] = mapped_column(
|
||||
String(64),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="가사의 원본 오디오 ID",
|
||||
)
|
||||
|
||||
order_idx: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
comment="오디오 내에서 가사의 순서",
|
||||
)
|
||||
|
||||
lyric_line: Mapped[str] = mapped_column(
|
||||
Text,
|
||||
nullable=False,
|
||||
comment="가사 한 줄의 내용",
|
||||
)
|
||||
|
||||
start_time: Mapped[float] = mapped_column(
|
||||
Float,
|
||||
nullable=False,
|
||||
comment="가사 시작 시점 (초)",
|
||||
)
|
||||
|
||||
end_time: Mapped[float] = mapped_column(
|
||||
Float,
|
||||
nullable=False,
|
||||
comment="가사 종료 시점 (초)",
|
||||
)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
comment="생성 일시",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
def truncate(value: str | None, max_len: int = 10) -> str:
|
||||
if value is None:
|
||||
return "None"
|
||||
return (value[:max_len] + "...") if len(value) > max_len else value
|
||||
|
||||
return (
|
||||
f"<SongTimestamp("
|
||||
f"id={self.id}, "
|
||||
f"suno_audio_id='{truncate(self.suno_audio_id)}', "
|
||||
f"order_idx={self.order_idx}, "
|
||||
f"start_time={self.start_time}, "
|
||||
f"end_time={self.end_time}"
|
||||
f")>"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -114,24 +114,33 @@ class SongClipData(BaseModel):
|
|||
|
||||
|
||||
class PollingSongResponse(BaseModel):
|
||||
"""노래 생성 상태 조회 응답 스키마
|
||||
"""노래 생성 상태 조회 응답 스키마 (Suno API)
|
||||
|
||||
Usage:
|
||||
GET /song/status/{song_id}
|
||||
Suno API 작업 상태를 조회합니다.
|
||||
|
||||
Note:
|
||||
상태 값:
|
||||
- PENDING: 대기 중
|
||||
- processing: 생성 중
|
||||
- SUCCESS / TEXT_SUCCESS / complete: 생성 완료
|
||||
- failed: 생성 실패
|
||||
상태 값 (Suno API 응답):
|
||||
- PENDING: Suno API 대기 중
|
||||
- processing: Suno API에서 노래 생성 중
|
||||
- SUCCESS: Suno API 노래 생성 완료 (백그라운드 Blob 업로드 시작)
|
||||
- TEXT_SUCCESS: Suno API 노래 생성 완료
|
||||
- failed: Suno API 노래 생성 실패
|
||||
- error: API 조회 오류
|
||||
|
||||
SUCCESS 상태 시:
|
||||
- 백그라운드에서 MP3 파일 다운로드 시작
|
||||
- Song 테이블의 status를 completed로 업데이트
|
||||
- song_result_url에 로컬 파일 경로 저장
|
||||
- 백그라운드에서 MP3 파일 다운로드 및 Azure Blob 업로드 시작
|
||||
- Song 테이블의 status가 uploading으로 변경
|
||||
- 업로드 완료 시 status가 completed로 변경, song_result_url에 Blob URL 저장
|
||||
|
||||
Example Response (Pending):
|
||||
{
|
||||
"success": true,
|
||||
"status": "PENDING",
|
||||
"message": "노래 생성 대기 중입니다.",
|
||||
"error_message": null
|
||||
}
|
||||
|
||||
Example Response (Processing):
|
||||
{
|
||||
|
|
@ -160,7 +169,7 @@ class PollingSongResponse(BaseModel):
|
|||
|
||||
success: bool = Field(..., description="조회 성공 여부")
|
||||
status: Optional[str] = Field(
|
||||
None, description="작업 상태 (PENDING, processing, SUCCESS, failed)"
|
||||
None, description="Suno API 작업 상태 (PENDING, processing, SUCCESS, TEXT_SUCCESS, failed, error)"
|
||||
)
|
||||
message: str = Field(..., description="상태 메시지")
|
||||
error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)")
|
||||
|
|
@ -192,17 +201,18 @@ class SongListItem(BaseModel):
|
|||
|
||||
|
||||
class DownloadSongResponse(BaseModel):
|
||||
"""노래 다운로드 응답 스키마
|
||||
"""노래 다운로드 응답 스키마 (DB Polling)
|
||||
|
||||
Usage:
|
||||
GET /song/download/{task_id}
|
||||
Polls for song completion and returns project info with song URL.
|
||||
DB의 Song 테이블 상태를 조회하고 완료 시 Project 정보와 노래 URL을 반환합니다.
|
||||
|
||||
Note:
|
||||
상태 값:
|
||||
- processing: 노래 생성 진행 중 (song_result_url은 null)
|
||||
- completed: 노래 생성 완료 (song_result_url 포함)
|
||||
- failed: 노래 생성 실패
|
||||
상태 값 (DB 상태):
|
||||
- processing: Suno API에서 노래 생성 중 (song_result_url은 null)
|
||||
- uploading: MP3 다운로드 및 Azure Blob 업로드 중 (song_result_url은 null)
|
||||
- completed: 모든 작업 완료 (song_result_url에 Azure Blob URL 포함)
|
||||
- failed: 노래 생성 또는 업로드 실패
|
||||
- not_found: task_id에 해당하는 Song 없음
|
||||
- error: 조회 중 오류 발생
|
||||
|
||||
|
|
@ -221,6 +231,21 @@ class DownloadSongResponse(BaseModel):
|
|||
"error_message": null
|
||||
}
|
||||
|
||||
Example Response (Uploading):
|
||||
{
|
||||
"success": true,
|
||||
"status": "uploading",
|
||||
"message": "노래 파일을 업로드 중입니다.",
|
||||
"store_name": null,
|
||||
"region": null,
|
||||
"detail_region_info": null,
|
||||
"task_id": "019123ab-cdef-7890-abcd-ef1234567890",
|
||||
"language": null,
|
||||
"song_result_url": null,
|
||||
"created_at": null,
|
||||
"error_message": null
|
||||
}
|
||||
|
||||
Example Response (Completed):
|
||||
{
|
||||
"success": true,
|
||||
|
|
@ -231,7 +256,7 @@ class DownloadSongResponse(BaseModel):
|
|||
"detail_region_info": "군산 신흥동 말랭이 마을",
|
||||
"task_id": "019123ab-cdef-7890-abcd-ef1234567890",
|
||||
"language": "Korean",
|
||||
"song_result_url": "http://localhost:8000/media/2025-01-15/스테이머뭄.mp3",
|
||||
"song_result_url": "https://blob.azure.com/.../song.mp3",
|
||||
"created_at": "2025-01-15T12:00:00",
|
||||
"error_message": null
|
||||
}
|
||||
|
|
@ -252,8 +277,8 @@ class DownloadSongResponse(BaseModel):
|
|||
}
|
||||
"""
|
||||
|
||||
success: bool = Field(..., description="다운로드 성공 여부")
|
||||
status: str = Field(..., description="처리 상태 (processing, completed, failed, not_found, error)")
|
||||
success: bool = Field(..., description="조회 성공 여부")
|
||||
status: str = Field(..., description="DB 처리 상태 (processing, uploading, completed, failed, not_found, error)")
|
||||
message: str = Field(..., description="응답 메시지")
|
||||
store_name: Optional[str] = Field(None, description="업체명")
|
||||
region: Optional[str] = Field(None, description="지역명")
|
||||
|
|
|
|||
|
|
@ -173,90 +173,6 @@ async def download_and_save_song(
|
|||
await _update_song_status(task_id, "failed")
|
||||
|
||||
|
||||
async def download_and_upload_song_to_blob(
|
||||
task_id: str,
|
||||
audio_url: str,
|
||||
store_name: str,
|
||||
) -> None:
|
||||
"""백그라운드에서 노래를 다운로드하고 Azure Blob Storage에 업로드한 뒤 Song 테이블을 업데이트합니다.
|
||||
|
||||
Args:
|
||||
task_id: 프로젝트 task_id
|
||||
audio_url: 다운로드할 오디오 URL
|
||||
store_name: 저장할 파일명에 사용할 업체명
|
||||
"""
|
||||
logger.info(f"[download_and_upload_song_to_blob] START - task_id: {task_id}, store_name: {store_name}")
|
||||
temp_file_path: Path | None = None
|
||||
|
||||
try:
|
||||
# 파일명에 사용할 수 없는 문자 제거
|
||||
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
|
||||
logger.info(f"[download_and_upload_song_to_blob] Temp directory created - path: {temp_file_path}")
|
||||
|
||||
# 오디오 파일 다운로드
|
||||
logger.info(f"[download_and_upload_song_to_blob] Downloading audio - task_id: {task_id}, url: {audio_url}")
|
||||
|
||||
content = await _download_audio(audio_url, task_id)
|
||||
|
||||
async with aiofiles.open(str(temp_file_path), "wb") as f:
|
||||
await f.write(content)
|
||||
|
||||
logger.info(f"[download_and_upload_song_to_blob] File downloaded - task_id: {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
|
||||
logger.info(f"[download_and_upload_song_to_blob] Uploaded to Blob - task_id: {task_id}, url: {blob_url}")
|
||||
|
||||
# Song 테이블 업데이트
|
||||
await _update_song_status(task_id, "completed", blob_url)
|
||||
logger.info(f"[download_and_upload_song_to_blob] SUCCESS - task_id: {task_id}")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"[download_and_upload_song_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_song_status(task_id, "failed")
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[download_and_upload_song_to_blob] DB ERROR - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_song_status(task_id, "failed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[download_and_upload_song_to_blob] EXCEPTION - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_song_status(task_id, "failed")
|
||||
|
||||
finally:
|
||||
# 임시 파일 삭제
|
||||
if temp_file_path and temp_file_path.exists():
|
||||
try:
|
||||
temp_file_path.unlink()
|
||||
logger.info(f"[download_and_upload_song_to_blob] Temp file deleted - path: {temp_file_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[download_and_upload_song_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_song_by_suno_task_id(
|
||||
suno_task_id: str,
|
||||
audio_url: str,
|
||||
|
|
|
|||
|
|
@ -716,7 +716,7 @@ from app.services.image_processor import ImageProcessorService
|
|||
from app.config import settings
|
||||
|
||||
|
||||
router = APIRouter(prefix="/images", tags=["images"])
|
||||
router = APIRouter(prefix="/images", tags=["Images"])
|
||||
|
||||
# 외부 서비스 인스턴스
|
||||
processor_service = ImageProcessorService(settings.IMAGE_PROCESSOR_URL)
|
||||
|
|
|
|||
Loading…
Reference in New Issue