""" Default BGM Initializer Downloads pre-selected royalty-free BGM tracks on first startup. Tracks are from Kevin MacLeod (incompetech.com) - CC-BY 4.0 License. Free for commercial use with attribution: "Kevin MacLeod (incompetech.com)" """ import os import httpx import aiofiles import asyncio from typing import List, Tuple, Optional from pydantic import BaseModel class DefaultBGM(BaseModel): """Default BGM track info.""" id: str name: str url: str category: str description: str # Curated list of royalty-free BGM from Kevin MacLeod (incompetech.com) # CC-BY 4.0 License - Free for commercial use with attribution # Attribution: "Kevin MacLeod (incompetech.com)" DEFAULT_BGM_TRACKS: List[DefaultBGM] = [ # === 활기찬/에너지 (Upbeat/Energetic) === DefaultBGM( id="upbeat_energetic", name="Upbeat Energetic", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Vivacity.mp3", category="upbeat", description="활기차고 에너지 넘치는 BGM - 피트니스, 챌린지 영상", ), DefaultBGM( id="happy_pop", name="Happy Pop", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Carefree.mp3", category="upbeat", description="밝고 경쾌한 팝 BGM - 제품 소개, 언박싱", ), DefaultBGM( id="upbeat_fun", name="Upbeat Fun", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Happy%20Happy%20Game%20Show.mp3", category="upbeat", description="신나는 게임쇼 비트 - 트렌디한 쇼츠", ), # === 차분한/편안한 (Chill/Relaxing) === DefaultBGM( id="chill_lofi", name="Chill Lo-Fi", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Gymnopedie%20No%201.mp3", category="chill", description="차분하고 편안한 피아노 BGM - 일상, 브이로그", ), DefaultBGM( id="calm_piano", name="Calm Piano", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Prelude%20No.%201.mp3", category="chill", description="잔잔한 피아노 BGM - 감성적인 콘텐츠", ), DefaultBGM( id="soft_ambient", name="Soft Ambient", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Dreamlike.mp3", category="chill", description="부드러운 앰비언트 - ASMR, 수면 콘텐츠", ), # === 유머/코미디 (Funny/Comedy) === DefaultBGM( id="funny_comedy", name="Funny Comedy", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Sneaky%20Snitch.mp3", category="funny", description="유쾌한 코미디 BGM - 코미디, 밈 영상", ), DefaultBGM( id="quirky_playful", name="Quirky Playful", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Monkeys%20Spinning%20Monkeys.mp3", category="funny", description="장난스럽고 귀여운 BGM - 펫, 키즈 콘텐츠", ), # === 드라마틱/시네마틱 (Cinematic) === DefaultBGM( id="cinematic_epic", name="Cinematic Epic", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Epic%20Unease.mp3", category="cinematic", description="웅장한 시네마틱 BGM - 리뷰, 소개 영상", ), DefaultBGM( id="inspirational", name="Inspirational", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Hero%20Theme.mp3", category="cinematic", description="영감을 주는 BGM - 동기부여, 성장 콘텐츠", ), # === 생활용품/제품 리뷰 (Lifestyle/Product) === DefaultBGM( id="lifestyle_modern", name="Lifestyle Modern", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Acoustic%20Breeze.mp3", category="lifestyle", description="모던한 라이프스타일 BGM - 제품 리뷰", ), DefaultBGM( id="shopping_bright", name="Shopping Bright", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Pleasant%20Porridge.mp3", category="lifestyle", description="밝은 쇼핑 BGM - 하울, 추천 영상", ), DefaultBGM( id="soft_corporate", name="Soft Corporate", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Laid%20Back%20Guitars.mp3", category="lifestyle", description="부드러운 기업형 BGM - 정보성 콘텐츠", ), # === 어쿠스틱/감성 (Acoustic/Emotional) === DefaultBGM( id="soft_acoustic", name="Soft Acoustic", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Peaceful.mp3", category="acoustic", description="따뜻한 어쿠스틱 BGM - 요리, 일상 브이로그", ), DefaultBGM( id="gentle_guitar", name="Gentle Guitar", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Sunflower%20Slow%20Drag.mp3", category="acoustic", description="잔잔한 기타 BGM - 여행, 풍경 영상", ), # === 트렌디/일렉트로닉 (Trendy/Electronic) === DefaultBGM( id="electronic_chill", name="Electronic Chill", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Digital%20Lemonade.mp3", category="electronic", description="일렉트로닉 칠아웃 - 테크, 게임 콘텐츠", ), DefaultBGM( id="driving_beat", name="Driving Beat", url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Cipher.mp3", category="electronic", description="드라이빙 비트 - 스포츠, 액션 영상", ), ] async def download_bgm_file( url: str, output_path: str, timeout: int = 60, ) -> Tuple[bool, str]: """ Download a single BGM file. Args: url: Download URL output_path: Full path to save the file timeout: Download timeout in seconds Returns: Tuple of (success, message) """ headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Accept": "audio/mpeg,audio/*;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", } try: async with httpx.AsyncClient(follow_redirects=True, headers=headers) as client: response = await client.get(url, timeout=timeout) if response.status_code != 200: return False, f"HTTP {response.status_code}" # Ensure directory exists os.makedirs(os.path.dirname(output_path), exist_ok=True) # Save file async with aiofiles.open(output_path, 'wb') as f: await f.write(response.content) return True, "Downloaded successfully" except httpx.TimeoutException: return False, "Download timeout" except Exception as e: return False, str(e) async def initialize_default_bgm( bgm_dir: str, force: bool = False, ) -> Tuple[int, int, List[str]]: """ Initialize default BGM tracks. Downloads default BGM tracks if not already present. Args: bgm_dir: Directory to save BGM files force: Force re-download even if files exist Returns: Tuple of (downloaded_count, skipped_count, error_messages) """ os.makedirs(bgm_dir, exist_ok=True) downloaded = 0 skipped = 0 errors = [] for track in DEFAULT_BGM_TRACKS: output_path = os.path.join(bgm_dir, f"{track.id}.mp3") # Skip if already exists (unless force=True) if os.path.exists(output_path) and not force: skipped += 1 print(f"[BGM] Skipping {track.name} (already exists)") continue print(f"[BGM] Downloading {track.name}...") success, message = await download_bgm_file(track.url, output_path) if success: downloaded += 1 print(f"[BGM] Downloaded {track.name}") else: errors.append(f"{track.name}: {message}") print(f"[BGM] Failed to download {track.name}: {message}") return downloaded, skipped, errors async def get_default_bgm_list() -> List[dict]: """ Get list of default BGM tracks with metadata. Returns: List of BGM info dictionaries """ return [ { "id": track.id, "name": track.name, "category": track.category, "description": track.description, } for track in DEFAULT_BGM_TRACKS ] def check_default_bgm_status(bgm_dir: str) -> dict: """ Check which default BGM tracks are installed. Args: bgm_dir: BGM directory path Returns: Status dictionary with installed/missing tracks """ installed = [] missing = [] for track in DEFAULT_BGM_TRACKS: file_path = os.path.join(bgm_dir, f"{track.id}.mp3") if os.path.exists(file_path): installed.append(track.id) else: missing.append(track.id) return { "total": len(DEFAULT_BGM_TRACKS), "installed": len(installed), "missing": len(missing), "installed_ids": installed, "missing_ids": missing, }