Skip to main content

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:

  1. Detects your distro (apt / dnf / pacman / zypper / apk) and installs ffmpeg, mediainfo, xdg-utils.
  2. Creates a venv at .venv/ (falls back to --user if python3-venv isn't installable).
  3. Installs musicorg editable, plus the optional [shazam] and gamdl extras on consent.
  4. Launches the wizard immediately./install.sh is enough to install and run; pass --no-run to skip the launch.

Run it

musicorg

The bare command launches the guided wizard:

  1. 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.
  2. 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.
  3. Stage 3 — Lossless upgrade (optional). Installs gamdl if absent. Prompts for cookies.txt and .wvd device 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 thin undo_phase*.py that 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 shazamio failures writes SHAZAMIO_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.
  • audioTraits is never trusted. Every gamdl output is ffprobe'd; tracks claimed lossless but served as AAC are marked alac_listed_but_not_servable.
  • Collision handling. Filename collisions get (2), (3) suffixes, logged to 11_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

musicorg-0.2.0.tar.gz (102.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

musicorg-0.2.0-py3-none-any.whl (114.6 kB view details)

Uploaded Python 3

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

Hashes for musicorg-0.2.0.tar.gz
Algorithm Hash digest
SHA256 1410c38e5c46f3515b57da4cd7722584d9daf11292624529404450a0147c0902
MD5 ae88825bc5ff01cfc6d583d584af92f9
BLAKE2b-256 d52038cd556440b36c07b5e19acbec5c443814b2f035de0bccf136dc9fe20820

See more details on using hashes here.

Provenance

The following attestation bundles were made for musicorg-0.2.0.tar.gz:

Publisher: publish.yml on R15hav/musicorg

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for musicorg-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8e9c66198ec18e4c4d13070c8e61b8d9250b77a07f686ad582c1ea7dc7ac883d
MD5 ff1d48761dd6d04116c39e8f9431eeb8
BLAKE2b-256 ea6d608431f387566709f6b92804df060758fa3ad230e881fd15b239a1eeeb05

See more details on using hashes here.

Provenance

The following attestation bundles were made for musicorg-0.2.0-py3-none-any.whl:

Publisher: publish.yml on R15hav/musicorg

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page