Skip to content

feat(library): local artist images + picker UI#33

Merged
InstaZDLL merged 6 commits into
mainfrom
feat/local-artist-images
May 16, 2026
Merged

feat(library): local artist images + picker UI#33
InstaZDLL merged 6 commits into
mainfrom
feat/local-artist-images

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 16, 2026

Closes #31.

Summary

  • Scanner picks up sidecar artist.jpg / <artist_name>.jpg files from up to 3 parent directories and links them via the existing artist.artwork_id column (no migration). Covers both common layouts: <root>/<Artist>/<Album>/track and <root>/<Album>/track with <Artist>.jpg beside it.
  • Resolution priority in get_artist_detail becomes local sidecar → Deezer cache → live Deezer fetch, so offline libraries get real artist photos without network access.
  • Manual override via a pencil-overlay button on the artist photo: pick from Deezer search, upload a local file, or clear back to auto-resolution.
  • Backfill for existing libraries via rescan_local_artist_images (Settings → Library → Local artist images) — needed because the scanner's mtime/size fast path otherwise skips folders whose audio files haven't changed.

What's new

Backend (src-tauri/src/commands/)

  • scan.rs::extract_artist_image + maybe_link_artist_images — walks ancestors, matches ARTIST_IMAGE_STEMS = [artist, performer, band] or any stem whose canonical_name equals the artist's. Strict name-match prevents mistaking cover.jpg for an artist photo in single-folder layouts. Various Artists explicitly excluded.
  • scan.rs::rescan_local_artist_images — backfill command for libraries scanned before this feature.
  • deezer.rssearch_artists_deezer, set_artist_artwork_from_deezer, set_artist_artwork_from_file, clear_artist_artwork. The two set_* overwrite artwork_id unconditionally (explicit user pick > auto-resolution).

Frontend

  • ArtistDetailView prefers artwork_path over picture_path, guards against late-arriving Deezer responses clobbering a local image, and renders a hover-revealed pencil overlay on the artist photo.
  • New ArtistImagePickerModal mirrors CoverPickerModal (Deezer search tab + local file tab + remove action).
  • Settings card under Library → "Local artist images" exposes the rescan command with status feedback.

i18nsettings.localArtistImages and artistImagePicker keys propagated to all 17 locales with native translations.

DocsCLAUDE.md feature catalogue and docs/features/library.md updated with the resolution chain, layout examples, and manual-override flow.

Test plan

  • bun run typecheck clean
  • bun run lint clean
  • cargo check --manifest-path src-tauri/Cargo.toml --all-targets clean
  • cargo test --manifest-path src-tauri/Cargo.toml --lib commands::scan::tests — 4 new tests pass (matches stem in parent dir / canonical-name match / ignores unrelated cover.jpg / empty canonical bail-out)
  • Manual: drop an artist.jpg in an artist folder, run a folder scan, confirm the photo appears on the artist page
  • Manual: rename <Artist>.jpg next to an album folder, scan, confirm match
  • Manual: existing library — click Settings → Library → Local artist images, confirm linked-count summary
  • Manual: hover the artist photo, click the pencil, pick a Deezer result → image updates immediately
  • Manual: open the picker on an artist with a local sidecar, click "Remove image", confirm fallback to Deezer cache
  • Manual offline mode: confirm local sidecars still render (no Deezer round-trip)

Summary by CodeRabbit

  • New Features

    • Local artist image discovery from sidecar files during scans and a rescan action to backfill links.
    • Manual artist image picker on artist pages: search remote service, pick a local file, or remove an image.
    • Artist detail view now prefers local images over remote artwork and provides an edit action.
  • Documentation

    • Library docs updated describing local artist image behavior and manual overrides.
  • Localization

    • Added translations for the artist image picker and local-artist-images setting.

Review Change Stack

