feat(dz): add cache clearing and database reset functionality

Add ability to fully clear cached online library by deleting and recreating the database file.
Integrate new Clear Cache option in settings UI, which restarts the app after clearing.
Remove unused artist/album fields from cache and UI. Add process plugin for relaunch.
This commit is contained in:
2025-10-02 13:40:13 -04:00
parent d8456ce912
commit d774aba0d4
11 changed files with 143 additions and 61 deletions

View File

@@ -10,6 +10,7 @@
"@tauri-apps/plugin-fs": "^2.4.2", "@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "~2",
"@tauri-apps/plugin-sql": "^2.3.0", "@tauri-apps/plugin-sql": "^2.3.0",
"@tauri-apps/plugin-store": "~2", "@tauri-apps/plugin-store": "~2",
"blowfish-node": "^1.1.4", "blowfish-node": "^1.1.4",
@@ -187,6 +188,8 @@
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA=="], "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA=="],
"@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ=="],
"@tauri-apps/plugin-sql": ["@tauri-apps/plugin-sql@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-JYwIocfsLaDWa41LMiZWuzts7yCJR+EpZPRmgpO7Gd7XiAS9S67dKz306j/k/d9XntB0YopMRBol2OIWMschuA=="], "@tauri-apps/plugin-sql": ["@tauri-apps/plugin-sql@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-JYwIocfsLaDWa41LMiZWuzts7yCJR+EpZPRmgpO7Gd7XiAS9S67dKz306j/k/d9XntB0YopMRBol2OIWMschuA=="],
"@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-PjBnlnH6jyI71MGhrPaxUUCsOzc7WO1mbc4gRhME0m2oxLgCqbksw6JyeKQimuzv4ysdpNO3YbmaY2haf82a3A=="], "@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-PjBnlnH6jyI71MGhrPaxUUCsOzc7WO1mbc4gRhME0m2oxLgCqbksw6JyeKQimuzv4ysdpNO3YbmaY2haf82a3A=="],

View File

@@ -19,6 +19,7 @@
"@tauri-apps/plugin-fs": "^2.4.2", "@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "~2",
"@tauri-apps/plugin-sql": "^2.3.0", "@tauri-apps/plugin-sql": "^2.3.0",
"@tauri-apps/plugin-store": "~2", "@tauri-apps/plugin-store": "~2",
"blowfish-node": "^1.1.4", "blowfish-node": "^1.1.4",

11
src-tauri/Cargo.lock generated
View File

@@ -16,6 +16,7 @@ dependencies = [
"tauri-plugin-fs", "tauri-plugin-fs",
"tauri-plugin-http", "tauri-plugin-http",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-sql", "tauri-plugin-sql",
"tauri-plugin-store", "tauri-plugin-store",
] ]
@@ -4750,6 +4751,16 @@ dependencies = [
"zbus", "zbus",
] ]
[[package]]
name = "tauri-plugin-process"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]] [[package]]
name = "tauri-plugin-sql" name = "tauri-plugin-sql"
version = "2.3.0" version = "2.3.0"

View File

@@ -29,4 +29,5 @@ tauri-plugin-http = { version = "2", features = ["unsafe-headers"] }
tauri-plugin-sql = { version = "2", features = ["sqlite"] } tauri-plugin-sql = { version = "2", features = ["sqlite"] }
id3 = "1.16.3" id3 = "1.16.3"
metaflac = "0.2.8" metaflac = "0.2.8"
tauri-plugin-process = "2"

View File

@@ -59,6 +59,7 @@
] ]
}, },
"sql:default", "sql:default",
"sql:allow-execute" "sql:allow-execute",
"process:default"
] ]
} }

View File

