├── src ├── vite-env.d.ts ├── icons │ └── icons8-playlist-96.png ├── styles │ └── notifications.module.css ├── components │ ├── SectionHeading.tsx │ ├── StatusIcon.tsx │ ├── Footer.tsx │ └── OptionsModal.tsx ├── main.tsx ├── services │ ├── Utils.ts │ ├── Logger.ts │ ├── RustFunctions.ts │ ├── SettingsManager.ts │ └── Suno.ts ├── App.css └── App.tsx ├── src-tauri ├── build.rs ├── icons │ ├── 32x32.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── StoreLogo.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square310x310Logo.png │ ├── ios │ │ ├── AppIcon-512@2x.png │ │ ├── AppIcon-20x20@1x.png │ │ ├── AppIcon-20x20@2x.png │ │ ├── AppIcon-20x20@3x.png │ │ ├── AppIcon-29x29@1x.png │ │ ├── AppIcon-29x29@2x.png │ │ ├── AppIcon-29x29@3x.png │ │ ├── AppIcon-40x40@1x.png │ │ ├── AppIcon-40x40@2x.png │ │ ├── AppIcon-40x40@3x.png │ │ ├── AppIcon-60x60@2x.png │ │ ├── AppIcon-60x60@3x.png │ │ ├── AppIcon-76x76@1x.png │ │ ├── AppIcon-76x76@2x.png │ │ ├── AppIcon-20x20@2x-1.png │ │ ├── AppIcon-29x29@2x-1.png │ │ ├── AppIcon-40x40@2x-1.png │ │ └── AppIcon-83.5x83.5@2x.png │ └── android │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ └── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png ├── .gitignore ├── src │ ├── main.rs │ └── lib.rs ├── capabilities │ ├── desktop.json │ └── default.json ├── tauri.conf.json ├── Cargo.toml └── tauri.conf copy.json ├── docs ├── suno-app.png ├── style.css └── index.htm ├── tasks ├── build.bash ├── build.ps1 ├── notes.txt └── latest.json ├── .vscode └── extensions.json ├── README.md ├── public └── assets │ └── copy-playlist.png ├── scripts └── build-win.ps1 ├── tsconfig.node.json ├── latest.json ├── index.html ├── .gitignore ├── postcss.config.cjs ├── tsconfig.json ├── vite.config.ts ├── package.json ├── LICENSE └── yarn.lock /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /docs/suno-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/docs/suno-app.png -------------------------------------------------------------------------------- /tasks/build.bash: -------------------------------------------------------------------------------- 1 | export TAURI_SIGNING_PRIVATE_KEY="D:\Documents\MEGA\_Projects\Suno-downloader\keys\app.key" -------------------------------------------------------------------------------- /tasks/build.ps1: -------------------------------------------------------------------------------- 1 | $env:TAURI_SIGNING_PRIVATE_KEY="D:\Documents\MEGA\_Projects\Suno-downloader\keys\app.key" -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Suno Music downloader 2 | 3 | A tauri app to easily download entire Suno playlists in a few clicks 4 | 5 | -------------------------------------------------------------------------------- /public/assets/copy-playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/public/assets/copy-playlist.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src/icons/icons8-playlist-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src/icons/icons8-playlist-96.png -------------------------------------------------------------------------------- /tasks/notes.txt: -------------------------------------------------------------------------------- 1 | Version config URL 2 | https://github.com/DrummerSi/suno-downloader/releases/latest/download/latest.json 3 | -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /scripts/build-win.ps1: -------------------------------------------------------------------------------- 1 | $env:TAURI_SIGNING_PRIVATE_KEY="D:\Documents\MEGA\_Projects\Suno-downloader\secret.pem" 2 | $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="" 3 | yarn tauri build -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrummerSi/suno-downloader/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | suno_downloader_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "", 3 | "notes": "", 4 | "pub_date": "", 5 | "platforms": { 6 | "windows-x86_64": { 7 | "signature": "", 8 | "url": "" 9 | }, 10 | "darwin-x86_64": { 11 | "signature": "", 12 | "url": "" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "desktop-capability", 3 | "platforms": [ 4 | "macOS", 5 | "windows", 6 | "linux" 7 | ], 8 | "windows": [ 9 | "main" 10 | ], 11 | "permissions": [ 12 | "updater:default", 13 | "updater:default", 14 | "opener:default" 15 | ] 16 | } -------------------------------------------------------------------------------- /tasks/latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1", 3 | "notes": "", 4 | "pub_date": "", 5 | "platforms": { 6 | "windows-x86_64": { 7 | "signature": "", 8 | "url": "" 9 | }, 10 | "darwin-x86_64": { 11 | "signature": "", 12 | "url": "" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Suno Music Downloader 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | keys 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /src/styles/notifications.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | background-color: var(--notification-color, var(--mantine-primary-color-filled)); 3 | 4 | &::before { 5 | background-color: var(--mantine-color-white); 6 | } 7 | } 8 | 9 | .description, 10 | .title { 11 | color: var(--mantine-color-white); 12 | } 13 | 14 | .closeButton { 15 | color: var(--mantine-color-white); 16 | 17 | @mixin hover { 18 | background-color: rgba(0, 0, 0, 0.1); 19 | } 20 | } -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-preset-mantine': {}, 4 | 'postcss-simple-vars': { 5 | variables: { 6 | 'mantine-breakpoint-xs': '36em', 7 | 'mantine-breakpoint-sm': '48em', 8 | 'mantine-breakpoint-md': '62em', 9 | 'mantine-breakpoint-lg': '75em', 10 | 'mantine-breakpoint-xl': '88em', 11 | }, 12 | }, 13 | }, 14 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": false, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/components/SectionHeading.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Group, Title } from "@mantine/core" 2 | 3 | import { FC } from "react" 4 | 5 | interface Props { 6 | number: string 7 | title: string 8 | children?: React.ReactNode 9 | } 10 | const SectionHeading: FC = (props) => { 11 | const { number, title, children } = props 12 | return ( 13 | 14 | {number} 20 | {title} 21 | {children} 22 | 23 | ) 24 | } 25 | 26 | export default SectionHeading -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import '@mantine/core/styles.css'; 2 | import '@mantine/notifications/styles.css' 3 | 4 | import { MantineProvider, createTheme } from '@mantine/core'; 5 | 6 | import App from "./App"; 7 | import { ModalsProvider } from '@mantine/modals' 8 | import { Notifications } from '@mantine/notifications' 9 | import React from "react"; 10 | import ReactDOM from "react-dom/client"; 11 | 12 | const theme = createTheme({ 13 | /** Put your mantine theme override here */ 14 | primaryColor: "blue", 15 | }); 16 | 17 | 18 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | , 27 | ); 28 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "Suno Music Downloader", 4 | "version": "1.1.0", 5 | "identifier": "com.suno-downloader.app", 6 | "build": { 7 | "beforeDevCommand": "yarn dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "yarn build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "withGlobalTauri": true, 14 | "windows": [ 15 | { 16 | "title": "Suno Music Downloader", 17 | "decorations": false, 18 | "resizable": true, 19 | "width": 800, 20 | "height": 900, 21 | "minWidth": 800, 22 | "minHeight": 600 23 | } 24 | ], 25 | "security": { 26 | "csp": null 27 | } 28 | }, 29 | "bundle": { 30 | "active": true, 31 | "targets": "all", 32 | "icon": [ 33 | "icons/32x32.png", 34 | "icons/128x128.png", 35 | "icons/128x128@2x.png", 36 | "icons/icon.icns", 37 | "icons/icon.ico" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // @ts-expect-error process is a nodejs global 5 | const host = process.env.TAURI_DEV_HOST; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig(async () => ({ 9 | plugins: [react()], 10 | 11 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 12 | // 13 | // 1. prevent vite from obscuring rust errors 14 | clearScreen: false, 15 | // 2. tauri expects a fixed port, fail if that port is not available 16 | server: { 17 | port: 1420, 18 | strictPort: true, 19 | host: host || false, 20 | hmr: host 21 | ? { 22 | protocol: "ws", 23 | host, 24 | port: 1421, 25 | } 26 | : undefined, 27 | watch: { 28 | // 3. tell vite to ignore watching `src-tauri` 29 | ignored: ["**/src-tauri/**"], 30 | }, 31 | }, 32 | css: { 33 | postcss: './postcss.config.cjs' 34 | } 35 | })); 36 | -------------------------------------------------------------------------------- /src/components/StatusIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconCheck, IconSquareRoundedCheckFilled, IconSquareRoundedXFilled } from "@tabler/icons-react" 2 | import { Loader, Text } from "@mantine/core" 3 | 4 | import { FC } from "react" 5 | import { IPlaylistClipStatus } from "../services/Suno" 6 | 7 | interface Props { 8 | status: IPlaylistClipStatus 9 | } 10 | 11 | const StatusIcon: FC = (props) => { 12 | const { status } = props 13 | switch (status) { 14 | case IPlaylistClipStatus.None: 15 | return null 16 | 17 | case IPlaylistClipStatus.Processing: 18 | return 19 | 20 | case IPlaylistClipStatus.Success: 21 | return 22 | 23 | 24 | 25 | case IPlaylistClipStatus.Skipped: 26 | return 27 | 28 | 29 | 30 | case IPlaylistClipStatus.Error: 31 | return 32 | 33 | 34 | 35 | default: 36 | return null 37 | } 38 | } 39 | 40 | export default StatusIcon -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: Arial, sans-serif; 4 | background-color: #121212; /* Dark background */ 5 | color: #ffffff; /* Light text for contrast */ 6 | } 7 | 8 | .hero { 9 | padding: 60px 20px 20px; /* Add padding at the top */ 10 | text-align: center; 11 | background-color: #121212; 12 | } 13 | 14 | .container { 15 | max-width: 800px; 16 | margin: 0 auto; 17 | } 18 | 19 | h1 { 20 | font-size: 2.5em; 21 | margin-bottom: 20px; 22 | } 23 | 24 | p { 25 | font-size: 1.2em; 26 | margin-bottom: 30px; 27 | } 28 | 29 | .buttons { 30 | display: flex; 31 | justify-content: center; 32 | gap: 15px; 33 | } 34 | 35 | .btn { 36 | text-decoration: none; 37 | padding: 10px 20px; 38 | border-radius: 5px; 39 | font-size: 1em; 40 | transition: background-color 0.3s; 41 | } 42 | 43 | .btn.primary { 44 | background-color: #1e88e5; /* Primary button color */ 45 | color: #ffffff; 46 | } 47 | 48 | .btn.primary:hover { 49 | background-color: #1565c0; /* Darker shade on hover */ 50 | } 51 | 52 | .btn.secondary { 53 | background-color: #ffffff; /* Secondary button color */ 54 | color: #121212; 55 | } 56 | 57 | .btn.secondary:hover { 58 | background-color: #e0e0e0; /* Slightly darker on hover */ 59 | } -------------------------------------------------------------------------------- /src/services/Utils.ts: -------------------------------------------------------------------------------- 1 | import classes from "../styles/notifications.module.css"; 2 | import { notifications } from "@mantine/notifications"; 3 | 4 | export const stringToArrayBuffer = (str: string) => { 5 | // Convert the string to a Uint8Array 6 | const encoder = new TextEncoder(); 7 | const uint8Array = encoder.encode(str); 8 | 9 | // Return the underlying ArrayBuffer 10 | return uint8Array.buffer; 11 | } 12 | 13 | export const delay = (ms: number): Promise => { 14 | return new Promise((resolve) => setTimeout(resolve, ms)); 15 | } 16 | 17 | export const showError = (message: string, title?: string) => { 18 | notifications.show({ 19 | color: "red", 20 | title: title || "An error occured", 21 | message: message, 22 | position: "bottom-center", 23 | classNames: classes 24 | }) 25 | } 26 | 27 | export const showSuccess = (message: string, title?: string) => { 28 | notifications.show({ 29 | color: "green", 30 | title: title || "Success", 31 | message: message, 32 | position: "bottom-center", 33 | classNames: classes 34 | }) 35 | } 36 | 37 | export const getRandomBetween = (min: number, max: number) => { 38 | return Math.random() * (max - min) + min 39 | } -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:default", 10 | "core:window:default", 11 | "core:window:allow-close", 12 | "core:window:allow-center", 13 | "core:window:allow-minimize", 14 | "core:window:allow-maximize", 15 | "core:window:allow-set-size", 16 | "core:window:allow-set-focus", 17 | "core:window:allow-is-maximized", 18 | "core:window:allow-toggle-maximize", 19 | "core:window:allow-start-dragging", 20 | "opener:default", 21 | "dialog:default", 22 | { 23 | "identifier": "http:default", 24 | "allow": [ 25 | { 26 | "url": "https://*.suno.ai" 27 | } 28 | ] 29 | }, 30 | "fs:default", 31 | { 32 | "identifier": "fs:write-all", 33 | "allow": [ 34 | { 35 | "path": "$DESKTOP" 36 | }, 37 | { 38 | "path": "$DESKTOP/**" 39 | } 40 | ] 41 | }, 42 | "clipboard-manager:default", 43 | "notification:default", 44 | "log:default", 45 | "process:default", 46 | "store:default" 47 | ] 48 | } -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "suno-downloader" 3 | version = "0.1.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "suno_downloader_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2", features = [] } 19 | 20 | [dependencies] 21 | tauri = { version = "2", features = [] } 22 | tauri-plugin-opener = "2" 23 | serde = { version = "1", features = ["derive"] } 24 | serde_json = "1" 25 | tauri-plugin-dialog = "2" 26 | tauri-plugin-http = "2" 27 | tauri-plugin-fs = "2" 28 | tauri-plugin-clipboard-manager = "2.2.0" 29 | tauri-plugin-notification = "2" 30 | tauri-plugin-log = "2" 31 | id3 = "0.4" 32 | tauri-plugin-process = "2" 33 | tauri-plugin-store = "2" 34 | tauri-plugin-decorum = "1.1.1" 35 | tauri-plugin-shell = "2" 36 | 37 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 38 | tauri-plugin-updater = "2" 39 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf copy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "Suno Music Downloader", 4 | "version": "1.1.0", 5 | "identifier": "com.suno-downloader.app", 6 | "build": { 7 | "beforeDevCommand": "yarn dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "yarn build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "withGlobalTauri": true, 14 | "windows": [ 15 | { 16 | "title": "Suno Music Downloader", 17 | "decorations": false, 18 | "resizable": true, 19 | "width": 800, 20 | "height": 900, 21 | "minWidth": 800, 22 | "minHeight": 600 23 | } 24 | ], 25 | "security": { 26 | "csp": null 27 | } 28 | }, 29 | "bundle": { 30 | "active": true, 31 | "targets": "all", 32 | "icon": [ 33 | "icons/32x32.png", 34 | "icons/128x128.png", 35 | "icons/128x128@2x.png", 36 | "icons/icon.icns", 37 | "icons/icon.ico" 38 | ], 39 | "createUpdaterArtifacts": true 40 | }, 41 | "_OLD_plugins": { 42 | "updater": { 43 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDg4QTVFOTRDRUQ0ODBFN0MKUldSOERranRUT21saUQ2QVhnVGpSbE5WYU9WRUVxVzJVbWdxRnhHcDRUQ2tjOFY0UXlpeFFWbGsK", 44 | "endpoints": [ 45 | "https://github.com/DrummerSi/suno-downloader/releases/latest/download/latest.json" 46 | ] 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "suno-downloader", 3 | "private": true, 4 | "version": "1.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri" 11 | }, 12 | "dependencies": { 13 | "@mantine/core": "^7.15.3", 14 | "@mantine/form": "^7.15.3", 15 | "@mantine/hooks": "^7.15.3", 16 | "@mantine/modals": "^7.15.3", 17 | "@mantine/notifications": "^7.15.3", 18 | "@tabler/icons-react": "^3.28.1", 19 | "@tauri-apps/api": "^2", 20 | "@tauri-apps/plugin-clipboard-manager": "^2.2.0", 21 | "@tauri-apps/plugin-dialog": "~2", 22 | "@tauri-apps/plugin-fs": "~2", 23 | "@tauri-apps/plugin-http": "~2", 24 | "@tauri-apps/plugin-log": "~2", 25 | "@tauri-apps/plugin-notification": "~2", 26 | "@tauri-apps/plugin-opener": "^2.2.5", 27 | "@tauri-apps/plugin-process": "~2", 28 | "@tauri-apps/plugin-shell": "~2", 29 | "@tauri-apps/plugin-store": "~2", 30 | "@tauri-apps/plugin-updater": "~2", 31 | "filenamify": "^6.0.0", 32 | "p-limit": "^6.2.0", 33 | "react": "^18.3.1", 34 | "react-dom": "^18.3.1", 35 | "scroll-into-view-if-needed": "^3.1.0" 36 | }, 37 | "devDependencies": { 38 | "@tauri-apps/cli": "^2", 39 | "@types/react": "^18.3.1", 40 | "@types/react-dom": "^18.3.1", 41 | "@vitejs/plugin-react": "^4.3.4", 42 | "postcss": "^8.4.49", 43 | "postcss-preset-mantine": "^1.17.0", 44 | "postcss-simple-vars": "^7.0.1", 45 | "typescript": "~5.6.2", 46 | "vite": "^6.0.3" 47 | }, 48 | "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" 49 | } 50 | -------------------------------------------------------------------------------- /src/services/Logger.ts: -------------------------------------------------------------------------------- 1 | import { getSettingsManager } from "../services/SettingsManager" 2 | import { app } from '@tauri-apps/api' 3 | 4 | export interface IData { 5 | playlistUrl: string 6 | noSongs: number 7 | } 8 | 9 | const API_URL = 'https://gaozpdxgljx1zmw413194.cleavr.xyz/log'; 10 | 11 | 12 | class Logger { 13 | 14 | static async log(data: IData): Promise { 15 | 16 | const bodyData: { [key: string]: any } = { 17 | playlistUrl: data.playlistUrl, 18 | noSongs: data.noSongs, 19 | version: await app.getVersion() 20 | } 21 | 22 | const userId = await this.getUserId() 23 | if (userId) { 24 | bodyData.userId = userId 25 | } 26 | 27 | try { 28 | const response = await fetch(API_URL, { 29 | method: 'POST', 30 | headers: { 31 | "Content-Type": "application/json", // Indicate that we're sending JSON 32 | }, 33 | body: JSON.stringify(bodyData) 34 | }) 35 | 36 | if (response.ok) { 37 | const json = await response.json() 38 | this.setUserId(json.userId) 39 | } 40 | return true 41 | 42 | } catch (error) { 43 | //Do nothing. It's not important if the logger fails 44 | return false 45 | } 46 | 47 | } 48 | 49 | static async getUserId(): Promise { 50 | return (await getSettingsManager()).getSetting('userId', null) 51 | } 52 | 53 | private static async setUserId(userId: string): Promise { 54 | await (await getSettingsManager()).setSetting('userId', userId) 55 | } 56 | 57 | } 58 | 59 | 60 | 61 | export default Logger -------------------------------------------------------------------------------- /src/services/RustFunctions.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from "@tauri-apps/api/core" 2 | 3 | export async function addImageToMp3(mp3Path: string, imagePath: string): Promise { 4 | try { 5 | const result = await invoke("add_image_to_mp3", { mp3Path, imagePath }); 6 | return result; // "Successfully added image to MP3 at ..." 7 | } catch (error) { 8 | console.error("Error adding image to MP3:", error); 9 | throw new Error(error as string); 10 | } 11 | } 12 | 13 | export async function deletePath(targetPath: string) { 14 | try { 15 | const result = await invoke("delete_path", { targetPath }); 16 | console.log(result); // "Directories ensured for path: ..." 17 | } catch (error) { 18 | console.error("Failed to delete path:", error); 19 | } 20 | } 21 | 22 | export async function ensureDir(dirPath: string) { 23 | try { 24 | const result = await invoke("ensure_directory_exists", { dirPath }); 25 | console.log(result); // "Directories ensured for path: ..." 26 | } catch (error) { 27 | console.error("Failed to create directories:", error); 28 | } 29 | } 30 | 31 | export async function writeFile(name: string, content: ArrayBuffer) { 32 | const uint8ArrayContent = new Uint8Array(content); 33 | try { 34 | const result = await invoke("write_file", { name, content: uint8ArrayContent }); 35 | console.log(result); // On success, log the success message 36 | } catch (error) { 37 | console.error('Error:', error); // On failure, log the error message 38 | } 39 | } 40 | 41 | export async function existsFile(path: string) { 42 | try { 43 | const result = await invoke("exists_file", { path }); 44 | return result 45 | } catch (error) { 46 | console.error('Error:', error); 47 | return false 48 | } 49 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | 2 | html, body, #root { 3 | height: 100vh; 4 | } 5 | 6 | 7 | /* .logo.vite:hover { 8 | filter: drop-shadow(0 0 2em #747bff); 9 | } 10 | 11 | .logo.react:hover { 12 | filter: drop-shadow(0 0 2em #61dafb); 13 | } 14 | :root { 15 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 16 | font-size: 16px; 17 | line-height: 24px; 18 | font-weight: 400; 19 | 20 | color: #0f0f0f; 21 | background-color: #f6f6f6; 22 | 23 | font-synthesis: none; 24 | text-rendering: optimizeLegibility; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | -webkit-text-size-adjust: 100%; 28 | } 29 | 30 | .container { 31 | margin: 0; 32 | padding-top: 10vh; 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: center; 36 | text-align: center; 37 | } 38 | 39 | .logo { 40 | height: 6em; 41 | padding: 1.5em; 42 | will-change: filter; 43 | transition: 0.75s; 44 | } 45 | 46 | .logo.tauri:hover { 47 | filter: drop-shadow(0 0 2em #24c8db); 48 | } 49 | 50 | .row { 51 | display: flex; 52 | justify-content: center; 53 | } 54 | 55 | a { 56 | font-weight: 500; 57 | color: #646cff; 58 | text-decoration: inherit; 59 | } 60 | 61 | a:hover { 62 | color: #535bf2; 63 | } 64 | 65 | h1 { 66 | text-align: center; 67 | } 68 | 69 | input, 70 | button { 71 | border-radius: 8px; 72 | border: 1px solid transparent; 73 | padding: 0.6em 1.2em; 74 | font-size: 1em; 75 | font-weight: 500; 76 | font-family: inherit; 77 | color: #0f0f0f; 78 | background-color: #ffffff; 79 | transition: border-color 0.25s; 80 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); 81 | } 82 | 83 | button { 84 | cursor: pointer; 85 | } 86 | 87 | button:hover { 88 | border-color: #396cd8; 89 | } 90 | button:active { 91 | border-color: #396cd8; 92 | background-color: #e8e8e8; 93 | } 94 | 95 | input, 96 | button { 97 | outline: none; 98 | } 99 | 100 | #greet-input { 101 | margin-right: 5px; 102 | } 103 | 104 | @media (prefers-color-scheme: dark) { 105 | :root { 106 | color: #f6f6f6; 107 | background-color: #2f2f2f; 108 | } 109 | 110 | a:hover { 111 | color: #24c8db; 112 | } 113 | 114 | input, 115 | button { 116 | color: #ffffff; 117 | background-color: #0f0f0f98; 118 | } 119 | button:active { 120 | background-color: #0f0f0f69; 121 | } 122 | } */ 123 | -------------------------------------------------------------------------------- /src/services/SettingsManager.ts: -------------------------------------------------------------------------------- 1 | import { Store, load } from '@tauri-apps/plugin-store'; 2 | 3 | // Define the Settings interface with explicit types 4 | interface Settings { 5 | name_templates: string 6 | overwrite_files: string 7 | embed_images: string 8 | } 9 | 10 | // Define defaultSettings with the correct types 11 | const defaultSettings: Settings = { 12 | name_templates: "{trackno} - {name}", 13 | overwrite_files: "false", 14 | embed_images: "true", 15 | }; 16 | 17 | class SettingsManager { 18 | private store!: Store; 19 | 20 | private constructor(store: Store) { 21 | this.store = store; 22 | } 23 | 24 | static async create(): Promise { 25 | const store = await load('store.json', { autoSave: false }); 26 | return new SettingsManager(store); 27 | } 28 | 29 | async getSetting(key: string, defaultValue: T): Promise { 30 | const value = await this.store.get(key); 31 | return value ?? defaultValue; 32 | } 33 | 34 | async setSetting(key: string, value: T, save = true) { 35 | await this.store.set(key, value); 36 | if (save) await this.store.save(); 37 | } 38 | 39 | async save() { 40 | await this.store.save(); 41 | } 42 | 43 | async loadAll(): Promise { 44 | const settings: Partial = {}; 45 | 46 | for (const key in defaultSettings) { 47 | const typedKey = key as keyof Settings; // Explicitly type the key 48 | const defaultValue = defaultSettings[typedKey]; // Get the default value 49 | settings[typedKey] = await this.getSetting(typedKey, defaultValue); // Assign the value 50 | } 51 | 52 | return settings as Settings; 53 | } 54 | 55 | async saveAll(newSettings: Settings) { 56 | for (const key in newSettings) { 57 | await this.store.set(key, newSettings[key as keyof Settings]); 58 | } 59 | await this.store.save(); 60 | } 61 | } 62 | 63 | 64 | // Initialize settings asynchronously 65 | let settingsManagerPromise: Promise; 66 | 67 | export function initializeSettingsManager() { 68 | settingsManagerPromise = SettingsManager.create(); 69 | } 70 | 71 | // Get the settings manager instance 72 | export async function getSettingsManager(): Promise { 73 | if (!settingsManagerPromise) { 74 | await initializeSettingsManager() 75 | } 76 | return await settingsManagerPromise; 77 | } 78 | 79 | 80 | // export const Settings = await SettingsManager.create(); -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Divider, Flex, Group, Stack } from "@mantine/core" 2 | import { FC, useEffect, useRef, useState } from "react" 3 | import { IconBrandGithub, IconCoffee } from "@tabler/icons-react" 4 | 5 | import { useScrollIntoView } from "@mantine/hooks" 6 | 7 | interface Props { 8 | firstComponent: JSX.Element 9 | secondComponent: JSX.Element 10 | currentView: 1 | 2 11 | } 12 | const Footer: FC = (props) => { 13 | 14 | const { firstComponent, secondComponent, currentView } = props 15 | 16 | // Scroll hook 17 | const { targetRef: containerRef } = useScrollIntoView(); 18 | 19 | 20 | // Handler to toggle scrolling 21 | // const toggleView = () => { 22 | // const container = containerRef.current; 23 | // if (!container) return; 24 | 25 | // if (isFirstVisible) { 26 | // container.scrollTo({ 27 | // top: container.offsetHeight, // Scroll to second div 28 | // behavior: 'smooth', 29 | // }); 30 | // } else { 31 | // container.scrollTo({ 32 | // top: 0, // Scroll back to first div 33 | // behavior: 'smooth', 34 | // }); 35 | // } 36 | 37 | // setIsFirstVisible((prev) => !prev); 38 | // }; 39 | 40 | useEffect(() => { 41 | const container = containerRef.current 42 | if (!container) return 43 | 44 | //console.log(currentView) 45 | 46 | const targetScrollTop = (currentView === 1) 47 | ? 0 48 | : container.offsetHeight 49 | //console.log("SCROLL TO", targetScrollTop) 50 | 51 | container.scrollTo({ 52 | top: targetScrollTop, 53 | behavior: "smooth" 54 | }) 55 | 56 | 57 | // const height = container.clientHeight 58 | // container.style.height="auto" 59 | // setTimeout(() => { 60 | // container.setAttribute("style", "height: " + height + "px") 61 | // }, 0) 62 | 63 | console.log(container) 64 | }, [currentView]) 65 | 66 | 67 | return ( 68 | 79 | 80 | 86 | {firstComponent} 87 | 88 | 94 | {secondComponent} 95 | 96 | 97 | 98 | ) 99 | } 100 | 101 | export default Footer 102 | 103 | -------------------------------------------------------------------------------- /src/services/Suno.ts: -------------------------------------------------------------------------------- 1 | export enum IPlaylistClipStatus { 2 | None, 3 | Processing, 4 | Skipped, 5 | Success, 6 | Error 7 | } 8 | 9 | export interface IPlaylist { 10 | name: string 11 | image: string 12 | //clips: IPlaylistClip[] | undefined 13 | } 14 | 15 | export interface IPlaylistClip { 16 | id: string 17 | no: number 18 | title: string 19 | duration: number 20 | tags: string 21 | model_version: string 22 | audio_url: string 23 | video_url: string 24 | image_url: string 25 | image_large_url: string 26 | status: IPlaylistClipStatus 27 | } 28 | 29 | class Suno { 30 | 31 | static async getSongsFromPlayList(url: string): Promise<[IPlaylist, IPlaylistClip[]]> { 32 | 33 | //TODO: Check we're in format: https://suno.com/playlist/8ebe794f-d640-46b6-bde8-121622e1a4c2 (https://suno.com/playlist/liked not supported) 34 | //Scrape URL: https://studio-api.prod.suno.com/api/playlist/8ebe794f-d640-46b6-bde8-121622e1a4c2/?page=1 35 | 36 | // ─── Extract Playlist Id ───────────────────────────────────── 37 | const regex = /suno\.com\/playlist\/(.*)/ 38 | const match = url.match(regex) 39 | let playlistId = "" 40 | 41 | if (match && match[1]) { 42 | playlistId = match[1] 43 | } else { 44 | throw new Error("Invalid URL or no playlist ID found") 45 | } 46 | 47 | // ─── Fetch Playlist Data ───────────────────────────────────── 48 | let currentPage = 1 49 | let songNo = 1 50 | let endOfPlaylist = false 51 | 52 | let playlistName = "" 53 | let playListImage = "" 54 | const clips: IPlaylistClip[] = [] 55 | 56 | while (!endOfPlaylist) { 57 | const response = await fetch(`https://studio-api.prod.suno.com/api/playlist/${playlistId}/?page=${currentPage}`) 58 | 59 | if (response.status !== 200) { 60 | throw new Error("Failed to fetch playlist data") 61 | } 62 | 63 | const data = await response.json() 64 | if (data.playlist_clips.length == 0) { 65 | endOfPlaylist = true 66 | } else { 67 | playlistName = data.name 68 | playListImage = data.image_url 69 | 70 | data.playlist_clips.forEach(({ clip }: any) => { 71 | const itemData: IPlaylistClip = { 72 | id: clip.id, 73 | no: songNo, 74 | title: clip.title, 75 | duration: clip.metadata.duration, 76 | tags: clip.metadata.tags, 77 | model_version: clip.major_model_version, 78 | audio_url: clip.audio_url, 79 | video_url: clip.video_url, 80 | image_url: clip.image_url, 81 | image_large_url: clip.image_large_url, 82 | status: IPlaylistClipStatus.None 83 | } 84 | clips.push(itemData) 85 | songNo++ 86 | }) 87 | } 88 | currentPage++ 89 | } 90 | 91 | return [ 92 | { 93 | name: playlistName, 94 | image: playListImage 95 | }, 96 | clips 97 | ] 98 | } 99 | 100 | 101 | 102 | } 103 | 104 | 105 | export default Suno -------------------------------------------------------------------------------- /docs/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Suno Music Downloader 7 | 10 | 11 | 18 | 69 | 70 | 71 | 72 | 73 |
74 |
75 |

