diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfc7a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Byte-compiled +__pycache__/ + +# Distribution / packaging +build/ +dist/ +*.egg +*.egg-info/ + +# Jupyter Notebook +.ipynb_checkpoints/ +*.ipynb + +# Suno-Downloads +.downloads/ + +# Desktop Services Store +.DS_Store + +# PyPI setup.py +setup.py + +# Python venv +.venv +helper.txt \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f0d4af3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.11-slim + +# Set the working directory inside the container +WORKDIR /app + +# Copy the requirements file to the working directory +COPY requirements.txt . + +# Install Requirements +RUN pip3 install -U pip +COPY requirements.txt . +RUN pip3 install --no-cache-dir -U -r requirements.txt + +# Copying All Source +COPY . . + +# Set Port +EXPOSE 8080 + +# Run API Server +CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8080"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2d4efc1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Malith Rukshan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Logo.png b/Logo.png new file mode 100644 index 0000000..661729f Binary files /dev/null and b/Logo.png differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..86ba50b --- /dev/null +++ b/README.md @@ -0,0 +1,338 @@ +

+ +

+

โœจ Suno AI API ๐ŸŽต

+
+ +[![PyPI Package](https://img.shields.io/badge/PyPi-Library-1cd760?logo=pypi&style=flat)](https://pypi.org/project/SunoAI/) +[![Updates Telegram Channel](https://img.shields.io/badge/Updates-@SunoAPI-blue?logo=telegram&style=flat)](https://t.me/SunoAPI) + +
+

โœจ Python API Library for Suno AI โ€” Create Music with Generative AI ! ๐Ÿš€

+
+ - Available as Both Python Library and REST API - +
+
+ Python Library + ยท + Update Channel +
+
+ +**๐Ÿ“š SunoAI API Library is an unofficial Python client for interacting with [Suno AI](https://suno.ai/)'s music generator**. This library facilitates generating music using Suno's Chirp v3 model and includes main functions of Suno AI with a built-in music downloader. It can be deployed as a **[REST API](#-deployment---rest-api)** using FastAPI, Local, Docker, on a PaaS provider like Heroku. + +## โœจ Features +- **Python Client ๐Ÿ**: Easily interact with Suno AI. +- **Song Generation ๐ŸŽถ**: Utilize the Chirp v3 model for generating music. +- **Retrieve Song Info by ID ๐ŸŽต**: Access detailed information about any song on Suno AI. +- **Music Downloader ๐Ÿ“ฅ**: Built-in functionality to download any music on Suno AI directly. +- **REST API Deployment ๐ŸŒ**: Deployable as a REST API on PasS Platform , VPS or Local. +- **Comprehensive Documentation ๐Ÿ“š**: Includes detailed examples and usage guides. +- **Docker Support ๐Ÿณ**: Enables containerized deployment with Docker for flexibility. +- **PaaS Deployment โ˜๏ธ:** Facilitates deployment on platforms like Heroku for convenient accessibility. + +# ๐Ÿท Prerequisites + ๐Ÿ“‹ Before using the library or REST API, you must sign up on the [suno.ai](https://app.suno.ai/) website and obtain your cookie as shown in this screenshot. + +๐Ÿ’ก You can find cookie from the Web Browser's **Developer Tools -> Network Tab** + +
+ Click to view - Screenshot +How to get Cookie from Suno.AI + +Just right click & open Inspect. Filter : `_clerk_js_version` +
+
+ +Set this cookie as `SUNO_COOKIE` environment variable or initialize the library as shown below. + +``` +import suno +client = suno.Suno(cookie='YOUR_COOKIE_HERE') +``` + +## ๐Ÿ’พ Installation +Install the library using pip: + +``` +pip install SunoAI +``` + + + + + +## ๐Ÿš€ Deployment - REST API + +### Deploy on PasS + +Set `SUNO_COOKIE` as an Environmental variable before deploy. - [Instructions](#-prerequisites) + +[![Deploy with heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) +[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/Malith-Rukshan/Suno-API) + +[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy) +
+ +### Deploy on Local or VPS + +``` +export SUNO_COOKIE="YOUR_COOKIE_HERE" +git clone git@github.com:Malith-Rukshan/Suno-API.git +pip3 install -r requirements.txt +cd Suno-API +fastapi run api.py --port 8080 +``` +๐Ÿ”— Available at : http://127.0.0.1:8080 + +## ๐Ÿ› ๏ธ Usage +### ๐Ÿ“š Library Methods + +`generate()` +- Arguments: + - prompt (str): Description or lyrics for the song. + - is_custom (bool): Determines whether to use custom lyrics (True) or a description (False). + - tags (Optional[str]): Describes desired voice type or characteristics. + - title (Optional[str]): Title for the generated music. + - make_instrumental (Optional[bool]): Generates an instrumental version if True. + - wait_audio (bool): Waits until the audio URLs are ready if True. +- Returns: A list of `Clip` objects containing song data with IDs. +- Example: + - **By Description** + ``` + clips = client.generate( + prompt="A peaceful melody reflecting a serene landscape", + is_custom=False, + wait_audio=True + ) + print(clips) + ``` + - **By Lyrics - Custom** + ``` + clips = client.generate( + prompt="I found a love, for me\nDarling, just dive right in and follow my lead\nWell, I found a girl, beautiful and sweet\nOh, I never knew you were the someone waiting for me...", + tags="English men voice", + title="Perfect by Malith-Rukshan/Suno-API", + make_instrumental= False, + is_custom=True, + wait_audio=True + ) + print(clips) + ``` +**โœ๏ธ Usage Note :** + - When setting `is_custom` to `True`, ensure that the prompt parameter contains the lyrics of the song you wish to generate. Conversely, if `is_custom` is set to `False`, provide a descriptive prompt detailing the essence of the song you want. + - When `wait_audio` is set to **True**, the request will take longer as it waits for the audio URLs to be ready. If not set, the response will return without `audio_url` but with audio IDs. In such cases, you'll need to call the **get_songs** or **get_song** method after a short interval to retrieve the response with the `audio_url` included, once the generation process is complete. + +`get_songs()` +- Arguments: + - song_ids (Optional[str]): A list of song IDs to fetch specific songs. +- Returns: A list of `Clip` objects representing the retrieved songs. +- Example: + ``` + songs = client.get_songs(song_ids="123,456") + print(songs) + ``` + +`get_credits()` +- Returns: Current billing and credits information as a `CreditsInfo` object. +- Example: + ``` + credits_info = client.get_credits() + print(credits_info) + ``` + +`download()` +- Arguments: + - song (str | Clip): The song to be downloaded. This can be either the ID of the song as a string or a Clip object containing the song's metadata. + - path (str): The directory path where the song will be saved. If not specified, defaults to "./downloads". +- Returns: str - The full filepath to the downloaded song. +- Raises: + - TypeError: If the song argument is neither a string ID nor a Clip object. + - Exception: If the download fails due to issues like an invalid URL or network errors. +- Example: + ``` + # Using a song ID + file_path = client.download(song="uuid-type-songid-1234") + print(f"Song downloaded to: {file_path}") + + # Using a Clip object + clip = client.get_song("uuid-type-songid-1234") + file_path = client.download(song=clip) + print(f"Song downloaded to: {file_path}") + ``` + +### ๐Ÿ“š Library Responses + +- **Clip Model:** + + The **Clip** class encapsulates the details of a music track generated by the Suno AI. Each attribute of this class provides specific information about the track: + - **id** (str): Unique identifier for the clip. + - **video_url** (str): URL of the video version of the song, if available. + - **audio_url** (str): URL where the audio track can be streamed or downloaded. + - **image_url** (str): URL of the song's image cover. + - **image_large_url** (str): URL of a larger version of the song's image cover. + - **is_video_pending** (bool): Indicates whether the video for the song is still processing. + - **major_model_version** (str): The major version of the model used to generate the song. + - **model_name** (str): Name of the model used to generate the track. + - **metadata** (ClipMetadata): Additional metadata related to the clip including tags, prompts, and other information. + - **is_liked** (bool): Indicates whether the song has been liked by the user. + - **user_id** (str): User ID of the person who created or requested the song. + - **display_name** (str): Display name of the user associated with the song. + - **handle** (str): User's handle or username. + - **is_handle_updated** (bool): Specifies whether the user's handle has been updated. + - **is_trashed** (bool): Indicates if the clip has been marked as trashed. + - **reaction** (dict): Reactions to the song from users, if any. + - **created_at** (str): Timestamp indicating when the song was created. + - **status** (str): Current status of the song (e.g., processing, completed). + - **title** (str): Title of the song. + - **play_count** (int): How many times the song has been played. + - **upvote_count** (int): Number of upvotes the song has received. + - **is_public** (bool): Indicates whether the song is publicly accessible. + +- **CreditsInfo Model:** + + The **CreditsInfo** class provides information about the user's credit balance and usage within the Suno AI system. + - **credits_left** (int): The number of credits remaining for the user. + - **period** (int): The current billing period for the credits, represented in some form of date or timeframe. + - **monthly_limit** (int): The total number of credits allocated to the user for the current month. + - **monthly_usage** (int): The amount of credits used by the user during the current month. + +## ๐ŸŒ REST API Usage + +**1. Generate Music** + +`POST /generate` + + - **Request Body:** + ``` + { + "prompt": "A serene melody about the ocean", + "is_custom": false, + "tags": "relaxing, instrumental", + "title": "Ocean Waves", + "make_instrumental": true, + "wait_audio": true + } + ``` + + - **Response:** +
+ Click to view + + ``` + [ + { + "id": "124b735f-7fb0-42b9-8b35-761aed65a7f6", + "video_url": "", + "audio_url": "https://audiopipe.suno.ai/item_id=124b735f-7fb0-42b9-8b35-761aed65a7f6", + "image_url": "https://cdn1.suno.aiimage_124b735f-7fb0-42b9-8b35-761aed65a7f6.png", + "image_large_url": "https://cdn1.suno.aiimage_large_124b735f-7fb0-42b9-8b35-761aed65a7f.png", + "is_video_pending": False, + "major_model_version": "v3", + "model_name": "chirp-v3", + "metadata": { + "tags": "English men voice", + "prompt": "I found a love, for me\nDarling,just dive right in and follow mylead\nWell, I found a girl, beautiful andsweet\nOh, I never knew you were thesomeone waiting for me\n\nโ€ฒCause we werejust kids when we fell in love\nNot knowingwhat it was\nI will not give you up thistime\nBut darling, just kiss me slow\nYourheart is all I own\nAnd in your eyes,you're holding mine\n\nBaby, Iโ€ฒm dancing inthe dark\nWith you between myarms\nBarefoot on the grass\nListening toour favourite song\nWhen you said youlooked a mess\nI whispered underneath mybreath\nBut you heard it\nDarling, you lookperfect tonight", + "gpt_description_prompt": None, + "audio_prompt_id": None, + "history": None, + "concat_history": None, + "type": "gen", + "duration": None, + "refund_credits": None, + "stream": True, + "error_type": None, + "error_message": None + }, + "is_liked": False, + "user_id":"2340653f-32cb-4343-artb-09203ty749e9", + "display_name": "Snonymous", + "handle": "anonymous", + "is_handle_updated": False, + "is_trashed": False, + "reaction": None, + "created_at": "2024-05-05T11:54:09.356Z", + "status": "streaming", + "title": "Perfect by Malith-Rukshan/Suno-API", + "play_count": 0, + "upvote_count": 0, + "is_public": False + } + ] + ``` +
+ +**2. Retrieve Songs** + +`POST /songs` + + - **Request Body:** + ``` + { + "song_ids": "uuid-format-1234,4567-abcd" + } + ``` + - **Response:** + Array of Clips - Same to `/generate` Response + +**3. Get a Specific Song** + +`POST /get_song` + + - **Request Body:** + ``` + { + "song_id": "uuid-song-id" + } + ``` + - **Response:** + Just Clip Response - Same to `/generate` Response but Only Clip + +**4. Retrieve Credit Information** + +`GET /credits` + + - **Response:** + ``` + { + "credits_left": 50, + "period": "2024-05", + "monthly_limit": 100, + "monthly_usage": 25 + } + ``` +> According to [Suno.ai](https://suno.ai/) Each song generation consumes 5 credits, thus a total of 10 credits is necessary for each successful call. + + +## ๐Ÿค Contributing +Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. + +## ๐ŸŽฏ Credits and Other +All content and music generated through this library are credited to [Suno AI](https://suno.ai/). This unofficial API provides a convenient way to interact with Suno AI's services but does not claim any ownership or rights over the music generated. Please respect the terms of service of Suno AI when using their platform โค๏ธ. + +> This library is intended primarily for educational and development purposes. It aims to enhance and simplify access to Suno AI's music generation capabilities. If you enjoy the music generated, consider supporting Suno AI directly. +> Logo Credit : [@rejaul43](https://dribbble.com/rejaul43) + +## โš–๏ธ License +This project is distributed under the MIT License. This license allows everyone to use, modify, and redistribute the code. However, it comes with no warranties regarding its functionality. For more details, see the [LICENSE](https://github.com/Malith-Rukshan/Suno-API/blob/main/LICENSE) file in the repository. + +## ๐ŸŒŸ Support and Community +If you found this project helpful, **don't forget to give it a โญ on GitHub.** This helps others find and use the project too! ๐Ÿซถ + +Join our Telegram channels, + +- [@SingleDevelopers](https://t.me/SingleDevelopers), for more amazing projects and updates โœ“ +- [@SunoAPI](https://t.me/SunoAPI), for this project updates โœ“ + +## ๐Ÿ“ฌ Contact +If you have any questions, feedback, or just want to say hi, you can reach out to me: + +- Developer : [@MalithRukshan](https://t.me/MalithRukshan) +- Support Group : [@Suno_API](https://t.me/Suno_API) + +๐Ÿง‘โ€๐Ÿ’ป Built with ๐Ÿ’– by [Single Developers ](https://t.me/SingleDevelopers) + + + + + diff --git a/Screenshot.jpg b/Screenshot.jpg new file mode 100644 index 0000000..8a2f1dc Binary files /dev/null and b/Screenshot.jpg differ diff --git a/api.py b/api.py new file mode 100644 index 0000000..5954da1 --- /dev/null +++ b/api.py @@ -0,0 +1,90 @@ +# ยฉ [2024] Malith-Rukshan. All rights reserved. +# Repository: https://github.com/Malith-Rukshan/Suno-API + +import os +from typing import List +from suno import suno +from suno.models import RequestParams, CreditsInfo, Clip +import fastapi +from fastapi.responses import RedirectResponse, JSONResponse +from suno import __version__ + +COOKIE = os.getenv("SUNO_COOKIE") + +# Initilize Suno API Client +client = suno.Suno(cookie=COOKIE) + +description = """ +### Suno AI Unofficial API + + + + + + + + + + + +This is an **unofficial API for [Suno AI](https://www.suno.ai/)**, a platform that utilizes artificial intelligence to generate music. + +### ๐Ÿš€ Main Features +- **Generate Music:** Leverage Suno AI's capabilities to create music based on different styles and inputs. +- **Retrieve Music Data:** Access details about generated music tracks, including audio files, metadata, and more. +- **Get Credit Balance Info** +- **Documentation:** [๐Ÿ“š Redoc](/redoc) | [๐Ÿท Usage](https://github.com/Malith-Rukshan/Suno-API?tab=readme-ov-file#-rest-api-usage) + +### Repository +You can find the source code for this API at [GitHub](https://github.com/Malith-Rukshan/Suno-API). + +### Disclaimer +This API is not officially associated with Suno AI. It was developed to facilitate easier access and manipulation of the music generation capabilities provided by Suno AI's official website. + +### Usage +Please note that this API is intended for educational and development purposes. Ensure you respect Suno AI's terms of service when using their services. +""" + +# FastAPI app +app = fastapi.FastAPI( + title="Suno API", + summary="An Unofficial Python Library for Suno AI API", + description=description, + version=__version__, + contact={ + "name": "Malith Rukshan", + "url": "https://MalithRukshan.t.me", + "email": "singledeveloper.lk@gmail.com", + } +) + +# Redirect to Docs :) + + +@app.get("/", include_in_schema=False) +async def redirect_to_docs(): + return RedirectResponse(url='/docs') + + +@app.post(f"/generate", response_model=List[Clip]) +def generate(params: RequestParams) -> JSONResponse: + clips = client.generate(**params.model_dump()) + return JSONResponse(content=[clip.model_dump() for clip in clips]) + + +@app.post(f"/songs", response_model=List[Clip]) +def generate(song_ids: str | None = None) -> JSONResponse: + clips = client.get_songs(song_ids) + return JSONResponse(content=[clip.model_dump() for clip in clips]) + + +@app.post(f"/get_song", response_model=Clip) +def generate(song_id: str) -> JSONResponse: + clip = client.get_song(song_id) + return JSONResponse(content=clip.model_dump()) + + +@app.get(f"/credits", response_model=CreditsInfo) +def credits() -> JSONResponse: + credits = client.get_credits() + return JSONResponse(content=credits.model_dump()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f2ae559 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi +uvicorn +requests +pydantic \ No newline at end of file diff --git a/suno/__init__.py b/suno/__init__.py new file mode 100644 index 0000000..379ca79 --- /dev/null +++ b/suno/__init__.py @@ -0,0 +1,11 @@ +from .suno import Suno +from .models import Clip, CreditsInfo, RequestParams + +__all__ = ( + "Suno", + "Clip", + "RequestParams", + "CreditsInfo" +) + +__version__ = "1.0" \ No newline at end of file diff --git a/suno/models.py b/suno/models.py new file mode 100644 index 0000000..cc41d8e --- /dev/null +++ b/suno/models.py @@ -0,0 +1,126 @@ +# ยฉ [2024] Malith-Rukshan. All rights reserved. +# Repository: https://github.com/Malith-Rukshan/Suno-API + + +from pydantic import BaseModel, ConfigDict + +class ClipMetadata(BaseModel): + tags: str | None = None + prompt: str | None = None + gpt_description_prompt: str | None = None + audio_prompt_id: str | None = None + history: str | None = None + concat_history: str | None = None + type: str | None = None + duration: float | None = None + refund_credits: float | None = None + stream: bool | None = None + error_type: str | None = None + error_message: str | None = None + + model_config = ConfigDict(protected_namespaces=()) + + +class Clip(BaseModel): + id: str + video_url: str | None = None + audio_url: str | None = None + image_url: str | None = None + image_large_url: str | None = None + is_video_pending: bool + major_model_version: str + model_name: str + metadata: ClipMetadata + is_liked: bool + user_id: str + display_name: str + handle: str + is_handle_updated: bool + is_trashed: bool + reaction: dict | None = None + created_at: str + status: str + title: str + play_count: int | None = None + upvote_count: int | None = None + is_public: bool + + class Config: + protected_namespaces = () + json_schema_extra = { + "example": { + "id": "124b735f-7fb0-42b9-8b35-761aed65a7f6", + "video_url": "", + "audio_url": "https://audiopipe.suno.ai/?item_id=124b735f-7fb0-42b9-8b35-761aed65a7f6", + "image_url": "https://cdn1.suno.ai/image_124b735f-7fb0-42b9-8b35-761aed65a7f6.png", + "image_large_url": "https://cdn1.suno.ai/image_large_124b735f-7fb0-42b9-8b35-761aed65a7f6.png", + "is_video_pending": False, + "major_model_version": "v3", + "model_name": "chirp-v3", + "metadata": { + "tags": "English men voice", + "prompt": "I found a love, for me\nDarling, just dive right in and follow my lead\nWell, I found a girl, beautiful and sweet\nOh, I never knew you were the someone waiting for me\n\nโ€ฒCause we were just kids when we fell in love\nNot knowing what it was\nI will not give you up this time\nBut darling, just kiss me slow\nYour heart is all I own\nAnd in your eyes, you're holding mine\n\nBaby, Iโ€ฒm dancing in the dark\nWith you between my arms\nBarefoot on the grass\nListening to our favourite song\nWhen you said you looked a mess\nI whispered underneath my breath\nBut you heard it\nDarling, you look perfect tonight", + "gpt_description_prompt": None, + "audio_prompt_id": None, + "history": None, + "concat_history": None, + "type": "gen", + "duration": None, + "refund_credits": None, + "stream": True, + "error_type": None, + "error_message": None + }, + "is_liked": False, + "user_id": "2340653f-32cb-4343-artb-09203ty749e9", + "display_name": "Snonymous", + "handle": "anonymous", + "is_handle_updated": False, + "is_trashed": False, + "reaction": None, + "created_at": "2024-05-05T11:54:09.356Z", + "status": "streaming", + "title": "Perfect by Malith-Rukshan/Suno-API", + "play_count": 0, + "upvote_count": 0, + "is_public": False + } + } + + +class RequestParams(BaseModel): + prompt: str + is_custom: bool = False + tags: str = "" + title: str = "" + make_instrumental: bool = False + wait_audio: bool = False + + class Config: + protected_namespaces = () + json_schema_extra = {"examples": [{ + "prompt": "I found a love, for me\nDarling, just dive right in and follow my lead\nWell, I found a girl, beautiful and sweet\nOh, I never knew you were the someone waiting for me\n\nโ€ฒCause we were just kids when we fell in love\nNot knowing what it was\nI will not give you up this time\nBut darling, just kiss me slow\nYour heart is all I own\nAnd in your eyes, you're holding mine\n\nBaby, Iโ€ฒm dancing in the dark\nWith you between my arms\nBarefoot on the grass\nListening to our favourite song\nWhen you said you looked a mess\nI whispered underneath my breath\nBut you heard it\nDarling, you look perfect tonight", + "is_custom": True, + "tags": "English men voice", + "title": "Perfect by Malith-Rukshan/Suno-API", + "make_instrumental": False, + "wait_audio": True + }]} + + +class CreditsInfo(BaseModel): + credits_left: int + period: int | None = None + monthly_limit: int + monthly_usage: int + + class Config: + protected_namespaces = () + json_schema_extra = { + "example": { + "credits_left": 50, + "period": "Date-Here", + "monthly_limit": 50, + "monthly_usage": 0 + } + } diff --git a/suno/suno.py b/suno/suno.py new file mode 100644 index 0000000..a9c30af --- /dev/null +++ b/suno/suno.py @@ -0,0 +1,239 @@ +# ยฉ [2024] Malith-Rukshan. All rights reserved. +# Repository: https://github.com/Malith-Rukshan/Suno-API + +import os +import pathlib +import random +import time +import logging +from typing import List, Optional +import requests + +from .models import Clip, CreditsInfo +from .utils import create_clip_from_data, response_to_clips, generate_fake_useragent + +# Setup basic logging configuration +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +# Fetch the cookie from environment variables; used for authentication +COOKIE = os.getenv("SUNO_COOKIE", "") + + +class Suno(): + """Main class for interacting with Suno API.""" + BASE_URL = 'https://studio-api.suno.ai' + CLERK_BASE_URL = 'https://clerk.suno.com' + + def __init__(self, cookie: Optional[str] = None) -> None: + """Initialize the Suno client with optional cookie. If no cookie is provided, it uses the one from the environment.""" + if cookie is None: + cookie = COOKIE + if cookie == "": + raise Exception("Environment variable SUNO_COOKIE is not found !") + + headers = { + # Generate a random User-agent for requests + 'User-Agent': generate_fake_useragent(), + 'Cookie': cookie + } + self.client = requests.Session() + self.client.headers.update(headers) + self.current_token = None + self.sid = None + + self._get_session_id() # Retrieve session ID upon initialization + self._keep_alive() # Keep session alive + + def _get_session_id(self) -> None: + """Retrieve a session ID from the Suno service.""" + url = f"{Suno.CLERK_BASE_URL}/v1/client?_clerk_js_version=4.72.1" + response = self.client.get(url) + if not response.json()['response']: + raise Exception( + "Failed to get session id, you may need to update the SUNO_COOKIE") + if 'last_active_session_id' in response.json()['response']: + self.sid = response.json()['response']['last_active_session_id'] + else: + raise Exception( + f"Failed to get Session ID: {response.status_code}") + + def _keep_alive(self, is_wait=False) -> None: + """Renew the authentication token periodically to keep the session active.""" + if not self.sid: + raise Exception("Session ID is not set. Cannot renew token.") + + renew_url = f"{Suno.CLERK_BASE_URL}/v1/client/sessions/{self.sid}/tokens?_clerk_js_version=4.72.0-snapshot.vc141245" + renew_response = self.client.post(renew_url) + logger.info("Renew Token โ™ป๏ธ") + + if is_wait: + # Sleep randomly to mimic human interaction + time.sleep(random.uniform(1, 2)) + + new_token = renew_response.json()['jwt'] + self.current_token = new_token + # Set New Token to Headers + self.client.headers['Authorization'] = f"Bearer {new_token}" + + # Generate Songs + def generate(self, prompt, is_custom, tags=None, title=None, make_instrumental=False, wait_audio=False) -> List[Clip]: + """ + Generate songs based on the provided parameters and optionally wait for the audio to be ready. + + Parameters: + - prompt (str): If is_custom=True, this should be the lyrics of the song. If False, it should be a brief description of what the song should be about. + - is_custom (bool): Determines whether the song should be generated from custom lyrics (True) or from a description (False). + - tags (Optional[str]): Describes the desired voice type or characteristics (e.g., "English male voice"). Default is None. + - title (Optional[str]): The title for the generated music. Default is None. + - make_instrumental (Optional[bool]): If True, generates an instrumental version of the track. Default is False. + - wait_audio (bool): If True, waits until the audio URLs are ready and returns them. If False, returns the IDs of the songs being processed, which can be used to fetch the songs later using get_song. + + Returns: + List[Clip]: A list of Clip objects containing either song IDs or complete song data, depending on the 'wait_audio' parameter. + """ + self._keep_alive() + logger.info("Generating Audio...") + payload = { + "make_instrumental": make_instrumental, + "mv": "chirp-v3-0", + "prompt": "" + } + if is_custom: + payload["tags"] = tags + payload["title"] = title + payload["prompt"] = prompt + else: + payload["gpt_description_prompt"] = prompt + + response = self.client.post( + f"{Suno.BASE_URL}/api/generate/v2/", json=payload) + + if response.status_code != 200: + logger.info("Audio Generate Failed โ‰๏ธ") + raise Exception(f"Error response: {response.text}") + + song_ids = [audio['id'] for audio in response.json()['clips']] + if wait_audio: + return self._wait_for_audio(song_ids) + else: + self._keep_alive(True) + logger.info("Generated Audio Successfully โœ…") + return response_to_clips(response.json()['clips']) + + def _wait_for_audio(self, song_ids): + """Helper method to wait for audio processing to complete.""" + start_time = time.time() + last_clips = [] + while time.time() - start_time < 100: # Timeout after 100 seconds + clips = self.get_songs(song_ids) + all_completed = all( + audio.status in ['streaming', 'complete'] for audio in clips) + if all_completed: + logger.info("Generated Audio Successfully โœ…") + return clips + last_clips = clips + time.sleep(random.uniform(3, 6)) # Wait with variation + self._keep_alive(True) + logger.info("Generated Audio Successfully โœ…") + return last_clips + + def get_songs(self, song_ids: str = None) -> List[Clip]: + """ + Retrieve songs from the library. If song IDs are provided, fetches specific songs; otherwise, retrieves a general list of songs. + + Parameters: + - song_ids (str): A list of song IDs to retrieve specific songs. If None, the function fetches a general list of songs from the library. Split by ",". + + Returns: + List[Clip]: A list of Clip objects representing the songs. Each Clip contains detailed information such as song status, URL, and metadata. + + Example: + - To retrieve specific songs: get_songs(song_ids=["123-abcd-456", "456-cdef-789"]) + - To retrieve a list of all songs in the library: get_songs() + """ + self._keep_alive() # Ensure session is active + url = f"{Suno.BASE_URL}/api/feed/" + if song_ids: + url += f"?ids={song_ids}" + logger.info("Getting Songs Info...") + response = self.client.get(url) # Call API + return response_to_clips(response.json()) + + def get_song(self, id: str) -> Clip: + """ + Retrieve a single song by its ID. + + Parameters: + - id (str): The ID of the song to retrieve. + + Returns: + Clip: A Clip object containing details about the song, such as the audio URL, song status, and other metadata. + """ + self._keep_alive() # Ensure session is active + logger.info("Getting Song Info...") + response = self.client.get( + f"{Suno.BASE_URL}/api/feed/?ids={id}") # Call API + return create_clip_from_data(response.json()[0]) + + def get_credits(self) -> CreditsInfo: + """Retrieve current billing and credits information.""" + self._keep_alive() # Ensure session is active + logger.info("Credits Info...") + response = self.client.get( + f"{Suno.BASE_URL}/api/billing/info/") # Call API + if response.status_code == 200: + data = response.json() + result = { + "credits_left": data["total_credits_left"], + "period": data["period"], + "monthly_limit": data["monthly_limit"], + "monthly_usage": data["monthly_usage"], + } + return CreditsInfo(**result) + else: + raise Exception(f"Error retrieving credits: {response.text}") + + def _get_dl_path(self, id: str, path: str) -> str: + output_dir = pathlib.Path(path) + output_dir.mkdir(parents=True, exist_ok=True) + return output_dir / f"SunoMusic-{id}.mp3" + + def download(self, song: str | Clip, path: str = "./downloads",) -> str: + """ + Downloads a Suno song to a specified location. + + Args: + song (str | Clip): Either the ID of the song or a Clip object representing the song. + path (str): The directory where the song should be saved. Defaults to "./downloads". + + Returns: + str: The full filepath of the downloaded song. + + Raises: + TypeError: If the 'song' argument is not of type str or Clip. + Exception: If the download fails (e.g., bad URL, HTTP errors). + """ + if isinstance(song, Clip): + id = song.id + url = song.audio_url + elif isinstance(song, str): + id = song + url = self.get_song(id).audio_url + else: + raise TypeError + logger.info(f"Audio URL : {url}") + response = requests.get(url) + if not response.ok: + raise Exception( + f"failed to download from audio url: {response.status_code}" + ) + response = requests.get(url, stream=True) + response.raise_for_status() # Check for HTTP errors + filename = self._get_dl_path(id, path) + with open(filename, 'wb') as f: + for chunk in response.iter_content(chunk_size=1024): + if chunk: # Filter out keep-alive chunks + f.write(chunk) + logger.info(f"Download complete: {filename}") + return filename diff --git a/suno/utils.py b/suno/utils.py new file mode 100644 index 0000000..09c0428 --- /dev/null +++ b/suno/utils.py @@ -0,0 +1,43 @@ +# ยฉ [2024] Malith-Rukshan. All rights reserved. +# Repository: https://github.com/Malith-Rukshan/Suno-API + +from .models import Clip, ClipMetadata +from typing import List +import random + +os_systems = [ + 'Windows NT 10.0; Win64; x64', + 'Windows NT 6.1; WOW64', + 'Macintosh; Intel Mac OS X 10_15_7', + 'Linux x86_64' +] + +browsers = [ + 'Chrome/103.0.0.0 Safari/537.36', + 'Firefox/102.0', + 'Edge/103.0.1264.37', + 'Opera/9.80 (X11; Linux x86_64) Presto/2.12.388 Version/12.16' +] + + +def generate_fake_useragent(): + os_system = random.choice(os_systems) + browser = random.choice(browsers) + return f'Mozilla/5.0 ({os_system}) AppleWebKit/537.36 (KHTML, like Gecko) {browser}' + + +def create_clip_from_data(clip_data) -> Clip: + metadata = ClipMetadata(**clip_data['metadata']) + clip_data['metadata'] = metadata + clip_instance = Clip(**clip_data) + + return clip_instance + + +def response_to_clips(clips_data) -> List[Clip]: + clips = [] + for clip_data in clips_data: + clip_instance = create_clip_from_data(clip_data) + clips.append(clip_instance) + + return clips