v0.15.0: ask the song what it is

Right-click → Identify Track… fingerprints the file with Chromaprint's
fpcalc and asks AcoustID who it is, then proposes the tags it found.

The year is the point. A 60s song kept getting stamped with the year its
CD reissue came out, which sorts the library wrong, so every candidate
carries the recording's *original* year — the earliest release across all
of its release groups — including the candidate that proposes a later
greatest-hits as the album. Ranking separately prefers a plain studio
Album over EP/Single over anything wearing a Compilation or Live
secondary type, so the default proposal is the real album as well.

Nothing is written until accepted: the dialog shows each current value
beside an editable proposed one, and a row starts checked only where the
proposal actually differs from what the file already has, so an untouched
field can never quietly blank a tag. Applying goes through
edit_track_fields, which is what buys the tag write, the abort-if-it-fails
rule, artist/album file relocation and one undo step without re-deriving
any of them.

A selection is a queue reviewed one track at a time; a failure is a
status-bar message that moves on rather than ending the batch. fpcalc is
detected at runtime like ffmpeg and the AcoustID key lives in preferences
(so it syncs, and stays out of git) — the round adds no new dependency.

Also fixes a pre-existing round 40 test that used the real tummult mount
point as its stand-in for an unreachable path, so it failed whenever that
drive was plugged in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsiFHdyVg1UhBSxDJfTNRm
This commit is contained in:
2026-09-02 15:40:52 -04:00
co-authored by Claude Opus 5
parent 8c097faacc
commit f0d4d1eff2
12 changed files with 946 additions and 2 deletions
+22
View File
@@ -195,6 +195,28 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
Stdlib only, cached, and every failure path returns `None` — a machine with no
Syncthing must still merge, just without naming anyone.
- **`lintunes/fingerprint.py`** — "Identify Track…": Chromaprint's `fpcalc`
CLI (detected with `shutil.which`, the `ffmpeg_available()` pattern — a
runtime tool, never a pip dep) fingerprints the file, and AcoustID's web API
says what it is. Shaped like `art_search.py`: pure parse/rank helpers first
(tested offline against canned JSON), then subprocess/network, then
`TrackIdentifier(QObject)` on a daemon thread emitting a dict carrying either
`candidates` or `error`. **The year rule is the point of the feature**: every
candidate carries the recording's *original* year — the earliest release
across **all** its release groups — even the candidate proposing a later
compilation as the album, because a 60s song must not sort by the year its CD
reissue came out. `_releasegroup_sort_key` separately ranks a plain studio
Album above EP/Single above anything with a Compilation/Live secondary type,
so the default proposal is the real album. `gui/identify_dialog.py` is passive
(the `AlbumArtDialog` contract — nothing written until accepted); the caller
applies `result_fields()` through `LibraryManager.edit_track_fields`, which is
what buys tag writes, undo and artist/album file relocation for free. A row is
pre-checked only where the proposal *differs* from the current value, so an
unchecked field can never quietly blank a tag. The API key is the user's own,
in `preferences.acoustid` (the Last.fm precedent — preferences.json syncs, so
it never belongs in git). Audio under ~3 s has no fingerprint at all
("Empty fingerprint"), which is a reported failure, not a crash.
- **`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`.