InstaZDLL added 2 commits May 16, 2026 17:23
Closes #31. Adds a folder-cover-style fallback for artist images: the
scanner now walks up to three parent directories from each track and
links any `artist.jpg`/`performer.png`/`<artist_name>.jpg` it finds to
the `artist.artwork_id` foreign key (no migration required). Resolution
priority in `get_artist_detail` becomes local sidecar → Deezer cache →
live Deezer fetch, so offline libraries no longer need network access
for artist photos.

A new `rescan_local_artist_images` command (exposed as Settings →
Library → Local artist images) backfills libraries scanned before this
feature shipped. Strict name-match in single-folder layouts prevents
album covers from being mistaken for artist photos, and the
`Various Artists` sentinel is explicitly excluded.
Hover overlay on the artist photo opens a Spotify-style picker modal
with three actions: search Deezer for a photo, upload a local file
(magic-byte validated jpg/png/webp), or remove the current image so
the resolution chain falls back to the Deezer cache.

Backed by four new Tauri commands in commands::deezer (search,
set-from-deezer, set-from-file, clear). User picks overwrite
`artist.artwork_id` unconditionally — an explicit choice beats both
local sidecar resolution and any cached Deezer fetch.
@github-actions github-actions Bot added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) scope: i18n Translations (src/i18n/) scope: docs Docs, README, assets type: feat New feature size: xl > 500 lines labels May 16, 2026
@InstaZDLL InstaZDLL self-assigned this May 16, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 521bb8cd-8877-474d-9758-662b54adfd20

📥 Commits

Reviewing files that changed from the base of the PR and between ea8cb79 and b3bb6e2.

📒 Files selected for processing (6)
  • src-tauri/src/commands/browse.rs
  • src-tauri/src/commands/lyrics.rs
  • src-tauri/src/commands/profile_io.rs
  • src-tauri/src/commands/scan.rs
  • src-tauri/src/commands/tray.rs
  • src-tauri/src/lib.rs

📝 Walkthrough

Walkthrough

Adds local artist image discovery during scans, backend Deezer/file artwork commands with download caps, a React picker modal and artist-detail edit flow, a settings rescan command/UI, TypeScript invoke wrappers, i18n across locales, docs, tests, and small formatting/dep bumps.


Changes

Local and Manual Artist Image Workflow

