import httpx from typing import List, Dict, Any, Optional RECCOBEATS_BASE_URL = "https://api.reccobeats.com/v1" class ReccoBeatsClient: def __init__(self): self.timeout = 30.0 async def get_tracks(self, spotify_ids: List[str]) -> List[Dict[str, Any]]: if not spotify_ids: return [] ids_param = ",".join(spotify_ids) async with httpx.AsyncClient(timeout=self.timeout) as client: try: response = await client.get( f"{RECCOBEATS_BASE_URL}/track", params={"ids": ids_param} ) if response.status_code != 200: print(f"ReccoBeats /track returned status {response.status_code}") return [] content = response.json().get("content", []) for item in content: href = item.get("href", "") if "spotify.com/track/" in href: item["spotify_id"] = href.split("/track/")[-1].split("?")[0] return content except Exception as e: print(f"ReccoBeats /track error: {e}") return [] async def get_audio_features(self, spotify_ids: List[str]) -> List[Dict[str, Any]]: """Fetch audio features for tracks. Batches in chunks of 40 (API limit).""" if not spotify_ids: return [] all_results = [] batch_size = 40 # ReccoBeats API returns 400 for 50+ IDs async with httpx.AsyncClient(timeout=self.timeout) as client: for i in range(0, len(spotify_ids), batch_size): batch = spotify_ids[i : i + batch_size] ids_param = ",".join(batch) try: response = await client.get( f"{RECCOBEATS_BASE_URL}/audio-features", params={"ids": ids_param}, ) if response.status_code != 200: print( f"ReccoBeats /audio-features returned status {response.status_code}" ) continue content = response.json().get("content", []) for item in content: href = item.get("href", "") if "spotify.com/track/" in href: item["spotify_id"] = href.split("/track/")[-1].split("?")[0] all_results.extend(content) except Exception as e: print(f"ReccoBeats /audio-features error: {e}") return all_results async def get_recommendations( self, seed_ids: List[str], size: int = 20, negative_seeds: Optional[List[str]] = None, ) -> List[Dict[str, Any]]: if not seed_ids: return [] if len(seed_ids) > 5: seed_ids = seed_ids[:5] params = {"seeds": ",".join(seed_ids), "size": size} if negative_seeds: if len(negative_seeds) > 5: negative_seeds = negative_seeds[:5] params["negativeSeeds"] = ",".join(negative_seeds) async with httpx.AsyncClient(timeout=self.timeout) as client: try: response = await client.get( f"{RECCOBEATS_BASE_URL}/track/recommendation", params=params ) if response.status_code != 200: print( f"ReccoBeats /recommendation returned status {response.status_code}" ) return [] content = response.json().get("content", []) for item in content: href = item.get("href", "") if "spotify.com/track/" in href: item["spotify_id"] = href.split("/track/")[-1].split("?")[0] return content except Exception as e: print(f"ReccoBeats /recommendation error: {e}") return [] async def get_audio_features_by_reccobeats_ids( self, reccobeats_ids: List[str] ) -> List[Dict[str, Any]]: if not reccobeats_ids: return [] results = [] async with httpx.AsyncClient(timeout=self.timeout) as client: for rb_id in reccobeats_ids: try: response = await client.get( f"{RECCOBEATS_BASE_URL}/track/{rb_id}/audio-features" ) if response.status_code == 200: data = response.json() data["reccobeats_id"] = rb_id results.append(data) except Exception as e: print(f"ReccoBeats audio-features for {rb_id} error: {e}") return results