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

Stricter error handling

Kasper (Sep 14, 2025, 3:38 PM +0200) afef1cb9 a14c1bc0

+96 -54
+8 -3
ferrum-addon/addon.d.ts
··· 23 23 /** Returns the deleted track lists, including folder children */ 24 24 export declare function delete_track_list(id: string): void 25 25 26 - export declare function delete_tracks_with_item_ids(itemIds: Array<ItemId>): void 26 + export declare function delete_tracks_with_item_ids(itemIds: Array<ItemId>): FerrumStatus 27 + 28 + export type FerrumStatus = 29 + | { type: 'Ok' } 30 + | { type: 'FileDeletionError', field0: string } 31 + | { type: 'SaveError', field0: string } 27 32 28 33 export interface Folder { 29 34 id: TrackListID ··· 127 132 128 133 export declare function remove_image(index: number): void 129 134 130 - export declare function save(): void 135 + export declare function save(): FerrumStatus 131 136 132 137 export declare function save_view_options(viewOptions: ViewOptions): void 133 138 134 139 export declare function set_image(index: number, pathStr: string): void 135 140 136 - export declare function set_image_data(index: number, bytes: Uint8Array): void 141 + export declare function set_image_data(index: number, bytes: ArrayBuffer): void 137 142 138 143 export interface Special { 139 144 id: TrackListID
+6 -3
src-native/data_js.rs
··· 1 + use crate::FerrumStatus; 1 2 use crate::data::Data; 2 3 use anyhow::Result; 3 4 use napi::Env; ··· 81 82 82 83 #[napi(js_name = "save")] 83 84 #[allow(dead_code)] 84 - pub fn save(env: Env) -> napi::Result<()> { 85 + pub fn save(env: Env) -> FerrumStatus { 85 86 let data: &mut Data = get_data(&env); 86 - data.save()?; 87 - Ok(()) 87 + match data.save() { 88 + Ok(_) => FerrumStatus::Ok, 89 + Err(err) => FerrumStatus::SaveError(err.to_string()), 90 + } 88 91 }
+15
src-native/lib.rs
··· 20 20 mod tracks; 21 21 mod view_options; 22 22 23 + #[napi] 24 + pub enum FerrumStatus { 25 + Ok, 26 + FileDeletionError(String), 27 + SaveError(String), 28 + } 29 + impl FerrumStatus { 30 + pub fn is_err(&self) -> bool { 31 + match self { 32 + FerrumStatus::Ok => false, 33 + _ => true, 34 + } 35 + } 36 + } 37 + 23 38 fn get_now_timestamp() -> i64 { 24 39 let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) { 25 40 Ok(n) => n.as_millis() as i64,
+4 -4
src-native/library.rs
··· 149 149 150 150 #[napi(js_name = "get_genres")] 151 151 #[allow(dead_code)] 152 - pub fn get_genres(env: Env) -> Result<Vec<String>> { 152 + pub fn get_genres(env: Env) -> Vec<String> { 153 153 let data: &mut Data = get_data(&env); 154 154 let genres = data.library.get_genres(); 155 - Ok(genres.clone()) 155 + genres.clone() 156 156 } 157 157 158 158 #[napi(js_name = "get_artists")] 159 159 #[allow(dead_code)] 160 - pub fn get_artists(env: Env) -> Result<Vec<String>> { 160 + pub fn get_artists(env: Env) -> Vec<String> { 161 161 let data: &mut Data = get_data(&env); 162 162 let genres = data.library.get_artists(); 163 - Ok(genres.clone()) 163 + genres.clone() 164 164 }
+14 -5
src-native/library_types.rs
··· 1 1 #![allow(non_snake_case)] 2 2 3 - use crate::get_now_timestamp; 4 3 use crate::playlists::{delete_file, remove_from_all_playlists}; 4 + use crate::{FerrumStatus, get_now_timestamp}; 5 5 use anyhow::{Context, Result, bail}; 6 6 use linked_hash_map::{Entry, LinkedHashMap}; 7 7 use nanoid::nanoid; ··· 138 138 track_id_map.push(id.clone()); 139 139 self.track_item_ids.insert(id, item_id); 140 140 } 141 - pub fn delete_track_and_file(&mut self, id: &TrackID, tracks_dir: &PathBuf) -> Result<()> { 141 + pub fn delete_track_and_file( 142 + &mut self, 143 + id: &TrackID, 144 + tracks_dir: &PathBuf, 145 + ) -> Result<FerrumStatus> { 142 146 let file_path = { 143 147 let track = self.get_track(id)?; 144 148 tracks_dir.join(&track.file) 145 149 }; 146 150 if !file_path.exists() { 147 - bail!("File does not exist: {}", file_path.to_string_lossy()); 151 + return Ok(FerrumStatus::FileDeletionError(format!( 152 + "File does not exist: {}", 153 + file_path.to_string_lossy() 154 + ))); 148 155 } 149 156 150 157 remove_from_all_playlists(self, id); ··· 154 161 self.track_item_ids 155 162 .remove(id) 156 163 .expect("Track ID not found when deleting (2)"); 157 - delete_file(&file_path)?; 158 - Ok(()) 164 + Ok(match delete_file(&file_path) { 165 + Ok(_) => FerrumStatus::Ok, 166 + Err(err) => FerrumStatus::FileDeletionError(err.to_string()), 167 + }) 159 168 } 160 169 pub fn generate_id(&self) -> String { 161 170 let alphabet: [char; 32] = [
+10 -8
src-native/playlists.rs
··· 4 4 ItemId, Library, SpecialTrackListName, TRACK_ID_MAP, TrackID, TrackList, 5 5 get_track_ids_from_item_ids, new_item_ids_from_track_ids, 6 6 }; 7 - use crate::str_to_option; 7 + use crate::{FerrumStatus, str_to_option}; 8 8 use anyhow::{Context, Result, bail}; 9 9 use linked_hash_map::LinkedHashMap; 10 10 use napi::{Env, Unknown}; ··· 25 25 26 26 #[napi(js_name = "get_track_lists_details")] 27 27 #[allow(dead_code)] 28 - pub fn get_track_lists_details(env: Env) -> Result<HashMap<String, TrackListDetails>> { 28 + pub fn get_track_lists_details(env: Env) -> HashMap<String, TrackListDetails> { 29 29 let data: &Data = get_data(&env); 30 - Ok(data 31 - .library 30 + data.library 32 31 .trackLists 33 32 .iter() 34 33 .map(|(id, tracklist)| { ··· 54 53 }, 55 54 ); 56 55 }) 57 - .collect()) 56 + .collect() 58 57 } 59 58 60 59 #[napi(js_name = "get_track_list", ts_return_type = "TrackList")] ··· 217 216 218 217 #[napi(js_name = "delete_tracks_with_item_ids")] 219 218 #[allow(dead_code)] 220 - pub fn delete_tracks_with_item_ids(item_ids: Vec<ItemId>, env: Env) -> Result<()> { 219 + pub fn delete_tracks_with_item_ids(item_ids: Vec<ItemId>, env: Env) -> Result<FerrumStatus> { 221 220 let data: &mut Data = get_data(&env); 222 221 let library = &mut data.library; 223 222 let track_ids = get_track_ids_from_item_ids(&item_ids); 224 223 for track_id in &track_ids { 225 - library.delete_track_and_file(track_id, &data.paths.tracks_dir)?; 224 + let status = library.delete_track_and_file(track_id, &data.paths.tracks_dir)?; 225 + if status.is_err() { 226 + return Ok(status); 227 + } 226 228 } 227 - return Ok(()); 229 + return Ok(FerrumStatus::Ok); 228 230 } 229 231 230 232 #[napi(js_name = "new_playlist")]
+5 -6
src-native/tracks/mod.rs
··· 50 50 51 51 #[napi(js_name = "get_track_ids")] 52 52 #[allow(dead_code)] 53 - pub fn get_track_ids(item_ids: Vec<ItemId>) -> Result<Vec<TrackID>> { 53 + pub fn get_track_ids(item_ids: Vec<ItemId>) -> Vec<TrackID> { 54 54 let id_map = TRACK_ID_MAP.read().unwrap(); 55 55 let track_ids = item_ids.into_iter().map(|item_id| { 56 56 let track_id = &id_map[item_id as usize]; 57 57 track_id.clone() 58 58 }); 59 - Ok(track_ids.collect()) 59 + track_ids.collect() 60 60 } 61 61 62 62 #[napi(js_name = "track_exists")] 63 63 #[allow(dead_code)] 64 - pub fn track_exists(id: String, env: Env) -> Result<bool> { 64 + pub fn track_exists(id: String, env: Env) -> bool { 65 65 let data: &mut Data = get_data(&env); 66 66 let tracks = &data.library.get_tracks(); 67 - Ok(tracks.contains_key(&id)) 67 + tracks.contains_key(&id) 68 68 } 69 69 70 70 #[napi(js_name = "add_play")] ··· 218 218 #[napi(js_name = "set_image_data")] 219 219 #[allow(dead_code)] 220 220 pub fn set_image_data(index: u32, bytes: ArrayBuffer, env: Env) -> Result<()> { 221 - let bytes: Vec<u8> = bytes.to_vec(); 222 221 let data: &mut Data = get_data(&env); 223 222 let tag = match &mut data.current_tag { 224 223 Some(tag) => tag, 225 224 None => bail!("No tag loaded"), 226 225 }; 227 - tag.set_image(index as usize, bytes)?; 226 + tag.set_image(index as usize, bytes.to_vec())?; 228 227 Ok(()) 229 228 } 230 229
+31 -22
src/lib/data.ts
··· 3 3 import type { 4 4 MsSinceUnixEpoch, 5 5 TrackID, 6 - TrackList, 7 6 TrackListID, 8 7 ItemId, 9 8 TrackMd, ··· 13 12 import { queue } from './queue' 14 13 import { current_playlist_id } from '@/components/TrackList.svelte' 15 14 import { navigate } from './router' 16 - import { call, get_error_message, strict_call } from './error' 15 + import { call, error_popup, get_error_message, strict_call } from './error' 17 16 18 17 export const is_dev = window.is_dev 19 18 export const local_data_path = window.local_data_path ··· 23 22 const inner_addon = window.addon 24 23 export const ItunesImport = inner_addon.ItunesImport 25 24 26 - call((addon) => addon.load_data(is_dev, local_data_path, library_path)) 25 + strict_call((addon) => addon.load_data(is_dev, local_data_path, library_path)) 27 26 28 - export const paths = call((addon) => addon.get_paths()) 27 + export const paths = strict_call((addon) => addon.get_paths()) 29 28 export function join_paths(...args: string[]) { 30 29 return args.join(paths.pathSeparator) 31 30 } 32 31 33 32 export const track_lists_details_map = (() => { 34 - const initial = call((addon) => addon.get_track_lists_details()) 33 + const initial = strict_call((addon) => addon.get_track_lists_details()) 35 34 36 35 const { subscribe, set } = writable(initial) 37 36 return { 38 37 subscribe, 39 38 refresh() { 40 - set(call((addon) => addon.get_track_lists_details())) 39 + set(strict_call((addon) => addon.get_track_lists_details())) 41 40 }, 42 41 } 43 42 })() ··· 47 46 check_duplicates = true, 48 47 ) { 49 48 if (check_duplicates) { 50 - const filtered_ids = call((addon) => addon.playlist_filter_duplicates(playlist_id, track_ids)) 49 + const filtered_ids = strict_call((addon) => 50 + addon.playlist_filter_duplicates(playlist_id, track_ids), 51 + ) 51 52 const duplicates = track_ids.length - filtered_ids.length 52 53 if (duplicates > 0) { 53 54 const result = await ipc_renderer.invoke('showMessageBox', false, { ··· 68 69 } 69 70 } 70 71 if (track_ids.length >= 1) { 71 - call((addon) => addon.add_tracks_to_playlist(playlist_id, track_ids)) 72 + strict_call((addon) => addon.add_tracks_to_playlist(playlist_id, track_ids)) 72 73 tracklist_updated.emit() 73 74 save() 74 75 } 75 76 } 76 77 export function remove_from_playlist(playlist_id: TrackListID, item_ids: ItemId[]) { 77 - call((addon) => addon.remove_from_playlist(playlist_id, item_ids)) 78 + strict_call((addon) => addon.remove_from_playlist(playlist_id, item_ids)) 78 79 tracklist_updated.emit() 79 80 save() 80 81 } 81 82 export function delete_tracks_with_item_ids(item_ids: ItemId[]) { 82 - call((addon) => addon.delete_tracks_with_item_ids(item_ids)) 83 + const result = strict_call((addon) => addon.delete_tracks_with_item_ids(item_ids)) 84 + if (result.type === 'FileDeletionError') { 85 + // Gracefully handle 86 + error_popup(result.field0) 87 + } 83 88 tracklist_updated.emit() 84 89 queue.removeDeleted() 85 90 save() ··· 93 98 editMode: boolean 94 99 } 95 100 export function new_playlist(info: PlaylistInfo) { 96 - call((addon) => addon.new_playlist(info.name, info.description, info.isFolder, info.id)) 101 + strict_call((addon) => addon.new_playlist(info.name, info.description, info.isFolder, info.id)) 97 102 track_lists_details_map.refresh() 98 103 save() 99 104 } 100 105 export function update_playlist(id: string, name: string, description: string) { 101 - call((addon) => addon.update_playlist(id, name, description)) 106 + strict_call((addon) => addon.update_playlist(id, name, description)) 102 107 track_lists_details_map.refresh() 103 108 tracklist_updated.emit() 104 109 save() ··· 109 114 to_parent: TrackListID, 110 115 to_index: number, 111 116 ) { 112 - call((addon) => addon.move_playlist(id, from_parent, to_parent, to_index)) 117 + strict_call((addon) => addon.move_playlist(id, from_parent, to_parent, to_index)) 113 118 track_lists_details_map.refresh() 114 119 save() 115 120 } ··· 138 143 } 139 144 140 145 export function get_default_sort_desc(field: string) { 141 - return call((data) => data.get_default_sort_desc(field)) 146 + return strict_call((data) => data.get_default_sort_desc(field)) 142 147 } 143 148 144 149 export function import_track(path: string, now: MsSinceUnixEpoch) { 145 150 call((data) => data.import_file(path, now)) 146 151 } 147 152 export function get_track(id: TrackID) { 148 - return call((data) => data.get_track(id)) 153 + return strict_call((data) => data.get_track(id)) 149 154 } 150 155 export function get_track_by_item_id(item_id: ItemId) { 151 - return call((data) => data.get_track_by_item_id(item_id)) 156 + return strict_call((data) => data.get_track_by_item_id(item_id)) 152 157 } 153 158 export function get_track_ids(item_ids: ItemId[]) { 154 - return call((data) => data.get_track_ids(item_ids)) 159 + return strict_call((data) => data.get_track_ids(item_ids)) 155 160 } 156 161 export function get_tracks_page(options: TracksPageOptions) { 157 - return call((data) => data.get_tracks_page(options)) 162 + return strict_call((data) => data.get_tracks_page(options)) 158 163 } 159 164 export function track_exists(id: TrackID) { 160 - return call((data) => data.track_exists(id)) 165 + return strict_call((data) => data.track_exists(id)) 161 166 } 162 167 export function get_track_list(id: TrackListID) { 163 - return call((data) => data.get_track_list(id)) as TrackList 168 + return strict_call((data) => data.get_track_list(id)) 164 169 } 165 170 export function delete_track_list(id: TrackListID) { 166 - call((data) => data.delete_track_list(id)) 171 + strict_call((data) => data.delete_track_list(id)) 167 172 if (id === get(current_playlist_id)) { 168 173 navigate('/playlist/root') 169 174 } ··· 171 176 save() 172 177 } 173 178 export function save() { 174 - return call((addon) => addon.save()) 179 + const result = strict_call((addon) => addon.save()) 180 + if (result.type === 'SaveError') { 181 + // Gracefully handle 182 + error_popup(result.field0) 183 + } 175 184 } 176 185 export function add_play(id: TrackID) { 177 186 call((data) => data.add_play(id))
+3 -1
src/lib/error.ts
··· 8 8 } else if (obj.code) { 9 9 return 'Code: ' + String(obj.message) 10 10 } 11 + } else if (typeof err === 'string') { 12 + return err 11 13 } 12 14 return 'No reason or code provided' 13 15 } ··· 20 22 } 21 23 return '' 22 24 } 23 - function error_popup(err: unknown, crash = false) { 25 + export function error_popup(err: unknown, crash = false) { 24 26 ipc_renderer.invoke( 25 27 'showMessageBox', 26 28 false,
-2
src/lib/selection.ts
··· 196 196 } else if (this.items.size === 0) { 197 197 this.add_index_unchecked(0) 198 198 } else if (this.last_added !== null) { 199 - console.log('go_forward', { ...this }) 200 199 const next_index = this.last_added.index + 1 201 200 this.clear() 202 201 this.add_index_unchecked(Math.min(next_index, this.all.length - 1)) 203 - console.log('.', { ...this }) 204 202 } 205 203 } 206 204 /** Expand or shrink selection backwards (shift+up) */