Metadata-Version: 2.4
Name: music-classifier-cleaner
Version: 0.3.1
Summary: Classify, organise, BPM-tap and download music
Requires-Python: >=3.10,<4.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: click (>=8.3.1,<9.0.0)
Requires-Dist: mutagen
Requires-Dist: pygame (>=2.6.1,<3.0.0)
Requires-Dist: pygame-ce (>=2.5.7,<3.0.0)
Requires-Dist: rapidfuzz
Requires-Dist: requests
Requires-Dist: tqdm
Requires-Dist: yt-dlp
Description-Content-Type: text/markdown

# Music Classifier & Cleaner

Classifies a music library by artist genre (via MusicBrainz), reorganises folders by genre, and taps BPMs for local MP3 collections.

## Commands

| Command | Purpose |
|---|---|
| `classify-organize` | Scan library, classify artists by genre via MusicBrainz, reorganise into genre folders |
| `scan-library` | Scan library for artists with few songs, output a CSV for manual review |
| `discover-from-library` | Process the review CSV — remove artist folders or explore top tracks via Deezer |
| `tag-library-genres` | Tag all audio files with top 3 MusicBrainz genres + language tag |
| `archive-download` | Download from archive.org, tap BPM interactively, and organize by BPM range |
| `music-tapper` | Tap BPM for local MP3s in a folder and organize by BPM range |
| `download-ytmusic` | Download audio from YouTube Music playlists as MP3 |

All commands are available system-wide after `pip install -e .`. Alternatively use `poetry run <command>`.

## Configuration

| Variable | Required | Description |
|---|---|---|
| `LASTFM_API_KEY` | No | Last.fm API key — enables Last.fm fallback when MusicBrainz has no data for an artist. Get one at https://www.last.fm/api/account/create |

Set it in your shell profile or pass it inline:

```bash
export LASTFM_API_KEY=your_key_here
# or
LASTFM_API_KEY=your_key_here classify-organize /path
```

---

### `classify-organize` — Classify and organise by genre

