Skip to content

Commit 5b12137

Browse files
committed
feat: modernize CLI UX with interactive prompts, spinners, and live displays
Add questionary for arrow-key artist selection in select/edit commands, Rich spinners for all long operations, Rich Live table for sync progress, diff panel with colored add/remove tables, delete/sync confirmations, and retry prompts for recoverable failures. Also adds codecov CI integration, pytest coverage thresholds, expanded CLAUDE.md with critical rules, Makefile dev/format/check-all targets, pre-push validation script, releasing docs, testing checklist, and normalizes docs/ to lowercase filenames.
1 parent 666c3fa commit 5b12137

17 files changed

Lines changed: 658 additions & 650 deletions

.github/workflows/ci.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,14 @@ jobs:
7878
python-version: ${{ matrix.python-version }}
7979
- name: Install package with dev dependencies
8080
run: pip install -e ".[dev]"
81-
- name: Run tests
82-
run: python -m pytest tests/ -v --tb=short
81+
- name: Run tests with coverage
82+
run: python -m pytest tests/ -v --tb=short --cov-report=xml
83+
- name: Upload coverage
84+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
85+
uses: codecov/codecov-action@v5
86+
with:
87+
files: coverage.xml
88+
fail_ci_if_error: false
8389

8490
claude-lint:
8591
name: Claude Lint

CLAUDE.md

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,54 +4,85 @@ A Python CLI for syncing a music library to a classic iPod from a modern Mac.
44

55
## Stack
66

7-
- **Python 3.11+** with Typer, Rich, tqdm, mutagen, pylast
7+
- **Python 3.11+** with Typer, Rich, questionary, tqdm, mutagen, pylast
88
- **SQLite** for library index, playlist storage, and scrobble cache
99
- **Vendored iOpenPodv2** for iPod database management (iTunesDB + ArtworkDB)
1010
- **beets** for metadata cleanup (called via subprocess)
1111

1212
## Project Layout
1313

14-
- `clickwheel/` — Python CLI package
14+
- `clickwheel/cli.py` — Typer command definitions (entry point)
15+
- `clickwheel/output.py` — Rich console helpers (tables, spinners, panels, errors)
16+
- `clickwheel/config.py` — config loading (~/.clickwheel/config.yaml, env vars)
17+
- `clickwheel/db.py` — SQLite database (tracks, playlists, scrobble cache)
18+
- `clickwheel/library.py` — music file scanning (mutagen)
19+
- `clickwheel/autoscan.py` — automatic library scan before commands
20+
- `clickwheel/scrobble.py` — Last.fm scrobbling (pylast)
1521
- `clickwheel/ipod/` — vendored iOpenPodv2 (excluded from ruff)
1622
- `tests/` — pytest test suite
17-
- `scripts/` — bash utilities (audit, fix-metadata, setup)
18-
- `beets/` — beets config template (generated config is gitignored)
19-
- `docs/` — architecture docs
23+
- `scripts/` — bash utilities (audit, fix-metadata, setup, pre-push)
24+
- `docs/` — architecture, releasing, and testing docs
2025

2126
## Commands
2227

2328
- `clickwheel scan` — index library metadata into SQLite (incremental by default)
2429
- `clickwheel fix` — clean up metadata via beets (requires `[fix]` extras)
25-
- `clickwheel select` — interactive iPod subset picker (auto-scans if stale)
30+
- `clickwheel select` — interactive iPod subset picker (questionary checkbox, auto-scans if stale)
2631
- `clickwheel playlist` — list saved playlists
27-
- `clickwheel edit` — add/remove artists from a playlist
28-
- `clickwheel delete` — delete a playlist
29-
- `clickwheel diff` — preview iPod sync changes
30-
- `clickwheel sync` — push playlist to iPod
32+
- `clickwheel edit` — add/remove artists (interactive questionary menus or `--add`/`--remove` flags)
33+
- `clickwheel delete` — delete a playlist (with confirmation)
34+
- `clickwheel diff` — preview iPod sync changes (panel + colored tables)
35+
- `clickwheel sync` — push playlist to iPod (Rich Live table, confirmation)
3136
- `clickwheel ls` — show iPod contents
3237
- `clickwheel eject` — safely unmount iPod
3338
- `clickwheel scrobble` — submit iPod listens to Last.fm (`--auth` for first-time setup)
3439

3540
## Development
3641