Suno Music Downloader

76 |

Quickly and easily download your Suno music playlists

77 | 83 |
Version 1.1.0
84 | 85 | 87 |
88 |
89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 2 | 3 | use std::fs; 4 | use std::path::Path; 5 | 6 | use id3::frame::Content; 7 | use id3::frame::Frame; 8 | use id3::frame::Picture; 9 | use id3::frame::PictureType; 10 | use id3::{Tag, Version}; 11 | 12 | #[tauri::command] 13 | fn write_file(name: String, content: Vec) -> Result { 14 | match fs::write(&name, content) { 15 | Ok(_) => Ok(format!("File written successfully to {}", name)), 16 | Err(e) => Err(format!("Failed to write file: {}", e)), 17 | } 18 | } 19 | 20 | #[tauri::command] 21 | fn exists_file(path: String) -> Result { 22 | match fs::metadata(&path) { 23 | Ok(_) => Ok(true), 24 | Err(_) => Ok(false), 25 | } 26 | } 27 | 28 | #[tauri::command] 29 | fn ensure_directory_exists(dir_path: String) -> Result { 30 | let path = Path::new(&dir_path); 31 | 32 | //Attempt to create the directories if they don't exist 33 | if let Err(e) = fs::create_dir_all(path) { 34 | return Err(format!("Failed to create directories: {}", e)); 35 | } 36 | 37 | Ok(format!("Directories ensured for path: {}", dir_path)) 38 | } 39 | 40 | #[tauri::command] 41 | fn delete_path(target_path: String) -> Result { 42 | let path = Path::new(&target_path); 43 | 44 | if !path.exists() { 45 | return Ok(format!("Path does not exist. Nothing to do")); 46 | } 47 | 48 | //Attempt to remove file or directory 49 | if path.is_file() { 50 | fs::remove_file(path).map_err(|e| format!("Failed to delete file: {}", e))?; 51 | } else if path.is_dir() { 52 | fs::remove_dir_all(path).map_err(|e| format!("Failed to delete directory: {}", e))?; 53 | } else { 54 | return Err(format!("Unknown path type: {}", target_path)); 55 | } 56 | 57 | return Ok(format!("Successfully deleted: {}", target_path)); 58 | } 59 | 60 | #[tauri::command] 61 | fn add_image_to_mp3(mp3_path: String, image_path: String) -> Result { 62 | // Load the MP3 file's ID3 tag or create a new one if it doesn't exist 63 | let mut tag = Tag::read_from_path(&mp3_path).unwrap_or_else(|_| Tag::new()); 64 | 65 | // Read the image data 66 | let image_data = 67 | fs::read(&image_path).map_err(|e| format!("Failed to read image file: {}", e))?; 68 | 69 | // Create a Picture frame for the image 70 | let picture = Picture { 71 | mime_type: "image/jpeg".to_string(), // Or "image/png" depending on your image format 72 | picture_type: PictureType::CoverFront, // This indicates it's a front cover 73 | description: String::from("Cover Art"), // Optional description 74 | data: image_data, // The image data 75 | }; 76 | 77 | // Convert Picture to Frame 78 | let frame = Frame::with_content("APIC", Content::Picture(picture)); 79 | 80 | // Add the picture frame to the tag 81 | tag.add_frame(frame); 82 | 83 | // Write the updated tag back to the MP3 file 84 | tag.write_to_path(&mp3_path, Version::Id3v24) 85 | .map_err(|e| format!("Failed to write ID3 tag: {}", e))?; 86 | 87 | Ok(format!("Successfully added image to MP3 at {}", mp3_path)) 88 | } 89 | 90 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 91 | pub fn run() { 92 | tauri::Builder::default() 93 | .plugin(tauri_plugin_shell::init()) 94 | .plugin(tauri_plugin_store::Builder::new().build()) 95 | .plugin(tauri_plugin_process::init()) 96 | .plugin(tauri_plugin_log::Builder::new().build()) 97 | .plugin(tauri_plugin_notification::init()) 98 | .plugin(tauri_plugin_clipboard_manager::init()) 99 | .plugin(tauri_plugin_fs::init()) 100 | .plugin(tauri_plugin_http::init()) 101 | .plugin(tauri_plugin_dialog::init()) 102 | .plugin(tauri_plugin_opener::init()) 103 | .invoke_handler(tauri::generate_handler![ 104 | add_image_to_mp3, 105 | ensure_directory_exists, 106 | delete_path, 107 | write_file, 108 | exists_file, 109 | ]) 110 | .run(tauri::generate_context!()) 111 | .expect("error while running tauri application"); 112 | } 113 | -------------------------------------------------------------------------------- /src/components/OptionsModal.tsx: -------------------------------------------------------------------------------- 1 | import { ActionIcon, Button, ComboboxItem, Divider, Group, Modal, Select, Stack, Text } from "@mantine/core" 2 | import { useEffect, useState } from "react" 3 | 4 | import { IconAdjustments } from "@tabler/icons-react" 5 | // import { Settings } from "../services/SettingsManager" 6 | import { getSettingsManager } from "../services/SettingsManager" 7 | import { useDisclosure } from "@mantine/hooks" 8 | import { getVersion } from "@tauri-apps/api/app" 9 | import { openUrl } from '@tauri-apps/plugin-opener' 10 | 11 | const SettingsPanel = () => { 12 | 13 | const [opened, { open, close }] = useDisclosure(false) 14 | 15 | const [nameTemplate, setNameTemplate] = useState(null) //Name format 16 | const [overwriteFiles, setOverwriteFiles] = useState(null) //Do we overwrite existing files 17 | const [embedImages, setEmbedImages] = useState(null) //Do we embed images in mp3 files 18 | 19 | const [appVersion, setAppVersion] = useState(null) 20 | 21 | const closeModal = async () => { 22 | 23 | const settings = { 24 | name_templates: nameTemplate.value, 25 | overwrite_files: overwriteFiles.value, 26 | embed_images: embedImages.value 27 | } 28 | console.log(settings) 29 | await (await getSettingsManager()).saveAll(settings) 30 | // await Settings.saveAll(settings) 31 | 32 | close() 33 | } 34 | 35 | useEffect(() => { 36 | (async () => { 37 | const settings = await (await getSettingsManager()).loadAll() 38 | setNameTemplate({ value: settings.name_templates, label: settings.name_templates }) 39 | setOverwriteFiles({ value: settings.overwrite_files, label: settings.overwrite_files }) 40 | setEmbedImages({ value: settings.embed_images, label: settings.embed_images }) 41 | setAppVersion(await getVersion()) 42 | })() 43 | }, []) 44 | 45 | return ( 46 | <> 47 | 48 | 49 | 50 | 51 | 56 | 57 | 58 | 59 | setOverwriteFiles(option)} 78 | /> 79 | 80 |