@wazolab/music-tagger (0.1.0)
Installation
@wazolab:registry=npm config set @wazolab:registry=npm install @wazolab/music-tagger@0.1.0"@wazolab/music-tagger": "0.1.0"About this package
music-tagger
A music tagging pipeline: non-destructively copies your library, converts everything to MP3 320 kbps, removes duplicates, normalizes filenames and ID3 tags, fetches cover art, detects genre with ML models, and prepares files for Rekordbox analysis.
Requirements
- Node.js >= 20
- Python >= 3.10 + pip (for
ot-rename,ot-artwork,ot-genre,ot-energy) ffmpeg+ffprobe(forot-convert) —sudo pacman -S ffmpeg
Install
pnpm add -g @wazolab/music-tagger
Or run directly without installing:
pnpm dlx @wazolab/music-tagger <source-dir>
Install Python dependencies
make install
# or manually:
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
Workflow
Step 1 — Copy source files (non-destructive)
ot-batch ~/Music/incoming
# or:
make copy SRC=~/Music/incoming OUT=~/Music/analysed-tracks
Files are copied to ./analysed-tracks. Source is never touched.
ot-batch <source-dir> [options]
Arguments:
source-dir source music folder (read-only, never modified)
Options:
-o, --output <dir> output folder (default: "./analysed-tracks")
--dry-run scan and show copy plan only — no writes
-h, --help display help
Step 2 — Format normalization (MP3 320 kbps)
ot-convert ./analysed-tracks
# or:
make convert OUT=./analysed-tracks
Accepts a folder (batch mode) or a single file (one-shot mode):
ot-convert track.flac # converts one file to MP3 320
ot-convert ./analysed-tracks # converts all files in folder
Enforces the project's target format (MP3 320 kbps):
- Non-MP3 files (FLAC, WAV, AIFF, M4A…) are converted via
ffmpeg -b:a 320k. Tags are preserved. Original file is removed after a successful conversion. - MP3 files below 310 kbps are listed as warnings — not re-encoded (upsampling a lossy file degrades quality).
- MP3 files at 310 kbps or above are left untouched.
ot-convert <file|folder> [options]
--dry-run show plan without converting or deleting
--workers <n> parallel ffmpeg workers (default: 4, folder mode only)
The conversion logic is also available as a programmatic API:
import { convertFile } from '@wazolab/music-tagger';
const result = await convertFile('/path/to/track.flac');
// { outputPath: '/path/to/track.mp3', bitrate: 320000, changed: true }
convertFile(filePath) returns { outputPath, bitrate, changed }:
outputPath— path to the resulting MP3 filebitrate— bitrate of the output file in bps (always320000for converted files, actual bitrate for MP3 pass-throughs)changed—trueif the file was transcoded,falseif it was an MP3 and returned as-is
Step 3 — Duplicate detection
ot-dedup ./analysed-tracks
# or:
make dedup OUT=./analysed-tracks
Detects duplicate MP3 tracks at two confidence levels:
- High confidence (auto-deleted by
make dedup) — identical file hash (MD5). Bit-for-bit identical files only. Keeps the highest bitrate / largest copy. - Review hints — same normalised artist+title tags, or same filename slug. Could be different versions of the same track. Writes
dedup-hints.json; surfaces inot-rename reviewwith a⚠ Duplicate?warning and a[d] deleteaction.
ot-dedup <folder> [options]
--dry-run show plan without deleting or writing hints
--auto delete all high-confidence duplicates without prompting
--hints <file> output file for rename hints (default: <folder>/dedup-hints.json)
Step 4 — Filename & tag normalization
make rename OUT=~/Music/analysed-tracks
# or step by step:
ot-rename scan ~/Music/analysed-tracks --musicbrainz
ot-rename review
ot-rename apply ~/Music/analysed-tracks
ot-rename normalizes filenames and ID3 tags in three stages:
Scan — parses every file into a rename plan:
- Reads existing tags (
artist,title,album,label,year) - Splits the filename into segments (
Artist - Title,Label - Artist - Title, etc.) - Fuzzy-matches filename segments against existing tags to confirm or flag mismatches
- Applies smart title case (preserves mixed-case words like
McFly, uppercases acronyms likeDJ/EP, handles dotted abbreviations likeO.B.F.) - Detects special patterns: compilations (
VA - Title-Artist),feat.in title, partial tags, label-name segments - Enriches ambiguous entries via MusicBrainz (enabled by default via
make rename, ~1 s/track) - Outputs a
rename-plan.jsonwithauto(high confidence) andreview(needs human input) entries
Review — interactive TTY session for review entries:
- Shows the proposed filename, artist, title, album, label, year
- Pre-fills empty fields from session memory (a segment resolved earlier in the session is reused automatically)
- Retroactively corrects previously approved entries when a casing or spelling is fixed mid-session
- Shows
⚠ Duplicate?warning for files flagged byot-dedup; adds[d] deleteaction - Press
[i]to display the full audit trail for the current file (rules tried, refinements applied, MusicBrainz result) - Accept (
a), edit (e), skip (s), delete (d), or inspect (i) each file
Apply — writes the approved plan:
- Renames files to
Artist - Title.ext - Writes
artist,title,album,label,yearID3 tags
Scan also writes rename-audit.ndjson alongside the plan — one JSON line per file recording every rule tried, refinement applied, and MusicBrainz result. See docs/rename-rules.md for the format.
ot-rename scan <folder> [options]
--confidence <n> fuzzy match threshold 0–100 (default: 85)
--output <file> plan file (default: rename-plan.json)
--musicbrainz enrich review entries via MusicBrainz API (~1s/track)
ot-rename review [options]
--plan <file> plan file (default: rename-plan.json)
ot-rename apply <folder> [options]
--plan <file> plan file (default: rename-plan.json)
Step 5 — Cover art
ot-artwork ./analysed-tracks
# or:
make artwork OUT=./analysed-tracks
Fetches and embeds cover art for MP3 files that don't already have one, using a cascade of free sources:
- Deezer API — free, no credentials, 500×500
- iTunes Search API — free, 500×500
- SoundCloud — via
yt-dlp, 500×500 - YouTube — via
yt-dlp, 16:9 thumbnail centre-cropped to 500×500
Art is embedded as APIC type 3 (Cover Front) at 500×500 JPEG. Files with existing artwork are skipped.
ot-artwork <folder> [options]
--dry-run fetch artwork but do not embed
--skip-tagged suppress output for already-tagged files
Step 6 — Genre detection
ot-genre ./analysed-tracks
# or:
make genre OUT=./analysed-tracks
Runs the Discogs-Effnet ML model on every file and writes the GENRE tag (FLAC) or TCON tag (MP3). Models are auto-downloaded to ~/.cache/ot-genre/ on first run (~100 MB total).
ot-genre <folder> [options]
--dry-run detect and print genres without writing tags
--top <n> number of subgenres to write (default: 3)
--skip-tagged skip files that already have a GENRE tag
--workers <n> parallel workers (default: 4)
Output format: top N subgenres joined with / — e.g. House / Tech House / Deep House
Step 7 — Energy detection (optional)
ot-energy ./analysed-tracks
# or:
make energy OUT=./analysed-tracks
Runs Essentia's emomusic arousal model on every file and writes the ENERGY tag (scale 1-10). Models are auto-downloaded to ~/.cache/ot-energy/ on first run (~30 MB total).
Not included in make start — run separately when needed.
ot-energy <folder> [options]
--dry-run detect and print energy without writing tags
--skip-tagged skip files that already have an ENERGY tag
--workers <n> parallel workers (default: 4)
Output format: integer 1-10 — e.g. 7 (low = chill/ambient, high = peak/hard)
Step 8 — BPM, key, and final tagging with Rekordbox
Import ./analysed-tracks into Rekordbox and let it analyze BPM and musical key. Once done, write the analysis back to the audio files so tags are portable:
File > Library > Write Tags to Files (or right-click selection → Write Tag Information to File)
This writes TBPM and TKEY directly into the audio files, making them readable by any other DJ software (Traktor, Serato, VirtualDJ…).
Why Rekordbox instead of
ot-key/ot-bpm? Rekordbox's BPM engine is more accurate, and writing tags through Rekordbox also keeps your library in sync with its internal database.
Full pipeline
make start SRC=~/Music/incoming OUT=~/Music/analysed-tracks
# optionally:
make energy OUT=~/Music/analysed-tracks
# then Rekordbox: import + File > Library > Write Tags to Files
make start runs: copy → convert → dedup → rename (interactive) → artwork → genre
After step 8, every file carries portable tags: ARTIST, TITLE, ALBUM, LABEL, YEAR, GENRE, ENERGY, TBPM, TKEY.
Non-destructive guarantee
ot-batch uses fs.copyFile to write copies and never touches the source directory. The source path is only read with readdir and stat. Tests verify that source file checksums are identical before and after a run.
Re-runs are idempotent
Files already present in the output folder with the same size are skipped. Running ot-batch twice on the same source is safe and fast.
Supported formats
ot-batch accepts: .mp3 .flac .aiff .aif .wav .m4a .ogg (case-insensitive)
ot-convert converts all non-MP3 formats to MP3 320 kbps. All subsequent steps operate on MP3 only.
Without Rekordbox
If you don't use Rekordbox, run local BPM and key analysis instead:
BPM detection
ot-bpm ./analysed-tracks
Requires: paru -S bpm-tools + sudo pacman -S id3v2
ot-bpm <folder> [options]
-m, --min <bpm> minimum BPM (default: 60)
-x, --max <bpm> maximum BPM (default: 220)
--dry-run detect and print BPM without writing tags
--concurrency <n> parallel workers (default: 4)
Key detection
ot-key ./analysed-tracks
Requires: paru -S keyfinder-cli + sudo pacman -S ffmpeg
ot-key <folder> [options]
-n, --notation <type> camelot (default), openkey, musical
--dry-run detect and print keys without writing tags
--concurrency <n> parallel workers (default: 4)
Limitations
- MusicBrainz rate-limits to 1 request/second — large collections take time. White labels and unreleased tracks are often not indexed.
ot-artworkdepends onyt-dlpfor SoundCloud and YouTube sources — install withpip install yt-dlp.ot-genremodels auto-download ~100 MB on first run. Strong coverage for electronic music; weaker for very niche or regional styles.ot-energymodels auto-download ~30 MB on first run.- Rekordbox's "Write Tags to Files" writes
TBPMandTKEY— cue points and loops stay in Rekordbox's internal database and are not portable.
Development
pnpm test # run tests
pnpm test:watch # watch mode
Dependencies
Dependencies
| ID | Version |
|---|---|
| commander | ^12.0.0 |
| p-limit | ^7.3.1 |
Development dependencies
| ID | Version |
|---|---|
| vitest | ^2.0.0 |