v0.23.0: covers for the whole album

- Multi-select Get Info keeps the art square; a pasted cover goes into
  every selected track (shared cover shown, else "mixed artwork").
- Paste Artwork button + Ctrl+V anywhere outside a text field; a caption
  says what happened. Clipboard reads that don't decode are retried and
  never staged; every attempt is logged at INFO.
- Album art search queries Deezer alongside iTunes and ranks by album
  match (iTunes has no copy of Digable Planets' Reachin' at all).
- embed_artwork shared by Get Info and Download Album Art; single-track
  Get Info now invalidates the MPRIS art cache.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-22 09:38:39 -07:00
co-authored by Claude Opus 5
parent d448f64c2b
commit 3150df0f89
9 changed files with 723 additions and 93 deletions
+14
View File
@@ -195,6 +195,20 @@ 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/art_search.py`** — Download Album Art. Queries **both** the
iTunes Search API and Deezer's public album search (neither needs a key)
and merges them through `rank_candidates`: album match beats artist match,
so a right-artist, wrong-album hit never wins. Two sources because neither
catalog is complete: iTunes has **no copy at all** of Digable Planets'
*Reachin'*, which Deezer finds on the first query (Round 60). One source
failing is not an error while the other answers. Embedding, whether from
there or from Get Info, goes through `gui/art_ops.embed_artwork`: write,
`mpris.invalidate_artwork`, size refresh. Get Info's `ArtSquare` **never
stages bytes that don't decode** (`clipboard_image` checks every candidate
with `QImage.loadFromData`), because a clipboard can advertise `image/png`
and hand over nothing. Every paste attempt is logged at INFO
(`lintunes.gui.info_dialog`), so it shows up in the journal.
- **`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