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.
Files changed (44) hide show
  1. musicorg-0.2.0/PKG-INFO +310 -0
  2. musicorg-0.2.0/README.md +265 -0
  3. musicorg-0.2.0/pyproject.toml +64 -0
  4. musicorg-0.2.0/setup.cfg +4 -0
  5. musicorg-0.2.0/src/musicorg/__init__.py +221 -0
  6. musicorg-0.2.0/src/musicorg/approval.py +307 -0
  7. musicorg-0.2.0/src/musicorg/backup.py +736 -0
  8. musicorg-0.2.0/src/musicorg/canonicalize.py +710 -0
  9. musicorg-0.2.0/src/musicorg/clean.py +228 -0
  10. musicorg-0.2.0/src/musicorg/config.py +239 -0
  11. musicorg-0.2.0/src/musicorg/dedupe.py +160 -0
  12. musicorg-0.2.0/src/musicorg/executor.py +318 -0
  13. musicorg-0.2.0/src/musicorg/extensions/__init__.py +23 -0
  14. musicorg-0.2.0/src/musicorg/extensions/protocol.py +236 -0
  15. musicorg-0.2.0/src/musicorg/identity.py +115 -0
  16. musicorg-0.2.0/src/musicorg/lookup/__init__.py +118 -0
  17. musicorg-0.2.0/src/musicorg/lookup/breaker.py +81 -0
  18. musicorg-0.2.0/src/musicorg/lookup/itunes.py +110 -0
  19. musicorg-0.2.0/src/musicorg/lookup/jiosaavn.py +130 -0
  20. musicorg-0.2.0/src/musicorg/lookup/scoring.py +130 -0
  21. musicorg-0.2.0/src/musicorg/lookup/shazam.py +199 -0
  22. musicorg-0.2.0/src/musicorg/misc.py +110 -0
  23. musicorg-0.2.0/src/musicorg/models.py +151 -0
  24. musicorg-0.2.0/src/musicorg/planner.py +165 -0
  25. musicorg-0.2.0/src/musicorg/refingerprint.py +464 -0
  26. musicorg-0.2.0/src/musicorg/resolve.py +175 -0
  27. musicorg-0.2.0/src/musicorg/scan.py +163 -0
  28. musicorg-0.2.0/src/musicorg/tags.py +281 -0
  29. musicorg-0.2.0/src/musicorg/upgrade.py +529 -0
  30. musicorg-0.2.0/src/musicorg/zip_probe.py +111 -0
  31. musicorg-0.2.0/src/musicorg.egg-info/PKG-INFO +310 -0
  32. musicorg-0.2.0/src/musicorg.egg-info/SOURCES.txt +42 -0
  33. musicorg-0.2.0/src/musicorg.egg-info/dependency_links.txt +1 -0
  34. musicorg-0.2.0/src/musicorg.egg-info/entry_points.txt +2 -0
  35. musicorg-0.2.0/src/musicorg.egg-info/requires.txt +16 -0
  36. musicorg-0.2.0/src/musicorg.egg-info/top_level.txt +2 -0
  37. musicorg-0.2.0/src/musicorg_cli/__init__.py +12 -0
  38. musicorg-0.2.0/src/musicorg_cli/main.py +704 -0
  39. musicorg-0.2.0/src/musicorg_cli/tui/__init__.py +1 -0
  40. musicorg-0.2.0/src/musicorg_cli/tui/canonical_app.py +626 -0
  41. musicorg-0.2.0/src/musicorg_cli/tui/dup_review_app.py +418 -0
  42. musicorg-0.2.0/src/musicorg_cli/tui/fill_app.py +317 -0
  43. musicorg-0.2.0/src/musicorg_cli/tui/review_app.py +508 -0
  44. musicorg-0.2.0/src/musicorg_cli/wizard.py +490 -0
@@ -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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+