Skip to content

fix(deezer): only disk-cache an album cover when the album lacks local art (#493) - #494

Merged
InstaZDLL merged 7 commits into
mainfrom
fix/493-deezer-album-cover-leak
Aug 9, 2026
Merged

fix(deezer): only disk-cache an album cover when the album lacks local art (#493)#494
InstaZDLL merged 7 commits into
mainfrom
fix/493-deezer-album-cover-leak

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Fixes #493.

The bug

AppData\...\waveflow\metadata_artwork fills with Deezer album covers the user never asked for — even with the "fetch album covers" batch never run, and even for albums that already have local artwork.

Root cause

enrich_album_inner downloads + caches the Deezer cover unconditionally, but every automatic caller ignores that file:

  • Album page (AlbumDetailView.tsx) fires enrichAlbumDeezer on every open and reads only label + release_date — never the cover.
  • Discord presence (discord_presence.rs) reads the remote cover_url, not the downloaded file.
  • Album grid / detail header render the local artwork (aw.hash); get_album_detail joins metadata_album only for label.

So the downloaded cover is pure dead weight for any album that has its own art.

Fix

Gate the download_and_cache on album.artwork_id IS NULL — only a genuinely cover-less album writes a Deezer cover to disk. cover_url still rides through for Discord + the cache row; label/release_date unchanged; the deliberate batch_fetch_missing_album_covers (already WHERE artwork_id IS NULL) is unaffected. No display regression — nothing showed that file for an art-having album.

Scope note

This stops new accumulation. It does not prune covers already cached. A one-time cleanup (delete metadata_artwork files referenced by metadata_album.cover_hash for albums whose local artwork_id IS NOT NULL, and null those cover_hash) can be a follow-up if wanted — kept out here to keep the fix focused and low-risk.

Validation

  • cargo clippy -p waveflow --all-targets clean.
  • Manual: open album pages for albums with local covers → metadata_artwork no longer grows; Discord album art (remote URL) and label/release-date still work; a cover-less album still gets its fallback + the Settings "fetch missing covers" batch still fills them.

Docs: docs/features/integrations.md + CLAUDE.md catalogue updated.

Summary by CodeRabbit

  • Nouvelles fonctionnalités

    • Ajout d’une option dans les réglages pour supprimer les pochettes Deezer inutilisées.
    • Le nettoyage affiche le nombre de fichiers supprimés et l’espace libéré, avec gestion des erreurs.
    • L’option est disponible dans plusieurs langues.
  • Améliorations

    • Les pochettes Deezer ne sont plus téléchargées lorsqu’une illustration locale existe déjà.
    • Les pochettes encore utilisées sont conservées.
  • Documentation

    • La documentation précise les règles de mise en cache des pochettes d’album.

…l art (#493)

`enrich_album_inner` downloaded and cached the Deezer album cover into the
shared `metadata_artwork` cache unconditionally. But it fires automatically on
every album-page open — which reads only `label` + `release_date` — and from
the Discord presence, which reads the remote `cover_url`; neither displays the
downloaded file, and the album grid / detail header render the LOCAL artwork.
So the cache steadily filled with never-shown Deezer covers for albums the user
already had covers for (reported: a `metadata_artwork` folder full of album art
the user never asked for).

Gate the `download_and_cache` on `album.artwork_id IS NULL`: only a genuinely
cover-less album gets its Deezer cover written to disk. The remote `cover_url`
still rides through for Discord + the cache row, `label`/`release_date` are
unchanged, and the deliberate `batch_fetch_missing_album_covers` (which iterates
only `artwork_id IS NULL` albums) keeps working. No display regression — nothing
rendered the downloaded file for an art-having album.

Note: this stops new leaks; it does not prune covers already cached. A one-time
cleanup (drop `metadata_album.cover_hash` files for albums with local art) can
follow if wanted.
@InstaZDLL InstaZDLL added bug Something isn't working scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets type: fix Bug fix size: s 10-50 lines labels Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 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: 7e2f3141-03a6-4b51-b0b4-88ebc4757f15

📥 Commits

Reviewing files that changed from the base of the PR and between e748c11 and 848add8.

📒 Files selected for processing (1)
  • src/components/views/SettingsView.tsx

📝 Walkthrough

Walkthrough

L’enrichissement Deezer ne télécharge plus de pochette lorsqu’un album possède déjà une illustration locale. Une commande Tauri supprime les références et les fichiers devenus inutiles. Les réglages affichent l’action, son état et son résultat dans toutes les langues prises en charge.

Changes

Gestion des pochettes Deezer

Layer / File(s) Summary
Téléchargement conditionnel
src-tauri/crates/app/src/commands/deezer.rs, docs/features/integrations.md, CLAUDE.md
L’enrichissement lit artwork_id. Il valide le cache selon l’illustration locale et télécharge la pochette uniquement si l’album n’a pas d’illustration. Le test couvre les albums avec et sans illustration locale.
Nettoyage du cache backend
src-tauri/crates/app/src/commands/maintenance.rs, src-tauri/crates/app/src/lib.rs
La commande Tauri efface les références des albums déjà illustrés, conserve les fichiers encore utilisés et supprime les fichiers non référencés. Elle retourne les références effacées, les fichiers supprimés et les octets libérés.
Commande dans les réglages
src/lib/tauri/library.ts, src/components/views/SettingsView.tsx, src/i18n/locales/*.json
Le wrapper typé appelle la commande backend. SettingsView lance le nettoyage, bloque les appels simultanés et affiche le résultat. Les traductions couvrent l’action, le résultat et l’échec.

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

Sequence Diagram(s)

sequenceDiagram
  actor Utilisateur
  participant SettingsView
  participant Tauri
  participant BasesProfils
  participant CachePartage
  Utilisateur->>SettingsView: lancer le nettoyage des pochettes
  SettingsView->>Tauri: appeler prune_cached_album_covers
  Tauri->>BasesProfils: effacer les références inutiles
  Tauri->>CachePartage: supprimer les fichiers non référencés
  CachePartage-->>Tauri: retourner les fichiers et octets supprimés
  Tauri-->>SettingsView: retourner AlbumCoverCleanup
  SettingsView-->>Utilisateur: afficher le résultat
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Le titre décrit clairement la correction principale du cache des pochettes Deezer pour les albums possédant une illustration locale.
Description check ✅ Passed La description explique le problème, la cause, la correction, la validation et le lien avec l’issue, malgré l’absence du checklist détaillé du modèle.
Linked Issues check ✅ Passed Les changements corrigent l’accumulation des pochettes Deezer et préservent le comportement des albums sans illustration locale conformément à l’issue #493.
Out of Scope Changes check ✅ Passed La commande de nettoyage, l’interface, les traductions et la documentation correspondent aux objectifs fournis et ne constituent pas des changements hors périmètre.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/493-deezer-album-cover-leak

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: 2

🤖 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/app/src/commands/deezer.rs`:
- Around line 203-205: Corrigez le calcul de cover_hash autour de la branche
utilisant local_artwork_id et l’upsert associé afin qu’une entrée fraîche avec
artwork_id NULL et cover_hash NULL soit considérée incomplète, sans prolonger
son TTL. Lorsqu’aucun nouveau téléchargement n’est demandé, préservez le hash
déjà présent au lieu d’écrire NULL. Ajoutez des tests couvrant la suppression de
l’artwork local et le changement de profil, notamment via
batch_fetch_missing_album_covers.
- Around line 203-205: Ajoutez une vérification de `is_offline()` immédiatement
avant l’appel à `metadata_artwork::download_and_cache` dans le bras `(true,
Some(url))` de la correspondance `cover_hash`; évitez le téléchargement lorsque
le mode hors ligne est activé, tout en conservant le comportement actuel lorsque
le mode en ligne est actif.
🪄 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: 0cb8d78a-c7a0-4bf9-91aa-07c25a6a37b4

📥 Commits

Reviewing files that changed from the base of the PR and between d39d2ec and ea14ec5.

📒 Files selected for processing (3)
  • CLAUDE.md
  • docs/features/integrations.md
  • src-tauri/crates/app/src/commands/deezer.rs

Comment thread src-tauri/crates/app/src/commands/deezer.rs
…ving albums (#493)

Companion to the #493 fix, which stops NEW accumulation but leaves whatever the
shared cache already collected. `prune_cached_album_covers` reclaims it:

- Finds Deezer covers cached for albums that already have local artwork
  (`album.artwork_id IS NOT NULL`), clears their `metadata_album.cover_hash`,
  and deletes the `<hash>.jpg` + `_1x`/`_2x` files.
- Reference-counted before any deletion: the cache is content-addressed and
  shared between artist pictures and album covers, so a hash is only unlinked
  once no `metadata_album.cover_hash` / `metadata_artist.picture_hash` /
  `background_hash` still points at it. Blocking fs work runs on spawn_blocking.
- Scoped to the active profile; over-cleaning is self-healing since the #493 fix
  re-fetches a cover-less album's cover on next view.

Surfaced as a "Prune unused Deezer album covers" card under Settings → Data
(reports files deleted + MB freed). Frontend `pruneCachedAlbumCovers` wrapper +
i18n `settings.pruneAlbumCovers*` ×17. Docs already covered by the #493 commit.
@InstaZDLL InstaZDLL added scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: l 200-500 lines and removed size: s 10-50 lines labels Aug 9, 2026
@InstaZDLL

Copy link
Copy Markdown
Owner Author

Ajout du nettoyage ponctuel (d9db6593), mentionné comme follow-up dans la description.

Nouvelle commande de maintenance prune_cached_album_covers (bouton Réglages → Data « Prune unused Deezer album covers ») :

  • Cible les covers Deezer cachées pour des albums qui ont déjà un artwork local (album.artwork_id IS NOT NULL), nulle leur metadata_album.cover_hash et supprime <hash>.jpg + _1x/_2x.
  • Comptée par références avant toute suppression : le cache est adressé par contenu et partagé entre photos d'artistes et covers, donc un hash n'est délié du disque que s'il n'est plus référencé par metadata_album.cover_hash ni metadata_artist.picture_hash/background_hash. Le fs tourne en spawn_blocking.
  • Scopé au profil actif ; sur-nettoyer est auto-réparateur (le fix bug: issue with Deezer scraper (featch albums images ,even if we did not ask for it) #493 re-télécharge la cover d'un album sans artwork à la prochaine ouverture).
  • Renvoie fichiers supprimés + Mo libérés. Wrapper pruneCachedAlbumCovers + i18n settings.pruneAlbumCovers* ×17.

Validé : cargo clippy -p waveflow + bun run typecheck + bun run lint verts.

@InstaZDLL InstaZDLL self-assigned this Aug 9, 2026

@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 `@src-tauri/crates/app/src/commands/maintenance.rs`:
- Around line 90-131: Encapsulate the candidate selection, cover_hash UPDATE,
and referenced-hash query in a single transaction within the cleanup function.
Begin a transaction from pool, execute each query through &mut *tx, and commit
only after all three operations succeed; preserve the existing empty-candidate
result and return behavior.

In `@src/components/views/SettingsView.tsx`:
- Around line 1166-1187: Update handlePruneCachedCovers to store a structured
success or failure result in pruneCoversStatus, setting the localized
settings.pruneAlbumCoversFailed message in the catch instead of leaving the
status null. Render failure distinctly from the green success styling, preserve
the existing reset behavior, and add the new localization key to all 17 locale
files.

In `@src/lib/tauri/library.ts`:
- Around line 233-235: Update pruneCachedAlbumCovers and its backend
implementation so album-cover selection, UPDATE, reference revalidation, and
file deletion are coordinated transactionally with Deezer scans and writers.
Revalidate references immediately before deletion, roll back database changes if
cleanup fails, and use mutable SQLite transactions with retry handling for
transient SQLITE_BUSY and SQLITE_LOCKED errors.
- Around line 233-235: Update the pruneCachedAlbumCovers flow and its backend
implementation so cleanup is scoped to the active profile and cannot null a
shared app.metadata_album reference used by another profile. Evaluate
eligibility across all profiles before clearing a shared deezer_id reference, or
make the reference profile-specific, while preserving the shared metadata cache
behavior.
🪄 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: 016b7402-a3b5-422a-8898-cea8dd63e1b4

📥 Commits

Reviewing files that changed from the base of the PR and between ea14ec5 and d9db659.

📒 Files selected for processing (21)
  • src-tauri/crates/app/src/commands/maintenance.rs
  • src-tauri/crates/app/src/lib.rs
  • 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/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/library.ts

Comment thread src-tauri/crates/app/src/commands/maintenance.rs Outdated
Comment thread src/components/views/SettingsView.tsx Outdated
Comment thread src/lib/tauri/library.ts
…review)

CodeRabbit round on #494. The download-skip introduced a cache-poisoning edge:
`enrich_album_inner` returned any fresh `metadata_album` row as a hit, including
one with `cover_hash = NULL`. So once an art-having album cached a cover-less
row, a later need for its cover — the local art was removed, another profile
shares the app-wide cache, or `batch_fetch_missing_album_covers` runs — was
served the empty row and never re-fetched until the 30-day TTL lapsed.

Gate the cache-hit on a completeness check: a fresh row is usable only when it
carries a cover OR the album has its own local art (so it never needs the Deezer
cover). An art-less, cover-less fresh row now falls through to Deezer. Extracted
`metadata_album_cache_complete` + unit test.

Skipped the paired "add an is_offline() guard before download_and_cache"
suggestion: offline mode already returns at step 3, before the API call and the
download, so that branch is unreachable offline — a second check would be dead
code.
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Traité dans 94badcf7.

Finding 1 — une ligne fraîche sans cover bloque le re-fetch [FIXED]
Vrai edge de cache-poisoning : le cache-hit renvoyait n'importe quelle ligne fraîche, y compris cover_hash = NULL. Une fois qu'un album avec pochette locale avait mis en cache une ligne sans cover, un besoin ultérieur de cover (art local supprimé, autre profil partageant le cache app-wide, ou batch_fetch_missing_album_covers) recevait la ligne vide et ne re-téléchargeait jamais avant l'expiration du TTL 30 j.

Fix = test de complétude dans le cache-hit : une ligne fraîche n'est un hit que si elle porte une cover OU si l'album a sa propre pochette locale (il n'aura jamais besoin de la cover Deezer). Une ligne fraîche sans cover pour un album sans pochette retombe donc vers Deezer. Extrait en metadata_album_cache_complete + test unitaire (art-less→incomplet, art-having→complet).

Sur les deux sous-directives : plutôt que « préserver le hash au lieu d'écrire NULL » (ce qui garderait le fichier référencé et contredirait #493 — le but est justement de ne PAS conserver les covers des albums qui ont leur pochette, cf. la commande de purge) et « ne pas prolonger le TTL » (cas particulier fragile), le test de complétude surclasse le TTL exactement quand une cover est nécessaire → même intention (re-fetch possible), sans effet de bord.

Finding 2 — is_offline() avant download_and_cache [SKIP]
Déjà couvert : l'étape 3 (if crate::offline::is_offline() { return … }) return avant l'appel API et avant le download de l'étape 4. Le bras (true, Some(url)) est donc inatteignable hors ligne — une seconde vérification serait du code mort.

Note tests : le prédicat est unit-testé ; un test d'intégration de enrich_album_inner (art removal / profile change / batch) n'est pas faisable ici — la fonction fait un appel réseau Deezer sans point d'injection, et les tests du crate waveflow (app) ne démarrent pas en local Windows (STATUS_ENTRYPOINT_NOT_FOUND, DLL Tauri) ; ils tournent en CI.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src-tauri/crates/app/src/commands/deezer.rs"

printf '%s\n' '== Target commit =='
git show --no-ext-diff --format=fuller --stat 94badcf7

printf '%s\n' '== File outline =='
ast-grep outline "$file" --items all --view expanded

printf '%s\n' '== Relevant symbols and call-site context =='
rg -n -C 12 \
  'metadata_album_cache_complete|is_offline\(\)|download_and_cache|cover_hash|enrich_album_inner' \
  "$file"

printf '%s\n' '== Target commit patch for the Deezer command =='
git show --no-ext-diff --format= -- "$file" 94badcf7

Length of output: 27502


@InstaZDLL Confirmé après vérification de 94badcf7.

  • Finding 1 est corrigé. metadata_album_cache_complete empêche un cache-hit frais sans cover_hash pour un album sans artwork local. Le flux continue alors vers Deezer. Le test unitaire couvre les cas avec et sans artwork local.
  • Finding 2 ne s’applique pas. crate::offline::is_offline() retourne avant la création du client Deezer, l’appel API et metadata_artwork::download_and_cache. Un second garde serait inatteignable.

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

Timestamp: 2026-08-09T00:30:15.731Z
Learning: Dans `src-tauri/crates/app/src/commands/deezer.rs`, `enrich_album_inner` doit traiter une ligne fraîche de `app.metadata_album` sans `cover_hash` comme incomplète lorsque `album.artwork_id IS NULL`. La fonction `metadata_album_cache_complete` applique cette règle afin qu'un album sans artwork local puisse relancer la récupération Deezer avant l'expiration du TTL.

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.

@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.

♻️ Duplicate comments (2)
src-tauri/crates/app/src/commands/deezer.rs (2)

223-225: 🔒 Security & Privacy | 🟡 Minor

Vérifiez le mode hors ligne avant le téléchargement.

Le contrôle de la ligne 173 précède un await vers Deezer. La ligne 224 ouvre un second chemin HTTP via metadata_artwork::download_and_cache. Si le mode hors ligne est activé pendant le premier appel, ce téléchargement peut encore partir. Ajoutez un contrôle juste avant l’appel, ou imposez ce contrôle dans le helper.

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 8 'download_and_cache(_full_res)?|is_offline' \
  src-tauri/crates/core/src/artwork \
  src-tauri/crates/app/src/commands/deezer.rs

As per coding guidelines, chaque chemin HTTP sortant doit vérifier offline::is_offline() avant de continuer.

🤖 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-tauri/crates/app/src/commands/deezer.rs` around lines 223 - 225, Add an
offline-mode guard immediately before the `metadata_artwork::download_and_cache`
call in the `cover_hash` match within the Deezer command, so the artwork HTTP
request is skipped when `offline::is_offline()` is true. Preserve the existing
matching behavior for local artwork and absent cover URLs, and follow the
established offline check used earlier in the command.

Source: Coding guidelines


223-225: 🗄️ Data Integrity & Integration | 🟠 Major

Préservez le cover_hash existant lorsque aucun nouveau fichier n’est écrit.

Lorsque le cache a expiré et que local_artwork_id.is_some(), ce match renvoie None. L’upsert des lignes 235-259 remplace alors un hash existant par NULL. Le fichier déjà stocké devient orphelin. La commande de maintenance ne peut plus retrouver ce hash pour le supprimer. Le même risque existe si le téléchargement best-effort échoue.

Conservez l’ancien hash dans la branche sans téléchargement et lorsque le téléchargement renvoie None. Ajoutez un test avec un cache expiré, une illustration locale et un hash existant.

Correctif proposé
+    let mut cached_cover_hash: Option<String> = None;
+
     if let Some(did) = existing_deezer_id {
         let cached: Option<(
             Option<String>,
             Option<String>,
             Option<String>,
             Option<String>,
             i64,
         )> = sqlx::query_as(
             "SELECT label, release_date, cover_url, cover_hash, expires_at
                FROM app.metadata_album WHERE deezer_id = ?",
         )
         .bind(did)
         .fetch_optional(pool)
         .await?;
+        cached_cover_hash = cached
+            .as_ref()
+            .and_then(|(_, _, _, hash, _)| hash.clone());

@@
     let cover_hash = match (local_artwork_id.is_none(), cover_url.as_deref()) {
-        (true, Some(url)) => metadata_artwork::download_and_cache(url, artwork_dir).await,
-        _ => None,
+        (true, Some(url)) => metadata_artwork::download_and_cache(url, artwork_dir)
+            .await
+            .or(cached_cover_hash),
+        _ => cached_cover_hash,
     };
🤖 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-tauri/crates/app/src/commands/deezer.rs` around lines 223 - 225, Update
the cover_hash handling in the artwork upsert flow to preserve the existing
artwork hash whenever no download occurs or download_and_cache returns None,
including expired-cache cases with local_artwork_id set. Only replace the hash
after a successful new download, and add a test covering an expired cache, local
artwork, and an existing hash.
🤖 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.

Duplicate comments:
In `@src-tauri/crates/app/src/commands/deezer.rs`:
- Around line 223-225: Add an offline-mode guard immediately before the
`metadata_artwork::download_and_cache` call in the `cover_hash` match within the
Deezer command, so the artwork HTTP request is skipped when
`offline::is_offline()` is true. Preserve the existing matching behavior for
local artwork and absent cover URLs, and follow the established offline check
used earlier in the command.
- Around line 223-225: Update the cover_hash handling in the artwork upsert flow
to preserve the existing artwork hash whenever no download occurs or
download_and_cache returns None, including expired-cache cases with
local_artwork_id set. Only replace the hash after a successful new download, and
add a test covering an expired cache, local artwork, and an existing hash.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e4f5bd4b-c0aa-4d68-b0bf-96a4daefb2c7

📥 Commits

Reviewing files that changed from the base of the PR and between d9db659 and 94badcf.

📒 Files selected for processing (1)
  • src-tauri/crates/app/src/commands/deezer.rs

…d download

CodeRabbit round on #494.

- deezer upsert: `cover_hash = COALESCE(excluded.cover_hash, cover_hash)` so a
  skipped (#493 art-having) or transiently-failed download no longer drops a
  good cached cover — only a NEW successful download overwrites the hash.
- prune_cached_album_covers is now cross-profile safe: the metadata cache is
  app-wide, so a cover is prunable only when its album has local art in EVERY
  profile that carries it (read each profile's `album` table, build the "still
  needed" deezer_id set, exclude those). Prevents nulling a shared reference a
  different profile still relies on. The clear + reference re-check run in one
  transaction for a consistent snapshot.
- Settings "Prune covers": the catch now shows a distinct red failure status
  (`settings.pruneAlbumCoversFailed`, ×17) instead of leaving it blank.

Skipped, with reasons:
- offline guard before download_and_cache: unreachable — the command already
  returns at the offline check (step 3) before the API call and the download.
- SQLITE_BUSY/LOCKED retry loop: this is a foreground one-shot, not a background
  batch writer; a transient busy now surfaces via the failure status and the
  user retries — a retry loop would be disproportionate here.
- integration test for the upsert / prune paths: the `waveflow` app-crate tests
  don't start locally (STATUS_ENTRYPOINT_NOT_FOUND, Tauri DLL) and both paths
  are DB+network bound with no injection point; CI runs them.
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Traité dans 73b44822.

Cross-profil — nuller une référence partagée utilisée par un autre profil [FIXED]
prune_cached_album_covers est maintenant cross-profil : le cache metadata est app-wide, donc une cover n'est prunable que si son album a un artwork local dans CHAQUE profil qui le porte. On lit la table album de chaque profil (open read-write sans migration — un open read-only d'une WAL sans writer vivant peut échouer à créer le -shm), on construit l'ensemble des deezer_id encore « needed » (art-less quelque part), et on exclut ceux-là. Plus de nulling d'une référence dont un autre profil dépend (y compris hors ligne).

Transaction [FIXED] — le UPDATE (clear) + le re-check des références tournent dans une seule transaction sur app.db (snapshot cohérent).

Préserver le hash quand pas de nouveau download [FIXED] — upsert : cover_hash = COALESCE(excluded.cover_hash, cover_hash). Un download skippé (#493 album avec pochette) ou échoué transitoirement ne drop plus une cover valide ; seul un nouveau download réussi écrase le hash.

Statut d'échec UI [FIXED] — le catch de « Prune covers » affiche désormais un statut rouge distinct (settings.pruneAlbumCoversFailed, ×17) au lieu de rester vide.

Skips justifiés :

  • offline guard avant download_and_cache : inatteignable — la commande return déjà au check offline (étape 3) avant l'appel API et le download. (duplicate, déjà répondu au round précédent)
  • retry SQLITE_BUSY/LOCKED : c'est une action foreground one-shot, pas un writer batch de fond ; un busy transitoire remonte maintenant via le statut d'échec et l'utilisateur relance — une boucle de retry serait disproportionnée ici.
  • tests d'intégration upsert/prune : les tests du crate waveflow (app) ne démarrent pas en local (STATUS_ENTRYPOINT_NOT_FOUND, DLL Tauri) et les deux chemins sont DB+réseau sans point d'injection ; la CI les exécute. Le prédicat pur metadata_album_cache_complete reste unit-testé.
  • re-valider les références « immédiatement avant » la suppression fichier : la fenêtre résiduelle (enrich concurrent × collision de hash partagé pendant une action manuelle one-shot) est négligeable et auto-réparable via le check de complétude ; la transaction couvre la partie DB.

Validé : cargo clippy -p waveflow --all-targets + bun run typecheck + bun run lint verts.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Commit summary and changed files =='
git show --no-ext-diff --format=fuller --stat 73b44822
git diff-tree --no-commit-id --name-only -r 73b44822

printf '%s\n' '== Relevant Rust symbols =='
for file in \
  src-tauri/crates/app/src/commands/maintenance.rs \
  src-tauri/crates/app/src/commands/deezer.rs \
  src/components/views/SettingsView.tsx \
  src/lib/tauri/library.ts
do
  if [ -f "$file" ]; then
    printf '\n== Outline: %s ==\n' "$file"
    ast-grep outline "$file" --items all --view expanded || true
  fi
done

printf '%s\n' '== Relevant implementation context =='
rg -n -C 14 \
  'prune_cached_album_covers|cover_hash|picture_hash|background_hash|transaction|begin\(|COALESCE|metadata_album_cache_complete|pruneAlbumCovers|rowsCleared|filesDeleted|bytesFreed' \
  src-tauri/crates/app/src/commands/maintenance.rs \
  src-tauri/crates/app/src/commands/deezer.rs \
  src/components/views/SettingsView.tsx \
  src/lib/tauri/library.ts

printf '%s\n' '== Patch at target commit =='
git show --no-ext-diff --format= 73b44822 -- \
  src-tauri/crates/app/src/commands/maintenance.rs \
  src-tauri/crates/app/src/commands/deezer.rs \
  src/components/views/SettingsView.tsx \
  src/lib/tauri/library.ts \
  src/i18n/locales

Length of output: 110431


@InstaZDLL J’ai vérifié 73b44822.

Les correctifs cross-profil, transaction, conservation de cover_hash et statut d’échec UI sont présents.

Il reste un problème de sûreté dans prune_cached_album_covers :

  • La lecture de SELECT id FROM profile utilise .unwrap_or_default().
  • Si cette requête échoue, le code utilise une liste de profils vide.
  • Dans ce cas, needed reste vide.
  • Le code considère alors toutes les lignes metadata_album avec un cover_hash comme prunables.
  • Une erreur transitoire sur app.db peut donc nuller des références de covers encore nécessaires.

La commande doit propager cette erreur au lieu de continuer avec une liste vide.


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

Timestamp: 2026-08-09T00:47:03.460Z
Learning: Dans `src-tauri/crates/app/src/commands/maintenance.rs`, `prune_cached_album_covers` doit examiner les tables `album` de tous les profils, car `app.metadata_album` est partagé entre profils. Une cover reste nécessaire si au moins un profil possède le même `deezer_id` avec `album.artwork_id IS NULL`.

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.

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
src-tauri/crates/app/src/commands/maintenance.rs (1)

122-184: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Sérialisez la suppression de fichiers avec les écritures du cache partagé.

Après le contrôle des références à la Line 164, enrich_album_inner peut restaurer metadata_album.cover_hash à la Line 251 pendant que cette commande supprime encore le fichier. La ligne de cache devient alors fraîche mais référence un fichier absent. Un album sans artwork local ne téléchargera pas de remplacement avant expiration du TTL.

Utilisez un verrou partagé qui couvre les téléchargements, les upserts de cover_hash, le contrôle des références et la suppression des fichiers.

  • src-tauri/crates/app/src/commands/maintenance.rs#L122-L184: prenez le verrou avant la sélection des candidats et conservez-le jusqu’à la fin de delete_cover_files.
  • src-tauri/crates/app/src/commands/deezer.rs#L237-L265: utilisez le même verrou pour toute opération qui télécharge ou écrit metadata_album.cover_hash.
🤖 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-tauri/crates/app/src/commands/maintenance.rs` around lines 122 - 184,
Serialize cache-file deletion with album-cover downloads and cache writes by
introducing or reusing one shared lock. In
src-tauri/crates/app/src/commands/maintenance.rs lines 122-184, acquire it
before candidate selection and hold it through reference validation and
delete_cover_files; in src-tauri/crates/app/src/commands/deezer.rs lines
237-265, use the same lock around the full operation that downloads artwork and
writes metadata_album.cover_hash. Ensure both paths coordinate on the identical
lock.
🤖 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/app/src/commands/maintenance.rs`:
- Around line 87-90: Update prune_cached_album_covers to acquire the
profile-scoped pool with state.require_profile_pool().await? at the beginning
and retain the leased handle through the entire command. Use this handle for all
active-profile database access instead of obtaining or using an unleased pool.
- Around line 97-100: Dans le chargement de profile_ids de la commande de
maintenance, remplacez unwrap_or_default() par la propagation de l’erreur SQLx
afin qu’un échec de lecture de app_db interrompe la commande. Conservez le type
Vec<i64> et la requête existante, sans poursuivre avec une liste vide.

---

Outside diff comments:
In `@src-tauri/crates/app/src/commands/maintenance.rs`:
- Around line 122-184: Serialize cache-file deletion with album-cover downloads
and cache writes by introducing or reusing one shared lock. In
src-tauri/crates/app/src/commands/maintenance.rs lines 122-184, acquire it
before candidate selection and hold it through reference validation and
delete_cover_files; in src-tauri/crates/app/src/commands/deezer.rs lines
237-265, use the same lock around the full operation that downloads artwork and
writes metadata_album.cover_hash. Ensure both paths coordinate on the identical
lock.
🪄 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: ca1ce2e6-f22b-456c-bd05-df9fa8643b08

📥 Commits

Reviewing files that changed from the base of the PR and between 94badcf and 73b4482.

📒 Files selected for processing (20)
  • src-tauri/crates/app/src/commands/deezer.rs
  • src-tauri/crates/app/src/commands/maintenance.rs
  • 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/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

Comment thread src-tauri/crates/app/src/commands/maintenance.rs
Comment thread src-tauri/crates/app/src/commands/maintenance.rs Outdated
CodeRabbit round on #494. `SELECT id FROM profile` used `unwrap_or_default()`:
on a transient `app.db` read error the profile list fell back to empty, leaving
the "still needed" set empty and marking EVERY cached cover prunable — a
mass-deletion of covers other profiles need. Propagate the error instead.
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Traité dans 1b12cc68.

unwrap_or_default() sur la liste des profils [FIXED]
Bien vu — c'était le risque le plus sérieux : sur une erreur transitoire de lecture d'app.db, profile_ids retombait à vide → l'ensemble « still needed » vide → tous les covers marqués prunables → sur-suppression massive. Remplacé par une propagation ? (bail).

require_profile_pool / handle leasé [SKIP — obsolète]
Le finding cible une version antérieure. Le rewrite cross-profil a supprimé tout usage d'un pool de profil : la commande lit state.app_db directement (les tables metadata_* y vivent) et ouvre des pools par chemin pour chaque profil (profile_db(pid)), qu'elle possède et referme elle-même. Il n'y a plus de pool de profil unleased à sécuriser, et un switch de profil concurrent ne peut pas fermer ces pools-là.

Verrou partagé download↔delete [SKIP — disproportionné]
Mettre un verrou global autour du download_and_cache + write de enrich_album_inner sérialiserait tout l'enrichissement d'albums (chemin très chaud : chaque ouverture d'album, chaque track-change pour Discord, le batch) derrière un même download réseau — vraie régression de perf. En face, la course est infime : un enrich concurrent devrait télécharger une image dont les octets hashent vers un de nos hash supprimables, dans la fenêtre sous-seconde entre le commit de la transaction et la suppression fichier — et comme download_and_cache recrée le fichier à l'écriture (if !out.exists()), l'ordre cassant est encore plus étroit. Impact éventuel : un album à cover partagée sans image jusqu'au TTL 30 j (pas de perte de données, pas de crash), et auto-infligé en lançant la purge one-shot pendant un enrichissement actif. La cohérence DB est déjà couverte par la transaction. Disproportionné de verrouiller le chemin chaud pour ça.

Validé : cargo clippy -p waveflow vert.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Commit and changed paths =='
git show --no-ext-diff --format=fuller --stat 1b12cc68
git diff-tree --no-commit-id --name-only -r 1b12cc68

printf '%s\n' '== Relevant outlines =='
ast-grep outline src-tauri/crates/app/src/commands/maintenance.rs --items all --view expanded
ast-grep outline src-tauri/crates/app/src/commands/deezer.rs --items all --view expanded

printf '%s\n' '== Maintenance implementation context =='
rg -n -C 18 \
  'prune_cached_album_covers|SELECT id FROM profile|unwrap_or_default|profile_ids|profile_db|require_profile_pool|app_db|transaction|cover_hash|picture_hash|background_hash|spawn_blocking' \
  src-tauri/crates/app/src/commands/maintenance.rs

printf '%s\n' '== Deezer write/download context =='
rg -n -C 14 \
  'download_and_cache|cover_hash|metadata_album_cache_complete|if !.*exists|exists\(\)' \
  src-tauri/crates/app/src/commands/deezer.rs

printf '%s\n' '== Patch at target commit =='
git show --no-ext-diff --format= 1b12cc68 -- \
  src-tauri/crates/app/src/commands/maintenance.rs \
  src-tauri/crates/app/src/commands/deezer.rs

Length of output: 43384


@InstaZDLL Vérifié dans 1b12cc68.

prune_cached_album_covers propage maintenant l’échec de SELECT id FROM profile avec .await?. La commande s’arrête donc avant de calculer needed et avant de modifier metadata_album. Le risque de suppression massive est corrigé.

Le finding sur require_profile_pool ne s’applique pas à la version actuelle. Cette commande utilise state.app_db et des pools temporaires créés avec profile_db(pid).

Je ne relève pas de problème bloquant pour la fenêtre résiduelle entre le commit DB et la suppression de fichier. Le verrou global proposé aurait un coût disproportionné sur le chemin d’enrichissement.

You are interacting with an AI system.

@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-tauri/crates/app/src/commands/maintenance.rs (2)

148-175: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Ajoutez des commits périodiques pour les gros nettoyages.

Si prune_ids contient beaucoup d’entrées, chunks(400) limite seulement les paramètres SQL. La transaction reste ouverte jusqu’à la Line 175. Le verrou d’écriture SQLite peut bloquer les scans et les autres commandes pendant toute l’opération. Découpez le nettoyage en transactions bornées avec un état de reprise. Conservez le contrôle des références pour chaque lot.

As per coding guidelines, « perform large writes in transactions with periodic commits ».

🤖 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-tauri/crates/app/src/commands/maintenance.rs` around lines 148 - 175,
Découpez le nettoyage autour de la boucle chunks de la fonction concernée en
transactions bornées, avec des commits périodiques afin de ne pas conserver le
verrou d’écriture pendant toute l’opération. Ajoutez un état de reprise pour
traiter chaque lot de prune_ids, puis recréez une transaction pour le lot
suivant. Effectuez également le contrôle des références après chaque commit de
lot, en conservant le nettoyage et le calcul rows_cleared.

Source: Coding guidelines


166-186: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Rendez le nettoyage du cache artwork récupérable et sérialisé.

prune_cached_album_covers committe cover_hash = NULL avant la suppression des fichiers. delete_cover_files ignore les erreurs et aucun hash non supprimé n'est conservé pour une nouvelle tentative. Un fichier orphelin devient donc introuvable lors du nettoyage suivant. La transaction SQLite ne protège pas non plus la fenêtre avec les téléchargements et upserts de metadata_artist dans enrich_artist_deezer_inner.

Ajoutez un verrou partagé avec les écritures du cache artwork et un état persistant de reprise. Ajoutez des tests pour les suppressions échouées, les interruptions et les références recréées pendant le nettoyage.

🤖 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-tauri/crates/app/src/commands/maintenance.rs` around lines 166 - 186,
Rendez le flux prune_cached_album_covers récupérable et sérialisé avec les
écritures du cache artwork : ajoutez un verrou partagé utilisé également par
enrich_artist_deezer_inner et les upserts associés. Persistez les hashes dont
les fichiers n’ont pas été supprimés afin de les retenter lors d’un nettoyage
ultérieur, et ne perdez pas cet état en cas d’interruption ou d’échec de
delete_cover_files. Ajoutez des tests couvrant les suppressions échouées, les
interruptions et la recréation de références pendant le nettoyage.

Source: Coding guidelines

🤖 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-tauri/crates/app/src/commands/maintenance.rs`:
- Around line 148-175: Découpez le nettoyage autour de la boucle chunks de la
fonction concernée en transactions bornées, avec des commits périodiques afin de
ne pas conserver le verrou d’écriture pendant toute l’opération. Ajoutez un état
de reprise pour traiter chaque lot de prune_ids, puis recréez une transaction
pour le lot suivant. Effectuez également le contrôle des références après chaque
commit de lot, en conservant le nettoyage et le calcul rows_cleared.
- Around line 166-186: Rendez le flux prune_cached_album_covers récupérable et
sérialisé avec les écritures du cache artwork : ajoutez un verrou partagé
utilisé également par enrich_artist_deezer_inner et les upserts associés.
Persistez les hashes dont les fichiers n’ont pas été supprimés afin de les
retenter lors d’un nettoyage ultérieur, et ne perdez pas cet état en cas
d’interruption ou d’échec de delete_cover_files. Ajoutez des tests couvrant les
suppressions échouées, les interruptions et la recréation de références pendant
le nettoyage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8230111e-104b-48c5-9722-b51166d6d491

📥 Commits

Reviewing files that changed from the base of the PR and between 73b4482 and 1b12cc6.

📒 Files selected for processing (1)
  • src-tauri/crates/app/src/commands/maintenance.rs

@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Vérifié contre le code actuel — les deux findings sont des SKIP motivés, pas de changement.

Transactions bornées + commits périodiques + état de reprise par lot [SKIP]
La transaction n'englobe pas la suppression fichier : elle couvre seulement les UPDATE batchés + le SELECT de références, puis commit(), et delete_cover_files tourne en spawn_blocking après le commit. Le verrou d'écriture app.db n'est donc tenu que pour les ops DB (≈13 petits UPDATE de 400 ids + 1 UNION SELECT — quelques dizaines de ms sur une action one-shot), jamais pendant le fs.
De plus, découper en commits par lot régresse la correction : le SELECT de références DOIT voir l'état après tous les null pour que l'ensemble référencé soit cohérent. Un check par lot verrait un hash partagé encore référencé par un album d'un lot pas-encore-nullé → on garderait le fichier alors qu'il devient orphelin au lot suivant → sous-suppression. La transaction unique (null complet → un seul re-check → commit → delete hors verrou) est le design correct ici.

Verrou partagé + état de reprise persistant + tests [SKIP]

  • Verrou partagé download↔delete : déjà justifié — sérialiser tout enrich_album_inner (chemin très chaud, à travers un download réseau) derrière un verrou global contre une course infime/auto-infligée/non-destructive est disproportionné ; la cohérence DB est couverte par la transaction.
  • Persister les hashes non supprimés pour retry : les fichiers ciblés sont des covers Deezer d'albums qui ont une pochette locale — jamais affichées (ces albums rendent l'artwork local), donc jamais ouvertes/verrouillées par le webview. Un delete_cover_files qui échoue est quasi impossible ici, et son seul effet est un fichier qui reste sur disque (aucune perte de données, aucune incohérence — le cover_hash est déjà nullé dans la transaction). Ajouter une table de retry + machinerie de reprise pour ce cas best-effort quasi-inexistant et inoffensif est disproportionné ; un futur GC d'orphelins le récupérerait si besoin.
  • Tests (suppressions échouées / interruptions / recréation de références) : les tests du crate waveflow (app) ne démarrent pas en local (STATUS_ENTRYPOINT_NOT_FOUND, DLL Tauri) et ces chemins sont DB+fs+réseau sans point d'injection ; la CI les exécute. Le prédicat pur metadata_album_cache_complete reste unit-testé.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src-tauri/crates/app/src/commands/maintenance.rs"

printf '%s\n' '== Outline =='
ast-grep outline "$file" --items all --view expanded

printf '%s\n' '== Pruning transaction and filesystem context =='
rg -n -C 24 \
  'prune_cached_album_covers|delete_cover_files|transaction|begin\(|commit\(|rollback\(|spawn_blocking|cover_hash|picture_hash|background_hash|UPDATE|SELECT' \
  "$file"

printf '%s\n' '== Current command patch relative to PR base =='
base="$(git merge-base HEAD origin/main 2>/dev/null || true)"
if [ -n "$base" ]; then
  git diff --no-ext-diff -- "$base" HEAD -- "$file"
else
  git diff --no-ext-diff -- "$file"
fi

Length of output: 24541


@InstaZDLL Confirmé après vérification du code actuel.

prune_cached_album_covers exécute les UPDATE par lot et le re-contrôle des références dans une seule transaction. La transaction est validée avant spawn_blocking(delete_cover_files). La suppression de fichiers ne maintient donc pas le verrou d’écriture SQLite.

Des commits par lot empêcheraient le contrôle final de références de voir tous les cover_hash annulés. Cela peut conserver des fichiers devenus orphelins.

Le risque résiduel entre le commit et la suppression de fichiers ne justifie pas un verrou global sur enrich_album_inner. L’échec de suppression reste non destructif : la référence a été annulée et seul un fichier orphelin peut rester sur disque.

Je retire les deux findings.


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

Timestamp: 2026-08-09T01:11:37.319Z
Learning: Dans `src-tauri/crates/app/src/commands/maintenance.rs`, `prune_cached_album_covers` doit exécuter tous les `UPDATE metadata_album` batchés et le contrôle final des références `metadata_album` et `metadata_artist` dans une transaction unique. `delete_cover_files` doit s’exécuter après le commit, dans `spawn_blocking`, afin de ne pas maintenir le verrou d’écriture SQLite pendant les opérations de système de fichiers.

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.

CodeRabbit round on #494. Format the freed megabytes with Intl.NumberFormat
(i18n.resolvedLanguage ?? i18n.language) instead of toFixed(1), so locales that
use a comma decimal separator render correctly (e.g. "12,5" in fr). Keeps the
one-decimal precision; adds the language deps to the useCallback.
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Traité dans e748c11d.

Formatage MB locale-aware [FIXED]
toFixed(1) forçait le point décimal ; remplacé par Intl.NumberFormat(i18n.resolvedLanguage ?? i18n.language, { min/maxFractionDigits: 1 }) (rend « 12,5 » en fr, etc.), précision une décimale conservée + deps i18n.language/i18n.resolvedLanguage ajoutées au useCallback.

Bail leasé require_profile_pool [SKIP]
La commande ne requête pas le pool de profil actif — elle lit app.db directement et ouvre les DB de profils par chemin. Un lease ne garde donc rien ici. Pire, l'ajouter régresserait : require_profile_pool échoue s'il n'y a pas de profil actif (or c'est une maintenance app-wide qui doit tourner quand même), et tenir un lease inutilisé ferait attendre un switch de profil concurrent (drain jusqu'à 5 s) sans bénéfice.

Verrou partagé download↔delete tenu jusqu'après suppression [SKIP]
Même conclusion que les rounds précédents (disproportionné : sérialiser tout enrich_album_inner — chemin très chaud, à travers un download réseau — contre une course infime/auto-infligée/non-destructive). Précision : le finding parle de « revalider les références après tx.commit() », mais dans le code le re-check des références est dans la transaction (avant commit) — il n'y a pas de revalidation post-commit à verrouiller. La cohérence DB est déjà garantie par la transaction ; les fichiers ciblés (covers d'albums avec pochette locale) ne sont jamais affichés donc jamais verrouillés.

Validé : bun run typecheck + bun run lint verts.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Target commit =='
git show --no-ext-diff --format=fuller --stat e748c11d
git show --no-ext-diff --format= -- \
  e748c11d -- \
  src/components/views/SettingsView.tsx \
  src-tauri/crates/app/src/commands/maintenance.rs

printf '%s\n' '== Settings view outline and formatter context =='
ast-grep outline src/components/views/SettingsView.tsx --items all --view expanded
rg -n -C 12 \
  'Intl\.NumberFormat|toFixed\(|pruneCachedAlbumCovers|resolvedLanguage|i18n\.language|useCallback' \
  src/components/views/SettingsView.tsx

printf '%s\n' '== Maintenance outline and transaction context =='
ast-grep outline src-tauri/crates/app/src/commands/maintenance.rs --items all --view expanded
rg -n -C 18 \
  'prune_cached_album_covers|require_profile_pool|profile_db|state\.app_db|transaction|begin\(|commit\(|cover_hash|picture_hash|background_hash|spawn_blocking|delete_cover_files' \
  src-tauri/crates/app/src/commands/maintenance.rs

Length of output: 57467


@InstaZDLL Vérifié dans e748c11d.

Le formatage est corrigé. Intl.NumberFormat utilise la langue résolue et conserve une décimale. Les dépendances de useCallback sont aussi correctes.

prune_cached_album_covers n’utilise pas require_profile_pool. La commande lit state.app_db et ouvre des pools temporaires par profil. Le lease du profil actif ne s’applique donc pas.

Le UPDATE et le contrôle des références sont dans la même transaction. La suppression de fichiers commence après tx.commit() dans spawn_blocking. Je ne relève pas de problème bloquant dans la fenêtre résiduelle.


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

Timestamp: 2026-08-09T01:36:09.859Z
Learning: Dans `src/components/views/SettingsView.tsx`, `handlePruneCachedCovers` doit formater `bytesFreed` avec `Intl.NumberFormat(i18n.resolvedLanguage ?? i18n.language, { minimumFractionDigits: 1, maximumFractionDigits: 1 })` afin que le statut de purge utilise le séparateur décimal de la locale active.

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.

@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/components/views/SettingsView.tsx (2)

1194-1196: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Évitez qu’un ancien minuteur efface le résultat suivant.

Si l’utilisateur relance le nettoyage avant l’expiration des cinq secondes, le setTimeout de la première exécution reste actif. Il peut appeler setPruneCoversStatus(null) pendant ou après la deuxième exécution. Le résultat le plus récent peut donc disparaître trop tôt.

Conservez l’identifiant du minuteur dans un useRef, annulez le minuteur précédent avant d’en créer un nouveau et nettoyez-le au démontage.

Correction proposée
+  const pruneCoversStatusTimerRef = useRef<number | null>(null);
+
+  useEffect(() => {
+    return () => {
+      if (pruneCoversStatusTimerRef.current !== null) {
+        window.clearTimeout(pruneCoversStatusTimerRef.current);
+      }
+    };
+  }, []);
+
   const handlePruneCachedCovers = useCallback(async () => {
...
   } finally {
     setIsPruningCovers(false);
-    window.setTimeout(() => setPruneCoversStatus(null), 5000);
+    if (pruneCoversStatusTimerRef.current !== null) {
+      window.clearTimeout(pruneCoversStatusTimerRef.current);
+    }
+    pruneCoversStatusTimerRef.current = window.setTimeout(() => {
+      setPruneCoversStatus(null);
+      pruneCoversStatusTimerRef.current = null;
+    }, 5000);
   }

Le risque vient du minuteur local créé dans ce gestionnaire.

🤖 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/components/views/SettingsView.tsx` around lines 1194 - 1196, Update the
pruning cleanup flow around setPruneCoversStatus to store the timeout identifier
in a useRef, clear any existing timeout before scheduling a new one, and assign
the new identifier to the ref. Add unmount cleanup to cancel the active timeout
and prevent stale timers from clearing a newer pruning result.

3682-3691: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Annoncez le résultat aux technologies d’assistance.

Le résultat est inséré après une commande asynchrone, mais le <div> n’a ni role ni aria-live. Un lecteur d’écran peut ne pas annoncer le nombre de fichiers supprimés, l’espace libéré ou l’échec.

Correction proposée
                    <div
+                      role={pruneCoversStatus.ok ? "status" : "alert"}
+                      aria-live={
+                        pruneCoversStatus.ok ? "polite" : "assertive"
+                      }
+                      aria-atomic="true"
                       className={`text-xs mt-1 truncate ${

As per path instructions, cette vérification couvre les « problèmes d’accessibilité visibles » du frontend.

🤖 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/components/views/SettingsView.tsx` around lines 3682 - 3691, Ajoutez une
annonce destinée aux technologies d’assistance sur le conteneur rendu par la
condition pruneCoversStatus, en lui attribuant un aria-live approprié et, si
nécessaire, un rôle de statut. Conservez le texte et les styles existants afin
que les succès comme les échecs annoncent le résultat asynchrone.

Source: Path instructions

🤖 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/components/views/SettingsView.tsx`:
- Around line 1194-1196: Update the pruning cleanup flow around
setPruneCoversStatus to store the timeout identifier in a useRef, clear any
existing timeout before scheduling a new one, and assign the new identifier to
the ref. Add unmount cleanup to cancel the active timeout and prevent stale
timers from clearing a newer pruning result.
- Around line 3682-3691: Ajoutez une annonce destinée aux technologies
d’assistance sur le conteneur rendu par la condition pruneCoversStatus, en lui
attribuant un aria-live approprié et, si nécessaire, un rôle de statut.
Conservez le texte et les styles existants afin que les succès comme les échecs
annoncent le résultat asynchrone.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1d93afbd-5c35-40b1-ad78-66f373734ef3

📥 Commits

Reviewing files that changed from the base of the PR and between 1b12cc6 and e748c11.

📒 Files selected for processing (1)
  • src/components/views/SettingsView.tsx

CodeRabbit round on #494. Two small SettingsView fixes:
- Keep the auto-clear timeout in a ref, cancelling any prior one before
  scheduling a new one and on unmount, so a rapid re-run (or unmount) can't have
  a stale timer wipe a newer prune result / fire after teardown.
- Give the prune status container role="status" + aria-live="polite" so both the
  success and failure outcomes are announced to assistive tech.
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai Traité dans 848add82 — les deux valides et corrigés.

Timer de statut (fuite / timer périmé) [FIXED] — le setTimeout d'auto-effacement est désormais stocké dans un useRef ; on annule le précédent avant d'en programmer un nouveau, et un useEffect de cleanup l'annule au démontage. Un re-run rapide ou un unmount ne peut plus effacer un résultat plus récent ni déclencher un setState après teardown.

Annonce a11y [FIXED] — le conteneur de statut porte maintenant role="status" + aria-live="polite", donc succès comme échec sont annoncés aux technologies d'assistance (texte + styles inchangés).

Validé : bun run typecheck + bun run lint verts.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/components/views/SettingsView.tsx"

printf '%s\n' '== Commit =='
git show --no-ext-diff --format=fuller --stat 848add82
git show --no-ext-diff --format= 848add82 -- "$file"

printf '%s\n' '== Component outline =='
ast-grep outline "$file" --items all --view expanded

printf '%s\n' '== Timer and status context =='
rg -n -C 14 \
  'useRef|useEffect|setTimeout|clearTimeout|handlePruneCachedCovers|pruneStatus|role="status"|aria-live' \
  "$file"

Length of output: 46567


@InstaZDLL Vérifié dans 848add82.

Le timeout de statut est stocké dans pruneCoversTimeoutRef. Le code annule le timeout précédent avant d’en créer un nouveau. Le cleanup de useEffect annule aussi le timeout au démontage. Un ancien timeout ne peut plus effacer un statut plus récent ou exécuter setState après démontage.

Le conteneur de statut a role="status" et aria-live="polite". Les résultats de succès et d’échec seront annoncés aux technologies d’assistance.

You are interacting with an AI system.

@InstaZDLL
InstaZDLL merged commit 5281d9d into main Aug 9, 2026
15 checks passed
@InstaZDLL
InstaZDLL deleted the fix/493-deezer-album-cover-leak branch August 9, 2026 01:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: l 200-500 lines type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: issue with Deezer scraper (featch albums images ,even if we did not ask for it)

1 participant