├── requirements.txt ├── README.md └── Suno_downloader.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | colorama 3 | mutagen 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Suno Bulk Downloader 2 | 3 | A simple command-line Python script to bulk download all of your private songs from [Suno AI](https://suno.com/). 4 | 5 | This tool iterates through your library pages, downloads each song, and can optionally embed the cover art directly into the MP3 file's metadata. 6 | 7 | 8 | ## Features 9 | 10 | - **Bulk Download:** Downloads all songs from your private library. 11 | - **Metadata Embedding:** Automatically embeds the title, artist, and cover art (thumbnail) into the MP3 file. 12 | - **File Sanitization:** Cleans up song titles to create valid filenames for any operating system. 13 | - **Duplicate Handling:** If a file with the same name already exists, it saves the new file with a version suffix (e.g., `My Song v2.mp3`) to avoid overwriting. 14 | - **Proxy Support:** Allows routing traffic through an HTTP/S proxy. 15 | - **User-Friendly Output:** Uses colored console output for clear and readable progress updates. 16 | 17 | 18 | https://imgur.com/a/Ox9goh7 19 | 20 | 21 | ## Requirements 22 | 23 | - [Python 3.6+](https://www.python.org/downloads/) 24 | - `pip` (Python's package installer, usually comes with Python) 25 | 26 | ## Installation 27 | 28 | 1. **Clone the repository:** 29 | ```bash 30 | git clone https://github.com/your-username/your-repo-name.git 31 | cd your-repo-name 32 | ``` 33 | *(Alternatively, you can download the repository as a ZIP file and extract it.)* 34 | 35 | 2. **Install the required Python packages:** 36 | ```bash 37 | pip install -r requirements.txt 38 | ``` 39 | 40 | ## How to Use 41 | 42 | The script requires a **Suno Authorization Token** to access your private library. Here’s how to find it: 43 | 44 | ### Step 1: Find Your Authorization Token 45 | 46 | 1. Open your web browser and go to [suno.com](https://suno.com/) and log in. 47 | 2. Open your browser's **Developer Tools**. You can usually do this by pressing `F12` or `Ctrl+Shift+I` (Windows/Linux) or `Cmd+Option+I` (Mac). 48 | 3. Go to the **Network** tab in the Developer Tools. 49 | 4. In the filter box, type `feed` to easily find the right request. 50 | 5. Refresh the Suno page or click around your library. You should see a new request appear in the list. 51 | 6. Click on that request (it might be named something like `v2?hide_disliked=...`). 52 | 7. In the new panel that appears, go to the **Headers** tab. 53 | 8. Scroll down to the **Request Headers** section. 54 | 9. Find the `Authorization` header. The value will look like `Bearer [long_string_of_characters]`. 55 | 10. **Copy only the long string of characters** (the token itself), *without* the word `Bearer `. 56 | 57 | Example (Copy the whole string) 58 | https://i.imgur.com/PQtOIM5.jpeg 59 | 60 | 61 | **Important:** Your token is like a password. **Do not share it with anyone.** 62 | 63 | ### Step 2: Run the Script 64 | 65 | Open your terminal or command prompt, navigate to the script's directory, and run it using the following command structure. 66 | 67 | **Basic Usage (downloads audio only):** 68 | ```bash 69 | python suno_downloader.py --token "your_token_here" 70 | ``` 71 | This will download all songs into a new folder named `suno-downloads`. 72 | 73 | **Full-Featured Usage (with thumbnails and a custom directory):** 74 | ```bash 75 | python suno_downloader.py --token "your_token_here" --directory "My Suno Music" --with-thumbnail 76 | ``` 77 | This will download all songs and their thumbnails into a folder named `My Suno Music`. 78 | 79 | ### Command-Line Arguments 80 | 81 | - `--token` **(Required)**: Your Suno authorization token. 82 | - `--directory` (Optional): The local directory where files will be saved. Defaults to `suno-downloads`. 83 | - `--with-thumbnail` (Optional): A flag to download and embed the song's cover art. 84 | - `--proxy` (Optional): A proxy server URL (e.g., `http://user:pass@127.0.0.1:8080`). You can provide multiple proxies separated by commas. 85 | 86 | ## Disclaimer 87 | 88 | This is an unofficial tool and is not affiliated with Suno, Inc. It is intended for personal use only to back up your own creations. Please respect Suno's Terms of Service. The developers of this script are not responsible for any misuse. 89 | 90 | ## License 91 | 92 | This project is licensed under the MIT License. See the `LICENSE` file for details. 93 | -------------------------------------------------------------------------------- /Suno_downloader.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import random 4 | import re 5 | import sys 6 | import time 7 | 8 | import requests 9 | from colorama import Fore, init 10 | from mutagen.id3 import ID3, APIC, TIT2, TPE1, error 11 | from mutagen.mp3 import MP3 12 | 13 | init(autoreset=True) 14 | 15 | FILENAME_BAD_CHARS = r'[<>:"/\\|?*\x00-\x1F]' 16 | 17 | def sanitize_filename(name, maxlen=200): 18 | safe = re.sub(FILENAME_BAD_CHARS, "_", name) 19 | safe = safe.strip(" .") 20 | return safe[:maxlen] if len(safe) > maxlen else safe 21 | 22 | def pick_proxy_dict(proxies_list): 23 | if not proxies_list: return None 24 | proxy = random.choice(proxies_list) 25 | return {"http": proxy, "https": proxy} 26 | 27 | def embed_metadata(mp3_path, image_url=None, title=None, artist=None, proxies_list=None, token=None, timeout=15): 28 | headers = {"Authorization": f"Bearer {token}"} if token else {} 29 | proxy_dict = pick_proxy_dict(proxies_list) 30 | r = requests.get(image_url, proxies=proxy_dict, headers=headers, timeout=timeout) 31 | r.raise_for_status() 32 | image_bytes = r.content 33 | mime = r.headers.get("Content-Type", "image/jpeg").split(";")[0] 34 | 35 | audio = MP3(mp3_path, ID3=ID3) 36 | try: audio.add_tags() 37 | except error: pass 38 | 39 | if title: audio.tags["TIT2"] = TIT2(encoding=3, text=title) 40 | if artist: audio.tags["TPE1"] = TPE1(encoding=3, text=artist) 41 | 42 | for key in list(audio.tags.keys()): 43 | if key.startswith("APIC"): del audio.tags[key] 44 | 45 | audio.tags.add(APIC(encoding=3, mime=mime, type=3, desc="Cover", data=image_bytes)) 46 | audio.save(v2_version=3) 47 | 48 | def extract_private_song_info(token_string, proxies_list=None): 49 | print(f"{Fore.CYAN}Extracting private songs using Authorization Token...") 50 | base_api_url = "https://studio-api.prod.suno.com/api/feed/v2?hide_disliked=true&hide_gen_stems=true&hide_studio_clips=true&page=" 51 | headers = {"Authorization": f"Bearer {token_string}"} 52 | 53 | song_info = {} 54 | page = 1 55 | 56 | while True: 57 | api_url = f"{base_api_url}{page}" 58 | try: 59 | print(f"{Fore.MAGENTA}Fetching songs (Page {page})...") 60 | response = requests.get(api_url, headers=headers, proxies=pick_proxy_dict(proxies_list), timeout=15) 61 | if response.status_code in [401, 403]: 62 | print(f"{Fore.RED}Authorization failed (status {response.status_code}). Your token is likely expired or incorrect.") 63 | return {} 64 | response.raise_for_status() 65 | data = response.json() 66 | except requests.exceptions.RequestException as e: 67 | print(f"{Fore.RED}Request failed on page {page}: {e}") 68 | return {} 69 | 70 | clips = data if isinstance(data, list) else data.get("clips", []) 71 | if not clips: 72 | print(f"{Fore.YELLOW}No more clips found on page {page}.") 73 | break 74 | 75 | print(f"{Fore.GREEN}Found {len(clips)} clips on page {page}.") 76 | for clip in clips: 77 | uuid, title, audio_url, image_url = clip.get("id"), clip.get("title"), clip.get("audio_url"), clip.get("image_url") 78 | if (uuid and title and audio_url) and uuid not in song_info: 79 | song_info[uuid] = {"title": title, "audio_url": audio_url, "image_url": image_url, "display_name": clip.get("display_name")} 80 | page += 1 81 | time.sleep(5) 82 | return song_info 83 | 84 | def get_unique_filename(filename): 85 | if not os.path.exists(filename): return filename 86 | name, extn = os.path.splitext(filename) 87 | counter = 2 88 | while True: 89 | new_filename = f"{name} v{counter}{extn}" 90 | if not os.path.exists(new_filename): return new_filename 91 | counter += 1 92 | 93 | def download_file(url, filename, proxies_list=None, token=None, timeout=30): 94 | # This function now correctly handles finding a unique filename before saving 95 | unique_filename = get_unique_filename(filename) 96 | 97 | headers = {"Authorization": f"Bearer {token}"} if token else {} 98 | with requests.get(url, stream=True, proxies=pick_proxy_dict(proxies_list), headers=headers, timeout=timeout) as r: 99 | r.raise_for_status() 100 | with open(unique_filename, "wb") as f: 101 | for chunk in r.iter_content(chunk_size=8192): 102 | if chunk: f.write(chunk) 103 | return unique_filename 104 | 105 | def main(): 106 | parser = argparse.ArgumentParser(description="Bulk download your private suno songs") 107 | parser.add_argument("--token", type=str, required=True, help="Your Suno session Bearer Token.") 108 | parser.add_argument("--proxy", type=str, help="Proxy with protocol (comma-separated).") 109 | parser.add_argument("--directory", type=str, default="suno-downloads", help="Local directory for saving files.") 110 | parser.add_argument("--with-thumbnail", action="store_true", help="Embed the song's thumbnail.") 111 | args = parser.parse_args() 112 | 113 | songs = extract_private_song_info(args.token, args.proxy.split(",") if args.proxy else None) 114 | 115 | if not songs: 116 | print(f"{Fore.RED}No songs found. Please check your token.") 117 | sys.exit(1) 118 | 119 | if not os.path.exists(args.directory): 120 | os.makedirs(args.directory) 121 | 122 | print(f"\n{Fore.CYAN}--- Starting Download Process ({len(songs)} songs to check) ---") 123 | for uuid, obj in songs.items(): 124 | title = obj["title"] or uuid 125 | fname = sanitize_filename(title) + ".mp3" 126 | out_path = os.path.join(args.directory, fname) 127 | 128 | print(f"Processing: {Fore.GREEN}🎵 {title}") 129 | try: 130 | # FIX: The old 'if os.path.exists' check was removed from here. 131 | # We now call download_file directly and let it handle unique filenames. 132 | 133 | print(f" -> Downloading...") 134 | saved_path = download_file(obj["audio_url"], out_path, token=args.token) 135 | 136 | if args.with_thumbnail and obj.get("image_url"): 137 | print(f" -> Embedding thumbnail...") 138 | embed_metadata(saved_path, image_url=obj["image_url"], token=args.token, artist=obj.get("display_name"), title=title) 139 | 140 | # Let the user know if a new version was created 141 | if os.path.basename(saved_path) != os.path.basename(out_path): 142 | print(f"{Fore.YELLOW} -> Saved as new version: {os.path.basename(saved_path)}") 143 | 144 | except Exception as e: 145 | print(f"{Fore.RED}Failed on {title}: {e}") 146 | 147 | print(f"\n{Fore.BLUE}Download process complete. Files are in '{args.directory}'.") 148 | sys.exit(0) 149 | 150 | 151 | if __name__ == "__main__": 152 | main() --------------------------------------------------------------------------------