Introduction
rs-suno is a download-only command line tool that mirrors your Suno.ai
library to local files. It is written in Rust and modelled on
rclone: you point it at a destination directory and it
keeps that directory in step with your Suno library.
The binary is called suno. The crate published to crates.io
is rs-suno, so you install it with cargo install rs-suno and then run suno.
What it does
- Downloads your whole library, plus liked songs and playlists, as tagged audio files (MP3, FLAC, or WAV).
- Mirrors changes on every run: it downloads new clips, updates tags and
artwork, renames or re-encodes files that changed, and, with
sync, removes local files whose clips have left your library. - Embeds rich metadata: core tags (title, artist, album, date) plus Suno details (style, model, creator, and remix lineage), a front cover, and unsynced lyrics.
- Groups remixes and edits into lineage albums and writes M3U8 playlists, including a synthetic “Liked Songs” list.
- Is safe to run unattended from cron or a systemd timer, with careful deletion rules so a bad listing can never wipe your library.
Two verbs, like rclone
rs-suno follows the rclone model of two clear verbs:
syncmirrors a source to a destination, including deleting local files that are no longer present upstream. This is the full mirror.copyis additive: it downloads and updates, but never deletes.
If you only ever want to accumulate files, use copy. If you want the
destination to be a faithful mirror of your library, use sync. Deletion is
governed by strict safety rules described in
Sync, copy and deletion safety.
Requirements
- A Suno account and its
__clientsession token (see Authentication). ffmpegon yourPATH, built with FLAC and animated-WebP support (see Installation and ffmpeg).
Where to go next
- New here? Start with Installation and ffmpeg, then Authentication.
- Ready to run? See the Commands reference.
- Automating it? See Scheduling and exit codes.
Installation and ffmpeg
Install the CLI
With Cargo
If you have a Rust toolchain, install the published crate from crates.io:
cargo install rs-suno
This builds and installs the suno binary into ~/.cargo/bin. Make sure that
directory is on your PATH.
Pre-built binaries
Pre-built binaries for common platforms are attached to each
GitHub release. Download the
archive for your platform, extract the suno binary, and place it somewhere on
your PATH.
Verify the install
suno version
This prints the build version and target, the resolved config path, and the
detected ffmpeg:
suno 0.1.0 (x86_64-unknown-linux-gnu)
config: /home/alice/.config/suno/config.toml
ffmpeg: 6.1.1 (detected at /usr/bin/ffmpeg)
If the last line reads ffmpeg: not found on PATH, install ffmpeg as below.
ffmpeg
rs-suno shells out to ffmpeg for two jobs:
- transcoding the server-rendered lossless audio to FLAC, and
- transcoding a clip’s video preview to an animated WebP cover when you pass
--animated-covers.
You therefore need an ffmpeg build with FLAC and animated-WebP (libwebp_anim)
support. Most distribution packages include both. suno runs the first ffmpeg
it finds on your PATH.
Install ffmpeg
| Platform | Command |
|---|---|
| Debian, Ubuntu | sudo apt install ffmpeg |
| Fedora | sudo dnf install ffmpeg |
| Arch | sudo pacman -S ffmpeg |
| macOS (Homebrew) | brew install ffmpeg |
| Windows (winget) | winget install Gyan.FFmpeg |
Check ffmpeg has what you need
Confirm ffmpeg is on your PATH and can encode the formats:
ffmpeg -hide_banner -encoders | grep -E 'flac|libwebp'
You should see both the flac audio encoder and the libwebp/libwebp_anim
video encoders. FLAC is required for the default audio format; the WebP encoder
is only needed if you use --animated-covers. MP3 and WAV downloads do not need
ffmpeg for the audio itself, but FLAC does, so keeping a full ffmpeg build is the
simplest path.
Next steps
With the binary and ffmpeg in place, set up your token in
Authentication, then create a config with
suno config init.
Authentication
Suno has no public API and issues no API keys. rs-suno authenticates the same
way the Suno web app does: with your Clerk __client session cookie. You paste
that cookie into rs-suno once, and it mints the short-lived tokens it needs
from there.
How it works
- You supply your
__clientsession token (a long string). - On each run,
rs-sunosends that token to Clerk (clerk.suno.com) and mints a short-lived JSON Web Token (JWT). - It refreshes the JWT automatically, shortly before it expires, so long runs do not stall.
- Only the minted JWT is sent to the Suno API. Your
__clientcookie is sent only to Clerk, never to Suno’s API host.
If authentication fails partway through a run, rs-suno stops that account
cleanly rather than hammering the server, and re-authenticates on the next run.
Get your __client token
The token lives in your browser once you are logged in to Suno:
- Log in at suno.com in your browser.
- Open the browser developer tools (F12 on most browsers).
- Go to the storage or application panel and find Cookies.
- Select the Suno/Clerk origin and copy the value of the cookie named
__client.
rs-suno accepts the token in whichever form is convenient: the raw value, a
__client=<value> assignment, or the full Cookie: header string. Treat this
value like a password. Anyone with it can access your library.
Provide the token
You can supply the token three ways, in order of precedence:
- The
--token <TOKEN>flag. - The
SUNO_TOKENenvironment variable (or the per-accountSUNO_<LABEL>_TOKEN). - The
tokenfield in your config file, which is the usual place for it.
The interactive setup writes it to the config for you:
suno config init
See Configuration for the file format and for running multiple accounts.
Check and refresh a token
Confirm a stored token still works by re-minting its JWT:
suno auth refresh <account>
On success it prints the account and its display name. If the account label is
omitted, it uses your single configured account, or --all to check every one.
When a token stops working (you logged out, or Suno rotated the session), update it:
suno config add-account <account> --token <new-token>
Keeping the token safe
rs-suno never prints your token or a minted JWT:
suno config showredacts every token, printing[redacted].- The
--tokenflag hides its environment value in help output. - The
__clientcookie is only ever sent to Clerk; the Suno API only ever receives the short-lived JWT.
Never commit a token to source control or paste it into logs or issues.
Configuration
Most people keep their token and destination in a config file so a run is just
suno sync or suno copy. Flags and environment variables can still override
the file for one-off runs and automation.
Config file location
By default the config lives at:
- Linux and macOS:
$XDG_CONFIG_HOME/suno/config.toml, or~/.config/suno/config.toml. - Windows:
%APPDATA%\suno\config.toml.
Point at a different file with --config <PATH> or the SUNO_CONFIG
environment variable. suno version prints the resolved path.
Create a config
The quickest way is the interactive setup:
suno config init
It prompts for an account label (default default), your __client token, and
an optional library root, then writes the file. It will not overwrite an
existing config unless you pass --yes.
Add another account later:
suno config add-account work
Print the current config with every token redacted:
suno config show
File format
The config is TOML with an optional [defaults] table and one
[accounts.<label>] table per account:
[defaults]
format = "flac"
retries = 3
min_newest = 1
animated_covers = false
[accounts.me]
token = "<your __client token>"
root = "/home/alice/music/suno"
[accounts.work]
token = "<another token>"
root = "/home/alice/music/suno-work"
format = "mp3"
Account settings
| Key | Type | Default | Description |
|---|---|---|---|
token | string | The __client session token for the account. | |
root | path | Default destination directory. Used when a command omits DEST, and required by --all. | |
format | mp3 | flac | wav | flac | Audio format for downloads. |
retries | integer | 3 | Download retry attempts per clip before it is logged as failed. |
min_newest | integer | 1 | Minimum newest clips kept when a recency filter would otherwise select nothing. |
animated_covers | bool | false | Also write animated WebP covers from clip video previews. |
Any account key except token and root may also be set under [defaults] to
apply to every account.
Multiple accounts
Each account has its own token and its own root. Account roots must not nest
inside one another: a config where one account’s root is a parent of another’s
is rejected, so two libraries can never share or overwrite files. Run one
account with --account <label>, or every account in isolation with --all
(each writes to its own root).
If exactly one account is configured, it is used automatically and you can omit
--account.
Precedence
For every setting, the first value found wins, in this order:
- Command-line flag (for example
--format wav). - Environment variable (per-account
SUNO_<LABEL>_*before globalSUNO_*). - Config file (
[accounts.<label>]before[defaults]). - The built-in default.
Environment variables
| Variable | Equivalent | Notes |
|---|---|---|
SUNO_TOKEN | --token | Also SUNO_<LABEL>_TOKEN for one account. |
SUNO_ACCOUNT | --account | |
SUNO_CONFIG | --config | |
SUNO_DRY_RUN | --dry-run | |
SUNO_YES | --yes | |
SUNO_FORMAT | --format | mp3, flac, or wav. |
SUNO_RETRIES | --retries | |
SUNO_MIN_NEWEST | --min-newest | |
SUNO_ANIMATED_COVERS | --animated-covers | true or false. |
Per-account variants use the account label upper-cased with hyphens turned into
underscores, so account my-lib reads SUNO_MY_LIB_TOKEN,
SUNO_MY_LIB_FORMAT, and so on. A per-account variable overrides the matching
global one.
Running without a config
You do not need a config file for the read-only and one-off commands. With
--token (or SUNO_TOKEN) set and no config present, rs-suno runs against a
single implicit account, which is handy for ls, lsjson, and fetch.
Commands reference
suno [OPTIONS] <COMMAND>
| Command | Purpose |
|---|---|
sync | Mirror your library to a directory, including deletions. |
copy | Download and update, never delete. |
check | Report what sync or copy would change, touching nothing. |
ls | List clips in a readable table. |
lsjson | List clips as newline-delimited JSON. |
fetch | Download one clip by ID or URL. |
config | Create and inspect the config file. |
auth | Refresh and test authentication. |
version | Print version and environment information. |
completions | Emit a shell completion script. |
Global options
These apply to every command and may appear before or after the subcommand.
| Flag | Short | Env | Description |
|---|---|---|---|
--account <LABEL> | SUNO_ACCOUNT | Run against one configured account. | |
--all | Run every configured account in isolation (sync/copy). Conflicts with --account. | ||
--config <PATH> | SUNO_CONFIG | Path to the config file. | |
--dry-run | -n | SUNO_DRY_RUN | Report changes without writing to disk or deleting. |
--verbose | -v | Increase verbosity. Repeatable (-vv). | |
--quiet | -q | Decrease verbosity. Repeatable (-qq). | |
--yes | -y | SUNO_YES | Skip confirmation prompts (such as a destructive sync). |
--token <TOKEN> | SUNO_TOKEN | The __client token. Never printed. Overrides config and env. |
Verbosity
Verbosity is relative to the default level of 0.
| Level | Flag | Output |
|---|---|---|
| Silent | -qq | Errors only. |
| Quiet | -q | Per-run summary, warnings, and errors. |
| Default | Summary plus a single progress line. | |
| Verbose | -v | A line per clip as it is downloaded, tagged, renamed, skipped, or deleted. |
Machine-readable output (ls rows and lsjson objects) goes to stdout;
progress and summaries go to stderr, so a piped lsjson stays clean.
sync
Mirror selected clips into a destination: download new clips, update tags and artwork, rename or re-encode changed files, and remove local files whose clips have left your library. Deletion is governed by strict safety rules; see Sync, copy and deletion safety.
suno sync [OPTIONS] [DEST]
DEST is the local directory to mirror into. If omitted, the account’s
configured root is used.
| Flag | Default | Description |
|---|---|---|
--format <mp3|flac|wav> | flac | Audio format for downloads. |
--limit <N> | Mirror only the N most recent clips. | |
--since <SPEC> | Mirror clips newer than 7d, 2w, or last-run. | |
--min-newest <N> | 1 | Newest clips always kept when a recency filter applies. |
--retries <N> | 3 | Download retry attempts per clip. |
--animated-covers | off | Also write animated WebP covers from video previews. |
When sync would delete files and --yes was not passed, it lists them and
asks for confirmation on an interactive terminal. Without a terminal it refuses
and asks you to pass --yes or use copy.
# Mirror everything to the configured root, in FLAC:
suno sync
# Mirror the last two weeks to a specific directory, in MP3:
suno sync /music/suno --format mp3 --since 2w
copy
Additive download and update: same selection and flags as sync, but it never
deletes and never prompts.
suno copy /music/suno-archive
check
Report what sync or copy would do without writing anything. It accepts every
sync flag.
suno check [OPTIONS] [DEST]
| Flag | Description |
|---|---|
--exit-code | Exit 1 when changes are pending, 0 when up to date (useful in CI). |
suno check /music/suno --exit-code
check never touches disk, so it is safe to run at any time.
ls
List selected clips as a tab-separated table (ID, DURATION, TITLE,
TAGS). The title is truncated to 48 characters. A header prints only to a
terminal, so piping stays clean.
suno ls [OPTIONS]
| Flag | Default | Description |
|---|---|---|
--liked | off | List only liked clips. |
--limit <N> | Stop after the first N clips. | |
--since <SPEC> | Show clips newer than 7d, 2w, or last-run. | |
--format <text|json> | text | Output format; json matches lsjson. |
suno ls --limit 20
suno ls --liked | column -t -s $'\t'
lsjson
List selected clips as newline-delimited JSON (one object per line). Equivalent
to ls --format json, and it accepts the same flags. The schema is stable for
scripting: fields are only added, never removed or renamed. Every field is
present on every object; nullable fields are null when Suno supplied no value.
| Field | Type | Description |
|---|---|---|
id | string | Suno clip UUID. |
title | string | Display title; Untitled when blank. |
status | string | For example complete. |
duration | number | Seconds. |
created_at | string | ISO 8601 UTC. |
is_liked | bool | Whether the clip is liked. |
has_vocal | bool | Whether the clip has a vocal track. |
clip_type | string | For example gen or edit. |
tags | string | Comma-separated style tags. |
prompt | string | null | User prompt. |
gpt_description_prompt | string | null | Auto-generated description prompt. |
lyrics | string | null | Lyrics text; null if instrumental. |
model_name | string | For example chirp-v4. |
major_model_version | string | For example v4. |
display_name | string | Account display name. |
handle | string | Account handle. |
album_title | string | null | Lineage album title. |
root_ancestor_id | string | null | Root clip of the lineage. |
lineage_status | string | null | For example root. |
edited_clip_id | string | null | Source clip if this is a remix. |
audio_url | string | Audio CDN URL. |
image_url | string | Cover image URL. |
image_large_url | string | Large cover image URL. |
video_url | string | Clip video URL. |
video_cover_url | string | Video cover image URL. |
# Titles of liked clips:
suno lsjson --liked | jq -r '.title'
fetch
Download one clip by ID or URL to a path outside any mirrored library. The clip
is written directly and is never tracked or reconciled, so fetch never affects
a sync destination.
suno fetch [OPTIONS] <ID_OR_URL> [DEST]
ID_OR_URL is a clip UUID or a Suno URL containing it. DEST defaults to the
current directory; when it is a directory the file is named <id>.<ext>.
| Flag | Short | Default | Description |
|---|---|---|---|
--format <mp3|flac|wav> | flac | Audio format. | |
--output <PATH> | -o | Explicit output file path, overriding DEST and auto-naming. |
suno fetch 3f2a1b4c-aaaa-bbbb-cccc-ddddeeee0001
suno fetch https://suno.com/song/3f2a1b4c-... -o track.flac
config
Manage the config file. See Configuration for the file format.
suno config init # interactively create a config
suno config add-account [LABEL] # add an account to an existing config
suno config show # print the config with tokens redacted
auth
suno auth refresh [ACCOUNT]
Re-mint an account’s JWT to confirm its stored token still works. With no
account it uses your single configured account, or --all to check every one.
See Authentication.
version
suno version
Print the build version and target, the resolved config path, and the detected
ffmpeg.
completions
suno completions <SHELL>
Emit a shell completion script to stdout. SHELL is one of bash, zsh,
fish, powershell, or elvish. Redirect it to the location your shell reads.
suno completions bash > ~/.local/share/bash-completion/completions/suno
Summaries
A sync or copy run ends with a summary on stderr:
Sync complete: me
downloaded 12
tagged 3
renamed 1
deleted 2
skipped 129
failed 0
total 147
Duration: 43.2s
A --dry-run or check run reports the pending counts instead, and makes no
changes:
Dry run: me (no changes made)
to download 12
to tag 3
to rename 1
to delete 2
up to date 129
total 147
Sync, copy and deletion safety
sync is the reason rs-suno exists: it keeps a local directory as a faithful
mirror of your Suno library, and it does so without ever putting your files at
risk. This chapter explains what a run does and the rules that make deletion
safe.
The mirror model
copyis additive. It downloads new clips and updates existing files, but it never deletes anything.syncis a full mirror. It does everythingcopydoes, and it also removes local files whose clips are no longer in your library.
Both verbs share the same selection and the same incremental engine. The only difference is whether local files may be removed.
What a run does
Each run works in three stages:
- Select. Enumerate the library, liked feed, and playlists, then apply any
--limitor--sincefilter. - Plan. Compare the desired state against a manifest of what is already on disk, and decide a set of actions.
- Execute. Apply the actions: download, re-encode, retag, rename, write
artwork, and (for
sync) delete.
A --dry-run, or the check command, stops after the plan and prints what it
would do, touching nothing.
Incremental by default
rs-suno keeps a manifest beside the destination and only does work that is
needed:
- Skip unchanged. A clip whose metadata hash, artwork hash, and file size all match the manifest is left alone.
- Retag and re-art in place. When only tags or artwork changed, the file is updated in place. The audio is not downloaded again.
- Rename in place. When only the target path changed (for example a retitled clip), the existing file is moved, not re-downloaded.
- Re-encode on format change. Changing
--formatreplaces the file by re-encoding, without pre-deleting the old one. - Re-download missing or empty files. A clip whose local file is absent, or is zero bytes, is treated as missing and downloaded again.
This makes repeat runs fast and cheap, which is what makes frequent scheduled runs practical.
Deletion safety
Deletion is the one irreversible action, so it is hedged with several independent rules. All of them must agree before a single file is removed.
Delete only what has truly left every source
A file is a candidate for deletion only when its clip is absent from every mirror source feeding that destination. A clip that is still present in any source is kept. In addition:
copyalways wins. A clip held by acopysource is never deleted, even if asyncsource no longer lists it.- Private clips are preserved. A clip marked private is never deleted.
- Trashed counts as removed. A clip you have trashed in Suno is treated as
gone and its local file is removed (unless a
copysource or the private rule preserves it).
The fully-enumerated gate
rs-suno will not delete anything unless the listing it is comparing against was
fully enumerated: the feed drained completely, with no transport error and
no truncation, and no narrowing filter was applied. In practice this means:
- A network or listing error disables deletion for that run.
--limitand--sincenarrow the listing, so a run using either never deletes. Use them freely for quick top-ups without any deletion risk.
A missing clip in a partial or filtered listing might still exist upstream, so it is never read as a deletion.
The mass-deletion abort
As a final backstop, a run aborts before deleting when the listing looks catastrophically wrong:
- An empty listing that would delete your whole library is refused.
- A delete that would remove at least half of a non-trivial library is refused.
Either abort exits with the safety code (7) and removes nothing. If you really
do intend a mass deletion, confirm it explicitly with --min-newest 0 --yes. A
stored min_newest = 0 or a habitual --yes alone will not disarm the
empty-listing guard.
The confirmation prompt
When a sync would delete files and you did not pass --yes:
- On an interactive terminal, it lists the files and asks
Proceed? [y/N]. Anything other thanyoryesaborts with no changes. - Without a terminal (a pipe, cron, or CI), it refuses and tells you to pass
--yesor usecopy.
suno sync will delete 3 local file(s) that are no longer in the source:
me/Weather/me-Old Draft [b3c4d5e6].flac
...
Proceed? [y/N]
Tidying up
After removing files, sync prunes any directories left empty, so the tree does
not accumulate stale folders. The destination root itself is always kept.
Robustness
Beyond deletion, several rules protect an in-progress run:
- One run at a time. A
syncorcopytakes an exclusive lock (.suno.lock) on the destination, so two runs cannot corrupt the same library. - Atomic writes. Files are written to a temporary sibling and renamed into place, so an interrupted write never leaves a half-written file.
- Size verification. A download whose byte count does not match what the server promised is rejected as a truncated transfer and retried.
- Rate-limit backoff. A
429response is retried with exponential backoff that honours the server’sRetry-Afterheader. - Resumable. Progress is recorded as it happens, so an interrupted run simply continues on the next run. This is what makes unattended cron or systemd runs safe.
Failure handling
Failures are classified so one bad clip never derails a whole run:
- Authentication failure stops that account cleanly and re-authenticates on the next run.
- Transient failure (a timeout, a
5xx, a rate limit) is retried up to--retriestimes, then recorded and skipped. - A single clip’s failure never aborts the run. Other clips still download, and the failure is reported in the summary and log.
What a run leaves behind
Alongside the mirrored audio, a run keeps a few dotfiles at the destination:
| File | Purpose |
|---|---|
.suno-manifest.json | The record of what is on disk, used for incremental runs. |
.suno-lineage.json | The durable archive of resolved remix and edit lineage. |
.suno-last-run | Timestamp used by --since last-run. |
.suno-audit.log | Append-only log of every deletion and rename. |
.suno-failures.log | Append-only log of clips that failed after all retries. |
.suno.lock | Present only while a run is active. |
The audit and failure logs are not written during a --dry-run or check.
Recipes
# Full mirror to the configured root, prompting before any deletion:
suno sync
# Full mirror, unattended (approve deletions up front):
suno sync --yes
# Fast top-up of just the last week, with no deletion risk:
suno sync --since 7d
# See exactly what a mirror would change, changing nothing:
suno check --exit-code
Lineage and albums
Suno lets you build on a clip: remix it, extend it, or edit it. Each new clip
records the one it came from, which forms a lineage that runs back to an original
root clip. rs-suno follows that lineage to group related clips into albums and
to lay out files predictably.
Root resolution
For every clip, rs-suno walks the lineage back to its root ancestor, the
original clip a family of remixes and edits grew from. It fills gaps by looking
up parents directly, and it keeps a durable archive of what it has resolved (see
the lineage store below), so ancestry stays stable across
runs even after Suno purges an intermediate clip.
Albums from lineage
Clips that share a root are grouped into one lineage album. The album title is chosen simply:
- If the root ancestor is a real, distinct clip, the album takes the root clip’s title.
- Otherwise (a clip that is its own root), the album takes the clip’s own title.
So a song and all its remixes and extensions land in one album named after the original, while a standalone clip sits in an album of its own name.
File and folder layout
Files are named deterministically from the clip and its lineage:
{creator}/{album}/{creator}-{title} [{id8}]
{creator}is your display name (falling back to your handle).{album}is the lineage album title described above.{title}is the clip’s title.{id8}is the first eight characters of the clip id.
For example, a FLAC download might land at:
alice/Neon Horizon/alice-Neon Horizon (Remix) [8d9e0f1a].flac
Names are made safe for the filesystem. Unicode is preserved where it is valid in a path; awkward characters are replaced, and over-long components are shortened.
Collision safety
Two protections make the layout collision-free:
- Same-title clips never clash. The
[{id8}]suffix in every file name keeps two clips with the same title in separate files. - Distinct roots never share a folder. If two different roots happen to have the same album title, the folders are separated by a short root-id suffix, so one album can never absorb another’s tracks.
Lineage tags
The lineage is also written into each file’s metadata, including the parent clip, the root clip, and a compact summary of the chain. See Artwork and animated covers for the full list of tags.
The lineage store
Resolved ancestry is saved beside your library in .suno-lineage.json. It is an
append-durable archive: once an ancestor is known, it is kept, even if the clip
is later trashed or removed upstream. This keeps album grouping stable over time.
Because it cannot be rebuilt once Suno purges old clips, a corrupt store stops
the run rather than being silently discarded.
Artwork and animated covers
Every download is tagged and carries cover art, so your library looks right in any music player. Artwork also lands as sidecar files so folder-based browsers and media servers pick it up.
Metadata tags
rs-suno writes rich metadata into each file. MP3 uses ID3v2.4 frames and FLAC
uses Vorbis comments.
Core tags
- Title
- Artist and album artist (your creator name, falling back to
Suno) - Album (the lineage album title; see Lineage and albums)
- Date (the clip’s creation date)
Suno tags
- Style (the clip’s style tags) and a style summary
- Model (name and version, for example
chirp-v4) - Creator handle
- Parent clip, root clip, and a compact lineage summary
Lyrics
Unsynced lyrics (plain text, without timestamps) are embedded when the clip has them.
Cover art
Each download carries and produces static JPEG cover art:
- Embedded front cover inside the audio file, so players that read embedded art show it.
- A per-song cover written beside each audio file, sharing the track’s name
with a
.jpgextension. - An album cover named
folder.jpgin each album folder, which folder-based players and media servers use as the album thumbnail. It is chosen deterministically from the most-played art-bearing clip in the album.
Animated covers
Suno clips have a short looping video preview. rs-suno can turn that into an
animated WebP cover. This is opt-in, because it costs an extra transcode per
clip.
Enable it per run with --animated-covers, or set animated_covers = true in
your config.
With animated covers on, and for clips that have a video preview, rs-suno also
writes:
- a per-song animated cover beside each audio file, sharing the track’s name
with a
.webpextension, and - an album animated cover named
cover.webpin each album folder, chosen from the earliest clip in the album that has a video preview.
The static .jpg covers are always written as well, so players without WebP
support still show art.
ffmpeg requirement
Animated covers are transcoded from the clip’s video preview with ffmpeg, so
you need an ffmpeg build with animated-WebP (libwebp_anim) support. See
Installation and ffmpeg. Without it, use
the default static covers.
A note on WAV
The WAV format carries only limited metadata. When you download in WAV, lyrics
and embedded album art are omitted, and rs-suno warns you. Choose FLAC (the
default) or MP3 if you want the full set of tags and embedded art.
What lands on disk
For an album with animated covers enabled, the layout looks like:
alice/
Neon Horizon/
folder.jpg
cover.webp
alice-Neon Horizon [a1b2c3d4].flac
alice-Neon Horizon [a1b2c3d4].jpg
alice-Neon Horizon [a1b2c3d4].webp
alice-Neon Horizon (Remix) [8d9e0f1a].flac
alice-Neon Horizon (Remix) [8d9e0f1a].jpg
alice-Neon Horizon (Remix) [8d9e0f1a].webp
Without --animated-covers, the .webp files and cover.webp are simply not
written.
Playlists (M3U8)
rs-suno writes your Suno playlists as .m3u8 files so any player can open them
against your mirrored library.
What gets written
- One playlist per Suno playlist. Each of your playlists is written as an extended M3U8 file, with its members in the order Suno holds them.
- A synthetic “Liked Songs” playlist. Your liked clips are written as a
Liked Songs.m3u8, in order, even though Suno has no explicit playlist for them.
Playlist files are written at the root of the destination directory. Each file
is named after the playlist, made safe for the filesystem, with an .m3u8
extension.
Format
The files are extended M3U8: a header, the playlist name, and one #EXTINF
entry per track giving its duration and title, followed by the track’s path
relative to the playlist. Relative paths mean the playlist keeps working if you
move the whole library.
#EXTM3U
#PLAYLIST:Neon Nights
#EXTINF:217,Neon Horizon
alice/Neon Horizon/alice-Neon Horizon [a1b2c3d4].flac
#EXTINF:182,Electric Storm
alice/Weather/alice-Electric Storm [3f2a1b4c].flac
Members not in your library
A playlist can reference clips you have not downloaded (for example someone
else’s track, or a clip excluded by a filter). Rather than write a broken path,
rs-suno records the member as a comment noting it is not in the library, using
the member’s own title. The rest of the playlist stays valid and in order.
Ordering and safety
- Order is preserved exactly as Suno reports it.
- A playlist is only written when its members were listed completely. If a playlist’s listing fails, that playlist is skipped for the run rather than written half-empty. The synthetic “Liked Songs” playlist is likewise only written when the liked feed was fully enumerated.
Playlists are regular mirror artefacts: they are rewritten when their name,
order, or any member’s path, title, or duration changes, and kept in step by
every sync or copy.
Scheduling and exit codes
rs-suno is built to run unattended. Runs are incremental and resumable, and
every outcome maps to a distinct exit code so a scheduler or CI job can react
correctly.
Exit codes
| Code | Meaning | When |
|---|---|---|
| 0 | Success | All requested work completed. |
| 1 | General error | An unexpected, uncategorised failure. |
| 2 | Usage error | Unknown command, invalid flag, or missing argument. |
| 3 | Config error | Missing or invalid config, unknown account, conflicting flags. |
| 4 | Authentication failure | The token expired or was rejected and could not be refreshed. |
| 5 | Partial failure | Some clips failed after all retries; others succeeded. |
| 6 | Transient failure (exhausted) | Every clip failed with transient errors; nothing progressed. |
| 7 | Safety abort | A deletion safety rule triggered; no files were deleted. |
| 8 | Interrupted | The run received an interrupt; partial progress is preserved. |
check --exit-code is the exception to this table: it exits 1 to signal that
changes are pending, and 0 when the destination is already up to date.
Running unattended
For a scheduled job, avoid interactive prompts:
- Use
copyif you never want deletions. It never prompts. - Use
sync --yesif you do want the full mirror including deletions. Without a terminal, asyncthat would delete files refuses unless--yesis passed. - Provide the token from the environment or the config file, not a flag in a script.
The deletion safety rules still apply under --yes: the fully-enumerated gate
and the mass-deletion abort will still stop a run that looks wrong. See
Sync, copy and deletion safety.
Incremental top-ups
--since last-run mirrors only what changed since the previous successful run,
using a timestamp kept beside the library. It is a cheap way to run often.
Remember that any recency filter (--since or --limit) disables deletion for
that run, so pair frequent top-ups with an occasional full sync if you want
deletions reconciled.
cron
# Full mirror every night at 02:30, additive only.
30 2 * * * SUNO_TOKEN=... /usr/local/bin/suno copy /music/suno >> /var/log/suno.log 2>&1
Prefer keeping the token in the config file (readable only by your user) over putting it in the crontab.
systemd timer
~/.config/systemd/user/suno.service:
[Unit]
Description=Mirror the Suno library
After=network-online.target
[Service]
Type=oneshot
ExecStart=%h/.cargo/bin/suno sync --yes
~/.config/systemd/user/suno.timer:
[Unit]
Description=Run suno sync daily
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
Enable it with:
systemctl --user enable --now suno.timer
Persistent=true catches up a run missed while the machine was off, which pairs
well with rs-suno being resumable.
After a run
- The per-run summary reports counts and duration on stderr.
- Clips that failed after all retries are listed in
.suno-failures.log. - Every deletion and rename is recorded in
.suno-audit.log.
Because runs are resumable, a job that exits 5, 6, or 8 can simply be run again; it continues from where it left off. An exit of 4 means the token needs attention (see Authentication); an exit of 7 means a safety rule stopped a suspicious deletion and should be investigated before forcing it.
Troubleshooting and FAQ
Troubleshooting
ffmpeg is not found
suno version ends with ffmpeg: not found on PATH. Install ffmpeg and make
sure it is on your PATH. See
Installation and ffmpeg. rs-suno needs
ffmpeg to produce FLAC (the default format) and animated WebP covers.
FLAC or animated covers fail to encode
Your ffmpeg build is missing an encoder. Check it with:
ffmpeg -hide_banner -encoders | grep -E 'flac|libwebp'
If flac is missing, download in MP3 or install a fuller ffmpeg build. If
libwebp/libwebp_anim is missing, drop --animated-covers or install an
ffmpeg with animated-WebP support.
Authentication failed (exit 4)
The stored token has expired or was rejected. Confirm and re-mint it:
suno auth refresh <account>
If that still fails, the session was rotated (you logged out, or Suno reset it).
Get a fresh __client token and update the account:
suno config add-account <account> --token <new-token>
See Authentication.
“multiple accounts configured; pass –account”
You have more than one account and ran a single-account command without saying
which. Add --account <label>, or use --all for sync/copy to run every
account.
“account has no configured root and no DEST was given”
The account has no root in its config and you did not pass a destination. Give
a DEST on the command line, or set root for the account. --all always
needs each account to have a root.
“another suno run is active”
A run holds an exclusive lock (.suno.lock) on the destination while it works.
If a previous run crashed, the lock file can be left behind. Once you are sure no
run is active, delete the .suno.lock file in the destination directory and try
again.
A sync aborted with a safety warning (exit 7)
A deletion safety rule stopped the run because the deletion looked wrong: the
listing was empty, or it would have removed a large fraction of your library.
Nothing was deleted. This is usually a transient listing problem, so try again
later. If you genuinely intend a mass deletion, confirm it explicitly with
--min-newest 0 --yes. See
deletion safety.
Nothing is being deleted
That is expected when you use --since or --limit. A narrowed or filtered
listing is not authoritative, so deletion is disabled for that run. Run a plain
sync (no recency filter) to reconcile deletions.
A run stopped saying the manifest or lineage store is corrupt
rs-suno refuses to run against a damaged .suno-manifest.json or
.suno-lineage.json rather than risk re-downloading everything or losing
archived lineage. Restore the file from a backup, or move it aside to start
fresh (a fresh manifest will re-verify existing files rather than re-download
unchanged ones).
FAQ
What is the difference between sync and copy?
sync mirrors, including deleting local files whose clips have left your
library. copy only ever adds and updates. Use copy if you want an archive
that never loses files.
How do I preview what a run would do?
Use check (or add --dry-run to sync/copy). Both report the pending
changes and touch nothing. check --exit-code exits 1 when changes are pending,
for CI.
Can I mirror more than one account?
Yes. Configure each account with its own token and root, then run --all to
mirror every account into its own directory, or --account <label> for one.
Account roots may not nest inside one another.
Does fetch need a config?
No. fetch can run with just --token (or SUNO_TOKEN). It downloads a single
clip to a path you choose and never touches a mirrored library.
Where does my library go?
To the DEST you pass, or the account’s configured root. The config path
itself is shown by suno version.
Which format should I choose?
FLAC (the default) is lossless and carries full metadata and embedded art. MP3 is smaller and widely compatible. WAV is lossless but carries limited metadata, so lyrics and embedded art are omitted.
Is it safe to run on a schedule?
Yes. Runs are incremental and resumable, an exclusive lock prevents overlap, and the deletion safety rules prevent a bad listing from wiping files. See Scheduling and exit codes.