musicorg 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- musicorg-0.2.0/PKG-INFO +310 -0
- musicorg-0.2.0/README.md +265 -0
- musicorg-0.2.0/pyproject.toml +64 -0
- musicorg-0.2.0/setup.cfg +4 -0
- musicorg-0.2.0/src/musicorg/__init__.py +221 -0
- musicorg-0.2.0/src/musicorg/approval.py +307 -0
- musicorg-0.2.0/src/musicorg/backup.py +736 -0
- musicorg-0.2.0/src/musicorg/canonicalize.py +710 -0
- musicorg-0.2.0/src/musicorg/clean.py +228 -0
- musicorg-0.2.0/src/musicorg/config.py +239 -0
- musicorg-0.2.0/src/musicorg/dedupe.py +160 -0
- musicorg-0.2.0/src/musicorg/executor.py +318 -0
- musicorg-0.2.0/src/musicorg/extensions/__init__.py +23 -0
- musicorg-0.2.0/src/musicorg/extensions/protocol.py +236 -0
- musicorg-0.2.0/src/musicorg/identity.py +115 -0
- musicorg-0.2.0/src/musicorg/lookup/__init__.py +118 -0
- musicorg-0.2.0/src/musicorg/lookup/breaker.py +81 -0
- musicorg-0.2.0/src/musicorg/lookup/itunes.py +110 -0
- musicorg-0.2.0/src/musicorg/lookup/jiosaavn.py +130 -0
- musicorg-0.2.0/src/musicorg/lookup/scoring.py +130 -0
- musicorg-0.2.0/src/musicorg/lookup/shazam.py +199 -0
- musicorg-0.2.0/src/musicorg/misc.py +110 -0
- musicorg-0.2.0/src/musicorg/models.py +151 -0
- musicorg-0.2.0/src/musicorg/planner.py +165 -0
- musicorg-0.2.0/src/musicorg/refingerprint.py +464 -0
- musicorg-0.2.0/src/musicorg/resolve.py +175 -0
- musicorg-0.2.0/src/musicorg/scan.py +163 -0
- musicorg-0.2.0/src/musicorg/tags.py +281 -0
- musicorg-0.2.0/src/musicorg/upgrade.py +529 -0
- musicorg-0.2.0/src/musicorg/zip_probe.py +111 -0
- musicorg-0.2.0/src/musicorg.egg-info/PKG-INFO +310 -0
- musicorg-0.2.0/src/musicorg.egg-info/SOURCES.txt +42 -0
- musicorg-0.2.0/src/musicorg.egg-info/dependency_links.txt +1 -0
- musicorg-0.2.0/src/musicorg.egg-info/entry_points.txt +2 -0
- musicorg-0.2.0/src/musicorg.egg-info/requires.txt +16 -0
- musicorg-0.2.0/src/musicorg.egg-info/top_level.txt +2 -0
- musicorg-0.2.0/src/musicorg_cli/__init__.py +12 -0
- musicorg-0.2.0/src/musicorg_cli/main.py +704 -0
- musicorg-0.2.0/src/musicorg_cli/tui/__init__.py +1 -0
- musicorg-0.2.0/src/musicorg_cli/tui/canonical_app.py +626 -0
- musicorg-0.2.0/src/musicorg_cli/tui/dup_review_app.py +418 -0
- musicorg-0.2.0/src/musicorg_cli/tui/fill_app.py +317 -0
- musicorg-0.2.0/src/musicorg_cli/tui/review_app.py +508 -0
- musicorg-0.2.0/src/musicorg_cli/wizard.py +490 -0
musicorg-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: musicorg
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Linux terminal music organizer + canonical-metadata + lossless upgrader
|
|
5
|
+
Author: Music Upgrader
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/R15hav/musicorg
|
|
8
|
+
Project-URL: Repository, https://github.com/R15hav/musicorg
|
|
9
|
+
Project-URL: Documentation, https://github.com/R15hav/musicorg/blob/main/PUBLIC_API.md
|
|
10
|
+
Project-URL: Issues, https://github.com/R15hav/musicorg/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/R15hav/musicorg/releases
|
|
12
|
+
Keywords: music,library,organizer,tags,id3,metadata,alac,lossless,shazam,itunes,jiosaavn,cli,tui
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Environment :: Console :: Curses
|
|
16
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
26
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
|
+
Classifier: Topic :: System :: Filesystems
|
|
29
|
+
Classifier: Topic :: Utilities
|
|
30
|
+
Classifier: Typing :: Typed
|
|
31
|
+
Requires-Python: >=3.10
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
Requires-Dist: mutagen>=1.47
|
|
34
|
+
Requires-Dist: requests>=2.31
|
|
35
|
+
Provides-Extra: shazam
|
|
36
|
+
Requires-Dist: shazamio>=0.7; extra == "shazam"
|
|
37
|
+
Provides-Extra: cli
|
|
38
|
+
Requires-Dist: typer>=0.12; extra == "cli"
|
|
39
|
+
Requires-Dist: textual>=0.60; extra == "cli"
|
|
40
|
+
Requires-Dist: rich>=13.7; extra == "cli"
|
|
41
|
+
Provides-Extra: web
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
45
|
+
|
|
46
|
+
# musicorg
|
|
47
|
+
|
|
48
|
+
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](https://github.com/glomatico/gamdl). Every phase generates an undo script.
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone <this repo>
|
|
58
|
+
cd music_upgrader
|
|
59
|
+
./install.sh # interactive — asks before each piece
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Flags:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
./install.sh --full # everything non-interactive (system + Shazam + gamdl)
|
|
66
|
+
./install.sh --base # CLI only, no Shazam, no gamdl
|
|
67
|
+
./install.sh --user # install into ~/.local instead of .venv/
|
|
68
|
+
./install.sh --no-run # install only — don't launch the app afterwards
|
|
69
|
+
./install.sh --uninstall # remove the app (leaves config + state)
|
|
70
|
+
./install.sh --skip-system # don't touch system packages
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The installer:
|
|
74
|
+
1. Detects your distro (apt / dnf / pacman / zypper / apk) and installs `ffmpeg`, `mediainfo`, `xdg-utils`.
|
|
75
|
+
2. Creates a venv at `.venv/` (falls back to `--user` if `python3-venv` isn't installable).
|
|
76
|
+
3. Installs `musicorg` editable, plus the optional `[shazam]` and `gamdl` extras on consent.
|
|
77
|
+
4. **Launches the wizard immediately** — `./install.sh` is enough to install *and* run; pass `--no-run` to skip the launch.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Run it
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
musicorg
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The bare command launches the guided wizard:
|
|
88
|
+
|
|
89
|
+
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`.
|
|
90
|
+
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`.
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
Every stage is opt-out. Interrupt with Ctrl+C any time — state is checkpointed; the next run picks up where you left off.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Try it now (sample library)
|
|
98
|
+
|
|
99
|
+
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.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Build (or rebuild) the fixture
|
|
103
|
+
python3 tests/fixtures/build_fixture.py
|
|
104
|
+
ls tests/fixtures/library-small/
|
|
105
|
+
|
|
106
|
+
# Run the wizard against it
|
|
107
|
+
musicorg
|
|
108
|
+
# → music folder: tests/fixtures/library-small
|
|
109
|
+
# → library name: demo
|
|
110
|
+
# → default country: bollywood
|
|
111
|
+
# → mode: move
|
|
112
|
+
# → say "y" to Stage 1, "n" to Stage 2 and 3 for the dry test
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Expected outcome (Stage 1 only):
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
tests/fixtures/library-small/Music/
|
|
119
|
+
├── Bollywood/2010s/
|
|
120
|
+
│ ├── Ek Villain (2014)/
|
|
121
|
+
│ │ ├── 01 - Galliyan.mp3 # site junk stripped
|
|
122
|
+
│ │ ├── 02 - Hamdard.mp3 # dup winner from RISHAV/
|
|
123
|
+
│ │ └── 03 - Banjaara.mp3 # PagalWorld junk stripped
|
|
124
|
+
│ ├── Jab Tak Hai Jaan (2012)/01..03 - *.mp3
|
|
125
|
+
│ └── Single (2014)/01..02 - *.mp3
|
|
126
|
+
├── Hollywood/2010s/Linkin Park/Living Things (2012)/01..02 - *.mp3
|
|
127
|
+
├── Singles/Bollywood/
|
|
128
|
+
│ ├── Shraddha Kapoor/Galliyan (Unplugged).mp3 # version variant preserved
|
|
129
|
+
│ └── Unknown Artist/Track 06.mp3 # tag-less fallback
|
|
130
|
+
└── _duplicates/.../RISHAV/05-Hamdard.mp3 # loser preserved by path
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Then to roll the whole thing back:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
musicorg --library demo undo
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Power-user CLI
|
|
142
|
+
|
|
143
|
+
The wizard is just a thin orchestrator around individual commands. Every phase is also a standalone subcommand:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
musicorg --library home scan ~/Music
|
|
147
|
+
musicorg --library home dedupe # or --interactive for TUI
|
|
148
|
+
musicorg --library home resolve
|
|
149
|
+
musicorg --library home plan
|
|
150
|
+
musicorg --library home apply --dry-run
|
|
151
|
+
musicorg --library home apply --mode move # commit; generates undo_<TS>.sh
|
|
152
|
+
musicorg --library home undo
|
|
153
|
+
|
|
154
|
+
musicorg --library home canonicalize # tiered iTunes → JioSaavn → Shazam
|
|
155
|
+
musicorg --library home review --export # write a review CSV
|
|
156
|
+
$EDITOR ~/.local/share/musicorg/home/19_review.csv # fill the approve column
|
|
157
|
+
musicorg --library home review --import ~/.local/share/musicorg/home/19_review.csv
|
|
158
|
+
musicorg --library home approve --rule "jiosaavn>shazam>itunes" # batch alternative
|
|
159
|
+
musicorg --library home canonical-apply --dry-run
|
|
160
|
+
musicorg --library home canonical-apply
|
|
161
|
+
musicorg --library home canonical-undo --latest
|
|
162
|
+
|
|
163
|
+
musicorg --library home refingerprint # Shazam pass + harvest Apple Music URLs
|
|
164
|
+
musicorg --library home upgrade --cookies ./cookies.txt --wvd ./device.wvd
|
|
165
|
+
musicorg --library home recover-staging # rescue orphans from failed gamdl runs
|
|
166
|
+
musicorg --library home permanent-skip-report
|
|
167
|
+
|
|
168
|
+
# Textual TUIs
|
|
169
|
+
musicorg --library home review --interactive # canonical CSV-edit-reimport flow
|
|
170
|
+
musicorg --library home dedupe --interactive # two-pane dup picker
|
|
171
|
+
musicorg --library home fill # per-row card resolver
|
|
172
|
+
|
|
173
|
+
# Config
|
|
174
|
+
musicorg config init
|
|
175
|
+
musicorg config set acoustid.api_key XXXX
|
|
176
|
+
musicorg config show
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Run `musicorg --help` for the full list (~30 commands).
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## State + output layout
|
|
184
|
+
|
|
185
|
+
For a library at `~/Music` (slug `home`), state lives at `~/.local/share/musicorg/home/`:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
~/.local/share/musicorg/home/
|
|
189
|
+
├── config.ini # per-library overrides
|
|
190
|
+
├── 01_tags.csv # scan
|
|
191
|
+
├── 07_winners.csv, 07_duplicates.csv, 07_groups.csv # dedupe
|
|
192
|
+
├── 08_resolved.csv # resolve
|
|
193
|
+
├── 09_plan.csv # plan
|
|
194
|
+
├── 16_merged.csv # canonicalize (merged tier view)
|
|
195
|
+
├── 17_dryrun_diff.csv # canonical-apply --dry-run
|
|
196
|
+
├── 19_review.csv, 19_approvals.json # user-approval round-trip
|
|
197
|
+
├── 30_shazam_refingerprint.csv # refingerprint
|
|
198
|
+
├── upgrade_skips.csv # permanent-skip taxonomy
|
|
199
|
+
├── backups/tag_snapshot_<TS>.json # tag snapshots
|
|
200
|
+
├── logs/<phase>.log
|
|
201
|
+
└── undo_<TS>.sh, undo_phase18_<TS>.py, undo_upgrade_<TS>.py
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Organized output (after `apply`):
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
<library_root>/Music/
|
|
208
|
+
├── Bollywood/<decade>/<movie (year)>/NN - Track.mp3
|
|
209
|
+
├── Hollywood/<decade>/<artist>/<album (year)>/NN - Track.mp3
|
|
210
|
+
├── Singles/{Bollywood,Punjabi,Hollywood}/<artist>/Track.mp3
|
|
211
|
+
├── _duplicates/<original-subpath>/ # dup losers preserved
|
|
212
|
+
├── _misc/ # non-audio sweep target
|
|
213
|
+
├── _replaced/ # original lossy after ALAC upgrade
|
|
214
|
+
└── _upgrade_staging/<run-id>/ # per-run gamdl staging
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Global config: `~/.config/musicorg/config.ini`.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Safety
|
|
222
|
+
|
|
223
|
+
- **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).
|
|
224
|
+
- **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).
|
|
225
|
+
- **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.
|
|
226
|
+
- **gamdl idempotency trap mitigation.** Each upgrade run uses a unique staging subdir so gamdl re-downloads instead of silently no-op'ing.
|
|
227
|
+
- **`audioTraits` is never trusted.** Every gamdl output is ffprobe'd; tracks claimed lossless but served as AAC are marked `alac_listed_but_not_servable`.
|
|
228
|
+
- **Collision handling.** Filename collisions get ` (2)`, ` (3)` suffixes, logged to `11_collisions.csv`.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Permanent skip taxonomy
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
lossy_only_on_apple_music gamdl confirms ALAC unavailable
|
|
236
|
+
alac_listed_but_not_servable audioTraits claims lossless but ffprobe sees AAC
|
|
237
|
+
remix_dj_not_on_apple remix/mashup absent from Apple's catalog
|
|
238
|
+
wrong_match_permanent Apple URL points to a different recording
|
|
239
|
+
shazam_no_match audio fingerprint failed
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`musicorg permanent-skip-report` shows counts + paths.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## What's where
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
src/musicorg/ library — pure Python, no CLI deps
|
|
250
|
+
├── clean.py junk regex, query prep, version-marker logic
|
|
251
|
+
├── tags.py mutagen → ffprobe → mediainfo cascade + writers
|
|
252
|
+
├── identity.py audio-stream sha256 (content-addressed join key)
|
|
253
|
+
├── models.py dataclasses (Track, ResolvedTrack, TierMatch, ApplyResult, ProgressEvent, SkipReason)
|
|
254
|
+
├── config.py XDG-Linux config + per-library state dirs
|
|
255
|
+
├── scan.py walk + per-file tag read
|
|
256
|
+
├── dedupe.py group + score winners
|
|
257
|
+
├── resolve.py folder/tag/filename reconciliation + country heuristics
|
|
258
|
+
├── planner.py destination tree building + album-counts demotion
|
|
259
|
+
├── executor.py move/copy/symlink + collision handling + undo .sh
|
|
260
|
+
├── misc.py non-audio sweep
|
|
261
|
+
├── zip_probe.py detect zip backups of already-organized folders
|
|
262
|
+
├── canonicalize.py diff + apply + guardrails
|
|
263
|
+
├── backup.py snapshot + thin undo script generator
|
|
264
|
+
├── approval.py CSV round-trip + batch rule
|
|
265
|
+
├── upgrade.py upgrade orchestration + ffprobe verification + skip taxonomy
|
|
266
|
+
├── refingerprint.py Shazam pass + orphan recovery
|
|
267
|
+
├── extensions/ plugin protocol for third-party upgraders (gamdl, ...)
|
|
268
|
+
│ └── protocol.py UpgradeExtension, UpgradeCandidate, UpgradeResult, PreflightResult
|
|
269
|
+
└── lookup/
|
|
270
|
+
├── itunes.py, jiosaavn.py, shazam.py
|
|
271
|
+
├── scoring.py title × 0.55, artist × 0.25, duration × 0.20, +bonuses/-penalties
|
|
272
|
+
├── breaker.py circuit breaker for unofficial APIs
|
|
273
|
+
└── __init__.py chain() orchestrator
|
|
274
|
+
|
|
275
|
+
src/musicorg_cli/ reference CLI consumer — Typer + Textual + Rich
|
|
276
|
+
├── main.py Typer entry — 30 subcommands
|
|
277
|
+
├── wizard.py guided end-to-end wizard
|
|
278
|
+
└── tui/ review_app, fill_app, dup_review_app, canonical_app
|
|
279
|
+
|
|
280
|
+
tests/fixtures/build_fixture.py regenerates the demo library
|
|
281
|
+
|
|
282
|
+
examples/ embedding patterns
|
|
283
|
+
├── 01_basic_scan.py
|
|
284
|
+
├── 02_progress_callback.py
|
|
285
|
+
├── 03_full_pipeline.py
|
|
286
|
+
├── 04_custom_extension.py
|
|
287
|
+
├── 05_embed_in_fastapi.py
|
|
288
|
+
└── 06_embed_in_pyside.py
|
|
289
|
+
|
|
290
|
+
PUBLIC_API.md library API contract (SemVer after v1.0)
|
|
291
|
+
install.sh distro-aware installer
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
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`.
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Uninstall
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
./install.sh --uninstall # removes the venv / user install
|
|
303
|
+
rm -rf ~/.config/musicorg ~/.local/share/musicorg # optional: nukes config + library state
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## License
|
|
309
|
+
|
|
310
|
+
MIT.
|
musicorg-0.2.0/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# musicorg
|
|
2
|
+
|
|
3
|
+
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](https://github.com/glomatico/gamdl). Every phase generates an undo script.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
git clone <this repo>
|
|
13
|
+
cd music_upgrader
|
|
14
|
+
./install.sh # interactive — asks before each piece
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Flags:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
./install.sh --full # everything non-interactive (system + Shazam + gamdl)
|
|
21
|
+
./install.sh --base # CLI only, no Shazam, no gamdl
|
|
22
|
+
./install.sh --user # install into ~/.local instead of .venv/
|
|
23
|
+
./install.sh --no-run # install only — don't launch the app afterwards
|
|
24
|
+
./install.sh --uninstall # remove the app (leaves config + state)
|
|
25
|
+
./install.sh --skip-system # don't touch system packages
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The installer:
|
|
29
|
+
1. Detects your distro (apt / dnf / pacman / zypper / apk) and installs `ffmpeg`, `mediainfo`, `xdg-utils`.
|
|
30
|
+
2. Creates a venv at `.venv/` (falls back to `--user` if `python3-venv` isn't installable).
|
|
31
|
+
3. Installs `musicorg` editable, plus the optional `[shazam]` and `gamdl` extras on consent.
|
|
32
|
+
4. **Launches the wizard immediately** — `./install.sh` is enough to install *and* run; pass `--no-run` to skip the launch.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Run it
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
musicorg
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The bare command launches the guided wizard:
|
|
43
|
+
|
|
44
|
+
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`.
|
|
45
|
+
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`.
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
Every stage is opt-out. Interrupt with Ctrl+C any time — state is checkpointed; the next run picks up where you left off.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Try it now (sample library)
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Build (or rebuild) the fixture
|
|
58
|
+
python3 tests/fixtures/build_fixture.py
|
|
59
|
+
ls tests/fixtures/library-small/
|
|
60
|
+
|
|
61
|
+
# Run the wizard against it
|
|
62
|
+
musicorg
|
|
63
|
+
# → music folder: tests/fixtures/library-small
|
|
64
|
+
# → library name: demo
|
|
65
|
+
# → default country: bollywood
|
|
66
|
+
# → mode: move
|
|
67
|
+
# → say "y" to Stage 1, "n" to Stage 2 and 3 for the dry test
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Expected outcome (Stage 1 only):
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
tests/fixtures/library-small/Music/
|
|
74
|
+
├── Bollywood/2010s/
|
|
75
|
+
│ ├── Ek Villain (2014)/
|
|
76
|
+
│ │ ├── 01 - Galliyan.mp3 # site junk stripped
|
|
77
|
+
│ │ ├── 02 - Hamdard.mp3 # dup winner from RISHAV/
|
|
78
|
+
│ │ └── 03 - Banjaara.mp3 # PagalWorld junk stripped
|
|
79
|
+
│ ├── Jab Tak Hai Jaan (2012)/01..03 - *.mp3
|
|
80
|
+
│ └── Single (2014)/01..02 - *.mp3
|
|
81
|
+
├── Hollywood/2010s/Linkin Park/Living Things (2012)/01..02 - *.mp3
|
|
82
|
+
├── Singles/Bollywood/
|
|
83
|
+
│ ├── Shraddha Kapoor/Galliyan (Unplugged).mp3 # version variant preserved
|
|
84
|
+
│ └── Unknown Artist/Track 06.mp3 # tag-less fallback
|
|
85
|
+
└── _duplicates/.../RISHAV/05-Hamdard.mp3 # loser preserved by path
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then to roll the whole thing back:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
musicorg --library demo undo
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Power-user CLI
|
|
97
|
+
|
|
98
|
+
The wizard is just a thin orchestrator around individual commands. Every phase is also a standalone subcommand:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
musicorg --library home scan ~/Music
|
|
102
|
+
musicorg --library home dedupe # or --interactive for TUI
|
|
103
|
+
musicorg --library home resolve
|
|
104
|
+
musicorg --library home plan
|
|
105
|
+
musicorg --library home apply --dry-run
|
|
106
|
+
musicorg --library home apply --mode move # commit; generates undo_<TS>.sh
|
|
107
|
+
musicorg --library home undo
|
|
108
|
+
|
|
109
|
+
musicorg --library home canonicalize # tiered iTunes → JioSaavn → Shazam
|
|
110
|
+
musicorg --library home review --export # write a review CSV
|
|
111
|
+
$EDITOR ~/.local/share/musicorg/home/19_review.csv # fill the approve column
|
|
112
|
+
musicorg --library home review --import ~/.local/share/musicorg/home/19_review.csv
|
|
113
|
+
musicorg --library home approve --rule "jiosaavn>shazam>itunes" # batch alternative
|
|
114
|
+
musicorg --library home canonical-apply --dry-run
|
|
115
|
+
musicorg --library home canonical-apply
|
|
116
|
+
musicorg --library home canonical-undo --latest
|
|
117
|
+
|
|
118
|
+
musicorg --library home refingerprint # Shazam pass + harvest Apple Music URLs
|
|
119
|
+
musicorg --library home upgrade --cookies ./cookies.txt --wvd ./device.wvd
|
|
120
|
+
musicorg --library home recover-staging # rescue orphans from failed gamdl runs
|
|
121
|
+
musicorg --library home permanent-skip-report
|
|
122
|
+
|
|
123
|
+
# Textual TUIs
|
|
124
|
+
musicorg --library home review --interactive # canonical CSV-edit-reimport flow
|
|
125
|
+
musicorg --library home dedupe --interactive # two-pane dup picker
|
|
126
|
+
musicorg --library home fill # per-row card resolver
|
|
127
|
+
|
|
128
|
+
# Config
|
|
129
|
+
musicorg config init
|
|
130
|
+
musicorg config set acoustid.api_key XXXX
|
|
131
|
+
musicorg config show
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Run `musicorg --help` for the full list (~30 commands).
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## State + output layout
|
|
139
|
+
|
|
140
|
+
For a library at `~/Music` (slug `home`), state lives at `~/.local/share/musicorg/home/`:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
~/.local/share/musicorg/home/
|
|
144
|
+
├── config.ini # per-library overrides
|
|
145
|
+
├── 01_tags.csv # scan
|
|
146
|
+
├── 07_winners.csv, 07_duplicates.csv, 07_groups.csv # dedupe
|
|
147
|
+
├── 08_resolved.csv # resolve
|
|
148
|
+
├── 09_plan.csv # plan
|
|
149
|
+
├── 16_merged.csv # canonicalize (merged tier view)
|
|
150
|
+
├── 17_dryrun_diff.csv # canonical-apply --dry-run
|
|
151
|
+
├── 19_review.csv, 19_approvals.json # user-approval round-trip
|
|
152
|
+
├── 30_shazam_refingerprint.csv # refingerprint
|
|
153
|
+
├── upgrade_skips.csv # permanent-skip taxonomy
|
|
154
|
+
├── backups/tag_snapshot_<TS>.json # tag snapshots
|
|
155
|
+
├── logs/<phase>.log
|
|
156
|
+
└── undo_<TS>.sh, undo_phase18_<TS>.py, undo_upgrade_<TS>.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Organized output (after `apply`):
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
<library_root>/Music/
|
|
163
|
+
├── Bollywood/<decade>/<movie (year)>/NN - Track.mp3
|
|
164
|
+
├── Hollywood/<decade>/<artist>/<album (year)>/NN - Track.mp3
|
|
165
|
+
├── Singles/{Bollywood,Punjabi,Hollywood}/<artist>/Track.mp3
|
|
166
|
+
├── _duplicates/<original-subpath>/ # dup losers preserved
|
|
167
|
+
├── _misc/ # non-audio sweep target
|
|
168
|
+
├── _replaced/ # original lossy after ALAC upgrade
|
|
169
|
+
└── _upgrade_staging/<run-id>/ # per-run gamdl staging
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Global config: `~/.config/musicorg/config.ini`.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Safety
|
|
177
|
+
|
|
178
|
+
- **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).
|
|
179
|
+
- **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).
|
|
180
|
+
- **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.
|
|
181
|
+
- **gamdl idempotency trap mitigation.** Each upgrade run uses a unique staging subdir so gamdl re-downloads instead of silently no-op'ing.
|
|
182
|
+
- **`audioTraits` is never trusted.** Every gamdl output is ffprobe'd; tracks claimed lossless but served as AAC are marked `alac_listed_but_not_servable`.
|
|
183
|
+
- **Collision handling.** Filename collisions get ` (2)`, ` (3)` suffixes, logged to `11_collisions.csv`.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Permanent skip taxonomy
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
lossy_only_on_apple_music gamdl confirms ALAC unavailable
|
|
191
|
+
alac_listed_but_not_servable audioTraits claims lossless but ffprobe sees AAC
|
|
192
|
+
remix_dj_not_on_apple remix/mashup absent from Apple's catalog
|
|
193
|
+
wrong_match_permanent Apple URL points to a different recording
|
|
194
|
+
shazam_no_match audio fingerprint failed
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`musicorg permanent-skip-report` shows counts + paths.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## What's where
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
src/musicorg/ library — pure Python, no CLI deps
|
|
205
|
+
├── clean.py junk regex, query prep, version-marker logic
|
|
206
|
+
├── tags.py mutagen → ffprobe → mediainfo cascade + writers
|
|
207
|
+
├── identity.py audio-stream sha256 (content-addressed join key)
|
|
208
|
+
├── models.py dataclasses (Track, ResolvedTrack, TierMatch, ApplyResult, ProgressEvent, SkipReason)
|
|
209
|
+
├── config.py XDG-Linux config + per-library state dirs
|
|
210
|
+
├── scan.py walk + per-file tag read
|
|
211
|
+
├── dedupe.py group + score winners
|
|
212
|
+
├── resolve.py folder/tag/filename reconciliation + country heuristics
|
|
213
|
+
├── planner.py destination tree building + album-counts demotion
|
|
214
|
+
├── executor.py move/copy/symlink + collision handling + undo .sh
|
|
215
|
+
├── misc.py non-audio sweep
|
|
216
|
+
├── zip_probe.py detect zip backups of already-organized folders
|
|
217
|
+
├── canonicalize.py diff + apply + guardrails
|
|
218
|
+
├── backup.py snapshot + thin undo script generator
|
|
219
|
+
├── approval.py CSV round-trip + batch rule
|
|
220
|
+
├── upgrade.py upgrade orchestration + ffprobe verification + skip taxonomy
|
|
221
|
+
├── refingerprint.py Shazam pass + orphan recovery
|
|
222
|
+
├── extensions/ plugin protocol for third-party upgraders (gamdl, ...)
|
|
223
|
+
│ └── protocol.py UpgradeExtension, UpgradeCandidate, UpgradeResult, PreflightResult
|
|
224
|
+
└── lookup/
|
|
225
|
+
├── itunes.py, jiosaavn.py, shazam.py
|
|
226
|
+
├── scoring.py title × 0.55, artist × 0.25, duration × 0.20, +bonuses/-penalties
|
|
227
|
+
├── breaker.py circuit breaker for unofficial APIs
|
|
228
|
+
└── __init__.py chain() orchestrator
|
|
229
|
+
|
|
230
|
+
src/musicorg_cli/ reference CLI consumer — Typer + Textual + Rich
|
|
231
|
+
├── main.py Typer entry — 30 subcommands
|
|
232
|
+
├── wizard.py guided end-to-end wizard
|
|
233
|
+
└── tui/ review_app, fill_app, dup_review_app, canonical_app
|
|
234
|
+
|
|
235
|
+
tests/fixtures/build_fixture.py regenerates the demo library
|
|
236
|
+
|
|
237
|
+
examples/ embedding patterns
|
|
238
|
+
├── 01_basic_scan.py
|
|
239
|
+
├── 02_progress_callback.py
|
|
240
|
+
├── 03_full_pipeline.py
|
|
241
|
+
├── 04_custom_extension.py
|
|
242
|
+
├── 05_embed_in_fastapi.py
|
|
243
|
+
└── 06_embed_in_pyside.py
|
|
244
|
+
|
|
245
|
+
PUBLIC_API.md library API contract (SemVer after v1.0)
|
|
246
|
+
install.sh distro-aware installer
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
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`.
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Uninstall
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
./install.sh --uninstall # removes the venv / user install
|
|
258
|
+
rm -rf ~/.config/musicorg ~/.local/share/musicorg # optional: nukes config + library state
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "musicorg"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Linux terminal music organizer + canonical-metadata + lossless upgrader"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "Music Upgrader"}]
|
|
13
|
+
keywords = ["music", "library", "organizer", "tags", "id3", "metadata", "alac", "lossless", "shazam", "itunes", "jiosaavn", "cli", "tui"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Environment :: Console :: Curses",
|
|
18
|
+
"Intended Audience :: End Users/Desktop",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
28
|
+
"Topic :: Multimedia :: Sound/Audio :: Analysis",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
|
+
"Topic :: System :: Filesystems",
|
|
31
|
+
"Topic :: Utilities",
|
|
32
|
+
"Typing :: Typed",
|
|
33
|
+
]
|
|
34
|
+
dependencies = [
|
|
35
|
+
"mutagen>=1.47",
|
|
36
|
+
"requests>=2.31",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/R15hav/musicorg"
|
|
41
|
+
Repository = "https://github.com/R15hav/musicorg"
|
|
42
|
+
Documentation = "https://github.com/R15hav/musicorg/blob/main/PUBLIC_API.md"
|
|
43
|
+
Issues = "https://github.com/R15hav/musicorg/issues"
|
|
44
|
+
Changelog = "https://github.com/R15hav/musicorg/releases"
|
|
45
|
+
|
|
46
|
+
[project.optional-dependencies]
|
|
47
|
+
shazam = ["shazamio>=0.7"]
|
|
48
|
+
cli = [
|
|
49
|
+
"typer>=0.12",
|
|
50
|
+
"textual>=0.60",
|
|
51
|
+
"rich>=13.7",
|
|
52
|
+
]
|
|
53
|
+
web = []
|
|
54
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.23"]
|
|
55
|
+
|
|
56
|
+
[project.scripts]
|
|
57
|
+
musicorg = "musicorg_cli.main:app"
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.packages.find]
|
|
60
|
+
where = ["src"]
|
|
61
|
+
include = ["musicorg*", "musicorg_cli*"]
|
|
62
|
+
|
|
63
|
+
[tool.setuptools.package-data]
|
|
64
|
+
"musicorg" = ["py.typed"]
|
musicorg-0.2.0/setup.cfg
ADDED