Last active
July 13, 2025 20:20
-
-
Save alexsanqp/5b9e848b73f9ce5086ee7369482fb091 to your computer and use it in GitHub Desktop.
This script provides a simple, dependency-light Python client to interact directly with the unofficial Suno AI API. It allows you to programmatically generate music, poll for the completion status of your tracks, and download the final audio files. #russiaterroriststate #sunoai #sunoapi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import requests | |
| import time | |
| import os | |
| import json | |
| from typing import List, Dict, Any | |
| from dotenv import load_dotenv | |
| class ApiConfig: | |
| BASE_URL = "https://studio-api.prod.suno.com" | |
| GENERATE_ENDPOINT = "/api/generate/v2-web/" | |
| FEED_ENDPOINT = "/api/feed/v2" | |
| class Track: | |
| def __init__(self, data: Dict[str, Any]): | |
| self.id = data.get("id") | |
| self.status = data.get("status") | |
| self.title = data.get("title") | |
| self.audio_url = data.get("audio_url") | |
| def __repr__(self) -> str: | |
| return f"Track(id='{self.id}', title='{self.title}', status='{self.status}')" | |
| class SunoClient: | |
| def __init__(self, bearer_token: str): | |
| if not bearer_token or not bearer_token.startswith("Bearer "): | |
| raise ValueError("A valid Bearer token must be provided.") | |
| self.session = requests.Session() | |
| self.session.headers.update({ | |
| "Accept": "*/*", | |
| "Authorization": bearer_token, | |
| }) | |
| self.api_config = ApiConfig() | |
| def _request(self, method: str, endpoint: str, **kwargs) -> Any: | |
| url = self.api_config.BASE_URL + endpoint | |
| try: | |
| response = self.session.request(method, url, **kwargs) | |
| response.raise_for_status() | |
| return response.json() if response.content else None | |
| except requests.exceptions.HTTPError as http_err: | |
| print(f"HTTP Error: {http_err}\nServer response: {http_err.response.text}") | |
| raise | |
| except requests.exceptions.RequestException as req_err: | |
| print(f"Connection Error: {req_err}") | |
| raise | |
| def create_track(self, title: str, prompt: str, tags: str, model: str = "chirp-v3-5") -> List[str]: | |
| print(f"Sending request to create track '{title}'...") | |
| payload = { | |
| "prompt": prompt, "tags": tags, "title": title, "mv": model, | |
| "continue_clip_id": None, "continue_at": 0, "generation_type": "TEXT", "task": None | |
| } | |
| headers = self.session.headers.copy() | |
| headers['Content-Type'] = 'text/plain;charset=UTF-8' | |
| response_data = self._request( | |
| "POST", self.api_config.GENERATE_ENDPOINT, data=json.dumps(payload), headers=headers | |
| ) | |
| clip_ids = [clip['id'] for clip in response_data.get('clips', [])] | |
| print(f"Request accepted. Generated clips with IDs: {clip_ids}") | |
| return clip_ids | |
| def get_tracks(self, ids: List[str]) -> List[Track]: | |
| if not ids: | |
| return [] | |
| id_string = ",".join(ids) | |
| response_data = self._request("GET", self.api_config.FEED_ENDPOINT, params={"ids": id_string}) | |
| clips_data = response_data.get('clips', []) | |
| return [Track(item) for item in clips_data if isinstance(item, dict)] | |
| def poll_for_completion(self, ids: List[str], poll_interval: int = 5, timeout: int = 300) -> List[Track]: | |
| print(f"Waiting for generation to complete for IDs: {ids}...") | |
| start_time = time.time() | |
| while time.time() - start_time < timeout: | |
| tracks = self.get_tracks(ids) | |
| completed_tracks = [t for t in tracks if t.status == 'complete'] | |
| print(f"Status: {len(completed_tracks)}/{len(ids)} tracks ready.") | |
| if len(completed_tracks) == len(ids): | |
| print("All tracks generated successfully!") | |
| return completed_tracks | |
| time.sleep(poll_interval) | |
| raise TimeoutError("Track generation timeout exceeded.") | |
| def download(self, track: Track, save_dir: str = 'suno_downloads'): | |
| if not track.audio_url: | |
| print(f"Error: track '{track.title}' has no download URL.") | |
| return | |
| os.makedirs(save_dir, exist_ok=True) | |
| safe_title = "".join(c for c in track.title if c.isalnum() or c in (' ', '_')).rstrip() | |
| filename = f"{safe_title}_{track.id[:8]}.mp3" | |
| filepath = os.path.join(save_dir, filename) | |
| print(f"Downloading '{track.title}' to file {filepath}...") | |
| try: | |
| response = requests.get(track.audio_url, stream=True) | |
| response.raise_for_status() | |
| with open(filepath, 'wb') as f: | |
| for chunk in response.iter_content(chunk_size=8192): | |
| f.write(chunk) | |
| print("Download complete.") | |
| except requests.exceptions.RequestException as e: | |
| print(f"Failed to download file: {e}") | |
| if __name__ == '__main__': | |
| load_dotenv() | |
| BEARER_TOKEN = os.getenv("BEARER_TOKEN") # BEARER_TOKEN="Bearer eyJhbGciOiJSUzI1NiIsIm..." | |
| try: | |
| client = SunoClient(bearer_token=BEARER_TOKEN) | |
| song_title = "Вечірній Київ" | |
| song_prompt = """ | |
| [Verse 1] | |
| Сирени знов розрізали світанок | |
| І дим закрив собою мирний ранок | |
| Сусід прийшов з війною, не з добром | |
| Лишаючи лиш попіл за вікном | |
| [Chorus] | |
| Імперія брехні, держава-терорист | |
| Твій кожен постріл – це кривавий, чорний лист | |
| Та не зламати дух, що прагне до життя | |
| Ми будем вільні! Без тебе. Без сміття. | |
| """ | |
| song_style = "Ukrainian patriotic rock, epic rock ballad, powerful" | |
| track_ids = client.create_track( | |
| title=song_title, prompt=song_prompt, tags=song_style | |
| ) | |
| if not track_ids: | |
| raise Exception("Failed to create tracks.") | |
| completed_tracks = client.poll_for_completion(track_ids) | |
| print("\n--- Downloading finished tracks ---") | |
| for t in completed_tracks: | |
| client.download(t) | |
| except (ValueError, requests.HTTPError, TimeoutError) as e: | |
| print(f"\nExecution interrupted due to an error: {e}") | |
| except Exception as e: | |
| print(f"\nAn unexpected error occurred: {e}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment