Skip to content

feat(artist): spotify-style hero backdrop on the artist page (#482) - #483

Merged
InstaZDLL merged 6 commits into
mainfrom
feat/482-artist-hero
Aug 5, 2026
Merged

feat(artist): spotify-style hero backdrop on the artist page (#482)#483
InstaZDLL merged 6 commits into
mainfrom
feat/482-artist-hero

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Closes #482.

Gives the artist detail page a full-bleed hero behind the header, Spotify-style, instead of today's flat surface with a circular avatar. Both tiers ship here: the real wide fanart pipeline and the blurred-photo fallback.

Backdrop precedence

real wide fanart (TheAudioDB) β†’ blurred square photo (Deezer / local artist.jpg) β†’ flat header

A crisp wide fanart gives the real look but not every artist has one; the blurred square photo is always available and works offline, so it's the universal fallback.

Backend

  • theaudiodb.rs β€” ArtistPayload now parses strArtistFanart (+ 2/3/4), strArtistWideThumb, strArtistBanner. artist_bio becomes artist_info and returns bio and fanart_url from a single lookup, picking widest-and-cleanest first (logo banner last, its baked-in text can clash with the header copy). It returns Some for any name match even with neither bio nor fanart, so the caller can cache the "looked, nothing there" outcome.
  • New app migration 20260802120000_metadata_artist_background.sql β€” background_url + background_hash + background_fetched_at. That last column is the "we already looked" marker: without it a NULL hash is indistinguishable from "never queried", and every fanart-less artist would re-hit a rate-limited API on each page visit. It's stamped only when the API was reached (match or not) and left NULL on a transport error, so a network blip retries instead of caching "no fanart" for the row's whole 30-day TTL. Pre-existing rows have NULL there and backfill once on their next refresh.
  • enrich_artist_deezer β€” resolves the fanart independently of metadata.bio_source (the issue's open question): Last.fm has no equivalent image, so gating on the bio source would leave every Last.fm user with no hero at all. One TheAudioDB request serves both consumers β€” the bio half is used only when TheAudioDB is the selected source, which matters on their shared rate-limited free key. Downloaded through the usual metadata_artwork::download_and_cache and kept full-res (no _1x/_2x β€” downscaling a full-bleed banner only softens it). Offline mode short-circuits before all of it.
  • get_artist_detail returns background_url / background_path too, so the hero paints on the first frame for an already-enriched artist instead of waiting for the enrichment round-trip.

Frontend

  • ArtistHeroBackdrop β€” fanart shown nearly crisp (blur(2px), just enough to keep JPEG artefacts from crawling under the white copy); the square photo gets the heavy colour-field treatment SkinAmbientBackdrop uses (blur(56px) saturate(190%), scale(1.35)) because a 1:1 image stretched across a banner is unreadable otherwise.
  • Legibility is not left to the theme: dark scrim + header copy (eyebrow / name / stats) forced white in every theme, matching Spotify, whose artist header is dark-on-image in light mode too. Secondary buttons swap to a translucent white treatment. The bottom edge fades through a CSS mask rather than a hard-coded colour stop, so the hero dissolves into whatever the current theme Γ— skin paints behind it.
  • ArtistDetailView breaks out of <main>'s p-8 with -mx-8 -mt-8 so the image reaches the column edges (and shrinks with the column when a right panel opens). The enrichment response only ever sets the fanart β€” an empty refresh (offline, TheAudioDB down) must not blank a hero the cache already produced.
  • Toggle ui.artist_hero, per-profile, default ON (a baseline visual, not extra motion), Settings β†’ Appearance; write machinery mirrors useCoverSlideshow. prefers-reduced-motion skips the cross-fade only β€” the image itself is static.
  • i18n propagated to all 17 locales.

Docs

docs/features/ui.md (new Artist hero section), docs/features/library.md (Wide artist fanart (hero) β€” the backend pipeline), CLAUDE.md (UI + Integrations catalogues).

Checks

  • bun run typecheck βœ… Β· bun run lint βœ…
  • cargo check --workspace --all-targets βœ… Β· cargo clippy β€” no new warning (the 7 reported are pre-existing in scanner/extract.rs + browse.rs:462)
  • cargo test -p waveflow-core β€” 102/102 βœ…, including 3 new theaudiodb tests covering the fanart fallback order
  • Migration applied against a real SQLite DB (full migrations/app/ chain + the exact INSERT the code issues) βœ…

Not verified by me: the visual result in the running app (crisp fanart vs blurred fallback, legibility across the 5 skins Γ— 14 themes). Worth a look before merge.

https://claude.ai/code/session_01N9WXrurarkoiT2jgimvyj1

Summary by CodeRabbit

  • Nouvelles fonctionnalitΓ©s

    • Ajout d’un arriΓ¨re-plan panoramique sur les pages de dΓ©tail des artistes.
    • PrioritΓ© Γ  l’image TheAudioDB, avec repli sur une photo locale ou Deezer floutΓ©e.
    • Activation ou dΓ©sactivation depuis les paramΓ¨tres d’apparence, avec prΓ©fΓ©rences par profil.
    • Respect de la rΓ©duction des animations.
  • AmΓ©liorations

    • Mise en cache en pleine rΓ©solution et nouvelles tentatives aprΓ¨s les erreurs rΓ©seau.
    • Traductions ajoutΓ©es dans les langues prises en charge.
  • Documentation

    • Documentation ajoutΓ©e sur le bandeau artiste et ses options.

The artist page shipped a flat surface with a circular avatar; the data
pipeline only ever carried the square artist photo. This adds a
full-bleed hero behind the header with two tiers.

Backend
- theaudiodb: parse strArtistFanart(+2/3/4) / strArtistWideThumb /
  strArtistBanner. artist_bio becomes artist_info and returns bio AND
  fanart_url from one lookup; it returns Some on any name match even
  with neither, so a "looked, nothing there" outcome is cacheable.
- new app migration: metadata_artist gains background_url /
  background_hash / background_fetched_at. The last one is the "we
  already looked" marker -- without it a NULL hash cannot be told apart
  from "never queried", and every fanart-less artist would re-hit a
  rate-limited API on each page visit. Stamped only when the API was
  reached, so a transport error retries instead of caching for 30 days.
- enrich_artist_deezer calls TheAudioDB independently of
  metadata.bio_source: Last.fm has no equivalent image, so gating on it
  would leave every Last.fm user with no hero at all. One request serves
  both consumers. The fanart is cached full-res (no _1x/_2x tier).
- get_artist_detail returns it too, so the hero paints on first frame.

Frontend
- ArtistHeroBackdrop: real fanart nearly crisp, square photo heavily
  blurred (SkinAmbientBackdrop's colour-field treatment), nothing at
  all otherwise. Dark scrim + white header copy in EVERY theme, and the
  bottom edge fades through a CSS mask rather than a hard-coded colour
  stop so it dissolves into any theme x skin.
- ArtistDetailView breaks out of main's p-8 via -mx-8 -mt-8. The
  enrichment response only ever sets the fanart, so an empty refresh
  (offline, API down) never blanks a hero the cache produced.
- per-profile toggle ui.artist_hero, default ON -- a baseline visual,
  not extra motion. prefers-reduced-motion skips the cross-fade only.
- i18n propagated to all 17 locales.

Claude-Session: https://claude.ai/code/session_01N9WXrurarkoiT2jgimvyj1
@InstaZDLL InstaZDLL 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 Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. πŸŽ‰

ℹ️ Recent review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: af9822c0-8d1d-4a6c-9754-d861021ea3cb

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 4ebc933 and da06a78.

πŸ“’ Files selected for processing (1)
  • src/hooks/useArtistHero.ts

πŸ“ Walkthrough

Walkthrough

L’intΓ©gration rΓ©cupΓ¨re le fanart TheAudioDB, le tΓ©lΓ©charge en pleine rΓ©solution, le met en cache et l’expose Γ  la vue artiste. La vue affiche un hero configurable par profil, avec repli sur la photo carrΓ©e et prise en charge de la rΓ©duction des mouvements.

Changes

Pipeline TheAudioDB et cache

Layer / File(s) Summary
Contrat TheAudioDB et sΓ©lection du fanart
src-tauri/crates/core/src/metadata/theaudiodb.rs
artist_info retourne la biographie et le premier fanart disponible. Les rΓ©sultats restent valides sans biographie ni image. Les tests couvrent la prioritΓ© des images et le repli vers le banner.
Cache, tΓ©lΓ©chargement et exposition backend
src-tauri/crates/app/src/commands/deezer.rs, src-tauri/crates/app/src/commands/browse.rs, src-tauri/crates/core/src/artwork/metadata.rs, src-tauri/migrations/app/*, src/lib/tauri/detail.ts, docs/features/library.md, CLAUDE.md
Le cache metadata_artist conserve l’URL, le hash et la date de rΓ©cupΓ©ration. Le tΓ©lΓ©chargement pleine rΓ©solution ne gΓ©nΓ¨re pas de miniatures. Une erreur rΓ©seau laisse la date vide pour permettre une nouvelle tentative. Les dΓ©tails artiste exposent l’URL et le chemin local.
PrΓ©fΓ©rence et rendu du hero
src/hooks/useArtistHero.ts, src/components/views/ArtistDetailView.tsx, src/components/common/ArtistHeroBackdrop.tsx, src/components/views/settings/ArtistHeroCard.tsx, src/components/views/SettingsView.tsx, src/app.css, src/i18n/locales/*, docs/features/ui.md
La prΓ©fΓ©rence useArtistHero est activΓ©e par dΓ©faut et persistΓ©e par profil. Le rendu privilΓ©gie le fanart, puis la photo carrΓ©e floutΓ©e. Le composant applique un scrim, un masque infΓ©rieur et une animation compatible avec prefers-reduced-motion. Les traductions et la documentation dΓ©crivent l’option.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant VueArtiste
  participant EnrichissementDeezer
  participant TheAudioDB
  participant CacheImages
  VueArtiste->>EnrichissementDeezer: demander l’enrichissement artiste
  EnrichissementDeezer->>TheAudioDB: appeler artist_info
  TheAudioDB-->>EnrichissementDeezer: retourner biographie et fanart_url
  EnrichissementDeezer->>CacheImages: tΓ©lΓ©charger et mettre en cache le fanart
  CacheImages-->>EnrichissementDeezer: retourner background_path
  EnrichissementDeezer-->>VueArtiste: retourner background_url et background_path
Loading

Possibly related PRs

πŸš₯ Pre-merge checks | βœ… 5
βœ… Passed checks (5 passed)
Check name Status Explanation
Title check βœ… Passed Le titre dΓ©crit clairement l’ajout principal et respecte le format Conventional Commits avec un scope valide.
Description check βœ… Passed La description couvre le rΓ©sumΓ©, les tests, les changements techniques, la documentation et l’issue liΓ©e; la validation visuelle reste explicitement non vΓ©rifiΓ©e.
Linked Issues check βœ… Passed Les changements couvrent les objectifs de [#482], notamment le fanart, le repli floutΓ©, le cache, le mode hors ligne, le rΓ©glage utilisateur et les traductions.
Out of Scope Changes check βœ… Passed Les modifications restent liΓ©es Γ  [#482] et incluent uniquement le code, les migrations, la documentation, les traductions et la validation nΓ©cessaires.
Docstring Coverage βœ… Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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/482-artist-hero

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

πŸ€– 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/ui.md`:
- Around line 97-101: Correct the fallback count in the β€œTwo tiers, in
precedence order” heading of the artist backdrop documentation to match the
three listed outcomes, including the β€œNothing” state; use β€œThree tiers” or
explicitly distinguish the no-image state from the two image tiers.

In `@src-tauri/crates/app/src/commands/deezer.rs`:
- Around line 567-578: Update metadata_artwork::download_and_cache, after
checking all its Rust call sites, to support skipping thumbnail generation and
use that no-thumbnail path for the full-resolution background download in the
Deezer enrichment flow. Preserve thumbnail generation for existing callers that
require _1x/_2x assets, and continue resolving background_path from the cached
full-resolution file.

In `@src/components/common/ArtistHeroBackdrop.tsx`:
- Around line 59-70: SΓ©curisez la valeur de src avant son utilisation dans la
propriΓ©tΓ© backgroundImage du style inline de ArtistHeroBackdrop, en Γ©chappant
les guillemets, parenthΓ¨ses et caractΓ¨res d’échappement afin qu’une URL de
mΓ©tadonnΓ©e ne puisse pas rompre url() ni injecter du CSS. Utilisez
systΓ©matiquement cette valeur encodΓ©e lors de l’interpolation.

In `@src/components/views/ArtistDetailView.tsx`:
- Around line 97-101: Expose a loading/resolution state from useArtistHero and
use it in ArtistDetailView so the hero remains hidden until the profile
preference has been loaded. Ensure heroSrc is not rendered while the preference
is unresolved, preventing the initial default true value from causing a flash;
preserve the existing disabled behavior once ui.artist_hero resolves to false.
πŸͺ„ 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5ac2330b-a67e-45b8-af09-3f4ddf0fd851

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between b5ef338 and 4b3f120.

πŸ“’ Files selected for processing (31)
  • CLAUDE.md
  • docs/features/library.md
  • docs/features/ui.md
  • src-tauri/crates/app/src/commands/browse.rs
  • src-tauri/crates/app/src/commands/deezer.rs
  • src-tauri/crates/core/src/metadata/theaudiodb.rs
  • src-tauri/migrations/app/20260802120000_metadata_artist_background.sql
  • src/app.css
  • src/components/common/ArtistHeroBackdrop.tsx
  • src/components/views/ArtistDetailView.tsx
  • src/components/views/SettingsView.tsx
  • src/components/views/settings/ArtistHeroCard.tsx
  • src/hooks/useArtistHero.ts
  • 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/ko.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/detail.ts

Comment thread docs/features/ui.md Outdated
Comment thread src-tauri/crates/app/src/commands/deezer.rs
Comment thread src/components/common/ArtistHeroBackdrop.tsx
Comment thread src/components/views/ArtistDetailView.tsx Outdated
- docs: the "two tiers" list has three entries (the no-image case is
  not an image tier) -- say so.
- metadata_artwork: download_and_cache always queued the _1x/_2x job,
  so the full-res-only fanart generated tiers nothing reads. Split out
  download_and_cache_full_res; the three existing callers (album cover,
  artist picture, similar-artist picture) keep the thumbnails they need.
- ArtistHeroBackdrop: escape the url("...") interpolation. Unlike the
  local paths other backdrops paint, a hero source can be a remote URL
  straight out of TheAudioDB -- third-party data that must not be able
  to close the string and inject CSS.
- useArtistHero: expose `resolved` and gate the hero on it. The
  preference defaults to ON and is read asynchronously, so the previous
  code flashed a hero at users who had turned it off. Resolved is set
  even when the read fails, otherwise the hero would never appear.

Claude-Session: https://claude.ai/code/session_01N9WXrurarkoiT2jgimvyj1

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useArtistHero.ts (1)

42-70: 🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

Liez resolved au profil actif.

Γ€ la Line 55, un changement de profil conserve resolved === true depuis le profil prΓ©cΓ©dent. ArtistDetailView peut donc afficher le hero avec l’ancienne valeur avant la lecture de ui.artist_hero du nouveau profil.

Stockez l’identifiant du profil dont la lecture est terminΓ©e. DΓ©rivez resolved en le comparant Γ  activeProfile?.id. Le rendu masquera alors le hero immΓ©diatement pendant chaque changement de profil.

πŸ€– Prompt for 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.

In `@src/hooks/useArtistHero.ts` around lines 42 - 70, Update the resolved state
in the useArtistHero hook to track the profile ID whose setting read completed,
rather than retaining a profile-independent boolean. Record the active profile
ID after the read succeeds or fails, and derive resolved by comparing that
recorded ID with activeProfile?.id so profile changes immediately remain
unresolved until the new read finishes.
πŸ€– 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/crates/core/src/artwork/metadata.rs`:
- Around line 83-91: Ajoutez une abstraction d’état hors ligne indΓ©pendante de
crates/app et utilisez-la dans download_and_cache_inner. Faites retourner None
immΓ©diatement lorsque le mode hors ligne est actif, avant de crΓ©er le client
reqwest ou d’effectuer toute requΓͺte HTTP; conservez ce contrΓ΄le commun aux
chemins download_and_cache_full_res et download_and_cache.

---

Outside diff comments:
In `@src/hooks/useArtistHero.ts`:
- Around line 42-70: Update the resolved state in the useArtistHero hook to
track the profile ID whose setting read completed, rather than retaining a
profile-independent boolean. Record the active profile ID after the read
succeeds or fails, and derive resolved by comparing that recorded ID with
activeProfile?.id so profile changes immediately remain unresolved until the new
read finishes.
πŸͺ„ 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fc4f1ec9-997c-477e-90d3-ea1b96ef2553

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 4b3f120 and 58d8348.

πŸ“’ Files selected for processing (6)
  • docs/features/ui.md
  • src-tauri/crates/app/src/commands/deezer.rs
  • src-tauri/crates/core/src/artwork/metadata.rs
  • src/components/common/ArtistHeroBackdrop.tsx
  • src/components/views/ArtistDetailView.tsx
  • src/hooks/useArtistHero.ts

Comment thread src-tauri/crates/core/src/artwork/metadata.rs
A bare "did we read once" boolean stayed true across a profile switch,
so the previous profile's value painted until the new read landed.
Stamp the profile the read completed for and derive `resolved` by
comparing it with the active id β€” a switch re-renders with a different
id, the stamp stops matching and the hero goes back to hidden for free,
with no set-state-in-effect dance.

Claude-Session: https://claude.ai/code/session_01N9WXrurarkoiT2jgimvyj1

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

πŸ€– 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/hooks/useArtistHero.ts`:
- Around line 23-26: In the [activeProfileId] effect, reset enabledRef.current,
confirmedEnabledRef.current, setEnabledState, and readProfileId to
undefined/default values before calling refresh; keep this reset outside refresh
because refresh also handles ARTIST_HERO_EVENT. Update the resolved
documentation and failure handling so a failed profile read uses DEFAULT_ENABLED
rather than retaining the previous profile’s preference, including write
rollback behavior.
πŸͺ„ 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d1264915-7c2c-46fd-90d2-3119acfb5e40

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 58d8348 and d9605ff.

πŸ“’ Files selected for processing (1)
  • src/hooks/useArtistHero.ts

Comment thread src/hooks/useArtistHero.ts Outdated
`resolved` going false on a switch hid the hero during the read, but a
read that *failed* still stamped the new profile and then exposed the
outgoing profile's value -- which the doc comment claimed could never
happen. Clear enabled + both refs to the default before the read, in the
effect rather than in `refresh`: that function also serves
ARTIST_HERO_EVENT, where resetting would flash the default on every
same-profile broadcast, and a failed same-profile refresh is better off
keeping what it had.

Claude-Session: https://claude.ai/code/session_01N9WXrurarkoiT2jgimvyj1

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useArtistHero.ts (1)

82-95: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

EmpΓͺchez une lecture obsolΓ¨te d’écraser une Γ©criture optimiste.

Une lecture refresh lancΓ©e avant setEnabled(false) peut se terminer aprΓ¨s l’écriture et aprΓ¨s le ARTIST_HERO_EVENT. Chaque lecture applique actuellement son rΓ©sultat sans contrΓ΄ler son ordre.

Dans ce cas, une ancienne valeur "true" peut remplacer enabled et confirmedEnabledRef.current alors que la prΓ©fΓ©rence persistΓ©e vaut "false". Invalidez les lectures en cours lors d’une Γ©criture et appliquez un rΓ©sultat seulement s’il correspond Γ  la derniΓ¨re lecture et Γ  la derniΓ¨re Γ©criture pertinentes.

Also applies to: 106-122

πŸ€– Prompt for 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.

In `@src/hooks/useArtistHero.ts` around lines 82 - 95, Update the refresh flow in
useArtistHero so each read is associated with a generation/token, and apply
parsed results only when that read is still the latest relevant read and no
subsequent optimistic write has occurred. Invalidate or advance the read
generation when setEnabled(false) or any preference write starts, and use the
same guards before updating enabledRef, confirmedEnabledRef, setEnabledState,
and setReadProfileId.
πŸ€– 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.

Outside diff comments:
In `@src/hooks/useArtistHero.ts`:
- Around line 82-95: Update the refresh flow in useArtistHero so each read is
associated with a generation/token, and apply parsed results only when that read
is still the latest relevant read and no subsequent optimistic write has
occurred. Invalidate or advance the read generation when setEnabled(false) or
any preference write starts, and use the same guards before updating enabledRef,
confirmedEnabledRef, setEnabledState, and setReadProfileId.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d8c4c1ca-8043-4038-8246-7434613682cc

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between d9605ff and 5ad81f2.

πŸ“’ Files selected for processing (1)
  • src/hooks/useArtistHero.ts

A read started at mount (or by ARTIST_HERO_EVENT) applied its result
unconditionally, so a toggle clicked while it was in flight got
overwritten by the pre-click value -- transient, since the write's own
broadcast re-reads afterwards, but visible as the checkbox snapping
back. Tag each read with a token that every read and every write bumps,
and apply the parsed value only while that token still holds. The
profile stamp in `finally` deliberately stays unconditional: the profile
HAS been read, and gating it would pin `resolved` false forever if the
racing write then failed.

Claude-Session: https://claude.ai/code/session_01N9WXrurarkoiT2jgimvyj1

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/hooks/useArtistHero.ts (2)

131-143: πŸ—„οΈ Data Integrity & Integration | 🟠 Major | ⚑ Quick win

Notifier les autres instances après le rollback.

Si plusieurs instances de useArtistHero sont montΓ©es, une premiΓ¨re Γ©criture peut rΓ©ussir puis une Γ©criture plus rΓ©cente Γ©chouer. La premiΓ¨re Γ©criture met Γ  jour confirmedEnabledRef.current, mais son Γ©vΓ©nement est supprimΓ© par seq !== writeSeqRef.current. Le catch de la seconde Γ©criture restaure seulement l’instance courante et n’émet aucun Γ©vΓ©nement. Les autres instances gardent alors une valeur obsolΓ¨te.

Après setEnabledState(rollback), diffusez ARTIST_HERO_EVENT, en conservant les contrôles de profil et de séquence.

Correction minimale
       const rollback = confirmedEnabledRef.current;
       enabledRef.current = rollback;
       setEnabledState(rollback);
+      window.dispatchEvent(new CustomEvent(ARTIST_HERO_EVENT));
πŸ€– Prompt for 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.

In `@src/hooks/useArtistHero.ts` around lines 131 - 143, Dans le catch de
l’écriture de useArtistHero, aprΓ¨s setEnabledState(rollback), Γ©mettez
ARTIST_HERO_EVENT afin de notifier les autres instances. Conservez les
vΓ©rifications activeProfileIdRef.current !== profileId et seq !==
writeSeqRef.current avant le rollback et la diffusion.

77-86: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

RΓ©initialiser readProfileId lors de chaque changement de profil.

Lorsque l’utilisateur revient rapidement du profil A au profil B, puis au profil A, readProfileId peut encore contenir A. resolved devient alors vrai avant la nouvelle lecture, alors que enabled contient encore la valeur par dΓ©faut appliquΓ©e pour B. Le hero peut afficher la mauvaise prΓ©fΓ©rence de A. Si la lecture Γ©choue, cet Γ©tat reste actif.

RΓ©initialisez readProfileId Γ  undefined avec les autres Γ©tats avant refresh.

Correction minimale
     enabledRef.current = DEFAULT_ENABLED;
     confirmedEnabledRef.current = DEFAULT_ENABLED;
+    // eslint-disable-next-line react-hooks/set-state-in-effect
+    setReadProfileId(undefined);
     confirmedEnabledRef.current = DEFAULT_ENABLED;

Also applies to: 149-150

πŸ€– Prompt for 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.

In `@src/hooks/useArtistHero.ts` around lines 77 - 86, Reset readProfileId to
undefined alongside enabledRef, confirmedEnabledRef, and setEnabledState when
the profile changes, before refresh runs. Update the profile-change reset logic
in useArtistHero so resolved cannot reuse the previous profile’s read marker,
while preserving the existing optimistic enabled behavior.
πŸ€– 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.

Outside diff comments:
In `@src/hooks/useArtistHero.ts`:
- Around line 131-143: Dans le catch de l’écriture de useArtistHero, aprΓ¨s
setEnabledState(rollback), Γ©mettez ARTIST_HERO_EVENT afin de notifier les autres
instances. Conservez les vΓ©rifications activeProfileIdRef.current !== profileId
et seq !== writeSeqRef.current avant le rollback et la diffusion.
- Around line 77-86: Reset readProfileId to undefined alongside enabledRef,
confirmedEnabledRef, and setEnabledState when the profile changes, before
refresh runs. Update the profile-change reset logic in useArtistHero so resolved
cannot reuse the previous profile’s read marker, while preserving the existing
optimistic enabled behavior.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7067c0ce-0546-4785-8260-fda80b0dc001

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 5ad81f2 and 4ebc933.

πŸ“’ Files selected for processing (1)
  • src/hooks/useArtistHero.ts

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #484

coderabbitai Bot added a commit that referenced this pull request Aug 4, 2026
Docstrings generation was requested by @InstaZDLL.

* #483 (comment)

The following files were modified:

* `src-tauri/crates/app/src/commands/browse.rs`
* `src-tauri/crates/app/src/commands/deezer.rs`
* `src-tauri/crates/core/src/artwork/metadata.rs`
* `src-tauri/crates/core/src/metadata/theaudiodb.rs`
* `src/components/common/ArtistHeroBackdrop.tsx`
* `src/components/views/ArtistDetailView.tsx`
* `src/components/views/SettingsView.tsx`
* `src/components/views/settings/ArtistHeroCard.tsx`
* `src/hooks/useArtistHero.ts`
* `src/lib/tauri/detail.ts`
Deriving `resolved` from the id comparison alone was not enough: switch
A -> B -> A faster than B's read completes and the stamp still says A,
so `resolved` goes true while `enabled` is sitting at the default B's
reset put there -- the very flash the stamp exists to prevent. Reset it
with the rest of the outgoing profile's state.

Claude-Session: https://claude.ai/code/session_01N9WXrurarkoiT2jgimvyj1

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/hooks/useArtistHero.ts (3)

77-105: 🎯 Functional Correctness | 🟠 Major | πŸ—οΈ Heavy lift

Ne rendez pas resolved vrai avec un état de lecture obsolète.

finally exΓ©cute setReadProfileId(activeProfileId) mΓͺme lorsque seq !== readSeqRef.current. Une lecture plus ancienne peut donc rendre resolved vrai sans avoir appliquΓ© sa valeur.

De plus, readProfileId n’est pas invalidΓ© quand le profil change. AprΓ¨s A β†’ B β†’ A, l’ancien marqueur de A rend resolved vrai pendant la nouvelle lecture de A. enabled peut alors contenir un Γ©tat par dΓ©faut non confirmΓ©.

RΓ©initialisez readProfileId Γ  undefined avec les autres Γ©tats de changement de profil. Renseignez-le uniquement pour la lecture courante. GΓ©rez explicitement le repli lorsqu’une Γ©criture invalide la lecture.

Also applies to: 149-150

πŸ€– Prompt for 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.

In `@src/hooks/useArtistHero.ts` around lines 77 - 105, Update the profile-change
handling in useArtistHero to reset readProfileId to undefined alongside
enabledRef, confirmedEnabledRef, and setEnabledState. In refresh, only
setReadProfileId(activeProfileId) for the current read sequence, never when
cancelled or seq !== readSeqRef.current, and explicitly preserve the default
fallback when a write invalidates the read so resolved cannot become true from
stale data.

131-143: πŸ—„οΈ Data Integrity & Integration | 🟠 Major | ⚑ Quick win

Émettez ARTIST_HERO_EVENT après le rollback.

Si l’écriture seq=1 rΓ©ussit et que seq=2 Γ©choue, aucun Γ©vΓ©nement n’est Γ©mis. L’instance qui a lancΓ© les Γ©critures revient Γ  la valeur confirmΓ©e, mais les autres instances peuvent conserver l’ancienne valeur. Ajoutez l’émission aprΓ¨s le rollback et testez deux consommateurs montΓ©s simultanΓ©ment.

πŸ€– Prompt for 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.

In `@src/hooks/useArtistHero.ts` around lines 131 - 143, Update the error path in
the write handling around the ARTIST_HERO_EVENT dispatch so it emits
ARTIST_HERO_EVENT after restoring the confirmed value and updating state.
Preserve the existing profile and sequence guards, and add coverage for two
simultaneously mounted consumers where a later write fails after an earlier
write succeeds.

121-127: πŸ—„οΈ Data Integrity & Integration | 🟑 Minor | ⚑ Quick win

Associez l’écriture au profil capturΓ©.

set_profile_setting utilise le pool actif au moment oΓΉ la commande acquiert son lease. Un changement de profil peut donc survenir aprΓ¨s le garde JavaScript, mais avant cette acquisition, et Γ©crire la prΓ©fΓ©rence dans le nouveau profil. Passez profileId Γ  la commande et validez-le sous le mΓͺme verrou que l’acquisition du pool.

πŸ€– Prompt for 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.

In `@src/hooks/useArtistHero.ts` around lines 121 - 127, Associate the queued
write in the enabled-setting flow with the captured profileId by passing it into
the underlying set_profile_setting command. Ensure the command validates the
captured profile ID while holding the same lock used to acquire the active pool,
preventing a profile switch between the JavaScript guard and lease acquisition
from writing to the wrong profile.
πŸ€– 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/hooks/useArtistHero.ts`:
- Around line 55-59: Add a real read/write barrier in the refresh flow around
readSeqRef, refresh, and setProfileSetting: prevent a read triggered by
ARTIST_HERO_EVENT from applying while the current write is still pending, either
by awaiting that write or validating a pending-write token before updating
enabled and confirmedEnabledRef. After the write completes, reread the persisted
value and apply that result, while preserving existing overlapping-read
invalidation.

---

Outside diff comments:
In `@src/hooks/useArtistHero.ts`:
- Around line 77-105: Update the profile-change handling in useArtistHero to
reset readProfileId to undefined alongside enabledRef, confirmedEnabledRef, and
setEnabledState. In refresh, only setReadProfileId(activeProfileId) for the
current read sequence, never when cancelled or seq !== readSeqRef.current, and
explicitly preserve the default fallback when a write invalidates the read so
resolved cannot become true from stale data.
- Around line 131-143: Update the error path in the write handling around the
ARTIST_HERO_EVENT dispatch so it emits ARTIST_HERO_EVENT after restoring the
confirmed value and updating state. Preserve the existing profile and sequence
guards, and add coverage for two simultaneously mounted consumers where a later
write fails after an earlier write succeeds.
- Around line 121-127: Associate the queued write in the enabled-setting flow
with the captured profileId by passing it into the underlying
set_profile_setting command. Ensure the command validates the captured profile
ID while holding the same lock used to acquire the active pool, preventing a
profile switch between the JavaScript guard and lease acquisition from writing
to the wrong profile.
πŸͺ„ Autofix

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f31f3771-1543-47d5-88f2-8a9ae67b3834

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 5ad81f2 and 4ebc933.

πŸ“’ Files selected for processing (1)
  • src/hooks/useArtistHero.ts

Comment thread src/hooks/useArtistHero.ts
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Two findings from this review are real but deliberately out of scope for this PR β€” tracked in #485:

  1. set_profile_setting isn't profile-scoped (TOCTOU). The JS guard runs before the await, so a switch_profile landing before require_profile_pool() acquires the lease writes into the wrong profile. Only the backend can close this, by validating an expected id under the same lock that resolves the pool. The command is shared by 22 call sites across 18 files β€” fixing it for this one hook would leave the other 21 exposed while looking covered.

  2. The preference-hook pattern is copy-pasted 7Γ—. This review hardened useArtistHero alone (stale-read invalidation, per-profile resolved marker). The other six carry the same races; they're invisible there only because those hooks default to OFF, so an unresolved frame paints nothing. The right fix is one shared useProfileBooleanSetting, not six more rounds of the same patch.

Everything else from the review is applied on this branch. The remaining suggestions I did not take, with reasons:

  • Read/write barrier in refresh: not reachable. ARTIST_HERO_EVENT has a single emitter, gated on a successful write and on seq !== writeSeqRef.current β€” so a newer in-flight write suppresses the older one's broadcast, and when an event does fire, the persisted value is already the one being read.
  • Gating the profile stamp on the read sequence: would regress. The sequence is invalidated by a write, so if that write then fails there's no broadcast and no further read β€” resolved would stay false forever and the hero would never appear. The value exposed in that window isn't stale; it's the optimistic click, newer than the read.
  • Broadcasting on write rollback: nothing to resynchronise. The optimistic update is local to the instance that clicked; other instances only move on the broadcast, which fires on success only. On failure they're already showing the last backend-confirmed value β€” exactly where confirmedEnabledRef rolls the writer back to.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Acknowledged.

I agree that the set_profile_setting TOCTOU issue requires a backend profile identity check under the pool-resolution lock. A hook-only change would not provide a complete fix. Tracking this work in #485 is appropriate.

I also agree that the duplicated preference-hook behavior needs one shared useProfileBooleanSetting implementation. Extending useArtistHero fixes to the other hooks independently would duplicate the same concurrency logic.

The stated ARTIST_HERO_EVENT emitter condition makes the proposed refresh read/write barrier unreachable. The optimistic value after a failed write is newer than the invalidated read, so sequence-gating the profile stamp would incorrectly retain resolved: false. A rollback broadcast is not required because other instances did not receive the failed optimistic update.

I will treat these items as deferred to #485.
πŸ‡


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/483

Timestamp: 2026-08-05T05:12:52.948Z
Learning: In the WaveFlow frontend, profile-scoped boolean settings are implemented by seven similar hooks. A future consolidation should provide a shared `useProfileBooleanSetting` hook that handles profile changes, stale reads, optimistic writes, rollback, event synchronization, and per-profile resolution state.

Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/483

Timestamp: 2026-08-05T05:12:52.948Z
Learning: In WaveFlow, `set_profile_setting` has a profile-switch TOCTOU risk because the frontend profile guard runs before the asynchronous backend call obtains the profile pool. A complete fix requires the backend command to validate an expected profile ID while it resolves the pool under the same lock. This is tracked in GitHub issue `#485`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@InstaZDLL
InstaZDLL merged commit b716b19 into main Aug 5, 2026
14 checks passed
@InstaZDLL
InstaZDLL deleted the feat/482-artist-hero branch August 5, 2026 11:22
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: 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: Spotify-style hero background on artist pages (TheAudioDB fanart + blurred-photo fallback)

1 participant