Linux terminal music organizer + canonical-metadata + lossless upgrader
Project description
musicorg
A Linux terminal app that organizes a messy music library end to end: walks your music folder, dedupes near-identical files, cleans junk-laden tags via iTunes / JioSaavn / Shazam, and optionally upgrades lossy tracks to ALAC via gamdl. Every phase generates an undo script.
Built from a 34-script production pipeline that organized a 580-file mixed Bollywood / Hollywood / Punjabi corpus. The library code lifts that pipeline into a shared core (musicorg/); on top of that sit a guided wizard, a Typer CLI for power users, and Textual TUIs for review/approval flows.
Install
git clone <this repo>
cd music_upgrader
./install.sh # interactive — asks before each piece
Flags:
./install.sh --full # everything non-interactive (system + Shazam + gamdl)
./install.sh --base # CLI only, no Shazam, no gamdl
./install.sh --user # install into ~/.local instead of .venv/
./install.sh --no-run # install only — don't launch the app afterwards
./install.sh --uninstall # remove the app (leaves config + state)
./install.sh --skip-system # don't touch system packages
The installer:
- Detects your distro (apt / dnf / pacman / zypper / apk) and installs
ffmpeg,mediainfo,xdg-utils. - Creates a venv at
.venv/(falls back to--userifpython3-venvisn't installable). - Installs
musicorgeditable, plus the optional[shazam]andgamdlextras on consent. - Launches the wizard immediately —
./install.shis enough to install and run; pass--no-runto skip the launch.
Run it
musicorg
The bare command launches the guided wizard:
- Stage 1 — Organize file tree. Asks for music folder, library name, default country, move/copy/symlink mode. Then scans → dedupes → resolves → plans → previews → applies. Generates an undo
.sh. - Stage 2 — Canonical metadata. Optionally installs
shazamio. Runs tiered iTunes → JioSaavn → Shazam lookup. For unresolved rows asks: batch-approve by priority rule, open a CSV in$EDITOR, or skip. Writes tags + renames with a thin undo.py. - Stage 3 — Lossless upgrade (optional). Installs
gamdlif absent. Prompts forcookies.txtand.wvddevice file. Runs Shazam refingerprint to harvest Apple Music URLs, then drives gamdl per track. Surfaces a permanent-skip report at the end.
Every stage is opt-out. Interrupt with Ctrl+C any time — state is checkpointed; the next run picks up where you left off.
Try it now (sample library)
A small fixture is included that exercises every code path: Bollywood album folders with (YYYY) naming, plain folders that get demoted to singles, site-junk in filenames ([Songs.PK], (www.PagalWorld.com)), duplicates across folders, a Hollywood artist (Linkin Park) for the allowlist routing, a Punjabi single (Guru Randhawa), and a garbage-named file with no tags (Track 06.mp3). Plus non-audio junk for misc-sweep to find.
# Build (or rebuild) the fixture
python3 tests/fixtures/build_fixture.py
ls tests/fixtures/library-small/
# Run the wizard against it
musicorg
# → music folder: tests/fixtures/library-small
# → library name: demo
# → default country: bollywood
# → mode: move
# → say "y" to Stage 1, "n" to Stage 2 and 3 for the dry test
Expected outcome (Stage 1 only):
tests/fixtures/library-small/Music/
├── Bollywood/2010s/
│ ├── Ek Villain (2014)/
│ │ ├── 01 - Galliyan.mp3 # site junk stripped
│ │ ├── 02 - Hamdard.mp3 # dup winner from RISHAV/
│ │ └── 03 - Banjaara.mp3 # PagalWorld junk stripped
│ ├── Jab Tak Hai Jaan (2012)/01..03 - *.mp3
│ └── Single (2014)/01..02 - *.mp3
├── Hollywood/2010s/Linkin Park/Living Things (2012)/01..02 - *.mp3
├── Singles/Bollywood/
│ ├── Shraddha Kapoor/Galliyan (Unplugged).mp3 # version variant preserved
│ └── Unknown Artist/Track 06.mp3 # tag-less fallback
└── _duplicates/.../RISHAV/05-Hamdard.mp3 # loser preserved by path
Then to roll the whole thing back:
musicorg --library demo undo
Power-user CLI
The wizard is just a thin orchestrator around individual commands. Every phase is also a standalone subcommand:
musicorg --library home scan ~/Music
musicorg --library home dedupe # or --interactive for TUI
musicorg --library home resolve
musicorg --library home plan
musicorg --library home apply --dry-run
musicorg --library home apply --mode move # commit; generates undo_<TS>.sh
musicorg --library home undo
musicorg --library home canonicalize # tiered iTunes → JioSaavn → Shazam
musicorg --library home review --export # write a review CSV
$EDITOR ~/.local/share/musicorg/home/19_review.csv # fill the approve column
musicorg --library home review --import ~/.local/share/musicorg/home/19_review.csv
musicorg --library home approve --rule "jiosaavn>shazam>itunes" # batch alternative
musicorg --library home canonical-apply --dry-run
musicorg --library home canonical-apply
musicorg --library home canonical-undo --latest
musicorg --library home refingerprint # Shazam pass + harvest Apple Music URLs
musicorg --library home upgrade --cookies ./cookies.txt --wvd ./device.wvd
musicorg --library home recover-staging # rescue orphans from failed gamdl runs
musicorg --library home permanent-skip-report
# Textual TUIs
musicorg --library home review --interactive # canonical CSV-edit-reimport flow
musicorg --library home dedupe --interactive # two-pane dup picker
musicorg --library home fill # per-row card resolver
# Config
musicorg config init
musicorg config set acoustid.api_key XXXX
musicorg config show
Run musicorg --help for the full list (~30 commands).
State + output layout
For a library at ~/Music (slug home), state lives at ~/.local/share/musicorg/home/:
~/.local/share/musicorg/home/
├── config.ini # per-library overrides
├── 01_tags.csv # scan
├── 07_winners.csv, 07_duplicates.csv, 07_groups.csv # dedupe
├── 08_resolved.csv # resolve
├── 09_plan.csv # plan
├── 16_merged.csv # canonicalize (merged tier view)
├── 17_dryrun_diff.csv # canonical-apply --dry-run
├── 19_review.csv, 19_approvals.json # user-approval round-trip
├── 30_shazam_refingerprint.csv # refingerprint
├── upgrade_skips.csv # permanent-skip taxonomy
├── backups/tag_snapshot_<TS>.json # tag snapshots
├── logs/<phase>.log
└── undo_<TS>.sh, undo_phase18_<TS>.py, undo_upgrade_<TS>.py
Organized output (after apply):
<library_root>/Music/
├── Bollywood/<decade>/<movie (year)>/NN - Track.mp3
├── Hollywood/<decade>/<artist>/<album (year)>/NN - Track.mp3
├── Singles/{Bollywood,Punjabi,Hollywood}/<artist>/Track.mp3
├── _duplicates/<original-subpath>/ # dup losers preserved
├── _misc/ # non-audio sweep target
├── _replaced/ # original lossy after ALAC upgrade
└── _upgrade_staging/<run-id>/ # per-run gamdl staging
Global config: ~/.config/musicorg/config.ini.
Safety
- Every phase generates an undo. File-move phases emit
undo_<TS>.sh. Tag-write phases emit a thinundo_phase*.pythat reads a separate JSON snapshot at runtime — the snapshot is never inlined (production runs produced 41 MB scripts that way). - Year-mismatch guardrail. If a folder is named
Movie (YYYY)and an API returns a year ≥ 3 years away, the folder year is kept (catches iTunes-returns-compilation cases). - Circuit breaker for Shazam. 5 consecutive
shazamiofailures writesSHAZAMIO_UNAVAILABLE.<date>.txt; subsequent runs skip the tier with a banner. Delete the marker to retry. - gamdl idempotency trap mitigation. Each upgrade run uses a unique staging subdir so gamdl re-downloads instead of silently no-op'ing.
audioTraitsis never trusted. Every gamdl output is ffprobe'd; tracks claimed lossless but served as AAC are markedalac_listed_but_not_servable.- Collision handling. Filename collisions get
(2),(3)suffixes, logged to11_collisions.csv.
Permanent skip taxonomy
lossy_only_on_apple_music gamdl confirms ALAC unavailable
alac_listed_but_not_servable audioTraits claims lossless but ffprobe sees AAC
remix_dj_not_on_apple remix/mashup absent from Apple's catalog
wrong_match_permanent Apple URL points to a different recording
shazam_no_match audio fingerprint failed
musicorg permanent-skip-report shows counts + paths.
What's where
src/musicorg/ library — pure Python, no CLI deps
├── clean.py junk regex, query prep, version-marker logic
├── tags.py mutagen → ffprobe → mediainfo cascade + writers
├── identity.py audio-stream sha256 (content-addressed join key)
├── models.py dataclasses (Track, ResolvedTrack, TierMatch, ApplyResult, ProgressEvent, SkipReason)
├── config.py XDG-Linux config + per-library state dirs
├── scan.py walk + per-file tag read
├── dedupe.py group + score winners
├── resolve.py folder/tag/filename reconciliation + country heuristics
├── planner.py destination tree building + album-counts demotion
├── executor.py move/copy/symlink + collision handling + undo .sh
├── misc.py non-audio sweep
├── zip_probe.py detect zip backups of already-organized folders
├── canonicalize.py diff + apply + guardrails
├── backup.py snapshot + thin undo script generator
├── approval.py CSV round-trip + batch rule
├── upgrade.py upgrade orchestration + ffprobe verification + skip taxonomy
├── refingerprint.py Shazam pass + orphan recovery
├── extensions/ plugin protocol for third-party upgraders (gamdl, ...)
│ └── protocol.py UpgradeExtension, UpgradeCandidate, UpgradeResult, PreflightResult
└── lookup/
├── itunes.py, jiosaavn.py, shazam.py
├── scoring.py title × 0.55, artist × 0.25, duration × 0.20, +bonuses/-penalties
├── breaker.py circuit breaker for unofficial APIs
└── __init__.py chain() orchestrator
src/musicorg_cli/ reference CLI consumer — Typer + Textual + Rich
├── main.py Typer entry — 30 subcommands
├── wizard.py guided end-to-end wizard
└── tui/ review_app, fill_app, dup_review_app, canonical_app
tests/fixtures/build_fixture.py regenerates the demo library
examples/ embedding patterns
├── 01_basic_scan.py
├── 02_progress_callback.py
├── 03_full_pipeline.py
├── 04_custom_extension.py
├── 05_embed_in_fastapi.py
└── 06_embed_in_pyside.py
PUBLIC_API.md library API contract (SemVer after v1.0)
install.sh distro-aware installer
The library (musicorg) is pure-Python with only mutagen + requests. The CLI (musicorg_cli) adds Typer/Textual/Rich and is installed via the [cli] extra. Embedders use the library directly — see examples/ and PUBLIC_API.md.
---
## Uninstall
```bash
./install.sh --uninstall # removes the venv / user install
rm -rf ~/.config/musicorg ~/.local/share/musicorg # optional: nukes config + library state
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file musicorg-0.2.0.tar.gz.
File metadata
- Download URL: musicorg-0.2.0.tar.gz
- Upload date:
- Size: 102.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1410c38e5c46f3515b57da4cd7722584d9daf11292624529404450a0147c0902
|
|
| MD5 |
ae88825bc5ff01cfc6d583d584af92f9
|
|
| BLAKE2b-256 |
d52038cd556440b36c07b5e19acbec5c443814b2f035de0bccf136dc9fe20820
|
Provenance
The following attestation bundles were made for musicorg-0.2.0.tar.gz:
Publisher:
publish.yml on R15hav/musicorg
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
musicorg-0.2.0.tar.gz -
Subject digest:
1410c38e5c46f3515b57da4cd7722584d9daf11292624529404450a0147c0902 - Sigstore transparency entry: 1615259460
- Sigstore integration time:
-
Permalink:
R15hav/musicorg@f5852519ba23fd4676676c7479acb2ab577e0158 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/R15hav
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f5852519ba23fd4676676c7479acb2ab577e0158 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file musicorg-0.2.0-py3-none-any.whl.
File metadata
- Download URL: musicorg-0.2.0-py3-none-any.whl
- Upload date:
- Size: 114.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e9c66198ec18e4c4d13070c8e61b8d9250b77a07f686ad582c1ea7dc7ac883d
|
|
| MD5 |
ff1d48761dd6d04116c39e8f9431eeb8
|
|
| BLAKE2b-256 |
ea6d608431f387566709f6b92804df060758fa3ad230e881fd15b239a1eeeb05
|
Provenance
The following attestation bundles were made for musicorg-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on R15hav/musicorg
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
musicorg-0.2.0-py3-none-any.whl -
Subject digest:
8e9c66198ec18e4c4d13070c8e61b8d9250b77a07f686ad582c1ea7dc7ac883d - Sigstore transparency entry: 1615259484
- Sigstore integration time:
-
Permalink:
R15hav/musicorg@f5852519ba23fd4676676c7479acb2ab577e0158 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/R15hav
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f5852519ba23fd4676676c7479acb2ab577e0158 -
Trigger Event:
workflow_dispatch
-
Statement type: