v0.10.0: a first launch that welcomes you

Three things that only ever hurt new users.

The startup font modal is gone. _ensure_now_playing_font ran before
MainWindow existed, so on a machine without Century Gothic the very first
thing LinTunes did was open a parentless dialog demanding a font decision.
theme.NOW_PLAYING_FALLBACKS now picks the closest geometric sans installed
(URW Gothic, the Avant Garde clone CG derives from, leads the chain), and
the choice moved to Preferences ▸ Now-playing font. An uninstalled saved
family falls back to automatic instead of the app default.

A non-iTunes user can finally set their music folder. library.music_folder
was written in exactly one place — the iTunes importer — and with it unset
_music_import_dir fell back to a *relative* Path("Music"), resolved against
a working directory GNOME's dash does not set predictably (see
packaging/install-desktop.sh). Music scattered somewhere unfindable. Now
the first launch asks one plain-language question, Preferences can change
it later, and an import with no folder set refuses rather than guessing.
Picking ~/Music files into ~/Music, not ~/Music/Music.

music_folder is portable at last. It was the only path in the library
stored raw absolute in the *synced* metadata, so machine 2 inherited
machine 1's paths. It is now also stored relative to the data dir, the
same trick Track.location has used all along. The absolute key stays
forever as the shared floor between versions: old code reads it and
behaves exactly as before, and old code that writes the file just drops
the new keys, so no version combination hard-fails.

Two traps worth naming. set_music_folder must call
mark_library_settings_dirty() or reload_from_disk reverts the change on
the next sync tick. And the dirty flag only guards until flush, so a
music_folder_set_at stamp decides adoption semantically — _merge_metadata
picks the whole file by mtime, which moves when someone resizes a column
(the Round 39 lesson, applied to metadata).

Also: correct CLAUDE.md's claim that the importer skips smart playlists —
it imports them; system playlists are what's skipped. README gains a
"never used a terminal?" on-ramp and loses the instruction to hand-write
library_metadata.json before first launch.

Verified against the live 21,490-track library: still resolves (via the
legacy key), metadata untouched, 646 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4Z46BMQYS57bcbxWbSS3C
This commit is contained in:
2026-08-22 13:04:24 -04:00
co-authored by Claude Opus 5
parent 574e476dc3
commit 9ef59dd3b2
15 changed files with 1340 additions and 124 deletions
+25 -2
View File
@@ -113,13 +113,36 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
- **`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
ext4). Imports user playlists, folders **and smart playlists** (criteria
parsed by `smart.parse_itunes_smart`; criteria it can't model are kept as a
static snapshot — `report.smart_unsupported`). System playlists are skipped
(`report.skipped_system`). 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.
Because it is synced, **anything machine-specific belongs in `config.py`'s
`config.json` instead** (see `music_folder.py`). `now_playing_font` has three
states — `None` automatic (best available from `theme.NOW_PLAYING_FALLBACKS`),
`""` the app font, or an explicit family. Startup never prompts for it; the
row in Preferences is the only place it's chosen.
- **`lintunes/music_folder.py`** — where the organized tree lives *on this
machine*. The library stores it three ways (`models/library.py`):
`music_folder` absolute (what pre-0.10 code reads — kept forever as the shared
floor between versions), `music_folder_rel` relative to the data dir (the
portable one, same trick as `Track.location`), and `music_folder_set_at`, a
stamp so a merge prefers the newest *setting* rather than the newest *file*.
`resolve()` tries rel → legacy → the per-machine override in `config.json`,
and every candidate must **exist** — the override is consulted last so a
machine that once needed one isn't pinned to it forever. Two rules that bite:
`LibraryManager.set_music_folder` must call `mark_library_settings_dirty()`
(without it `reload_from_disk` reverts the change on the next sync tick), and
`organize_root()` can return `None`, which every caller must handle rather
than inventing a relative path — `MainWindow._music_import_dir` used to fall
back to `Path("Music")`, which resolved against a working directory GNOME's
dash doesn't set predictably.
- **`lintunes/mpris.py`** — registers `org.mpris.MediaPlayer2.lintunes` over D-Bus
so the desktop's media keys / now-playing popup control playback. Spacebar and