3742
```bash
38-
pip install -e '.[dev]' # install with dev dependencies
43+
make dev # install with dev dependencies
44+
make test # run tests with coverage
45+
make lint # ruff check + format check
46+
make format # auto-format code
3947
pre-commit install --hook-type pre-commit --hook-type commit-msg
40-
python -m pytest tests/ -v # run tests
41-
ruff check clickwheel/ # lint
42-
ruff format clickwheel/ # format
4348
```
4449

50+
## Critical Rules
51+
52+
1. **All CLI output goes through `output.py`** — never call `console.print()` directly. A pre-commit pygrep hook enforces this. Use the semantic helpers: `error()`, `warn()`, `success()`, `info()`, `status()`, `dim()`, `confirm()`, `spinner()`.
53+
54+
2. **Never move or rename source files** — Plex reads from the same music library. Metadata changes are written in-place.
55+
56+
3. **The `scan` command is read-only** — it only reads metadata and writes to SQLite. No file modifications.
57+
58+
4. **FLAC files are excluded from iPod sync** — stock iPod firmware doesn't support FLAC. Don't add transcoding.
59+
60+
5. **`clickwheel/ipod/` is vendored code** — excluded from ruff linting. Don't refactor it unless fixing a bug in the iPod database writer.
61+
62+
6. **Interactive prompts use questionary** — not raw `typer.prompt()` or `input()`. Arrow-key selection with `questionary.select()` and `questionary.checkbox()`. Non-interactive flags (`--add`/`--remove`) are kept for scripting.
63+
64+
7. **Long operations use `spinner()` context manager** — beets phases, iPod reads, Last.fm calls, eject. Never leave the user staring at a frozen terminal.
65+
66+
8. **Destructive operations require confirmation** — delete, sync. Use `typer.confirm()` with sensible defaults.
67+
68+
9. **`select`, `edit`, `diff`, `sync` auto-scan** the library if stale. `--no-scan` skips this. Controlled by `auto_scan` and `auto_scan_staleness_minutes` in config.
69+
70+
10. **`fix` requires beets extras**`pip install 'clickwheel[fix]'`. Auto-generates beets config on first run.
71+
72+
## Code Style
73+
74+
- **Naming**: snake_case for functions/variables, UPPER_CASE for constants
75+
- **Error handling**: use `error()` + `raise typer.Exit(1)` for fatal errors. Offer retry for recoverable failures (sync DB write, scrobble submission).
76+
- **Imports**: lazy imports for heavy modules (questionary, subprocess, pylast) — import inside the function that uses them
77+
- **Output colors**: red=errors, yellow=warnings, green=success/confirm, bold=status, dim=hints, cyan=panels
78+
- **Progress**: `tqdm` for scan (many small items), Rich `Live` table for sync (fewer items, show detail), `spinner()` for async waits
79+
4580
## Configuration
4681

4782
Runtime config is in `~/.clickwheel/config.yaml`. Environment variables override the config file. See README for all settings.
4883

49-
## Key Constraints
84+
## Testing
5085

51-
- Never move or rename source files (Plex reads from the same library)
52-
- FLAC files are excluded from iPod sync (stock firmware limitation)
53-
- Metadata changes are written in-place to source files
54-
- The `scan` command is read-only (no file modifications)
55-
- `select`, `edit`, `diff`, `sync` auto-scan the library if stale (configurable, `--no-scan` to skip)
56-
- `fix` requires beets + Pillow (`pip install 'clickwheel[fix]'`); auto-generates beets config on first run
57-
- macOS only (iPod sync depends on macOS disk utilities)
86+
- pytest with coverage threshold (30% minimum, excluding vendored ipod/)
87+
- Coverage runs in CI and uploads to Codecov
88+
- Test matrix: Python 3.11-3.13 on Ubuntu and macOS

CONTRIBUTING.md

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,25 @@
55
```bash
66
git clone https://github.com/pdugan20/clickwheel.git
77
cd clickwheel
8-
pip install -e '.[dev]'
9-
pre-commit install --hook-type pre-commit --hook-type commit-msg
8+
make dev
109
```
1110

11+
This installs the package with dev dependencies and sets up pre-commit hooks.
12+
1213
## Running Tests
1314

1415
```bash
15-
python -m pytest tests/ -v
16+
make test
17+
```
18+
19+
Tests run with coverage reporting. The minimum threshold is 30% (excluding vendored `clickwheel/ipod/`).
20+
21+
## Linting and Formatting
22+
23+
```bash
24+
make lint # check only
25+
make format # auto-fix
26+
make check-all # lint + test + shellcheck + shfmt
1627
```
1728

