From 308eb36f05806f093e220da9e6cb630bfcf5803c Mon Sep 17 00:00:00 2001 From: Maxim Isaev Date: Sun, 10 May 2026 01:34:40 +0300 Subject: [PATCH] feat(analysis): re-analyze waveform when clearing loudness cache Add analysis_delete_waveform_for_track, invoke it from loudness reseed, clear waveformBins in the UI, and extend queue strings for tooltips/toast. --- .../src/analysis_cache/store.rs | 19 +++++++++++++++++++ .../crates/psysonic-analysis/src/commands.rs | 8 ++++++++ src-tauri/src/lib.rs | 1 + src/components/QueuePanel.tsx | 4 ++-- src/locales/de.ts | 2 ++ src/locales/en.ts | 2 ++ src/locales/es.ts | 2 ++ src/locales/fr.ts | 2 ++ src/locales/nb.ts | 2 ++ src/locales/nl.ts | 2 ++ src/locales/ru.ts | 2 ++ src/locales/zh.ts | 2 ++ src/store/playerStore.ts | 12 +++++++++++- 13 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src-tauri/crates/psysonic-analysis/src/analysis_cache/store.rs b/src-tauri/crates/psysonic-analysis/src/analysis_cache/store.rs index 1270d9d9..dd04f09e 100644 --- a/src-tauri/crates/psysonic-analysis/src/analysis_cache/store.rs +++ b/src-tauri/crates/psysonic-analysis/src/analysis_cache/store.rs @@ -109,6 +109,25 @@ impl AnalysisCache { Ok(total) } + /// Remove all `waveform_cache` rows for this logical track (bare id and `stream:` variant). + pub fn delete_waveform_for_track_id(&self, track_id: &str) -> Result { + if track_id.trim().is_empty() { + return Ok(0); + } + let conn = self + .conn + .lock() + .map_err(|_| "analysis_cache lock poisoned".to_string())?; + let mut total: u64 = 0; + for tid in track_id_cache_variants(track_id) { + let n = conn + .execute("DELETE FROM waveform_cache WHERE track_id = ?1", params![tid]) + .map_err(|e| e.to_string())?; + total = total.saturating_add(n as u64); + } + Ok(total) + } + /// Remove all cached waveform rows across all tracks/variants. pub fn delete_all_waveforms(&self) -> Result { let conn = self diff --git a/src-tauri/crates/psysonic-analysis/src/commands.rs b/src-tauri/crates/psysonic-analysis/src/commands.rs index efbf4dec..31f957fb 100644 --- a/src-tauri/crates/psysonic-analysis/src/commands.rs +++ b/src-tauri/crates/psysonic-analysis/src/commands.rs @@ -141,6 +141,14 @@ pub fn analysis_delete_loudness_for_track( cache.delete_loudness_for_track_id(&track_id) } +#[tauri::command] +pub fn analysis_delete_waveform_for_track( + track_id: String, + cache: tauri::State<'_, analysis_cache::AnalysisCache>, +) -> Result { + cache.delete_waveform_for_track_id(&track_id) +} + #[tauri::command] pub fn analysis_delete_all_waveforms( cache: tauri::State<'_, analysis_cache::AnalysisCache>, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a1b6f21e..8df92576 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -412,6 +412,7 @@ pub fn run() { psysonic_analysis::commands::analysis_get_waveform_for_track, psysonic_analysis::commands::analysis_get_loudness_for_track, psysonic_analysis::commands::analysis_delete_loudness_for_track, + psysonic_analysis::commands::analysis_delete_waveform_for_track, psysonic_analysis::commands::analysis_delete_all_waveforms, psysonic_analysis::commands::analysis_enqueue_seed_from_url, psysonic_analysis::commands::analysis_prune_pending_to_track_ids, diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index 5b1707c8..5dab5cda 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -814,8 +814,8 @@ function QueuePanelHostOrSolo() { setLufsTgtOpen(false); void reanalyzeLoudnessForTrack(currentTrack.id); }} - data-tooltip="Clear cached loudness and re-analyze this track" - aria-label="Clear cached loudness and re-analyze this track" + data-tooltip={t('queue.clearCachedLoudnessWaveform')} + aria-label={t('queue.clearCachedLoudnessWaveform')} > {liveGainLabel} diff --git a/src/locales/de.ts b/src/locales/de.ts index eed41a07..21e42bdb 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1184,6 +1184,8 @@ export const deTranslation = { sourceOffline: 'Wiedergabe aus der Offline-Bibliothek', sourceHot: 'Wiedergabe aus dem Cache', sourceStream: 'Wiedergabe aus dem Netzwerkstream', + clearCachedLoudnessWaveform: 'Clear cached loudness and waveform, then re-analyze this track', + recalculatingLoudnessWaveform: 'Recalculating loudness and waveform for this track…', }, miniPlayer: { showQueue: 'Warteschlange einblenden', diff --git a/src/locales/en.ts b/src/locales/en.ts index ce769300..8d97826c 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1189,6 +1189,8 @@ export const enTranslation = { sourceOffline: 'Playing from offline library', sourceHot: 'Playing from cache', sourceStream: 'Playing from network stream', + clearCachedLoudnessWaveform: 'Clear cached loudness and waveform, then re-analyze this track', + recalculatingLoudnessWaveform: 'Recalculating loudness and waveform for this track…', }, miniPlayer: { showQueue: 'Show queue', diff --git a/src/locales/es.ts b/src/locales/es.ts index 543f3bcf..bfc5523c 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1166,6 +1166,8 @@ export const esTranslation = { sourceOffline: 'Reproducción desde la biblioteca sin conexión', sourceHot: 'Reproducción desde la caché', sourceStream: 'Reproducción desde la transmisión en red', + clearCachedLoudnessWaveform: 'Clear cached loudness and waveform, then re-analyze this track', + recalculatingLoudnessWaveform: 'Recalculating loudness and waveform for this track…', }, miniPlayer: { showQueue: 'Mostrar cola', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 155758c7..4c87bac5 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -1162,6 +1162,8 @@ export const frTranslation = { sourceOffline: 'Lecture depuis la bibliothèque hors ligne', sourceHot: 'Lecture depuis le cache', sourceStream: 'Lecture depuis le flux réseau', + clearCachedLoudnessWaveform: 'Clear cached loudness and waveform, then re-analyze this track', + recalculatingLoudnessWaveform: 'Recalculating loudness and waveform for this track…', }, miniPlayer: { showQueue: 'Afficher la file', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 1ec71a20..0947b9f0 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -1161,6 +1161,8 @@ export const nbTranslation = { sourceOffline: 'Spiller fra offlinebibliotek', sourceHot: 'Spiller fra cache', sourceStream: 'Spiller fra nettverksstrøm', + clearCachedLoudnessWaveform: 'Clear cached loudness and waveform, then re-analyze this track', + recalculatingLoudnessWaveform: 'Recalculating loudness and waveform for this track…', }, miniPlayer: { showQueue: 'Vis kø', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index b0152a2e..477881ac 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -1161,6 +1161,8 @@ export const nlTranslation = { sourceOffline: 'Afspelen vanuit offlinebibliotheek', sourceHot: 'Afspelen vanuit cache', sourceStream: 'Afspelen vanuit netwerkstream', + clearCachedLoudnessWaveform: 'Clear cached loudness and waveform, then re-analyze this track', + recalculatingLoudnessWaveform: 'Recalculating loudness and waveform for this track…', }, miniPlayer: { showQueue: 'Wachtrij tonen', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 74e8ac26..98e94bae 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1214,6 +1214,8 @@ export const ruTranslation = { sourceOffline: 'Играет из офлайн-библиотеки', sourceHot: 'Играет из кэша', sourceStream: 'Играет из сетевого потока', + clearCachedLoudnessWaveform: 'Сбросить кэш громкости (LUFS) и формы волны и заново проанализировать трек', + recalculatingLoudnessWaveform: 'Пересчёт громкости и формы волны для этого трека…', }, miniPlayer: { showQueue: 'Показать очередь', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index cfd7862f..01ec59ab 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -1156,6 +1156,8 @@ export const zhTranslation = { sourceOffline: '正在从离线库播放', sourceHot: '正在从缓存播放', sourceStream: '正在从网络流播放', + clearCachedLoudnessWaveform: 'Clear cached loudness and waveform, then re-analyze this track', + recalculatingLoudnessWaveform: 'Recalculating loudness and waveform for this track…', }, miniPlayer: { showQueue: '显示队列', diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts index 9eefb62b..6f839ca6 100644 --- a/src/store/playerStore.ts +++ b/src/store/playerStore.ts @@ -3,6 +3,7 @@ import { persist, createJSONStorage } from 'zustand/middleware'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import { showToast } from '../utils/toast'; +import i18n from '../i18n'; import { buildCoverArtUrl, buildStreamUrl, getPlayQueue, savePlayQueue, reportNowPlaying, scrobbleSong, SubsonicSong, getSong, getRandomSongs, getSimilarSongs2, getTopSongs, InternetRadioStation, setRating, getAlbumInfo2 } from '../api/subsonic'; import { resolvePlaybackUrl, streamUrlTrackId, getPlaybackSourceKind, type PlaybackSourceKind } from '../utils/resolvePlaybackUrl'; import { redactSubsonicUrlForLog } from '../utils/redactSubsonicUrl'; @@ -1051,6 +1052,10 @@ async function reseedLoudnessForTrackId(trackId: string) { if (!trackId) return; const auth = useAuthStore.getState(); if (auth.normalizationEngine !== 'loudness') return; + bumpWaveformRefreshGen(trackId); + if (usePlayerStore.getState().currentTrack?.id === trackId) { + usePlayerStore.setState({ waveformBins: null }); + } clearLoudnessCacheStateForTrackId(trackId); resetLoudnessBackfillStateForTrackId(trackId); if (auth.normalizationEngine === 'loudness') { @@ -1060,6 +1065,11 @@ async function reseedLoudnessForTrackId(trackId: string) { normalizationEngineLive: 'loudness', }); } + try { + await invoke('analysis_delete_waveform_for_track', { trackId }); + } catch (e) { + console.error('[psysonic] analysis_delete_waveform_for_track failed:', e); + } try { await invoke('analysis_delete_loudness_for_track', { trackId }); } catch (e) { @@ -3573,7 +3583,7 @@ export const usePlayerStore = create()( reanalyzeLoudnessForTrack: async (trackId: string) => { try { - showToast('Recalculating loudness for this track…', 2000, 'info'); + showToast(i18n.t('queue.recalculatingLoudnessWaveform'), 2000, 'info'); } catch { // no-op }