Music library curation for Synology NAS. Deduplicate by sound, not by filename. Complete your tags from five sources. Convert formats. Never lose a file.
العربية · Roadmap · Install · Operations · Architecture · Third-party licences · Contributing
A music library that grew over fifteen years has the same song four times: a
FLAC rip, a 192 kbps MP3 from a phone backup, an untagged copy in
Downloads/New folder/, and a duplicate with different cover art. Filename
comparison finds none of these. Cadenza finds all four, tells you which one to
keep and why, and moves the rest somewhere you can get them back from.
- Four-layer duplicate detection — byte-identical files, identical audio streams under different tags, acoustic fingerprint matches across formats and bitrates, and a textual fallback for files that cannot be fingerprinted.
- Explainable decisions — every copy gets a scored breakdown across eight criteria. You see why the FLAC won before anything moves, and you can override it.
- Nothing is ever deleted — flagged files move to a quarantine folder and restore to their original path with one click. Permanent deletion is off by default and must be explicitly enabled.
- Metadata from five sources — MusicBrainz, Apple Music, Discogs, Last.fm and AcoustID, merged by a per-field weighted vote rather than first-source-wins. Discogs knows pressing years; Apple has the best artwork; MusicBrainz owns the identifiers.
- Audio conversion — twelve FFmpeg presets, from FLAC archival down to Opus 96k, with tags and cover art carried across.
- Arabic and English UI — full right-to-left support, switchable at runtime.
- Runs on a schedule — cron-based background scanning and cleanup.
Dashboard, duplicate review with score breakdown, and the conversion page. (Add screenshots to
docs/images/and reference them here.)
| Hardware | Synology NAS, x86_64 — DS1821+, DVA3221, DS920+, DS1621+, and similar |
| DSM | 7.0 or newer |
| Prerequisite | None for the DSM package. Container Manager only if you choose to run the container instead |
| Free space | ~1 GB for the package, plus the library index |
ARM-based models (DS220j, DS223, …) are not supported: the bundled interpreter
and FFmpeg are built for x86_64.
Two ways, and they are equals. Pick on how you prefer to run things, not on capability — both give you the same application.
DSM package (.spk) |
Container Manager | |
|---|---|---|
| Needs Docker | No | Yes |
| Managed from | Package Center | Container Manager |
| DSM desktop icon | Yes | No |
| Start/stop | Package Center | Container Manager |
| Updates | Install a newer .spk |
Pull a newer image |
| Download | Larger — it carries its own Python and ffmpeg | Smaller .spk, image pulled separately |
Full walkthrough for both, including what to prepare first: docs/INSTALL.md.
The package runs Cadenza itself. It bundles its own Python interpreter, its
dependencies and static ffmpeg/ffprobe/fpcalc, so it needs neither Docker
nor anything else installed on the NAS.
Nothing to prepare. Leave the wizard's data folder empty and Cadenza keeps
its database, settings and API keys inside its own package folder — always
writable by its service account, and preserved when you update. Name a folder
only if you want the data somewhere specific, and then grant the cadenza
account access to it the same way as the music folder.
Wherever the data ends up, Cadenza records it and keeps using it. An update never starts you from scratch, and if that folder ever becomes unreachable Cadenza refuses to start and says so rather than opening an empty library.
1. Download the .spk from Releases.
2. Package Center → Manual Install → select the file.
3. The wizard asks for your music folder, the web port, the config folder and the account to run as. Each field explains what it affects.
4. Open Cadenza from its DSM desktop icon, or http://<nas-address>:8760.
DSM's firewall, if enabled, may need the port opened by hand under Control Panel → Security → Firewall. The install log says so if the package could not do it. Package Center's own Open button always uses 8760, because that value is fixed when the package is built.
1. Install Container Manager from Package Center if it is not already there.
2. Download docker-compose.yml and edit three things:
| Line | Change it to |
|---|---|
/volume1/music |
the real path to your music share |
/volume1/docker/cadenza/config |
where the database and settings should live |
user: "1026:100" |
the UID:GID that owns your music — find it with id <your-dsm-user> over SSH |
3. Container Manager → Project → Create → point it at that file → Start.
4. Open http://<nas-address>:8760.
Getting user: wrong is the most common mistake: files Cadenza writes end up
owned by the wrong account. Check it rather than guessing.
Duplicate analysis and scanning never write to your library. To prove that to yourself before granting write access, mount the library read-only:
- /volume1/music:/music:roScan, review the duplicate report, then switch to :rw when you are satisfied.
- Settings → Provider API keys. Add at least an AcoustID key (free) — acoustic fingerprinting is what makes cross-format duplicate detection work. MusicBrainz needs no key, only a contact address.
- Dashboard → Scan library. A first scan hashes and fingerprints every file, so expect roughly 20–60 minutes for 50,000 tracks. Later scans are incremental and take seconds.
- Duplicates → Analyse. Review the report. Nothing has moved yet.
- When the proposed decisions look right, Move duplicates to quarantine.
Read this before exposing Cadenza beyond your own LAN.
You choose the username and the password, the first time you open Cadenza. Nothing is generated, nothing is written to a file for you to go and find, and there is no default credential — a shared one would look like protection while giving none.
The setup endpoint works exactly once and answers 409 afterwards, so nobody
can claim an install you have already set up, and it is rate limited like
sign-in because it is reachable without a session until it is used.
Earlier versions did generate a password and write it to initial-password.txt.
They no longer do, and that file is removed on upgrade.
Sessions last 14 days. Changing your password ends every other session, and Sign out everywhere does the same on demand if you think a session leaked.
These are real and tracked; read them before granting write access.
PATCH /api/v1/settingsaccepts any known field without validating the value (#6). An authenticated caller can repoint the paths of the binaries Cadenza executes. Until that is fixed, treat write access as equivalent to shell access.- Some destructive endpoints take parameters in the query string
(#5), which keeps them
reachable as simple cross-origin requests despite
SameSite=Strict. - Provider API keys are stored in plain text in
config/settings.json, along with the Apple Music token. Protect the config volume accordingly. convertwith "keep original" turned off deletes the source outright (#8), bypassing quarantine and the audit log. Leave it on.
Keep Cadenza on a trusted network and do not port-forward it. For remote access, put it behind DSM's reverse proxy with its own authentication.
The container runs as a normal user, never root, with no-new-privileges.
git clone https://github.com/AbdelmonemAwad/cadenza.git
cd cadenza
docker build -f docker/Dockerfile -t cadenza . # the application image
make spk # optional DSM packageThe .spk build needs only bash, tar and md5sum — no Docker — and
produces a small package containing the compose file, the cadenza CLI and the
docs. It does not start the application; see the note under Install.
See CONTRIBUTING.md for the development setup, which needs neither Docker nor a NAS.
Four layers run cheapest-first, and each file is attributed to the strongest layer that catches it.
| Layer | Method | Catches |
|---|---|---|
exact_file |
SHA-256 over the whole file | Literal copies |
exact_audio |
MD5 over the decoded audio stream | Same audio, different tags or artwork |
acoustic |
Chromaprint fingerprint + AcoustID | Same recording as FLAC vs MP3, any bitrate |
metadata |
Normalised fuzzy text match | Files that could not be fingerprinted |
The acoustic layer is the expensive one, so candidate pairs are narrowed by a duration window plus multi-band LSH bucketing before any comparison runs.
One calibration detail worth knowing. Fingerprint similarity is a Hamming bit-agreement ratio, so two completely different songs score around 0.50, not 0. The baseline is 0.5 because unrelated bits agree half the time. The default threshold is 0.90; a re-encode of the same master lands at 0.92–0.99. Setting the threshold below ~0.85 will produce false matches.
Eight weighted criteria, all visible in the UI:
| Criterion | Weight | Rationale |
|---|---|---|
| Format | 30% | Lossless outranks lossy |
| Bitrate | 22% | Measured against the best lossy peer in the cluster |
| Resolution | 12% | Sample rate and bit depth |
| Tag completeness | 16% | Weighted across ten fields |
| Artwork | 8% | Present, and at what resolution |
| Duration | 5% | Catches truncated copies |
| Lyrics | 4% | Synced beats plain |
| Path quality | 3% | Penalises Downloads/, New folder/, (1) |
When the top two scores are within 0.03 the cluster is marked ambiguous and is never auto-actioned — it waits for you.
| Source | Key needed | Strongest at |
|---|---|---|
| MusicBrainz | No (contact e-mail) | Identifiers, canonical titles |
| AcoustID | Free | Identifying a recording from audio alone |
| Apple Music | Developer Program | Artwork, ISRC, track numbers |
| Discogs | Free token | Pressing years, rare and regional releases |
| Last.fm | Free | Genres, correcting misspelled artists |
| LRCLIB | No | Synced lyrics |
Results are merged with a per-field weighted vote: each field is won by the source with the highest (match confidence × that source's trust for that field). Conflicts are surfaced in the UI rather than silently resolved.
Everything is editable in Settings, and stored in
/var/packages/Cadenza/var/config/settings.json on the NAS. Environment
variables use the CADENZA_ prefix — see .env.example.
Path templates support {albumartist} {artist} {album} {year} {track}
{disc} {title} {genre}:
{albumartist}/{year} - {album}/{track:02d} - {title}
This software operates on files people cannot re-download. The design reflects that:
- Every destructive job supports
dry_run, and defaults to it. - Deletion means moving to quarantine, with the original tree mirrored so restores are obvious. Retention is 30 days by default.
- Permanent purge is disabled unless explicitly turned on.
- Every file mutation writes an audit-log entry with source, destination, and whether it can be reversed.
- A duplicate cluster must always retain exactly one copy — the API refuses anything else.
- The container runs as a normal user, never root, so files written into your share stay owned by you.
backend/ FastAPI service, engines, providers, tests
frontend/ React + TypeScript UI with en/ar i18n
docker/ Dockerfile, compose file, nginx config, entrypoint
packaging/ Synology .spk build scripts and DSM metadata
docs/ Architecture notes
MIT.
Cadenza is not affiliated with Synology, Apple, MusicBrainz, Discogs or Last.fm. Respect each provider's terms of service and rate limits when configuring keys.