[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Close Cache.redb properly on quit

Kasper (Sep 17, 2025, 1:27 AM +0200) 02ba36da 2a9c1d2f

+104 -66
+2
ferrum-addon/addon.d.ts
··· 14 14 15 15 export declare function add_tracks_to_playlist(playlistId: string, trackIds: Array<string>): void 16 16 17 + export declare function close_cache_db(): Promise<void> 18 + 17 19 export interface CountObject { 18 20 count: number 19 21 fromDate: MsSinceUnixEpoch
+12 -12
package-lock.json
··· 14 14 "@sveltejs/vite-plugin-svelte": "^5.1.0", 15 15 "@tailwindcss/vite": "^4.1.8", 16 16 "@tsconfig/svelte": "^5.0.4", 17 - "electron": "^28.3.3", 17 + "electron": "^29.4.6", 18 18 "electron-builder": "^26.0.12", 19 19 "eslint": "^9.28.0", 20 20 "eslint-config-prettier": "^10.1.5", ··· 4469 4469 "license": "MIT" 4470 4470 }, 4471 4471 "node_modules/@types/node": { 4472 - "version": "18.19.110", 4473 - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.110.tgz", 4474 - "integrity": "sha512-WW2o4gTmREtSnqKty9nhqF/vA0GKd0V/rbC0OyjSk9Bz6bzlsXKT+i7WDdS/a0z74rfT2PO4dArVCSnapNLA5Q==", 4472 + "version": "20.19.15", 4473 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.15.tgz", 4474 + "integrity": "sha512-W3bqcbLsRdFDVcmAM5l6oLlcl67vjevn8j1FPZ4nx+K5jNoWCh+FC/btxFoBPnvQlrHHDwfjp1kjIEDfwJ0Mog==", 4475 4475 "dev": true, 4476 4476 "license": "MIT", 4477 4477 "dependencies": { 4478 - "undici-types": "~5.26.4" 4478 + "undici-types": "~6.21.0" 4479 4479 } 4480 4480 }, 4481 4481 "node_modules/@types/plist": { ··· 6256 6256 } 6257 6257 }, 6258 6258 "node_modules/electron": { 6259 - "version": "28.3.3", 6260 - "resolved": "https://registry.npmjs.org/electron/-/electron-28.3.3.tgz", 6261 - "integrity": "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==", 6259 + "version": "29.4.6", 6260 + "resolved": "https://registry.npmjs.org/electron/-/electron-29.4.6.tgz", 6261 + "integrity": "sha512-fz8ndj8cmmf441t4Yh2FDP3Rn0JhLkVGvtUf2YVMbJ5SdJPlc0JWll9jYkhh60jDKVVCr/tBAmfxqRnXMWJpzg==", 6262 6262 "dev": true, 6263 6263 "hasInstallScript": true, 6264 6264 "license": "MIT", 6265 6265 "dependencies": { 6266 6266 "@electron/get": "^2.0.0", 6267 - "@types/node": "^18.11.18", 6267 + "@types/node": "^20.9.0", 6268 6268 "extract-zip": "^2.0.1" 6269 6269 }, 6270 6270 "bin": { ··· 11164 11164 } 11165 11165 }, 11166 11166 "node_modules/undici-types": { 11167 - "version": "5.26.5", 11168 - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 11169 - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 11167 + "version": "6.21.0", 11168 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 11169 + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 11170 11170 "dev": true, 11171 11171 "license": "MIT" 11172 11172 },
+1 -1
package.json
··· 74 74 "@sveltejs/vite-plugin-svelte": "^5.1.0", 75 75 "@tailwindcss/vite": "^4.1.8", 76 76 "@tsconfig/svelte": "^5.0.4", 77 - "electron": "^28.3.3", 77 + "electron": "^29.4.6", 78 78 "electron-builder": "^26.0.12", 79 79 "eslint": "^9.28.0", 80 80 "eslint-config-prettier": "^10.1.5",
+47 -25
src-native/tracks/cover.rs
··· 5 5 use image::codecs::jpeg::JpegEncoder; 6 6 use image::codecs::png::PngEncoder; 7 7 use image::{ImageEncoder, ImageFormat, ImageReader}; 8 - use lazy_static::lazy_static; 9 8 use napi::bindgen_prelude::Buffer; 10 9 use redb::{Database, TableDefinition}; 11 10 use std::fs; 12 11 use std::io::{BufWriter, Cursor}; 13 12 use std::path::PathBuf; 14 - use std::sync::{Arc, RwLock}; 13 + use std::sync::{LazyLock, RwLock}; 15 14 use std::time::{Instant, UNIX_EPOCH}; 16 15 17 16 // (modified_timestamp_ms, image_bytes) ··· 19 18 20 19 const IMG_CACHE_TABLE: TableDefinition<&str, CacheEntry> = TableDefinition::new("img_cache"); 21 20 22 - lazy_static! { 23 - static ref CACHE_DB: Arc<RwLock<Option<Database>>> = Arc::new(RwLock::new(None)); 24 - } 21 + static CACHE_DB: LazyLock<RwLock<Option<Database>>> = LazyLock::new(|| RwLock::new(None)); 25 22 26 23 fn init_cache_db(path: String) -> Result<()> { 27 24 let now = Instant::now(); 28 - let cache_db_mutex = CACHE_DB.read().unwrap(); 29 - if cache_db_mutex.is_none() { 30 - drop(cache_db_mutex); 31 - let mut cache_db_mutex = CACHE_DB.write().unwrap(); 32 - if cache_db_mutex.is_none() { 33 - let db = Database::create(&path).context("Could not load image cache: {}")?; 34 - let init_txn = db 35 - .begin_write() 36 - .context("Could not begin write transaction: {}")?; 37 - { 38 - // Create table 39 - init_txn 40 - .open_table(IMG_CACHE_TABLE) 41 - .context("Could not open table: {}")?; 42 - } 43 - init_txn.commit().context("Could not commit cache: {}")?; 44 - *cache_db_mutex = Some(db); 45 - println!("Initialized Cache.redb: {}ms", now.elapsed().as_millis()); 25 + 26 + // Check quickly with a read-lock first 27 + { 28 + let cache_db_global = &*CACHE_DB; 29 + let cache_db_lock = cache_db_global.read().unwrap(); 30 + if cache_db_lock.is_some() { 31 + return Ok(()); 32 + } 33 + } 34 + 35 + let cache_db_global = &*CACHE_DB; 36 + let mut cache_db_lock = cache_db_global.write().unwrap(); 37 + if cache_db_lock.is_none() { 38 + let db = Database::create(&path).context("Could not load image cache")?; 39 + let init_txn = db 40 + .begin_write() 41 + .context("Could not begin write transaction")?; 42 + { 43 + // Create table 44 + init_txn 45 + .open_table(IMG_CACHE_TABLE) 46 + .context("Could not open table")?; 46 47 } 48 + init_txn.commit().context("Could not commit cache")?; 49 + *cache_db_lock = Some(db); 50 + println!("Initialized Cache.redb: {}ms", now.elapsed().as_millis()); 47 51 } 52 + Ok(()) 53 + } 54 + 55 + #[napi(js_name = "close_cache_db")] 56 + #[allow(dead_code)] 57 + pub async fn close_cache_db() -> napi::Result<()> { 58 + let cache_db_global = &*CACHE_DB; 59 + let mut cache_db_lock = cache_db_global.write().unwrap(); 60 + if let Some(cache_db) = cache_db_lock.take() { 61 + let now = Instant::now(); 62 + drop(cache_db); 63 + println!("Closed Cache.redb: {}ms", now.elapsed().as_millis()); 64 + } else { 65 + // This can happen if the app crashes during startup 66 + println!("Cache.redb was not open"); 67 + } 68 + 48 69 Ok(()) 49 70 } 50 71 ··· 129 150 130 151 init_cache_db(cache_db_path)?; 131 152 132 - let cache_db_mutex = CACHE_DB.read().unwrap(); 133 - let cache_db = cache_db_mutex.as_ref().unwrap(); 153 + let cache_db_global = &*CACHE_DB; 154 + let cache_db_lock = cache_db_global.read().unwrap(); 155 + let cache_db = cache_db_lock.as_ref().unwrap(); 134 156 135 157 let date_modified_ms: Option<i64> = 136 158 get_modified_timestamp_ms(&path)?.map(|n| n.try_into().unwrap());
+1 -1
src/electron/ipc.ts
··· 25 25 result = await dialog.showMessageBox(options) 26 26 } 27 27 if (crash) { 28 - trigger_crash() 28 + trigger_crash(null) 29 29 } 30 30 return result 31 31 })
+39 -25
src/electron/main.ts
··· 10 10 import url from 'url' 11 11 import { ipc_main } from './typed_ipc' 12 12 13 - export function trigger_crash() { 14 - app.once('will-quit', () => { 15 - process.exit(1) 16 - }) 17 - app.quit() 18 - setTimeout(() => { 19 - process.exit(1) 20 - }) 13 + async function close_cache_db() { 14 + await addon.close_cache_db() 21 15 } 22 16 23 - async function err_handler(msg: string, error: Error | string) { 24 - app.whenReady().then(() => { 25 - dialog.showMessageBoxSync({ 26 - type: 'error', 27 - message: msg, 28 - detail: error instanceof Error ? error.stack : error, 29 - title: 'Error', 17 + let triggering_crash = false 18 + export function trigger_crash(popup: { msg: string; error: Error | string } | null) { 19 + if (triggering_crash) { 20 + return 21 + } 22 + triggering_crash = true 23 + quitting = true 24 + const close_promise = close_cache_db() 25 + app.whenReady().then(async () => { 26 + if (popup) { 27 + dialog.showMessageBoxSync({ 28 + type: 'error', 29 + message: popup.msg, 30 + detail: popup.error instanceof Error ? popup.error.stack : popup.error, 31 + title: 'Error', 32 + }) 33 + } 34 + browser_windows.main_window?.close() 35 + await close_promise 36 + app.once('will-quit', () => { 37 + process.exit(1) 30 38 }) 31 - trigger_crash() 39 + app.quit() 40 + setTimeout(() => { 41 + process.exit(1) 42 + }) 32 43 }) 33 44 } 34 45 process.on('uncaughtException', (error) => { 35 - err_handler('Unhandled Error', error) 46 + trigger_crash({ msg: 'Unhandled Error', error }) 36 47 }) 37 48 process.on('unhandledRejection', (error: Error) => { 38 - err_handler('Unhandled Promise Rejection', error) 49 + trigger_crash({ msg: 'Unhandled Promise Rejection', error }) 39 50 }) 40 51 41 52 const app_data = app.getPath('appData') ··· 55 66 let quitting = false 56 67 let app_loaded = false 57 68 58 - app.on('window-all-closed', () => { 59 - app.quit() 69 + app.on('window-all-closed', async () => { 70 + if (!triggering_crash) { 71 + await close_cache_db() 72 + app.quit() 73 + } 60 74 }) 61 75 62 76 protocol.registerSchemesAsPrivileged([ ··· 180 194 main_window.webContents.on('render-process-gone', (_e, details) => { 181 195 if (details.reason === 'crashed') { 182 196 // we have a napi-rs panic handler message popup already 183 - trigger_crash() 197 + trigger_crash(null) 184 198 } else if ( 185 199 details.reason !== 'clean-exit' && 186 200 details.reason !== 'abnormal-exit' && 187 201 details.reason !== 'killed' 188 202 ) { 189 - err_handler( 190 - `Crashed with code ${details.exitCode} (${details.reason})`, 191 - 'Error message was likely logged to console.', 192 - ) 203 + trigger_crash({ 204 + msg: `Crashed with code ${details.exitCode} (${details.reason})`, 205 + error: 'Error message was likely logged to console.', 206 + }) 193 207 } 194 208 }) 195 209 ipc_main.handle('app_loaded', () => {
+1 -1
src/lib/player.ts
··· 236 236 } 237 237 } 238 238 239 - quit.setHandler('player', () => { 239 + quit.set_handler('player', () => { 240 240 stop() 241 241 }) 242 242
+1 -1
src/lib/quit.ts
··· 18 18 handlers[name] = callback 19 19 } 20 20 21 - export default { notReady: not_ready, ready, setHandler: set_handler } 21 + export default { not_ready, ready, set_handler } 22 22 23 23 function try_to_quit() { 24 24 if (unfinished_tasks === 0) {