diff --git a/bun.lock b/bun.lock index f9d0857..066fb1a 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "@tauri-apps/plugin-fs": "^2.4.2", "@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-process": "~2", "@tauri-apps/plugin-sql": "^2.3.0", "@tauri-apps/plugin-store": "~2", "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-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-store": ["@tauri-apps/plugin-store@2.4.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-PjBnlnH6jyI71MGhrPaxUUCsOzc7WO1mbc4gRhME0m2oxLgCqbksw6JyeKQimuzv4ysdpNO3YbmaY2haf82a3A=="], diff --git a/package.json b/package.json index 2941ebe..193c5ba 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@tauri-apps/plugin-fs": "^2.4.2", "@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-process": "~2", "@tauri-apps/plugin-sql": "^2.3.0", "@tauri-apps/plugin-store": "~2", "blowfish-node": "^1.1.4", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 35cd80f..1f71efc 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -16,6 +16,7 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-http", "tauri-plugin-opener", + "tauri-plugin-process", "tauri-plugin-sql", "tauri-plugin-store", ] @@ -4750,6 +4751,16 @@ dependencies = [ "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]] name = "tauri-plugin-sql" version = "2.3.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index be1a07b..49f8907 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,4 +29,5 @@ tauri-plugin-http = { version = "2", features = ["unsafe-headers"] } tauri-plugin-sql = { version = "2", features = ["sqlite"] } id3 = "1.16.3" metaflac = "0.2.8" +tauri-plugin-process = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 362343d..98c9a08 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -59,6 +59,7 @@ ] }, "sql:default", - "sql:allow-execute" + "sql:allow-execute", + "process:default" ] } \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6b21b8a..f380148 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,21 +16,15 @@ fn tag_audio_file( cover_data: Option>, embed_lyrics: bool, ) -> Result<(), String> { - tagger::tag_audio_file( - &path, - &metadata, - cover_data.as_deref(), - embed_lyrics, - ) + tagger::tag_audio_file(&path, &metadata, cover_data.as_deref(), embed_lyrics) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let library_migrations = vec![ - Migration { - version: 1, - description: "create_library_tables", - sql: " + let library_migrations = vec![Migration { + version: 1, + description: "create_library_tables", + sql: " CREATE TABLE IF NOT EXISTS artists ( id INTEGER PRIMARY KEY AUTOINCREMENT, 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_artist_title ON albums(artist_name, title); ", - kind: MigrationKind::Up, - } - ]; + kind: MigrationKind::Up, + }]; - let deezer_migrations = vec![ - Migration { - version: 1, - description: "create_deezer_cache_tables", - sql: " + let deezer_migrations = vec![Migration { + version: 1, + description: "create_deezer_cache_tables", + sql: " CREATE TABLE IF NOT EXISTS deezer_playlists ( id TEXT PRIMARY KEY, title TEXT NOT NULL, @@ -94,7 +86,6 @@ pub fn run() { CREATE TABLE IF NOT EXISTS deezer_artists ( id TEXT PRIMARY KEY, name TEXT NOT NULL, - nb_album INTEGER DEFAULT 0, picture_small TEXT, picture_medium TEXT, 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_tracks_title ON deezer_tracks(title); ", - kind: MigrationKind::Up, - } - ]; + kind: MigrationKind::Up, + }]; tauri::Builder::default() + .plugin(tauri_plugin_process::init()) .plugin( tauri_plugin_sql::Builder::new() .add_migrations("sqlite:library.db", library_migrations) .add_migrations("sqlite:deezer.db", deezer_migrations) - .build() + .build(), ) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_opener::init()) diff --git a/src-tauri/src/tagger.rs b/src-tauri/src/tagger.rs index 9912d8d..a005619 100644 --- a/src-tauri/src/tagger.rs +++ b/src-tauri/src/tagger.rs @@ -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 serde::{Deserialize, Serialize}; use std::path::Path; @@ -60,8 +63,7 @@ fn tag_mp3( embed_lyrics: bool, ) -> Result<(), String> { // Read or create tag - let mut tag = ID3Tag::read_from_path(path) - .unwrap_or_else(|_| ID3Tag::new()); + let mut tag = ID3Tag::read_from_path(path).unwrap_or_else(|_| ID3Tag::new()); // Basic metadata if let Some(ref title) = metadata.title { @@ -129,57 +131,75 @@ fn tag_mp3( // Custom text frames (TXXX) if let Some(ref barcode) = metadata.barcode { - use id3::frame::{ExtendedText, Content}; + use id3::frame::{Content, ExtendedText}; let ext_text = ExtendedText { description: "BARCODE".to_string(), 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 { - use id3::frame::{ExtendedText, Content}; + use id3::frame::{Content, ExtendedText}; let ext_text = ExtendedText { description: "ITUNESADVISORY".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 { - use id3::frame::{ExtendedText, Content}; + use id3::frame::{Content, ExtendedText}; let ext_text = ExtendedText { description: "REPLAYGAIN_TRACK_GAIN".to_string(), 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 { - use id3::frame::{ExtendedText, Content}; + use id3::frame::{Content, ExtendedText}; let source_text = ExtendedText { description: "SOURCE".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 { description: "SOURCEID".to_string(), 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) if embed_lyrics { if let Some(ref lyrics) = metadata.lyrics_unsync { - use id3::frame::{Lyrics, Content}; + use id3::frame::{Content, Lyrics}; let lyrics_frame = Lyrics { lang: "eng".to_string(), description: String::new(), 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(), 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]>, embed_lyrics: bool, ) -> Result<(), String> { - let mut tag = FlacTag::read_from_path(path) - .map_err(|e| format!("Failed to read FLAC file: {}", e))?; + let mut tag = + FlacTag::read_from_path(path).map_err(|e| format!("Failed to read FLAC file: {}", e))?; // Remove all existing vorbis comments to start fresh let vorbis = tag.vorbis_comments_mut(); @@ -279,7 +302,10 @@ fn tag_flac( } 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 { @@ -303,7 +329,11 @@ fn tag_flac( if !cover_bytes.is_empty() { let mime_type = detect_mime_type_str(cover_bytes); 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(), + ); } } diff --git a/src/lib/library/database.ts b/src/lib/library/database.ts index be8a136..a3dada2 100644 --- a/src/lib/library/database.ts +++ b/src/lib/library/database.ts @@ -120,7 +120,11 @@ export async function upsertArtist(artist: { [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; } /** diff --git a/src/lib/library/deezer-database.ts b/src/lib/library/deezer-database.ts index b478d69..8488bd0 100644 --- a/src/lib/library/deezer-database.ts +++ b/src/lib/library/deezer-database.ts @@ -1,4 +1,6 @@ import Database from '@tauri-apps/plugin-sql'; +import { remove } from '@tauri-apps/plugin-fs'; +import { appConfigDir } from '@tauri-apps/api/path'; export interface DeezerPlaylist { id: string; @@ -24,7 +26,6 @@ export interface DeezerAlbum { export interface DeezerArtist { id: string; name: string; - nb_album: number; picture_small?: string; picture_medium?: string; cached_at: number; @@ -51,6 +52,16 @@ export async function initDeezerDatabase(): Promise { return db; } +/** + * Close database connection (for cache clearing) + */ +export async function closeDeezerDatabase(): Promise { + if (db) { + await db.close(); + db = null; + } +} + /** * Get cached playlists */ @@ -177,12 +188,11 @@ export async function upsertArtists(artists: any[]): Promise { // Insert new artists for (const artist of artists) { await database.execute( - `INSERT INTO deezer_artists (id, name, nb_album, picture_small, picture_medium, cached_at) - VALUES ($1, $2, $3, $4, $5, $6)`, + `INSERT INTO deezer_artists (id, name, picture_small, picture_medium, cached_at) + VALUES ($1, $2, $3, $4, $5)`, [ String(artist.ART_ID), artist.ART_NAME || '', - artist.NB_ALBUM || 0, artist.ART_PICTURE || null, artist.PICTURE_TYPE || null, now @@ -233,10 +243,22 @@ export async function getCacheTimestamp(): Promise { * Clear all Deezer cache */ export async function clearDeezerCache(): Promise { - const database = await initDeezerDatabase(); - await database.execute('DELETE FROM deezer_playlists'); - await database.execute('DELETE FROM deezer_albums'); - await database.execute('DELETE FROM deezer_artists'); - await database.execute('DELETE FROM deezer_tracks'); - await database.execute('VACUUM'); + try { + // Close the database connection + await closeDeezerDatabase(); + + // Delete the entire database file + 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; + } } diff --git a/src/routes/services/deezer/+page.svelte b/src/routes/services/deezer/+page.svelte index f128506..294cae3 100644 --- a/src/routes/services/deezer/+page.svelte +++ b/src/routes/services/deezer/+page.svelte @@ -340,7 +340,6 @@ Playlist Tracks - Creator @@ -351,7 +350,6 @@ > {playlist.title} {playlist.nb_tracks} - {playlist.creator_name} {/each} @@ -403,7 +401,6 @@ Artist - Albums @@ -413,7 +410,6 @@ onclick={() => handleItemClick(i)} > {artist.name} - {artist.nb_album} {/each} @@ -433,7 +429,6 @@ Album Artist - Tracks Year @@ -445,7 +440,6 @@ > {album.title} {album.artist_name} - {album.nb_tracks} {album.release_date ? new Date(album.release_date).getFullYear() : '—'} {/each} diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index b267d53..b436de9 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -15,7 +15,9 @@ loadSettings } from '$lib/stores/settings'; 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 { relaunch } from '@tauri-apps/plugin-process'; let currentMusicFolder = $state(null); let currentPlaylistsFolder = $state(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' }); + } + } + }
@@ -292,6 +310,12 @@ This will delete all cached library data from the database. Your music files will not be affected.
+ +
+
Clear Deezer Cache
+ This will delete all cached Deezer favorites data. The next time you visit the Deezer page, it will refetch from the API. + +
{/if}