"""Freesound API 클라이언트 효과음 검색 및 다운로드 https://freesound.org/docs/api/ """ import os import logging from typing import Optional, List, Dict import httpx logger = logging.getLogger(__name__) class FreesoundClient: """Freesound API 클라이언트""" BASE_URL = "https://freesound.org/apiv2" def __init__(self): self.api_key = os.getenv("FREESOUND_API_KEY", "") self.timeout = httpx.Timeout(30.0, connect=10.0) def _get_headers(self) -> dict: """인증 헤더 반환""" return {"Authorization": f"Token {self.api_key}"} async def search( self, query: str, page: int = 1, page_size: int = 20, filter_fields: Optional[str] = None, sort: str = "score", min_duration: Optional[float] = None, max_duration: Optional[float] = None, ) -> Dict: """효과음 검색 Args: query: 검색어 page: 페이지 번호 page_size: 페이지당 결과 수 filter_fields: 필터 (예: "duration:[1 TO 5]") sort: 정렬 (score, duration_asc, duration_desc, created_desc 등) min_duration: 최소 길이 (초) max_duration: 최대 길이 (초) Returns: 검색 결과 딕셔너리 """ if not self.api_key: logger.warning("Freesound API 키가 설정되지 않음") return {"count": 0, "results": []} # 필터 구성 filters = [] if min_duration is not None or max_duration is not None: min_d = min_duration if min_duration is not None else 0 max_d = max_duration if max_duration is not None else "*" filters.append(f"duration:[{min_d} TO {max_d}]") if filter_fields: filters.append(filter_fields) params = { "query": query, "page": page, "page_size": min(page_size, 150), # Freesound 최대 150 "sort": sort, "fields": "id,name,description,duration,tags,previews,license,username", } if filters: params["filter"] = " ".join(filters) async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.get( f"{self.BASE_URL}/search/text/", params=params, headers=self._get_headers(), ) response.raise_for_status() data = response.json() # 결과 정리 results = [] for sound in data.get("results", []): results.append({ "freesound_id": sound["id"], "name": sound.get("name", ""), "description": sound.get("description", ""), "duration": sound.get("duration", 0), "tags": sound.get("tags", []), "preview_url": sound.get("previews", {}).get("preview-hq-mp3", ""), "license": sound.get("license", ""), "username": sound.get("username", ""), }) return { "count": data.get("count", 0), "page": page, "page_size": page_size, "results": results, } async def get_sound(self, sound_id: int) -> Dict: """사운드 상세 정보 조회""" if not self.api_key: raise ValueError("Freesound API 키 필요") async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.get( f"{self.BASE_URL}/sounds/{sound_id}/", headers=self._get_headers(), ) response.raise_for_status() return response.json() async def download_preview(self, preview_url: str) -> bytes: """프리뷰 오디오 다운로드 (인증 불필요)""" async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.get(preview_url) response.raise_for_status() return response.content async def get_similar_sounds( self, sound_id: int, page_size: int = 10, ) -> List[Dict]: """유사한 사운드 검색""" if not self.api_key: return [] async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.get( f"{self.BASE_URL}/sounds/{sound_id}/similar/", params={ "page_size": page_size, "fields": "id,name,description,duration,tags,previews,license", }, headers=self._get_headers(), ) response.raise_for_status() data = response.json() results = [] for sound in data.get("results", []): results.append({ "freesound_id": sound["id"], "name": sound.get("name", ""), "description": sound.get("description", ""), "duration": sound.get("duration", 0), "tags": sound.get("tags", []), "preview_url": sound.get("previews", {}).get("preview-hq-mp3", ""), "license": sound.get("license", ""), }) return results # 싱글톤 인스턴스 freesound_client = FreesoundClient()