Layer / File(s) Summary
Scan-time local artist image extraction and linking
src-tauri/src/commands/scan.rs
Core extraction pipeline walks ancestor directories to find artist image sidecars (stem allowlist or canonical name), caches by content hash, and idempotently links to artist.artwork_id during scan fast-path, rewrite, and insert. Tests added for matching and filtering.
Rescan local artist images command & summary
src-tauri/src/commands/scan.rs, src/lib/tauri/library.ts
New Tauri command rescan_local_artist_images scans artists with NULL artwork_id (excluding “Various Artists”), inspects up to 16 tracks per artist, links resolvable local images, and returns ArtistImageScanSummary { considered, linked }.
Deezer artist search and artwork management commands
src-tauri/src/commands/deezer.rs, src/lib/tauri/deezer.ts
Tauri commands: search artists (offline-safe), set artwork from Deezer (download → hash → write → upsert artwork row source=deezer), set artwork from file (detect format → hash → write → upsert source=manual), and clear artwork.
Image download safety
src-tauri/src/commands/deezer.rs
Introduces MAX_IMAGE_BYTES and changes download_image_bytes to check content-length and perform chunked reads with runtime caps to prevent oversized responses.
Artist image picker modal component
src/components/common/ArtistImagePickerModal.tsx
Modal with Deezer search (debounced, requestId to avoid stale results), file picker flow, async handlers for set/clear actions, inline errors, loading states, and state reset on close.
Artist detail view with local image preference and edit button
src/components/views/ArtistDetailView.tsx
Prefer detail.artwork_path over Deezer picture_path when seeding artwork; gate Deezer enrichment to avoid overwriting local images; add overlay Pencil edit button opening the picker and trigger refetch on success.
Settings rescan workflow for local artist images
src/components/views/SettingsView.tsx
Settings UI adds a "Local artist images" rescan card wired to rescanLocalArtistImages(), with loading/state feedback and formatted completion message.
Frontend-to-backend invocation wrappers and command registration
src-tauri/src/lib.rs, src/lib/tauri/deezer.ts, src/lib/tauri/library.ts
Register new backend commands in Tauri handler and export TypeScript wrappers/types: DeezerArtistLite, searchArtistsDeezer, setArtistArtworkFromDeezer, setArtistArtworkFromFile, clearArtistArtwork, and rescanLocalArtistImages.
Internationalization for artist image UI
src/i18n/locales/*.json
Add artistImagePicker and settings.localArtistImages entries across many locales to support picker labels and settings rescan text with interpolated counts.
Feature documentation and catalog updates
docs/features/library.md, CLAUDE.md
Document local sidecar detection (3-level ancestor walk, stem/canonical matching), resolution priority (local → Deezer cache → live fetch), manual overrides, and backfill rescan workflow; minor CLAUDE.md edit.
Build/dev dependency & small formatting
package.json, various UI/Rust/TSX files
Bump devDependencies["@tauri-apps/cli"] to ^2.11.2; several non-functional formatting/refactorings in lyrics, main, splash, and UI files.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes


Possibly related PRs

  • InstaZDLL/WaveFlow#25: Formatting/refactor of lyrics editor synced-word logic that is related to enhanced LRC/TTML word-level parsing used in this PR.

"I nibble through folders, sniff each art by name,
I patch the catalog, no online quest to blame,
A pencil click to pick, or a file to supply,
Local portraits linked — the library looks spry,
Hop on, rescan, and let the images claim!" 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and concisely summarizes the main changes: local artist image detection and picker UI, matching the core functionality described in the changeset.
Description check ✅ Passed The pull request description is well-structured, covering summary, testing approach, and objectives. It includes sections on backend changes, frontend additions, i18n updates, and comprehensive manual testing steps aligned with the template's expectations.
Linked Issues check ✅ Passed The pull request successfully addresses all coding requirements from issue #31: detects local artist images from ancestor directories using stem matching and canonical-name logic, links via artwork_id without migration, provides backfill command, implements manual picker UI with Deezer search/file upload/clear, and maintains offline functionality.
Out of Scope Changes check ✅ Passed Minor formatting changes in unrelated files (splash.html, lyrics editor, LyricsPanel) and a Tauri CLI dependency bump are present but ancillary. Core changes remain tightly focused on local artist image detection, linking, picker UI, and documentation as intended.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/local-artist-images

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/features/library.md`:
- Line 83: The doc sentence claiming rescan picks a single representative track
per artist is inaccurate; update the sentence to reflect that
rescan_local_artist_images (commands/scan.rs::rescan_local_artist_images) will
probe multiple tracks per artist (up to 16) to improve hit rate when running
extract_artist_image, so change the wording from "picks a representative track"
to something like "probes up to 16 tracks per artist" and mention that
already-linked rows remain filtered at the SQL level.

In `@src-tauri/src/commands/deezer.rs`:
- Around line 650-654: The UPDATE statements that set artwork_id currently
ignore whether any row was affected; capture the result of .execute(&pool).await
into a variable, call .rows_affected() and if it is 0 return an Err (e.g., via
anyhow::anyhow! or the crate's error type) indicating "artist not found" so the
caller/UI sees failure; apply this change to the UPDATE block shown (the
sqlx::query("UPDATE artist SET artwork_id = ? WHERE id = ?") call) and the
similar UPDATEs at the other two occurrences (the blocks around the second and
third occurrences referenced in the comment).
- Around line 640-641: The code reads entire images into memory
(download_image_bytes -> used where bytes are hashed with blake3::hash) without
size limits; update the image download/load path to enforce a hard size cap
(e.g. 5–10 MiB) before buffering: modify download_image_bytes (or replace with a
new download_image_bytes_capped) to stream the response/local file, accumulate
into a bounded buffer and return an error if the payload exceeds the cap, and
update the call sites (the places that compute hash from bytes at the
blake3::hash(...) uses around the shown locations) to handle the error instead
of assuming full payload was read.

In `@src-tauri/src/commands/scan.rs`:
- Around line 1431-1451: The call to maybe_link_artist_images(...) happens
before the track_artist rebuild, so when current_count != splits.len() newly
created artist IDs are skipped; move the artist-image linking to after the
branch that rebuilds track_artist entries (the code that checks current_count vs
splits.len() and inserts new artist rows for existing_track_id using tx), or
call maybe_link_artist_images a second time after that rebuild completes,
ensuring you pass the same tx, raw, current_ids/updated IDs, track_path and
artwork_dir so newly created artist IDs are included.

In `@src/components/common/ArtistImagePickerModal.tsx`:
- Around line 55-56: The Deezer search needs a request-sequencing guard so late
responses don't clobber newer results: add a requestIdRef = useRef(0) and,
inside the async search function (the handler that currently uses debounceRef to
call Deezer), increment requestIdRef.current before firing the request, capture
it in a local const requestId, and after the awaited fetch only call setResults
if requestId === requestIdRef.current; optionally also consider using an
AbortController per request and abort the previous controller when starting a
new search. Reference debounceRef and the async search handler (the function
that performs the Deezer fetch) when making this change.

In `@src/components/views/ArtistDetailView.tsx`:
- Around line 277-285: The edit overlay button currently uses absolute inset-0
and covers the whole image so onDoubleClick on the <img> never fires; update the
overlay so it doesn't intercept image clicks — either (preferred) add
pointer-events-none to the overlay container and pointer-events-auto to the
visible interactive button (keep setIsImagePickerOpen and the aria attributes),
or shrink the button hit area (remove absolute inset-0 and use explicit
width/height and placement like right-2 bottom-2) so the <img> can receive
double-clicks/lightbox events while still preserving the edit affordance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f4351237-fbd4-4f30-b99f-bdb1f18b2d75

📥 Commits

Reviewing files that changed from the base of the PR and between 9bbdd64 and d277519.

📒 Files selected for processing (27)
  • CLAUDE.md
  • docs/features/library.md
  • src-tauri/src/commands/deezer.rs
  • src-tauri/src/commands/scan.rs
  • src-tauri/src/lib.rs
  • src/components/common/ArtistImagePickerModal.tsx
  • src/components/views/ArtistDetailView.tsx
  • src/components/views/SettingsView.tsx
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/id.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/kr.json
  • src/i18n/locales/nl.json
  • src/i18n/locales/pt-BR.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/tr.json
  • src/i18n/locales/zh-CN.json
  • src/i18n/locales/zh-TW.json
  • src/lib/tauri/deezer.ts
  • src/lib/tauri/library.ts

Comment thread docs/features/library.md Outdated
Comment thread src-tauri/src/commands/deezer.rs
Comment thread src-tauri/src/commands/deezer.rs Outdated
Comment thread src-tauri/src/commands/scan.rs Outdated
Comment thread src/components/common/ArtistImagePickerModal.tsx
Comment thread src/components/views/ArtistDetailView.tsx Outdated
- docs/library: clarify rescan_local_artist_images probes up to 16
  tracks per artist (not "a representative track")
- deezer commands: return AppError when UPDATE artist affects 0 rows so
  the UI surfaces "artist not found" instead of silently succeeding
- download_image_bytes: enforce 10 MiB cap, early-reject via
  Content-Length and bound the streaming buffer to prevent OOM from a
  hostile or chunked-without-length response
- scan: move maybe_link_artist_images AFTER the track_artist rebuild so
  newly created artist IDs (when split count changes) are also probed
  for sidecar images
- ArtistImagePickerModal: add requestIdRef so a slow earlier Deezer
  search can't clobber the results of a newer query
- ArtistDetailView: shrink the pencil overlay to a corner button with
  pointer-events tweaks so the image keeps receiving double-click
  events (lightbox)
@github-actions github-actions Bot added the scope: deps Dependencies label May 16, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/commands/scan.rs`:
- Around line 1835-1851: The loop in rescan_local_artist_images is performing
individual writes (link_local_artist_image) causing SQLite lock churn; wrap the
work in a transaction by calling pool.begin() to get a transaction (tx) and use
that transaction as the mutable connection when calling
link_local_artist_image(&mut tx, artist_id, &cover). Track a counter and commit
tx.commit().await every ~200 writes, then start a new tx = pool.begin().await;
also ensure you acquire no separate connection inside the inner loop (remove
pool.acquire()) and use extract_artist_image as-is for reads while writes use
the open transaction.

In `@src/components/common/ArtistImagePickerModal.tsx`:
- Around line 269-277: The fan-count suffix is hardcoded as "fans" in
ArtistImagePickerModal.tsx (the JSX block that renders hit.nb_fan), causing
untranslated UI; replace the literal "fans" with a localized string via your
i18n helper (e.g., use t('fans') or t('artist.fans', { count: hit.nb_fan }) or
the project's pluralization API) so the suffix is translated and respects
pluralization, and add the corresponding key(s) to the translation files; ensure
the existing numeric formatting logic for K/M remains unchanged and only the
trailing word is localized.
- Around line 73-100: When abandoning a search (either because isOpen is
false/tab !== "deezer" or trimmed.length < 2) advance/invalidate the in-flight
requestIdRef by incrementing requestIdRef.current so any pending
searchArtistsDeezer then()/.catch() handlers do not apply outdated results;
specifically, in the early-return branch for (!isOpen || tab !== "deezer") and
in the branch for trimmed.length < 2 (after clearing debounceRef) increment
requestIdRef.current and ensure you still clear any timeouts and call
setResults([])/setIsSearching(false)/setError(null) as appropriate to keep UI
consistent (referencing requestIdRef, debounceRef, searchArtistsDeezer,
setResults, setIsSearching, setError).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 96eeffc3-0b04-4bbb-85d4-3c27c1087476

📥 Commits

Reviewing files that changed from the base of the PR and between d277519 and f67f496.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • docs/features/library.md
  • package.json
  • src-tauri/src/commands/deezer.rs
  • src-tauri/src/commands/scan.rs
  • src/components/common/ArtistImagePickerModal.tsx
  • src/components/views/ArtistDetailView.tsx

Comment thread src-tauri/src/commands/scan.rs
Comment thread src/components/common/ArtistImagePickerModal.tsx Outdated
Comment thread src/components/common/ArtistImagePickerModal.tsx
InstaZDLL added 2 commits May 16, 2026 19:46
- scan: wrap rescan_local_artist_images in a single transaction
  committed every 200 writes (same pattern as scan_folder_inner) so the
  WAL fsyncs once per batch instead of per artist
- ArtistImagePickerModal: localize the hardcoded "fans" suffix via
  i18next plural keys (`artistImagePicker.fansCount_*`) across all 17
  locales; only the trailing word is translated, K/M number formatting
  is preserved
- ArtistImagePickerModal: bump requestIdRef when abandoning a search
  (modal closed, tab switched, or query shorter than 2 chars) so any
  in-flight Deezer response is discarded as stale and the UI state is
  reset consistently
@InstaZDLL InstaZDLL merged commit af719b6 into main May 16, 2026
9 of 10 checks passed
@InstaZDLL InstaZDLL deleted the feat/local-artist-images branch May 16, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: deps Dependencies scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: xl > 500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: support a local artist image .

1 participant