feat(bgm): 카테고리당 3개 다운로드 및 카테고리별 그룹화
- 카테고리당 BGM 3개씩 다운로드 기능 추가 - 파일명에 카테고리 prefix 추가 (예: upbeat_trackname.mp3) - 중복 BGM 자동 스킵 기능 - 프론트엔드에서 BGM을 카테고리별로 그룹화하여 표시 - 분류되지 않은 BGM은 '기타' 섹션에 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,8 @@ class AutoBGMRequest(BaseModel):
|
||||
keywords: List[str] # Search keywords (from BGM recommendation)
|
||||
max_duration: int = 120
|
||||
commercial_only: bool = True # 상업적 사용 가능한 라이선스만
|
||||
count: int = 1 # Number of BGMs to download per request (default: 1)
|
||||
category: str | None = None # Category name to prefix filename (e.g., 'upbeat', 'chill')
|
||||
|
||||
|
||||
@router.get("/", response_model=list[BGMInfo])
|
||||
@@ -441,12 +443,17 @@ async def auto_download_bgm(request: AutoBGMRequest):
|
||||
|
||||
Set commercial_only=true (default) to only download CC0 licensed sounds
|
||||
that can be used commercially without attribution.
|
||||
|
||||
Set count to download multiple BGMs per category (default: 1).
|
||||
Existing BGMs with the same name are automatically skipped.
|
||||
"""
|
||||
success, message, file_path, matched_result = await search_and_download_bgm(
|
||||
keywords=request.keywords,
|
||||
output_dir=settings.BGM_DIR,
|
||||
max_duration=request.max_duration,
|
||||
commercial_only=request.commercial_only,
|
||||
count=request.count,
|
||||
category=request.category,
|
||||
)
|
||||
|
||||
if not success:
|
||||
|
||||
@@ -249,11 +249,25 @@ async def download_freesound(
|
||||
return False, f"Download error: {str(e)}", None
|
||||
|
||||
|
||||
def get_existing_bgm_ids(output_dir: str) -> set:
|
||||
"""Get set of existing BGM filenames (without extension) in the directory."""
|
||||
existing = set()
|
||||
if os.path.exists(output_dir):
|
||||
for filename in os.listdir(output_dir):
|
||||
if filename.endswith((".mp3", ".wav", ".m4a", ".ogg")):
|
||||
# Extract base name without extension
|
||||
base_name = os.path.splitext(filename)[0]
|
||||
existing.add(base_name.lower())
|
||||
return existing
|
||||
|
||||
|
||||
async def search_and_download_bgm(
|
||||
keywords: List[str],
|
||||
output_dir: str,
|
||||
max_duration: int = 120,
|
||||
commercial_only: bool = True,
|
||||
count: int = 1,
|
||||
category: Optional[str] = None,
|
||||
) -> Tuple[bool, str, Optional[str], Optional[BGMSearchResult]]:
|
||||
"""
|
||||
Search for BGM and download the best match.
|
||||
@@ -263,13 +277,19 @@ async def search_and_download_bgm(
|
||||
output_dir: Directory to save downloaded file
|
||||
max_duration: Maximum duration in seconds
|
||||
commercial_only: Only search commercially usable licenses (CC0)
|
||||
count: Number of BGMs to download (default: 1)
|
||||
category: Category name to prefix filename (e.g., 'upbeat', 'chill')
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message, file_path, matched_result)
|
||||
When count > 1, file_path contains the last downloaded file path
|
||||
"""
|
||||
if not settings.FREESOUND_API_KEY:
|
||||
return False, "Freesound API key not configured", None, None
|
||||
|
||||
# Get existing BGM files to skip duplicates
|
||||
existing_bgm = get_existing_bgm_ids(output_dir)
|
||||
|
||||
# Try searching with combined keywords
|
||||
query = " ".join(keywords[:3])
|
||||
|
||||
@@ -277,7 +297,7 @@ async def search_and_download_bgm(
|
||||
query=query,
|
||||
min_duration=15,
|
||||
max_duration=max_duration,
|
||||
page_size=10,
|
||||
page_size=max(count * 3, 15), # Get more results to filter duplicates
|
||||
commercial_only=commercial_only,
|
||||
)
|
||||
|
||||
@@ -288,7 +308,7 @@ async def search_and_download_bgm(
|
||||
query=keyword,
|
||||
min_duration=15,
|
||||
max_duration=max_duration,
|
||||
page_size=5,
|
||||
page_size=max(count * 3, 15),
|
||||
commercial_only=commercial_only,
|
||||
)
|
||||
if success and results:
|
||||
@@ -297,23 +317,56 @@ async def search_and_download_bgm(
|
||||
if not results:
|
||||
return False, "No matching BGM found on Freesound", None, None
|
||||
|
||||
# Select the best result (first one, sorted by relevance)
|
||||
best_match = results[0]
|
||||
# Download multiple BGMs
|
||||
downloaded_count = 0
|
||||
skipped_count = 0
|
||||
last_file_path = None
|
||||
last_match = None
|
||||
messages = []
|
||||
|
||||
# Download it
|
||||
safe_filename = best_match.title.lower().replace(" ", "_")[:50]
|
||||
safe_filename = "".join(c for c in safe_filename if c.isalnum() or c == "_")
|
||||
for result in results:
|
||||
if downloaded_count >= count:
|
||||
break
|
||||
|
||||
success, download_msg, file_path = await download_freesound(
|
||||
sound_id=best_match.id,
|
||||
output_dir=output_dir,
|
||||
filename=safe_filename,
|
||||
)
|
||||
# Generate safe filename with category prefix
|
||||
base_name = result.title.lower().replace(" ", "_")[:50]
|
||||
base_name = "".join(c for c in base_name if c.isalnum() or c == "_")
|
||||
if category:
|
||||
safe_filename = f"{category}_{base_name}"
|
||||
else:
|
||||
safe_filename = base_name
|
||||
|
||||
if not success:
|
||||
return False, download_msg, None, best_match
|
||||
# Skip if already exists
|
||||
if safe_filename.lower() in existing_bgm:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
return True, download_msg, file_path, best_match
|
||||
# Download it
|
||||
dl_success, download_msg, file_path = await download_freesound(
|
||||
sound_id=result.id,
|
||||
output_dir=output_dir,
|
||||
filename=safe_filename,
|
||||
)
|
||||
|
||||
if dl_success:
|
||||
downloaded_count += 1
|
||||
last_file_path = file_path
|
||||
last_match = result
|
||||
existing_bgm.add(safe_filename.lower()) # Add to existing set
|
||||
messages.append(f"Downloaded: {result.title}")
|
||||
else:
|
||||
messages.append(f"Failed: {result.title} - {download_msg}")
|
||||
|
||||
if downloaded_count == 0:
|
||||
if skipped_count > 0:
|
||||
return True, f"All {skipped_count} BGMs already exist, skipped", None, None
|
||||
return False, "Failed to download any BGM", None, None
|
||||
|
||||
summary = f"Downloaded {downloaded_count} BGM(s)"
|
||||
if skipped_count > 0:
|
||||
summary += f", skipped {skipped_count} existing"
|
||||
|
||||
return True, summary, last_file_path, last_match
|
||||
|
||||
|
||||
async def search_pixabay_music(
|
||||
|
||||
Reference in New Issue
Block a user