Add CLAUDE.md
Codebase guide for future Claude Code sessions: commands, the storage→model→manager→GUI architecture, and Qt/Wayland gotchas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
120
CLAUDE.md
Normal file
120
CLAUDE.md
Normal file
@ -0,0 +1,120 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this is
|
||||
|
||||
LinTunes is an iTunes-replacement music library manager and player for Linux,
|
||||
built with PyQt6 / Qt Multimedia. It imports an iTunes 12 library, stores the
|
||||
library as plain JSON (syncable via Syncthing), and reproduces the iTunes UI
|
||||
(left sidebar + right playlist view, column browser, customizable per-playlist
|
||||
columns). `spec.md` is the original design brief; `TASKS.md` is the live backlog
|
||||
and `tasks-done.md` records completed work.
|
||||
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
pip install -e . # install deps (PyQt6, mutagen, numpy, requests)
|
||||
python3 -m lintunes.main # run the app straight from the checkout
|
||||
python3 -m pytest # run the whole test suite
|
||||
python3 -m pytest tests/test_round9.py # one test file
|
||||
python3 -m pytest tests/test_round9.py::test_name # one test
|
||||
```
|
||||
|
||||
The app needs a `--data-dir`; config persists to `~/.config/lintunes/config.json`.
|
||||
One-time iTunes import:
|
||||
|
||||
```sh
|
||||
lintunes --import-xml "iTunes Library.xml" \
|
||||
--music-root "/path/to/iTunes Media" \
|
||||
--data-dir /path/to/library-data --save-config
|
||||
```
|
||||
|
||||
Runtime needs FFmpeg codecs for Qt Multimedia (`qt6-qtmultimedia` w/ ffmpeg).
|
||||
Tests run headless via the `qapp` fixture in `tests/conftest.py`
|
||||
(`QT_QPA_PLATFORM=offscreen`).
|
||||
|
||||
## Architecture
|
||||
|
||||
**Data flows in one direction through three layers:** storage (JSON on disk) →
|
||||
model (`Library`/`Track`/`Playlist` dataclasses) → `LibraryManager` (mutations +
|
||||
persistence) → GUI (Qt widgets that read the manager and connect to its signals).
|
||||
|
||||
- **`lintunes/main.py`** — CLI entry. `--import-xml` runs a headless import and
|
||||
exits; otherwise `run_gui()` resolves Syncthing conflicts, loads the library,
|
||||
builds `Preferences`/`LibraryManager`/`LastFm`/`MainWindow`, wires MPRIS, and
|
||||
installs the window as a global event filter (for media keys).
|
||||
|
||||
- **`lintunes/models/`** — pure dataclasses (`track.py`, `playlist.py`,
|
||||
`library.py`) with `to_dict`/`from_dict` round-tripping. `PlaylistType` is
|
||||
`REGULAR | FOLDER | SMART | SYSTEM`. Per-playlist `PlaylistSettings` holds
|
||||
visible columns / sort column / widths. Playlists are identified by an 8-char
|
||||
hex `persistent_id` (folder containment via `parent_persistent_id`).
|
||||
|
||||
- **`lintunes/storage/json_storage.py`** — the library is **multiple files** in
|
||||
the data dir: `library.json` (all tracks), `library_metadata.json`, and one
|
||||
`playlists/<persistent_id>.json` per playlist. Writes are atomic (`*.json.tmp`
|
||||
→ rename). **`storage/conflict_resolver.py`** merges Syncthing
|
||||
`*.sync-conflict-*` files on startup: play counts take the max, edited fields
|
||||
take the newest, playlist membership takes the union.
|
||||
|
||||
- **`lintunes/library_manager.py`** — `LibraryManager(QObject)` owns the
|
||||
`Library`, is the single funnel for all mutations, and persists them
|
||||
**debounced** (3 s) with per-area dirty tracking (a play-count bump rewrites
|
||||
only `library.json`; a playlist edit rewrites only that playlist file). User
|
||||
edits go through `undo_stack` (Ctrl+Z); the internal `_apply_*`/`_set_*`
|
||||
helpers do the real mutation + dirty-mark + signal and are reused by undo/redo
|
||||
without recursing. Widgets react to its signals (`playlists_changed`,
|
||||
`playlist_content_changed(pid)`, `track_updated(id)`, `track_fields_edited`).
|
||||
|
||||
- **`lintunes/player.py`** — `Player(QObject)` wraps `QMediaPlayer`/`QAudioOutput`.
|
||||
It is deliberately **context-agnostic** (knows a queue + shuffle walk order, not
|
||||
which view started playback). It exposes `track_changed`/`playing_changed`/
|
||||
`position_changed`/`duration_changed` signals consumed by the transport, MPRIS,
|
||||
and the visualizer (a `QAudioBufferOutput` tee feeds PCM to the spectrum bars).
|
||||
Playback *context* ("library" / "playlist:<pid>") is tracked in `MainWindow`,
|
||||
not the player.
|
||||
|
||||
- **`lintunes/gui/`** — `main_window.py` assembles a top `TransportBar` over a
|
||||
horizontal `QSplitter` (`SidebarPanel` | stacked `LibraryView`/`PlaylistView`).
|
||||
`track_table.py` is the shared track grid (drag/drop, copy/paste, drop
|
||||
indicator). `playlist_ops.py::add_tracks_with_dup_check` is the single funnel
|
||||
for every add-to-playlist path. `theme.py` applies the palette (highlight
|
||||
colors, UI scales) from prefs.
|
||||
|
||||
- **`lintunes/importers/itunes_importer.py`** — parses the iTunes XML plist.
|
||||
Remaps Mac `file:///Volumes/...` paths to the local `--music-root` with
|
||||
case/Unicode-normalization fuzzy matching (macOS is case-insensitive + NFD vs
|
||||
ext4). Imports user playlists/folders only; smart and system playlists are
|
||||
currently skipped. Album art is read live from embedded ID3 tags
|
||||
(`tagging.py`), never stored in the library JSON.
|
||||
|
||||
- **`lintunes/preferences.py`** — app settings in `<data_dir>/preferences.json`
|
||||
(rides the same Syncthing share). `Preferences.set(key, value)` saves and emits
|
||||
`changed`; `MainWindow._on_prefs_changed` re-applies theme/metrics live.
|
||||
|
||||
- **`lintunes/mpris.py`** — registers `org.mpris.MediaPlayer2.lintunes` over D-Bus
|
||||
so the desktop's media keys / now-playing popup control playback. Spacebar and
|
||||
arrow keys are handled locally via `MainWindow.eventFilter`.
|
||||
|
||||
## Conventions & gotchas
|
||||
|
||||
- **Tests are organized as `tests/test_roundN.py`** — each development round adds
|
||||
a new `test_roundN.py` alongside the topical files (`test_models.py`,
|
||||
`test_itunes_importer.py`, etc.). New feature work follows the same pattern.
|
||||
- **Keep `Player` and `track_table` manager-free where they already are** —
|
||||
cross-cutting data is injected via callbacks/signals (e.g. `track_table` takes
|
||||
a `playlists_for_track` callback rather than importing the manager).
|
||||
- **Qt/Wayland gotchas (GNOME/Mutter):** `QDrag.setPixmap` / `setDragCursor` /
|
||||
`QCursor.pos()` are unreliable during a drag — `gui/drag_ghost.py` paints its
|
||||
own child-widget overlay instead. `QAudioOutput` must not be constructed before
|
||||
a `QMainWindow` exists (Qt 6.10 deadlock). Some PyQt signal relays need explicit
|
||||
types/lambdas.
|
||||
- **Never move or rewrite the user's music files** except deliberate tag edits
|
||||
via `tagging.py`. The library JSON is the source of truth for everything else.
|
||||
- `scripts/` holds one-off maintenance tools (`audit_artwork.py`,
|
||||
`recover_artwork.py`, `clear_computed_ratings.py`) run manually against a data
|
||||
dir; most default to dry-run and need `--write` to mutate files.
|
||||
- **Not under version control until recently** — the `*~` files are editor
|
||||
backups (gitignored). `data/` and `itunes-test-library/` are gitignored (the
|
||||
user's real library + large import fixture).
|
||||
Reference in New Issue
Block a user