diff --git a/examples/custom_lyrics.ipynb b/examples/custom_lyrics.ipynb deleted file mode 100644 index 5cfef75..0000000 --- a/examples/custom_lyrics.ipynb +++ /dev/null @@ -1,200 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "from suno import SongsGen\n", - "from dotenv import load_dotenv\n", - "import os\n", - "load_dotenv() \n", - "\n", - "SUNO_COOKIE = os.getenv(\"SUNO_COOKIE\")\n", - "i = SongsGen(SUNO_COOKIE)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Waiting for results...\n",
-       "
\n" - ], - "text/plain": [ - "Waiting for results\u001b[33m...\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "output = i.get_songs_custom(\"\"\"(Verse 1)\n", - "Wake up, boot up, it’s a brand new day,\n", - "Open my laptop, where the code awaits.\n", - "GitHub’s buzzing, community's alive,\n", - "With open source, our dreams start to take flight.\n", - "\n", - "(Chorus)\n", - "Fork it, clone it, make it your own,\n", - "Push a commit, then you watch it grow.\n", - "Issues and pull requests, let's get 'em merged,\n", - "In the world of code, we surf the surge.\n", - "\n", - "(Verse 2)\n", - "Collaboration like the world's never seen,\n", - "On screens apart, but together we dream.\n", - "A patch, a feature, a bug squashed away,\n", - "In open source, we all have a say.\n", - "\n", - "(Chorus)\n", - "Fork it, clone it, make it your own,\n", - "Push a commit, then you watch it grow.\n", - "Issues and pull requests, let's get 'em merged,\n", - "In the world of code, we surf the surge.\"\"\", \"upbeat pop\")\n", - "link = output['song_url']" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "(Verse 1)\n", - "Wake up, boot up, it’s a brand new day,\n", - "Open my laptop, where the code awaits.\n", - "GitHub’s buzzing, community's alive,\n", - "With open source, our dreams start to take flight.\n", - "\n", - "(Chorus)\n", - "Fork it, clone it, make it your own,\n", - "Push a commit, then you watch it grow.\n", - "Issues and pull requests, let's get 'em merged,\n", - "In the world of code, we surf the surge.\n", - "\n", - "(Verse 2)\n", - "Collaboration like the world's never seen,\n", - "On screens apart, but together we dream.\n", - "A patch, a feature, a bug squashed away,\n", - "In open source, we all have a say.\n", - "\n", - "(Chorus)\n", - "Fork it, clone it, make it your own,\n", - "Push a commit, then you watch it grow.\n", - "Issues and pull requests, let's get 'em merged,\n", - "In the world of code, we surf the surge.\n" - ] - } - ], - "source": [ - "print(output['song_name'])\n", - "print(output['lyric'])" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "audio = i.get_mp3(link, stream=True)\n", - "for chunk in audio:\n", - " if chunk:\n", - " with open(\"custom_song.mp3\", \"ab\") as f:\n", - " f.write(chunk)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "mad", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/custom_song.mp3 b/examples/custom_song.mp3 deleted file mode 100644 index 8d9fdeb..0000000 Binary files a/examples/custom_song.mp3 and /dev/null differ diff --git a/examples/intrumental.ipynb b/examples/intrumental.ipynb deleted file mode 100644 index 5b03f9b..0000000 --- a/examples/intrumental.ipynb +++ /dev/null @@ -1,221 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from suno import SongsGen\n", - "from dotenv import load_dotenv\n", - "import os\n", - "load_dotenv() \n", - "\n", - "SUNO_COOKIE = os.getenv(\"SUNO_COOKIE\")\n", - "i = SongsGen(SUNO_COOKIE)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Waiting for results...\n",
-       "
\n" - ], - "text/plain": [ - "Waiting for results\u001b[33m...\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "output = i.get_songs_custom(\"\"\"\"\"\", \"grand orchestral space\", instrumental=True)\n", - "link = output['song_url']" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "audio = i.get_mp3(link, stream=True)\n", - "for chunk in audio:\n", - " if chunk:\n", - " with open(\"intrumental.mp3\", \"ab\") as f:\n", - " f.write(chunk)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "mad", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/intrumental.mp3 b/examples/intrumental.mp3 deleted file mode 100644 index ce39fdf..0000000 Binary files a/examples/intrumental.mp3 and /dev/null differ diff --git a/examples/song.ipynb b/examples/song.ipynb deleted file mode 100644 index a4fffb4..0000000 --- a/examples/song.ipynb +++ /dev/null @@ -1,316 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from suno import SongsGen\n", - "from dotenv import load_dotenv\n", - "import os\n", - "load_dotenv() \n", - "\n", - "SUNO_COOKIE = os.getenv(\"SUNO_COOKIE\")\n", - "i = SongsGen(SUNO_COOKIE)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Waiting for results...\n",
-       "
\n" - ], - "text/plain": [ - "Waiting for results\u001b[33m...\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
.
\n" - ], - "text/plain": [ - "." - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "output = i.get_songs(\"an upbeat pop song about open source development\")\n", - "link = output['song_url']" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Code Collaboration\n", - "\n", - "Open source, open minds\n", - "Coding together, we're intertwined (inter-twined)\n", - "Sharing our knowledge, building something new\n", - "The power of collaboration, here with me and you\n", - "\n", - "\n", - "Open source, open hearts\n", - "Bringing the world closer, with each line of code (ooh-yeah)\n", - "Coding side by side, with a global family\n", - "Open source, we're rewriting history (histooory)\n", - "\n", - "\n", - "Lines of code, like melodies\n", - "We're writing a symphony of possibilities (possibilities)\n", - "From bugs to features, we're blending our skills\n", - "Open source, the world we build\n" - ] - } - ], - "source": [ - "print(output['song_name'])\n", - "print(output['lyric'])" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "audio = i.get_mp3(link, stream=True)\n", - "for chunk in audio:\n", - " if chunk:\n", - " with open(\"song.mp3\", \"ab\") as f:\n", - " f.write(chunk)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "mad", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/song.mp3 b/examples/song.mp3 deleted file mode 100644 index 37e3061..0000000 Binary files a/examples/song.mp3 and /dev/null differ diff --git a/suno/suno.py b/suno/suno.py index fae0e84..bce4a46 100644 --- a/suno/suno.py +++ b/suno/suno.py @@ -12,6 +12,7 @@ from fake_useragent import UserAgent from requests import get as rget from rich import print +from typing import Union ua = UserAgent(browsers=["edge"]) @@ -87,7 +88,9 @@ def get_limit_left(self) -> int: def _parse_lyrics(self, data: dict) -> Tuple[str, str]: song_name = data.get("title", "") mt = data.get("metadata") - if not mt: # Remove checking for title because custom songs have no title if not specified + if ( + not mt + ): # Remove checking for title because custom songs have no title if not specified return "", "" lyrics = re.sub(r"\[.*?\]", "", mt.get("prompt")) return song_name, lyrics @@ -145,7 +148,13 @@ def _fetch_songs_metadata(self, ids): # Done here return True - def get_songs(self, prompt: str) -> dict: + def get_songs( + self, + prompt: str, + tags: Union[str, None] = None, + title: str = "", + is_custom: bool = False, + ) -> dict: url = f"{base_url}/api/generate/v2/" self.session.headers["user-agent"] = ua.random payload = { @@ -154,48 +163,12 @@ def get_songs(self, prompt: str) -> dict: "prompt": "", "make_instrumental": False, } - response = self.session.post( - url, - data=json.dumps(payload), - impersonate=browser_version, - ) - if not response.ok: - print(response.text) - raise Exception(f"Error response {str(response)}") - response_body = response.json() - songs_meta_info = response_body["clips"] - request_ids = [i["id"] for i in songs_meta_info] - start_wait = time.time() - print("Waiting for results...") - sleep_time = 10 - while True: - if int(time.time() - start_wait) > 600: - raise Exception("Request timeout") - # TODOs support all mp3 here - song_info = self._fetch_songs_metadata(request_ids) - # spider rule - if sleep_time > 2: - time.sleep(sleep_time) - sleep_time -= 2 - else: - time.sleep(2) - - if not song_info: - print(".", end="", flush=True) - else: - break - # keep the song info dict as old api - return self.song_info_dict - - def get_songs_custom(self, prompt: str, genre: str, instrumental : str = False) -> dict: - url = f"{base_url}/api/generate/v2/" - self.session.headers["user-agent"] = ua.random - payload = { - "mv": "chirp-v3-0", - "prompt": "" if instrumental else prompt, - "tags": genre, - "make_instrumental": instrumental, - } + if is_custom: + payload["prompt"] = prompt + payload["gpt_description_prompt"] = "" + payload["title"] = title + if not tags: + payload["tags"] = "pop" response = self.session.post( url, data=json.dumps(payload), @@ -233,51 +206,15 @@ def save_songs( self, prompt: str, output_dir: str, + tags: Union[str, None] = None, + title: Union[str, None] = None, + is_custom: bool = False, ) -> None: mp3_index = 0 try: - self.get_songs(prompt) # make the info dict - song_name = self.song_info_dict["song_name"] - lyric = self.song_info_dict["lyric"] - link = self.song_info_dict["song_url"] - except Exception as e: - print(e) - raise - with contextlib.suppress(FileExistsError): - os.mkdir(output_dir) - print() - while os.path.exists(os.path.join(output_dir, f"suno_{mp3_index}.mp3")): - mp3_index += 1 - print(link) - response = rget(link, allow_redirects=False, stream=True) - if response.status_code != 200: - raise Exception("Could not download song") - # save response to file - with open( - os.path.join(output_dir, f"suno_{mp3_index + 1}.mp3"), "wb" - ) as output_file: - for chunk in response.iter_content(chunk_size=1024): - # If the chunk is not empty, write it to the file. - if chunk: - output_file.write(chunk) - with open( - os.path.join(output_dir, f"{song_name.replace(' ', '_')}.lrc"), - "w", - encoding="utf-8", - ) as lyric_file: - lyric_file.write(f"{song_name}\n\n{lyric}") - - - def save_songs_custom( - self, - prompt: str, - genre: str, - output_dir: str, - instrumental: str = False - ) -> None: - mp3_index = 0 - try: - self.get_songs_custom(prompt, genre, instrumental) # make the info dict + self.get_songs( + prompt, tags=tags, title=title, is_custom=is_custom + ) # make the info dict song_name = self.song_info_dict["song_name"] lyric = self.song_info_dict["lyric"] link = self.song_info_dict["song_url"] @@ -301,32 +238,14 @@ def save_songs_custom( # If the chunk is not empty, write it to the file. if chunk: output_file.write(chunk) + if not song_name: + song_name = "Untitled" with open( os.path.join(output_dir, f"{song_name.replace(' ', '_')}.lrc"), "w", encoding="utf-8", ) as lyric_file: lyric_file.write(f"{song_name}\n\n{lyric}") - - - def get_mp3(self, link: str, stream: bool = False): - """ - Fetches the MP3 file from a given link. Can return the entire file - as bytes or stream it in chunks based on the 'stream' parameter. - - :param link: The URL to the MP3 file. - :param stream: Determines if the MP3 should be streamed (True) or returned as a full object (False). - :return: If stream is False, returns the MP3 file content as bytes. - If stream is True, returns a generator yielding the file in chunks. - """ - response = self.session.get(link, stream=True) - if response.status_code == 200: - if stream: - return response.iter_content() - else: - return b"".join(response.iter_content()) - else: - raise Exception(f"Failed to download MP3 from {link}, status code: {response.status_code}") def main(): @@ -345,6 +264,24 @@ def main(): type=str, default="./output", ) + parser.add_argument( + "--is_custom", + dest="is_custom", + action="store_true", + help="use custom mode, need to provide title and tags", + ) + parser.add_argument( + "--title", + help="Title of the song", + type=str, + default="", + ) + parser.add_argument( + "--tags", + help="Tags of the song", + type=str, + default="", + ) args = parser.parse_args() @@ -357,6 +294,9 @@ def main(): song_generator.save_songs( prompt=args.prompt, output_dir=args.output_dir, + title=args.title, + tags=args.tags, + is_custom=args.is_custom, )