@@ -16,21 +16,15 @@ fn tag_audio_file(
cover_data: Option<Vec<u8>>, cover_data: Option<Vec<u8>>,
embed_lyrics: bool, embed_lyrics: bool,
) -> Result<(), String> { ) -> Result<(), String> {
tagger::tag_audio_file( tagger::tag_audio_file(&path, &metadata, cover_data.as_deref(), embed_lyrics)
&path,
&metadata,
cover_data.as_deref(),
embed_lyrics,
)
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let library_migrations = vec![ let library_migrations = vec![Migration {
Migration { version: 1,
version: 1, description: "create_library_tables",
description: "create_library_tables", sql: "
sql: "
CREATE TABLE IF NOT EXISTS artists ( CREATE TABLE IF NOT EXISTS artists (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
@@ -61,15 +55,13 @@ pub fn run() {
CREATE INDEX IF NOT EXISTS idx_albums_year ON albums(year); CREATE INDEX IF NOT EXISTS idx_albums_year ON albums(year);
CREATE INDEX IF NOT EXISTS idx_albums_artist_title ON albums(artist_name, title); CREATE INDEX IF NOT EXISTS idx_albums_artist_title ON albums(artist_name, title);
", ",
kind: MigrationKind::Up, kind: MigrationKind::Up,
} }];
];
let deezer_migrations = vec![ let deezer_migrations = vec![Migration {
Migration { version: 1,
version: 1, description: "create_deezer_cache_tables",
description: "create_deezer_cache_tables", sql: "
sql: "
CREATE TABLE IF NOT EXISTS deezer_playlists ( CREATE TABLE IF NOT EXISTS deezer_playlists (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
title TEXT NOT NULL, title TEXT NOT NULL,
@@ -94,7 +86,6 @@ pub fn run() {
CREATE TABLE IF NOT EXISTS deezer_artists ( CREATE TABLE IF NOT EXISTS deezer_artists (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
nb_album INTEGER DEFAULT 0,
picture_small TEXT, picture_small TEXT,
picture_medium TEXT, picture_medium TEXT,
cached_at INTEGER NOT NULL cached_at INTEGER NOT NULL
@@ -114,16 +105,16 @@ pub fn run() {
CREATE INDEX IF NOT EXISTS idx_deezer_artists_name ON deezer_artists(name); CREATE INDEX IF NOT EXISTS idx_deezer_artists_name ON deezer_artists(name);
CREATE INDEX IF NOT EXISTS idx_deezer_tracks_title ON deezer_tracks(title); CREATE INDEX IF NOT EXISTS idx_deezer_tracks_title ON deezer_tracks(title);
", ",
kind: MigrationKind::Up, kind: MigrationKind::Up,
} }];
];
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_process::init())
.plugin( .plugin(
tauri_plugin_sql::Builder::new() tauri_plugin_sql::Builder::new()
.add_migrations("sqlite:library.db", library_migrations) .add_migrations("sqlite:library.db", library_migrations)
.add_migrations("sqlite:deezer.db", deezer_migrations) .add_migrations("sqlite:deezer.db", deezer_migrations)
.build() .build(),
) )
.plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())

View File

@@ -1,4 +1,7 @@
use id3::{Tag as ID3Tag, TagLike, Version, frame::{Picture, PictureType}}; use id3::{
frame::{Picture, PictureType},
Tag as ID3Tag, TagLike, Version,
};
use metaflac::Tag as FlacTag; use metaflac::Tag as FlacTag;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path; use std::path::Path;
@@ -60,8 +63,7 @@ fn tag_mp3(
embed_lyrics: bool, embed_lyrics: bool,
) -> Result<(), String> { ) -> Result<(), String> {
// Read or create tag // Read or create tag
let mut tag = ID3Tag::read_from_path(path) let mut tag = ID3Tag::read_from_path(path).unwrap_or_else(|_| ID3Tag::new());
.unwrap_or_else(|_| ID3Tag::new());
// Basic metadata // Basic metadata
if let Some(ref title) = metadata.title { if let Some(ref title) = metadata.title {
@@ -129,57 +131,75 @@ fn tag_mp3(
// Custom text frames (TXXX) // Custom text frames (TXXX)
if let Some(ref barcode) = metadata.barcode { if let Some(ref barcode) = metadata.barcode {
use id3::frame::{ExtendedText, Content}; use id3::frame::{Content, ExtendedText};
let ext_text = ExtendedText { let ext_text = ExtendedText {
description: "BARCODE".to_string(), description: "BARCODE".to_string(),
value: barcode.clone(), value: barcode.clone(),
}; };
tag.add_frame(id3::frame::Frame::with_content("TXXX", Content::ExtendedText(ext_text))); tag.add_frame(id3::frame::Frame::with_content(
"TXXX",
Content::ExtendedText(ext_text),
));
} }
if let Some(explicit) = metadata.explicit { if let Some(explicit) = metadata.explicit {
use id3::frame::{ExtendedText, Content}; use id3::frame::{Content, ExtendedText};
let ext_text = ExtendedText { let ext_text = ExtendedText {
description: "ITUNESADVISORY".to_string(), description: "ITUNESADVISORY".to_string(),
value: if explicit { "1" } else { "0" }.to_string(), value: if explicit { "1" } else { "0" }.to_string(),
}; };
tag.add_frame(id3::frame::Frame::with_content("TXXX", Content::ExtendedText(ext_text))); tag.add_frame(id3::frame::Frame::with_content(
"TXXX",
Content::ExtendedText(ext_text),
));
} }
if let Some(ref replay_gain) = metadata.replay_gain { if let Some(ref replay_gain) = metadata.replay_gain {
use id3::frame::{ExtendedText, Content}; use id3::frame::{Content, ExtendedText};
let ext_text = ExtendedText { let ext_text = ExtendedText {
description: "REPLAYGAIN_TRACK_GAIN".to_string(), description: "REPLAYGAIN_TRACK_GAIN".to_string(),
value: replay_gain.clone(), value: replay_gain.clone(),
}; };
tag.add_frame(id3::frame::Frame::with_content("TXXX", Content::ExtendedText(ext_text))); tag.add_frame(id3::frame::Frame::with_content(
"TXXX",
Content::ExtendedText(ext_text),
));
} }
if let Some(ref source_id) = metadata.source_id { if let Some(ref source_id) = metadata.source_id {
use id3::frame::{ExtendedText, Content}; use id3::frame::{Content, ExtendedText};
let source_text = ExtendedText { let source_text = ExtendedText {
description: "SOURCE".to_string(), description: "SOURCE".to_string(),
value: "Deezer".to_string(), value: "Deezer".to_string(),
}; };
tag.add_frame(id3::frame::Frame::with_content("TXXX", Content::ExtendedText(source_text))); tag.add_frame(id3::frame::Frame::with_content(
"TXXX",
Content::ExtendedText(source_text),
));
let sourceid_text = ExtendedText { let sourceid_text = ExtendedText {
description: "SOURCEID".to_string(), description: "SOURCEID".to_string(),
value: source_id.clone(), value: source_id.clone(),
}; };
tag.add_frame(id3::frame::Frame::with_content("TXXX", Content::ExtendedText(sourceid_text))); tag.add_frame(id3::frame::Frame::with_content(
"TXXX",
Content::ExtendedText(sourceid_text),
));
} }
// Lyrics (USLT frame) // Lyrics (USLT frame)
if embed_lyrics { if embed_lyrics {
if let Some(ref lyrics) = metadata.lyrics_unsync { if let Some(ref lyrics) = metadata.lyrics_unsync {
use id3::frame::{Lyrics, Content}; use id3::frame::{Content, Lyrics};
let lyrics_frame = Lyrics { let lyrics_frame = Lyrics {
lang: "eng".to_string(), lang: "eng".to_string(),
description: String::new(), description: String::new(),
text: lyrics.clone(), text: lyrics.clone(),
}; };
tag.add_frame(id3::frame::Frame::with_content("USLT", Content::Lyrics(lyrics_frame))); tag.add_frame(id3::frame::Frame::with_content(
"USLT",
Content::Lyrics(lyrics_frame),
));
} }
} }
@@ -193,7 +213,10 @@ fn tag_mp3(
description: "Cover".to_string(), description: "Cover".to_string(),
data: cover_bytes.to_vec(), data: cover_bytes.to_vec(),
}; };
tag.add_frame(id3::frame::Frame::with_content("APIC", id3::frame::Content::Picture(picture))); tag.add_frame(id3::frame::Frame::with_content(
"APIC",
id3::frame::Content::Picture(picture),
));
} }
} }
@@ -209,8 +232,8 @@ fn tag_flac(
cover_data: Option<&[u8]>, cover_data: Option<&[u8]>,
embed_lyrics: bool, embed_lyrics: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let mut tag = FlacTag::read_from_path(path) let mut tag =
.map_err(|e| format!("Failed to read FLAC file: {}", e))?; FlacTag::read_from_path(path).map_err(|e| format!("Failed to read FLAC file: {}", e))?;
// Remove all existing vorbis comments to start fresh // Remove all existing vorbis comments to start fresh
let vorbis = tag.vorbis_comments_mut(); let vorbis = tag.vorbis_comments_mut();
@@ -279,7 +302,10 @@ fn tag_flac(
} }
if let Some(explicit) = metadata.explicit { if let Some(explicit) = metadata.explicit {
tag.set_vorbis("ITUNESADVISORY", vec![if explicit { "1" } else { "0" }.to_string()]); tag.set_vorbis(
"ITUNESADVISORY",
vec![if explicit { "1" } else { "0" }.to_string()],
);
} }
if let Some(ref replay_gain) = metadata.replay_gain { if let Some(ref replay_gain) = metadata.replay_gain {
@@ -303,7 +329,11 @@ fn tag_flac(
if !cover_bytes.is_empty() { if !cover_bytes.is_empty() {
let mime_type = detect_mime_type_str(cover_bytes); let mime_type = detect_mime_type_str(cover_bytes);
tag.remove_picture_type(metaflac::block::PictureType::CoverFront); tag.remove_picture_type(metaflac::block::PictureType::CoverFront);
tag.add_picture(mime_type, metaflac::block::PictureType::CoverFront, cover_bytes.to_vec()); tag.add_picture(
mime_type,
metaflac::block::PictureType::CoverFront,
cover_bytes.to_vec(),
);
} }
} }

View File

@@ -120,7 +120,11 @@ export async function upsertArtist(artist: {
[artist.path] [artist.path]
); );
return artists[0]?.id || result.lastInsertId; const artistId = artists[0]?.id ?? result.lastInsertId;
if (artistId == null) {
throw new Error('Failed to get artist ID from upsert operation');
}
return artistId;
} }
/** /**

View File

@@ -1,4 +1,6 @@
import Database from '@tauri-apps/plugin-sql'; import Database from '@tauri-apps/plugin-sql';
import { remove } from '@tauri-apps/plugin-fs';
import { appConfigDir } from '@tauri-apps/api/path';
export interface DeezerPlaylist { export interface DeezerPlaylist {
id: string; id: string;
@@ -24,7 +26,6 @@ export interface DeezerAlbum {
export interface DeezerArtist { export interface DeezerArtist {
id: string; id: string;
name: string; name: string;
nb_album: number;
picture_small?: string; picture_small?: string;
picture_medium?: string; picture_medium?: string;
cached_at: number; cached_at: number;
@@ -51,6 +52,16 @@ export async function initDeezerDatabase(): Promise<Database> {
return db; return db;
} }
/**
* Close database connection (for cache clearing)
*/
export async function closeDeezerDatabase(): Promise<void> {
if (db) {
await db.close();
db = null;
}
}
/** /**
* Get cached playlists * Get cached playlists
*/ */
@@ -177,12 +188,11 @@ export async function upsertArtists(artists: any[]): Promise<void> {
// Insert new artists // Insert new artists
for (const artist of artists) { for (const artist of artists) {
await database.execute( await database.execute(
`INSERT INTO deezer_artists (id, name, nb_album, picture_small, picture_medium, cached_at) `INSERT INTO deezer_artists (id, name, picture_small, picture_medium, cached_at)
VALUES ($1, $2, $3, $4, $5, $6)`, VALUES ($1, $2, $3, $4, $5)`,
[ [
String(artist.ART_ID), String(artist.ART_ID),
artist.ART_NAME || '', artist.ART_NAME || '',
artist.NB_ALBUM || 0,
artist.ART_PICTURE || null, artist.ART_PICTURE || null,
artist.PICTURE_TYPE || null, artist.PICTURE_TYPE || null,
now now
@@ -233,10 +243,22 @@ export async function getCacheTimestamp(): Promise<number | null> {
* Clear all Deezer cache * Clear all Deezer cache
*/ */
export async function clearDeezerCache(): Promise<void> { export async function clearDeezerCache(): Promise<void> {
const database = await initDeezerDatabase(); try {
await database.execute('DELETE FROM deezer_playlists'); // Close the database connection
await database.execute('DELETE FROM deezer_albums'); await closeDeezerDatabase();
await database.execute('DELETE FROM deezer_artists');
await database.execute('DELETE FROM deezer_tracks'); // Delete the entire database file
await database.execute('VACUUM'); const configDir = await appConfigDir();
const dbPath = `${configDir}/deezer.db`;
await remove(dbPath);
// Reinitialize the database (this will run migrations)
await initDeezerDatabase();
console.log('[deezer-database] Deezer database file deleted and recreated successfully');
} catch (error) {
console.error('[deezer-database] Error clearing cache:', error);
throw error;
}
} }

View File

@@ -340,7 +340,6 @@
<tr> <tr>
<th>Playlist</th> <th>Playlist</th>
<th>Tracks</th> <th>Tracks</th>
<th>Creator</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -351,7 +350,6 @@
> >
<td>{playlist.title}</td> <td>{playlist.title}</td>
<td>{playlist.nb_tracks}</td> <td>{playlist.nb_tracks}</td>
<td>{playlist.creator_name}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -403,7 +401,6 @@
<thead> <thead>
<tr> <tr>
<th>Artist</th> <th>Artist</th>
<th>Albums</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -413,7 +410,6 @@
onclick={() => handleItemClick(i)} onclick={() => handleItemClick(i)}
> >
<td>{artist.name}</td> <td>{artist.name}</td>
<td>{artist.nb_album}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
@@ -433,7 +429,6 @@
<tr> <tr>
<th>Album</th> <th>Album</th>
<th>Artist</th> <th>Artist</th>
<th>Tracks</th>
<th>Year</th> <th>Year</th>
</tr> </tr>
</thead> </thead>
@@ -445,7 +440,6 @@
> >
<td>{album.title}</td> <td>{album.title}</td>
<td>{album.artist_name}</td> <td>{album.artist_name}</td>
<td>{album.nb_tracks}</td>
<td>{album.release_date ? new Date(album.release_date).getFullYear() : '—'}</td> <td>{album.release_date ? new Date(album.release_date).getFullYear() : '—'}</td>
</tr> </tr>
{/each} {/each}

View File

@@ -15,7 +15,9 @@
loadSettings loadSettings
} from '$lib/stores/settings'; } from '$lib/stores/settings';
import { clearLibrary as clearLibraryDb } from '$lib/library/database'; import { clearLibrary as clearLibraryDb } from '$lib/library/database';
import { clearDeezerCache } from '$lib/library/deezer-database';
import { open, confirm, message } from '@tauri-apps/plugin-dialog'; import { open, confirm, message } from '@tauri-apps/plugin-dialog';
import { relaunch } from '@tauri-apps/plugin-process';
let currentMusicFolder = $state<string | null>(null); let currentMusicFolder = $state<string | null>(null);
let currentPlaylistsFolder = $state<string | null>(null); let currentPlaylistsFolder = $state<string | null>(null);
@@ -100,6 +102,22 @@
} }
} }
} }
async function clearDeezerDatabase() {
const confirmed = await confirm(
'This will clear all cached Deezer favorites data and restart the app. Continue?',
{ title: 'Clear Deezer Cache', kind: 'warning' }
);
if (confirmed) {
try {
await clearDeezerCache();
await relaunch();
} catch (error) {
await message('Error clearing Deezer cache: ' + (error as Error).message, { title: 'Error', kind: 'error' });
}
}
}
</script> </script>
<div style="padding: 8px;"> <div style="padding: 8px;">
@@ -292,6 +310,12 @@
<small class="help-text">This will delete all cached library data from the database. Your music files will not be affected.</small> <small class="help-text">This will delete all cached library data from the database. Your music files will not be affected.</small>
<button onclick={clearLibraryDatabase}>Clear Library Database</button> <button onclick={clearLibraryDatabase}>Clear Library Database</button>
</div> </div>
<div class="field-row-stacked">
<div class="setting-heading">Clear Deezer Cache</div>
<small class="help-text">This will delete all cached Deezer favorites data. The next time you visit the Deezer page, it will refetch from the API.</small>
<button onclick={clearDeezerDatabase}>Clear Deezer Cache</button>
</div>
</section> </section>
{/if} {/if}
</div> </div>