1829
## Commit Messages
@@ -52,15 +63,22 @@ test: add unit tests for scrobble dedup logic
5263
- Keep the header under 100 characters
5364
- No period at the end of the subject
5465

55-
## Linting
66+
## Building
5667

5768
```bash
58-
ruff check clickwheel/
59-
ruff format clickwheel/
69+
make build
6070
```
6171

62-
## Building
72+
## Releasing
73+
74+
See [docs/releasing.md](docs/releasing.md) for how versioning and releases work.
75+
76+
## Pre-push Checks
77+
78+
Run the full validation suite before pushing:
6379

6480
```bash
65-
python -m build
81+
./scripts/pre-push-checks.sh
6682
```
83+
84+
This checks lint, tests, shell scripts, and debug artifacts.

Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
.PHONY: lint test build clean release release-dry release-patch release-minor release-major
1+
.PHONY: dev lint format test build clean check-all release release-dry release-patch release-minor release-major
2+
3+
dev:
4+
pip install -e '.[dev]'
5+
pre-commit install --hook-type pre-commit --hook-type commit-msg
26

37
lint:
48
ruff check clickwheel/
59
ruff format --check clickwheel/
610

11+
format:
12+
ruff check --fix clickwheel/
13+
ruff format clickwheel/
14+
715
test:
816
python -m pytest tests/ -v
917

@@ -13,6 +21,10 @@ build: clean
1321
clean:
1422
rm -rf dist/ build/ *.egg-info clickwheel/*.egg-info
1523

24+
check-all: lint test
25+
shellcheck scripts/*.sh
26+
shfmt -d scripts/*.sh
27+
1628
release-dry:
1729
semantic-release version --print
1830

README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
A CLI for syncing a music library to a classic iPod from a modern Mac — no iTunes required.
99

10-
Handles the full workflow: scan and clean up your library's metadata, interactively pick what goes on the iPod, and sync with a progress bar.
10+
Handles the full workflow: scan and clean up your library's metadata, interactively pick what goes on the iPod, and sync — all with a modern terminal UI.
1111

1212
## Install
1313

@@ -38,19 +38,19 @@ Commands like `select`, `edit`, `diff`, and `sync` automatically check for libra
3838

3939
## Commands
4040

41-
| Command | Description |
42-
| --------------------- | ----------------------------------------------------------------------------- |
43-
| `clickwheel scan` | Index your music library and report on metadata quality |
44-
| `clickwheel fix` | Clean up metadata, fetch album art, fill genres via beets |
45-
| `clickwheel select` | Interactive picker — browse by artist/album/genre |
46-
| `clickwheel playlist` | List saved playlists or show details for one |
47-
| `clickwheel edit` | Add or remove artists from a playlist (interactive or via `--add`/`--remove`) |
48-
| `clickwheel delete` | Delete a saved playlist |
49-
| `clickwheel diff` | Preview what would be added or removed on the iPod |
50-
| `clickwheel sync` | Push your playlist to the iPod |
51-
| `clickwheel ls` | Show what's on the iPod |
52-
| `clickwheel eject` | Safely unmount the iPod |
53-
| `clickwheel scrobble` | Submit recent iPod listens to Last.fm |
41+
| Command | Description |
42+
| --------------------- | ----------------------------------------------------------------------- |
43+
| `clickwheel scan` | Index your music library and report on metadata quality |
44+
| `clickwheel fix` | Clean up metadata, fetch album art, fill genres via beets |
45+
| `clickwheel select` | Interactive picker — checkbox artist selection |
46+
| `clickwheel playlist` | List saved playlists or show details for one |
47+
| `clickwheel edit` | Add or remove artists via interactive menus or `--add`/`--remove` flags |
48+
| `clickwheel delete` | Delete a saved playlist (with confirmation) |
49+
| `clickwheel diff` | Preview what would be added or removed on the iPod |
50+
| `clickwheel sync` | Push your playlist to the iPod (with live progress table) |
51+
| `clickwheel ls` | Show what's on the iPod |
52+
| `clickwheel eject` | Safely unmount the iPod |
53+
| `clickwheel scrobble` | Submit recent iPod listens to Last.fm |
5454

5555
## Configuration
5656

0 commit comments

Comments
 (0)