Skip to content

Repository files navigation

🐉 dragontag

Self-hosted music tagger and library organizer for Docker

CI Python License FastAPI

Drop an audio file. Get a perfectly tagged, organized result — automatically.


dragontag identifies every file against MusicBrainz (with optional AcoustID fingerprinting), writes a complete, casing-exact tag set, embeds cover art from the Cover Art Archive, fetches synced lyrics from LRCLIB, and moves the file into a clean Artist / Album / 01. Title.flac structure.

High-confidence matches flow through hands-free. Low-confidence files land in a review queue where you pick the right candidate, resolve conflicts, or fill in missing fields — all from the browser.


Features

Identification

Feature Description
MusicBrainz-first Short-circuits on an existing MUSICBRAINZ_TRACKID; otherwise searches by title / artist / album / duration with progressive fallback to maximise hit rate
AcoustID fingerprint Toggleable acoustic fingerprint fallback using fpcalc (bundled in the image) when text search comes up empty
Confidence scoring Matches above the threshold are tagged and moved automatically; everything else goes to review
Release consensus Near-tied candidates resolve to one deterministic release per album — Official status, then the release your library already uses for that release group, then the larger edition — so tracks of one album can't scatter across MusicBrainz editions and split into multiple album listings

Tagging & metadata

Feature Description
Format coverage FLAC · MP3 (ID3v2.4) · WAV (ID3 chunk) · M4A / MP4
Cover art Best available resolution from Cover Art Archive, resized to ≤ 1200 px, embedded in the file and written as cover.jpg
Lyrics Synced LRC or plain text from LRCLIB, embedded per-format
Advisory tagging Explicit content auto-classified from lyrics and written as ITUNESADVISORY
Smart formatting Title Case, qualifier parenthesization (Song LiveSong (Live)), grammar fixes (ALL-CAPS, contractions, possessives)
Genre filter MB community tags filtered against a ~1500-entry canonical genre list — kills noise like "billboard top 100"; clean tags survive as a fallback
Link to existing album Manually link a track's edit form to an album already in your library — inherits album, album artist, disc/track totals, and MB album IDs without any new MusicBrainz lookup; title, artist, and track number are left untouched

Library management

Feature Description
Organize Moves all tracks to their canonical paths based on current filename templates; picks up manual edits after a scan
Library scan Indexes existing on-disk files into the DB — useful after editing tags outside dragontag
One tagging pass Retag runs every file through the same identify → tag → move pipeline as the drop folder. Album folders are identified as a unit: one MusicBrainz release is elected per folder and every track is tagged from it, so release-level tags (album ID, release group, album artist, date, media, type/status) can never scatter across editions and split the album in players. Uncertain groups and files not on the elected release land in review
Helpers & reports Individual per-folder actions: fetch lyrics/covers, extract covers, tag advisories, fix genres, ReplayGain (in-place, single-field, never move files), plus read-only reports (verify integrity, validate tags, find duplicates, find missing tracks), prune junk, and cleanup
Cleanup & quarantine The Cleanup action merges case-twin artist folders (fakemink/Fakemink — strict case/punctuation equality, never fuzzy) and edition-suffix twin album folders (Afraid / Afraid - Single / Afraid (Deluxe)), and moves dead folders + leftover non-audio into a quarantine folder (<library>/.dragontag-trash). Cover art is conservative: only byte-identical duplicates are quarantined — distinct images always stay with their album. Report-only by default; the apply variant never deletes and never touches audio. Ingest also reuses existing case-variant/edition folders (race-proof, fail-closed on I/O errors) to prevent new twins
Damaged-library recovery Scan → Retag → Cleanup (apply): the retag pass unifies split albums and produces clean names; cleanup merges leftover twin folders
Completions page Library health in one place: coverage meters (lyrics/tagged/covers/genres) over live sections (songs without lyrics with per-row LRCLIB fetch, untagged files, duplicate tracks + twin album folders, tag problems) and snapshot sections with refresh jobs (incomplete albums vs MusicBrainz, missing covers/genres from a read-only file scan). Rows deep-link into fixes; the page never moves or deletes files
Library table Column sorting and pagination (10 / 25 / 50 / 100 / 200); explicit advisory badge on each row

