Auto-detects BPM for every track in your Navidrome library, writes it back to the file's metadata, and provides a password-protected React web UI for reviewing and correcting results.
Three detectors cross-validate every track — deeprhythm (CNN), essentia RhythmExtractor2013, and librosa — so octave errors are caught automatically and disagreements are flagged for manual review.
Multi-source playlists: watch playlists from Spotify and Navidrome, or build your own Local playlists with an "Add to playlist" button on any track — reconciled against your library (have / missing / new / removed) and usable as Run-mode sources. Navidrome and Local playlists work with just your Navidrome credentials or nothing at all — no grabber. With the optional Spotify grabber (GRABBER_ENABLED=true) you can also add Spotify playlists by URL and download the tracks you're missing (Deezer via your own ARL, yt-dlp fallback), transcode to one format, tag + BPM-analyze, and file them into your library by a path template — with an ambiguity inbox and ntfy pings. A Suggestions page recommends artists and tracks to grab next, derived from your library via the keyless Deezer catalog.
Source & full docs: github.com/PauloJf/BPM-Tagger · Licensed under AGPL-3.0-or-later
| Tag | Detectors | Peak RAM | Use when |
|---|---|---|---|
latest (default) | essentia + librosa | ~400 MB | NAS / low-memory devices |
full | deeprhythm (CNN) + essentia + librosa | ~1.8 GB | Servers with spare RAM |
services:
bpm-tagger:
image: gatoserio/bpm-tagger:latest # or :full for deeprhythm CNN
restart: unless-stopped
environment:
MODE: watch
MUSIC_DIR: /music
WRITE_TAGS: "true"
ENABLE_UI: "false"
UI_PORT: "5000"
UI_PASSWORD: "" # required if ENABLE_UI=true
NTFY_URL: https://ntfy.sh
NTFY_TOPIC: ""
volumes:
- /path/to/your/music:/music
- bpm_tagger_data:/data
ports:
- "5000:5000" # only needed if ENABLE_UI=true
# user: "1000:1000" # match Navidrome's UID:GID
volumes:
bpm_tagger_data:
docker compose up -d && docker compose logs -f
| Mode | Description |
|---|---|
watch | Scan new/changed files on start, then watch in real time. Default. |
watch_all | Re-analyze every file on start, then watch in real time |
scan_all | One-shot: re-analyze every file |
scan_unscanned | One-shot: analyze only new or changed files |
scan_review | One-shot: re-analyze flagged, errored, or fallback-only tracks |
report | Write a CSV of suspicious tracks; send ntfy summary |
lock | Lock a track's BPM (LOCK_FILE; optional LOCK_BPM) |
unlock | Unlock a track for re-analysis (UNLOCK_FILE) |
| Variable | Default | Description |
|---|---|---|
MODE | watch | Operating mode |
MUSIC_DIR | /music | Music directory inside the container |
WRITE_TAGS | true | Write BPM to audio file metadata |
PRESERVE_MTIME | true | Keep each file's modified time after tagging |
WORKERS | 1 | Parallel analysis threads (+~500 MB RAM each for deeprhythm) |
REFRESH_HASHES | false | Recompute hashes before scanning (migration from pre-1.0.0) |
BPM_MIN | 60 | BPM floor — values below are doubled |
BPM_MAX | 200 | BPM ceiling — values above are halved |
USE_DEEPRHYTHM | false | CNN detector (~500 MB/worker); only effective on the :full image — ignored on :latest (slim, no PyTorch) |
USE_ESSENTIA | true | Essentia RhythmExtractor2013 detector |
OCTAVE_CORRECTION | true | Auto-fix 2× BPM errors between detectors |
REVIEW_DISAGREE_THRESHOLD | 15 | BPM gap that flags a track for review |
ENABLE_UI | false | Start the web UI |
UI_PASSWORD | (empty) | Web UI password — required when UI is enabled |
UI_SESSION_HOURS | 24 | Admin session length — signed out after this many idle hours (sliding) |
RUN_PASSWORD | (empty) | Optional shared, full-access password for a locked-down player mode (Run page + a runner-focused About only). For per-person accounts with playlist scoping, create named run users in Settings → Player Access → Run users. Also settable in Settings → Player Access |
RUN_SESSION_DAYS | 30 | How long a player login stays signed in (days) |
RUN_FORCE_TEMPO | false | Default for the Run page's "play everything, force tempo" toggle (ignore the BPM tolerance, force every track onto the target; rates clamped) |
SYNC_INTERVAL_MINUTES | 0 | Minutes between automatic background sync passes for playlists (Spotify + Navidrome), star sync, and play-count pulls. 0 = off (manual only); floored to 5. Watch mode only |
INSTALL_PING / INSTALL_PING_URL | (ask on first run) | Opt-in anonymous install ping (version only; no identifier/data/cookies). Set INSTALL_PING=false or INSTALL_PING_URL="" to disable |
NTFY_TOPIC | (empty) | ntfy topic (leave empty to disable) |
NAVIDROME_URL | (empty) | Trigger Navidrome rescan after each scan |
NAVIDROME_STAR_SYNC | false | Two-way star sync toggle (Settings → Navidrome) |
NAVIDROME_SCROBBLE | false | Scrobble built-in-player plays to Navidrome (Settings → Navidrome) |
LYRICS_ENABLED | false | Auto-fetch lyrics (LRCLIB) for tracks the grabber downloads; manual/bulk fetch always available in the UI |
LYRICS_MODE | embed | Store lyrics in the file tag (embed) or as a .lrc sidecar (sidecar) |
All variables documented in docker-compose.yml.
Set ENABLE_UI: "true" and a strong UI_PASSWORD, then open http://your-host:5000.
⚠️ LAN access only by default. The UI runs over plain HTTP. Place a reverse proxy (nginx, Caddy) with TLS in front of port 5000 before exposing it outside your local network — and set
UI_TRUSTED_PROXIESto the number of proxies so the login lockout keys on the real client IP.
The UI password is stored as a salted hash once changed in Settings (never plaintext), a password change logs out all other devices, and settings.json is written 0600.
pending badge before analysis; live search as you type; BPM ± tolerance filter; error badge tooltips; back navigation restores filter/page/search stateartist.jpg (Navidrome convention), or opt-in, rate-limited fetching from Deezer's public API (FETCH_ARTIST_IMAGES), cached on disk; falls back to album art. ARTIST_IMAGES_TO_LIBRARY saves fetched/picked images as artist.jpg in the artist's folder so Navidrome sees them too.lrc sidecars (Navidrome-compatible); a player lyrics drawer follows synced lyrics live (click a line to seek) and steps plain lyrics manually.nsp with a bpm range) for cadence playlists in any Subsonic clientreviewed and removed from queuenative · stretch × octave → result breakdown, ±1/±5 steps, four named presets or an in-place queue view, lyrics drawer, waveform + large transport; a source picker builds the queue from your whole library or a specific playlist (with an "N of M available" count); auto-queues octave-folded BPM matches (starred tracks first, disliked tracks never), refills the queue automatically when the last track starts (a playlist run prefers its own tracks and tops up from your library at the same cadence when the playlist is too small, so it never loops one song), and locks the tempo so every song stretches onto your step, pitch preserved; an optional "play everything, force tempo" toggle ignores the BPM tolerance so any track can fill the queue (extreme rates clamped); the track's play count shows under the title/artist, and the desktop track-info column shows file audio quality (format + bit depth / sample rate, or bitrate)/healthz JSON endpointRUN_PASSWORD or a named run user (optional username field) created in Settings → Player Access → Run users — each user is either full-access or scoped to specific playlists (a restricted user's run sources are filtered, and the server refuses the library/starred pool and any other playlist). Reset/disable/delete a user and their sessions end at once; the shared password stays a full-access guest, so upgrades change nothing. Ideal for a shared phone/tablet or a dedicated running device; player logins stay signed in far longer (RUN_SESSION_DAYS, default 30). On desktop it keeps a slim sidebar with just Run + About; on phones that collapses to a compact top bar with the same tabs, and Run uses a one-screen layout with the waveform/transport pinned to the bottom.mp3 · .flac · .ogg · .opus · .m4a · .aac · .wav · .wv
If BPM Tagger has been useful to you, consider supporting its development:
Every bit helps keep this project maintained and open source.
Content type
Image
Digest
sha256:c04c75a82…
Size
382 MB
Last updated
7 days ago
docker pull gatoserio/bpm-tagger