Scans the library root for artist folders (any top-level directory that isn't a genre folder), plus loose MP3s without a parent artist folder. For each artist:

1. **Deduplicates** similar folder names using fuzzy matching (e.g. `"Greenday"` → `"Green Day"`)
2. **Renames** the folder to the canonical name, merging if the target already exists
3. **Updates** the `artist` ID3 tag in every MP3 inside the folder to match the canonical name
4. **Queries MusicBrainz** for the artist's genre tags
5. **Classifies** the tags into one of the predefined genre buckets via keyword matching
6. **Moves** the entire artist folder into the matching genre subfolder (or `other/` if nothing matched)

**Last.fm fallback:** If MusicBrainz has no data for an artist, the tool falls back to Last.fm tags. Set the `LASTFM_API_KEY` environment variable to enable this (see [Configuration](#configuration)).

```bash
classify-organize /path/to/music/library
```

---

### `scan-library` — Scan for sparse artists

Walks every genre subfolder, counts songs per artist, and writes a CSV of artists with few songs for manual review.

- Skips empty artist folders (prompts to delete them)
- Also checks loose MP3s in the library root (tagged as genre `"root"`)
- Outputs `artists_to_review.csv` in the library root with columns: `artist`, `genre`, `song_count`, `path`, `decision`

```bash
# Default threshold: 4 songs
scan-library /path/to/music/library

# Custom threshold
scan-library /path/to/music/library -t 3
```

After filling in the `decision` column (`remove` or `explore`), process the CSV with `discover-from-library`.

---

### `discover-from-library` — Process review decisions

Reads the CSV produced by `scan-library` and acts on each row:

- **`remove`** — deletes the entire artist folder via `shutil.rmtree`
- **`explore`** — looks up the artist on Deezer and prints their top 5 tracks with durations

```bash
discover-from-library /path/to/artists_to_review.csv
```

---

### `tag-library-genres` — Tag library with genres from MusicBrainz

Recursively walks every `.mp3` and `.flac` file in the library. For each file:

1. **Reads the artist** from the file's metadata (EasyID3 for MP3, FLAC Vorbis comments for FLAC)
2. **Looks up the artist** on MusicBrainz — fetches genre tags and detects language from tags
3. **Verifies the match** — only tags the file if the MusicBrainz matched name matches the file's artist tag (case-insensitive). Skips if they differ (e.g. MusicBrainz returned a different artist)
4. **Writes tags** — up to 3 genre tags and an ISO 639-2 language code
5. **Skips already-tagged** files — checks existing tags before querying or writing

Caches MusicBrainz results per artist so files by the same artist only trigger one API lookup.

**Last.fm fallback:** If MusicBrainz has no genre data for an artist, the tool falls back to Last.fm tags. Set the `LASTFM_API_KEY` environment variable to enable this (see [Configuration](#configuration)). Language detection is not available from Last.fm.

```bash
# Preview only (no files are modified)
tag-library-genres /path/to/music/library -n

# Tag all files
tag-library-genres /path/to/music/library
```

**Tags written:**

| Format | Genre | Language |
|---|---|---|
| MP3 | `TCON` frame — comma-separated string (e.g. `"Swing, Jazz, Big Band"`) | `TLAN` frame — ISO 639-2 code (e.g. `"eng"`) |
| FLAC | Multiple `GENRE` Vorbis comments | `LANGUAGE` Vorbis comment |

---

### `archive-download` — Download from archive.org

Downloads songs from archive.org, measures BPM interactively, and organizes files into BPM-range folders. Files that already have a BPM tag or `[BPM]` in the filename are skipped.

```bash
archive-download \
    --text "swing jazz" \
    --artist "count basie" \
    --year-from 1930 \
    --year-to 1945 \
    --out library/
```

---

### `music-tapper` — Tap BPM for local MP3s

Scans a single folder for MP3 files and opens an interactive BPM tapping UI. Files that already have a BPM tag or `[BPM]` in the filename are skipped. Organizes processed files into BPM-range folders (tens: `60s/`, `70s/`, ..., `250s/`).

```bash
music-tapper /path/to/mp3s
music-tapper /path/to/mp3s --out /path/to/output
```

**BPM Tapping Controls:**

| Key / Button | Action |
|---|---|
| `SPACE` | Tap BPM |
| `ENTER` | Save BPM |
| `ESC` | Exit |
| `[Play]` | Play |
| `[Pause]` | Stop |
| `[Restart]` | Restart track |
| `[next]` | Skip track |

Files are renamed to `(BPM) Artist - Title.mp3` and moved into BPM-range folders under the output directory.

---

### `download-ytmusic` — Download from YouTube Music playlists

Downloads audio from YouTube Music playlist URLs and converts to MP3 (320k) via ffmpeg.

```bash
download-ytmusic "https://music.youtube.com/playlist?list=PL..."
download-ytmusic "https://music.youtube.com/playlist?list=PL..." --out /path/to/dir
```

---

## Reorganizing existing music by BPM

A helper script is included for bulk-reorganizing existing MP3s into BPM tens-range folders:

```bash
# Preview what would move
python scripts/reorganize_by_bpm.py /path/to/music --dry-run

# Actually move files
python scripts/reorganize_by_bpm.py /path/to/music
```

Reads BPM from the `(BPM)` prefix in filenames. Files without a BPM tag are left in place.

---

## Running tests

```bash
# Run all tests (125 tests)
pytest tests/ -v

# Run with coverage
pytest tests/ --cov=music_classifier --cov-report=term-missing -v

# Generate HTML coverage report
pytest tests/ --cov=music_classifier --cov-report=html
open htmlcov/index.html
```

## Project structure

```
music-classifier-cleaner/
├── pyproject.toml              # Poetry config + console_scripts entry points
├── README.md
├── scripts/
│   └── reorganize_by_bpm.py    # Bulk reorganize MP3s into BPM tens-range folders
├── music_classifier/           # Installable Python package
│   ├── __init__.py
│   ├── utils.py                # Genre classification, MusicBrainz, tagging
│   ├── cli.py                  # CLI entry point functions (argparse)
│   ├── audio.py                # BPM read/write, sanitize, normalize filenames
│   ├── pipeline.py             # Click CLI: archive-download + music-tapper
│   ├── archive.py              # Archive.org search & download
│   ├── bpm_ui.py               # Pygame BPM tapping UI
│   ├── bpm_organizer.py        # BPM range folder mapping (tens: 60s, 70s, ...)
│   ├── csv_store.py            # CSV tracking of processed files
│   └── youtube.py              # YouTube Music playlist downloader
└── tests/
    ├── __init__.py
    ├── test_utils.py           # 95 tests for utils.py
    └── test_tapper.py          # 30 tests for audio, bpm_organizer, csv_store (125 total)
```