Workflow

Feature Description
Drop & ingest Drag-and-drop in the browser or drop files into the watched folder — both hit the same pipeline
Non-blocking retag Queueing a folder re-tag returns instantly — the enqueue walk runs as a background job with progress and cancel
Review queue Low-confidence matches, missing RELEASETYPE, and destination conflicts surface a candidate picker with scores and links, manual MB search, and action buttons
Dry-run mode Preview destination paths and assembled tags without touching any files — global setting plus per-run overrides on Library actions
Change history Every tag-write is recorded; revert a file's tags in place or move it back to its original directory with one click
Keyboard shortcuts / focuses search, ? opens the key reference, g then a letter navigates between pages, and every page wires its own keys (shown in the status bar at the bottom) for its primary actions

Automation & notifications

Feature Description
Scheduling Standard cron expressions for scans, organizes, folder re-tags, lyrics/cover fetches, cleanup, and backups — with run-now, next-run display, and live plain-English descriptions. Expressions are read in your display timezone (TZ / in-app setting), so "At 06:00 AM" fires at 6 AM your time
Webhooks Discord-compatible webhook on job completion or error
Universal progress bar Live progress line under the nav on every page: percentage, item counts, and current file

System

Feature Description
Scan filters Regex patterns (by filename), excluded directories, and excluded files — all applied to the watcher, scanner, and bulk re-tag; clearable with one click
Backup / restore Versioned tarball of the DB, settings, password hash, and AcoustID key; restore from the UI or the restore_backup CLI
Change retention Configurable cap on audit-log rows (0 = unlimited)
First-run wizard Set credentials and AcoustID key from the browser on first boot
SQLite-backed All jobs and history survive container restarts
API docs Auth-guarded Swagger UI at /api-docs and raw schema at /openapi.json

Quick start

git clone https://github.com/chropic/dragontag.git
cd dragontag

# 1. Hash a password for the web UI
mkdir -p secrets config
python -m dragontag.tools.hash_password 'your-password' > secrets/password.txt

# 2. (Optional) AcoustID key for fingerprint fallback
echo 'your-acoustid-key' > secrets/acoustid_key.txt

# 3. Point /library and /drop at your actual paths, then start
$EDITOR docker-compose.yml
docker compose up -d

Open http://localhost:7593 and log in. First boot redirects to /setup if no password is configured yet.

Building locally — swap image: for build: . in docker-compose.yml.


Configuration

Volumes

Mount Contents
/library Destination root — files land at Album Artist/Album/[Disc N/]NN. Title.ext
/drop Watched ingest folder — files dropped here are queued automatically
/config SQLite DB, settings.json, password hash, AcoustID key

Environment variables

Variable Purpose
DRAGONTAG_USERNAME Web UI login (default admin)
DRAGONTAG_PASSWORD_FILE Path to argon2-hashed password file (Docker secret recommended)
DRAGONTAG_PASSWORD Plain-text password — dev/testing only
DRAGONTAG_SESSION_SECRET_FILE Session signing secret; falls back to an ephemeral value
DRAGONTAG_ACOUSTID_KEY_FILE Path to AcoustID API key file
DRAGONTAG_LIBRARY_PATH Override /library mount
DRAGONTAG_DROP_PATH Override /drop mount
DRAGONTAG_CONFIG_PATH Override /config mount
TZ Timezone for displayed timestamps and cron schedule interpretation, e.g. America/New_York. Locks the Settings UI's timezone field when set — remove it to enable the in-app override instead

Migration note: Variables were renamed from the AIO_ prefix to DRAGONTAG_. Update your docker-compose.yml accordingly.

Reverse proxy and request security

