import json import httpx import logging from typing import Optional, Dict, Tuple import asyncio from pathlib import Path import uuid from datetime import datetime from app.models import SunoGenerateResponse, SunoMetadata logger = logging.getLogger(__name__) class SunoService: def __init__(self, api_key: str): self.api_key = api_key self.base_url = "https://studio-api.prod.suno.com/api/v2/external" self.headers = { "Authorization": f"Bearer {self.api_key}", "X-Auth-Type": "alexa", "Content-Type": "text/plain;charset=UTF-8", } async def get_song_status(self, song_id: str) -> SunoGenerateResponse: try: async with httpx.AsyncClient() as client: response = await client.get( f"{self.base_url}/clips", headers=self.headers, params={"ids": song_id}, timeout=30.0 ) response.raise_for_status() response_data = response.json() # Handle the case when response contains a list of clips if isinstance(response_data, list) and len(response_data) > 0: clip_data = response_data[0] # Create a metadata object with available fields metadata = SunoMetadata( tags=clip_data.get("metadata", {}).get("tags"), prompt=clip_data.get("metadata", {}).get("prompt"), gpt_description_prompt=clip_data.get("metadata", {}).get("gpt_description_prompt"), lyrics=clip_data.get("metadata", {}).get("lyrics"), type=clip_data.get("metadata", {}).get("type", "gen"), duration=clip_data.get("metadata", {}).get("duration"), error_type=clip_data.get("metadata", {}).get("error_type"), error_message=clip_data.get("metadata", {}).get("error_message") ) return SunoGenerateResponse( id=clip_data.get("id"), request_id=clip_data.get("request_id"), video_url=clip_data.get("video_url"), audio_url=clip_data.get("audio_url"), image_url=clip_data.get("image_url"), image_large_url=clip_data.get("image_large_url"), created_at=clip_data.get("created_at"), status=clip_data.get("status"), title=clip_data.get("title"), metadata=metadata ) else: logger.error(f"Invalid response format or no clips found: {response_data}") raise ValueError("Invalid response format or no clips found") except httpx.HTTPStatusError as e: logger.error(f"HTTP error fetching song status: {str(e)}") if e.response.status_code == 404: # For 404 errors, return a response with default values return SunoGenerateResponse( id=str(uuid.uuid4()), created_at=datetime.now(), status="not_found", metadata=SunoMetadata( type="gen", error_type="not_found", error_message="Song not found" ) ) raise except Exception as e: logger.error(f"Error fetching song status: {str(e)}") raise async def generate_song(self, topic: str, tags: Optional[str] = None, model: Optional[str] = None) -> SunoGenerateResponse: try: content = json.dumps({ "topic": topic, "tags": tags or "children friendly, music", "model": model, "extra": { "early_callback": True } }) print(content) async with httpx.AsyncClient() as client: response = await client.post( f"{self.base_url}/generate/", headers=self.headers, content=content, timeout=30.0 ) response.raise_for_status() response_data = response.json() # Extract request_id if present in the response response_obj = SunoGenerateResponse(**response_data) # If request_id is in the raw response but not captured by the model if 'request_id' in response_data and not response_obj.request_id: response_obj.request_id = response_data['request_id'] return response_obj except Exception as e: logger.error(f"Song generation error: {str(e)}") raise