Direct Docker use accepts the proxy/browser Host header by default. If dragontag sits behind HTTPS, set DRAGONTAG_ALLOWED_HOSTS to the public host (or a comma-separated allowlist), DRAGONTAG_TRUSTED_PROXY_IPS to the proxy's source IP, and DRAGONTAG_SESSION_COOKIE_SECURE=true. Request and per-upload limits are configured through DRAGONTAG_MAX_REQUEST_BYTES and DRAGONTAG_MAX_UPLOAD_BYTES (both default to 2 GiB).

Settings UI

The Settings page covers everything below — changes are written atomically to /config/settings.json:

  • AcoustID fingerprint on/off, auto-apply confidence threshold
  • Network timeout for outbound MusicBrainz/AcoustID calls (default 15s, so a stalled connection can't wedge the worker)
  • Filename templates (single-disc and multi-disc) and artist-folder split separators
  • Per-tag multi-value separators (ARTIST, GENRE, LABEL, …)
  • Genre limit, casing, and canonical-list junk filter
  • Fields to skip at write time across all formats
  • Watcher on/off, ignore patterns, settle window (a file is ingested only once its size stops changing, so partial SMB/NFS transfers aren't read half-written)
  • Cover-art minimum width and release-group fallback toggle
  • Discord webhook URL and on/off per event type
  • Dry-run mode, scan filters, change-retention cap, log verbosity (0–4)
  • Backup download and validated restore
  • MusicBrainz user-agent and server (for self-hosted mirrors)
  • Display timezone override — only editable when the container has no TZ environment variable set; TZ always wins

How it works

  Drop file / upload
        │
        ▼
  ┌─────────────────┐
  │  Watcher / UI   │──── enqueue ────► Job row (SQLite)
  └─────────────────┘                        │
                                             ▼
                          ┌──────────────────────────────────────┐
                          │  Pipeline                             │
                          │                                       │
                          │  1. Read existing tags + filename     │
                          │  2. MBID short-circuit (if present)   │
                          │     else  MusicBrainz text search     │
                          │     else  AcoustID fingerprint        │
                          │                                       │
                          │  3. Score top candidate               │
                          │     ≥ threshold ──► auto-apply        │
                          │     < threshold ──► review queue      │
                          │                                       │
                          │  4. Assemble TrackTags from MB        │
                          │  5. Fetch cover art (CAA)             │
                          │  6. Fetch lyrics (LRCLIB)             │
                          │  7. Write tags (format-aware)         │
                          │  8. Move into library + cover.jpg     │
                          │  9. Fire webhook (if configured)      │
                          └──────────────────────────────────────┘

Files in the review queue show the top 5 MB candidates with scores and links. Destination conflicts get replace / rename / skip / skip + delete buttons; the last option keeps the existing library file and permanently deletes the incoming duplicate after confirmation.

Data-integrity guarantees

  • Atomic tag writes — every tag write (full re-tag, single-field updates, and revert) is performed on a temp copy in the same directory and then atomically swapped in. A crash mid-write can only ever damage the throwaway temp, never your original audio file. Cover-art sidecars (cover.jpg) are written the same way.
  • Verified moves — a file move confirms the destination's byte count matches the source, catching a silently-truncated write over a flaky network volume.
  • Stalled-task recovery — a background task with no progress heartbeat for 15 minutes and no live worker thread is reaped to error, so a hung run can't block future scheduled work while a slow-but-healthy one (say, a long backup) is left alone; bounded network timeouts keep the single worker from hanging in the first place.
  • Honest failure reporting — if a move has to be rolled back and the rollback itself fails (the original spot is occupied), dragontag reports exactly where the file ended up instead of claiming success, and logs it critically.

Tag reference

dragontag writes a rich, source-traceable tag set — every field comes directly from MusicBrainz or one of its associated services. Nothing is inferred or guessed. The Vorbis Comment field names below are used for FLAC; MP3, WAV, and M4A map these to their native containers (ID3v2.4 frames and TXXX for non-standard fields; M4A atoms and ----:com.apple.iTunes: freeform atoms).

The canonical schema lives in schema.py.

Field What's stored
TITLE Track title, exactly as MusicBrainz records it
ARTIST Performing artist(s) — one value per artist (native multi-value)
ARTISTS Same as ARTIST; written separately for players that prefer this field
ARTISTSORT Artist sort names from MB (e.g. "Bowie, David")
ALBUM Release title
album_artist Album-level artist(s) — the name the folder is grouped under
ALBUMARTISTSORT Album artist sort names
COMPOSER · LYRICIST · ARRANGER · CONDUCTOR From MB recording and work relationships
DATE Release date
ORIGINALDATE · ORIGINALYEAR First release date of the release group (the original year the album came out, not this specific edition)
track Track number as NN/TT; also written as TRACKTOTAL and TOTALTRACKS (totals omitted when unknown)
disc Disc number as N/T; also written as DISCTOTAL and TOTALDISCS (totals omitted when unknown)
GENRE Top community-voted MB tags, filtered for quality
LABEL · MEDIA · BARCODE · ISRC Label, format, barcode, and recording ISRC from MB
RELEASECOUNTRY · RELEASESTATUS · RELEASETYPE · SCRIPT Release metadata from MB
LYRICS Synced .lrc or plain text from LRCLIB
ITUNESADVISORY 0 = clean · 1 = explicit (auto-classified from lyrics)
ACOUSTID_ID AcoustID fingerprint match, or carried over from a pre-existing tag
MUSICBRAINZ_TRACKID MB recording ID — the primary link back to the source
MUSICBRAINZ_RELEASETRACKID · _ALBUMID · _ALBUMARTISTID · _ARTISTID · _RELEASEGROUPID Full MB provenance trail
TAGGER tagged via dragontag/x.y.z

Development

git clone https://github.com/chropic/dragontag.git
cd dragontag

python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -e ".[dev]"

# Run tests
pytest -v

# Dev server
DRAGONTAG_LIBRARY_PATH=./library DRAGONTAG_DROP_PATH=./drop \
DRAGONTAG_CONFIG_PATH=./config DRAGONTAG_USERNAME=dev DRAGONTAG_PASSWORD=dev \
uvicorn dragontag.app.main:app --reload --port 7593

Front-end

The UI is a self-hosted terminal/TUI design (JetBrains Mono, monochrome, .dt-* panel primitives) — no CDN. The stylesheet is compiled ahead of time and committed:

# After editing templates or frontend/app.input.css, rebuild dragontag/app/web/static/app.css
bash frontend/build_css.sh

See frontend/README.md for the .dt-* component layer and font details.

Database migrations

# After changing models.py
DRAGONTAG_CONFIG_PATH=./config alembic revision --autogenerate -m "describe change"

# Apply
DRAGONTAG_CONFIG_PATH=./config alembic upgrade head

Code map

  • dragontag/app/main.py composes FastAPI routes and the HTMX web surface.
  • config.py, db.py, and models.py own configuration and persisted state.
  • ingest/ orchestrates queued files; identify/ resolves metadata; tagging/ renders and writes it; library/ owns paths, moves, indexing, and user-requested maintenance.
  • tasks.py and scheduler.py run tracked background and scheduled work.
  • dragontag/app/web/ and frontend/ contain templates, vendored assets, and stylesheet sources.

For implementation work, start with AGENTS.md. Stable data flow and extension points are documented in docs/ARCHITECTURE.md; file-safety and correctness contracts are in docs/INVARIANTS.md. These guides point to source symbols and representative tests instead of duplicating exhaustive inventories.

Tests

The pytest suite is self-contained and avoids live MusicBrainz, AcoustID, and lyrics services. Test modules follow the subsystem or regression behavior they cover, so search tests/ by the symbol or contract being changed. Run focused tests while iterating and pytest -q before handoff. Representative safety tests are linked from docs/INVARIANTS.md.


MIT License

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages