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

Panic if data load fails

Kasper (Sep 14, 2025, 9:50 AM +0200) b5c909d5 ef518d3a

+49 -46
+9 -6
src-native/data_js.rs
··· 1 1 use crate::data::Data; 2 - use anyhow::{Context, Result}; 2 + use anyhow::Result; 3 3 use napi::{Env, JsUndefined}; 4 4 use rfd::MessageDialog; 5 5 6 - pub fn get_data(env: &Env) -> Result<&mut Data> { 7 - let data = env.get_instance_data::<Data>()?.context("No data")?; 8 - return Ok(data); 6 + pub fn get_data(env: &Env) -> &mut Data { 7 + let data = env 8 + .get_instance_data::<Data>() 9 + .expect("Error getting data") 10 + .expect("No data"); 11 + return data; 9 12 } 10 13 11 14 #[napi(js_name = "load_data")] ··· 68 71 #[napi(js_name = "get_paths")] 69 72 #[allow(dead_code)] 70 73 pub fn get_paths(env: Env) -> Result<PathsJs> { 71 - let data: &mut Data = get_data(&env)?; 74 + let data: &mut Data = get_data(&env); 72 75 Ok(PathsJs { 73 76 library_dir: data.paths.library_dir.to_string_lossy().into(), 74 77 tracks_dir: data.paths.tracks_dir.to_string_lossy().into(), ··· 82 85 #[napi(js_name = "save")] 83 86 #[allow(dead_code)] 84 87 pub fn save(env: Env) -> napi::Result<JsUndefined> { 85 - let data: &mut Data = get_data(&env)?; 88 + let data: &mut Data = get_data(&env); 86 89 data.save()?; 87 90 env.get_undefined() 88 91 }
+6 -6
src-native/itunes_import.rs
··· 1 1 use crate::data_js::get_data; 2 2 use crate::get_now_timestamp; 3 3 use crate::library_types::{ 4 - new_item_ids_from_track_ids, CountObject, Folder, Library, Playlist, Track, TrackList, 4 + CountObject, Folder, Library, Playlist, Track, TrackList, new_item_ids_from_track_ids, 5 5 }; 6 6 use crate::tracks::generate_filename; 7 - use crate::tracks::import::{read_file_metadata, FileType}; 8 - use anyhow::{bail, Context, Result}; 7 + use crate::tracks::import::{FileType, read_file_metadata}; 8 + use anyhow::{Context, Result, bail}; 9 9 use lofty::file::{AudioFile, TaggedFileExt}; 10 10 use napi::Env; 11 11 use serde::{Deserialize, Serialize}; ··· 13 13 use std::fs; 14 14 use std::path::{Path, PathBuf}; 15 15 use std::sync::Mutex; 16 + use time::OffsetDateTime; 16 17 use time::serde::iso8601; 17 18 use time::serde::iso8601::option as iso8601_opt; 18 - use time::OffsetDateTime; 19 19 20 20 #[derive(Serialize, Deserialize, Debug)] 21 21 struct XmlLibrary { ··· 589 589 impl ItunesImport { 590 590 #[napi(factory)] 591 591 pub fn new(env: Env) -> napi::Result<Self> { 592 - let data = get_data(&env)?; 592 + let data = get_data(&env); 593 593 Ok(Self { 594 594 new_library: Some(data.library.clone()).into(), 595 595 itunes_track_paths: HashMap::new().into(), ··· 601 601 } 602 602 #[napi] 603 603 pub fn finish(&mut self, env: Env) -> napi::Result<()> { 604 - let data = get_data(&env)?; 604 + let data = get_data(&env); 605 605 let itunes_track_paths = &mut *self.itunes_track_paths.lock().unwrap(); 606 606 for (itunes_path, ferrum_file) in itunes_track_paths { 607 607 let new_path = data.paths.tracks_dir.join(ferrum_file);
+2 -2
src-native/library.rs
··· 150 150 #[napi(js_name = "get_genres")] 151 151 #[allow(dead_code)] 152 152 pub fn get_genres(env: Env) -> Result<Vec<String>> { 153 - let data: &mut Data = get_data(&env)?; 153 + let data: &mut Data = get_data(&env); 154 154 let genres = data.library.get_genres(); 155 155 Ok(genres.clone()) 156 156 } ··· 158 158 #[napi(js_name = "get_artists")] 159 159 #[allow(dead_code)] 160 160 pub fn get_artists(env: Env) -> Result<Vec<String>> { 161 - let data: &mut Data = get_data(&env)?; 161 + let data: &mut Data = get_data(&env); 162 162 let genres = data.library.get_artists(); 163 163 Ok(genres.clone()) 164 164 }
+1 -1
src-native/page.rs
··· 27 27 #[napi(js_name = "get_tracks_page")] 28 28 #[allow(dead_code)] 29 29 pub fn get_tracks_page(options: TracksPageOptions, env: Env) -> Result<TracksPage> { 30 - let data: &mut Data = get_data(&env)?; 30 + let data: &mut Data = get_data(&env); 31 31 let tracklist = data.library.get_tracklist(&options.playlist_id)?; 32 32 let item_ids = sort(options.clone(), &data.library)?; 33 33 let tracklist_length = item_ids.len();
+14 -14
src-native/playlists.rs
··· 1 1 use crate::data::Data; 2 2 use crate::data_js::get_data; 3 3 use crate::library_types::{ 4 - get_track_ids_from_item_ids, new_item_ids_from_track_ids, ItemId, Library, 5 - SpecialTrackListName, TrackID, TrackList, TRACK_ID_MAP, 4 + ItemId, Library, SpecialTrackListName, TRACK_ID_MAP, TrackID, TrackList, 5 + get_track_ids_from_item_ids, new_item_ids_from_track_ids, 6 6 }; 7 7 use crate::str_to_option; 8 - use anyhow::{bail, Context, Result}; 8 + use anyhow::{Context, Result, bail}; 9 9 use linked_hash_map::LinkedHashMap; 10 10 use napi::{Env, JsUnknown}; 11 11 use std::collections::{HashMap, HashSet}; ··· 26 26 #[napi(js_name = "get_track_lists_details")] 27 27 #[allow(dead_code)] 28 28 pub fn get_track_lists_details(env: Env) -> Result<HashMap<String, TrackListDetails>> { 29 - let data: &Data = get_data(&env)?; 29 + let data: &Data = get_data(&env); 30 30 Ok(data 31 31 .library 32 32 .trackLists ··· 60 60 #[napi(js_name = "get_track_list", ts_return_type = "TrackList")] 61 61 #[allow(dead_code)] 62 62 pub fn get_track_list(id: String, env: Env) -> Result<JsUnknown> { 63 - let data: &mut Data = get_data(&env)?; 63 + let data: &mut Data = get_data(&env); 64 64 let tracklist = data.library.get_tracklist(&id)?; 65 65 Ok(env.to_js_value(&tracklist)?) 66 66 } ··· 124 124 #[napi(js_name = "delete_track_list")] 125 125 #[allow(dead_code)] 126 126 pub fn delete_track_list(id: String, env: Env) -> Result<()> { 127 - let data: &mut Data = get_data(&env)?; 127 + let data: &mut Data = get_data(&env); 128 128 let parent_id = data.library.get_parent_id(&id).context("No parent found")?; 129 129 130 130 let mut ids = HashSet::new(); ··· 144 144 #[napi(js_name = "add_tracks_to_playlist")] 145 145 #[allow(dead_code)] 146 146 pub fn add_tracks(playlist_id: String, track_ids: Vec<String>, env: Env) -> Result<()> { 147 - let data: &mut Data = get_data(&env)?; 147 + let data: &mut Data = get_data(&env); 148 148 let playlist = match data.library.get_tracklist_mut(&playlist_id)? { 149 149 TrackList::Playlist(playlist) => playlist, 150 150 TrackList::Folder(_) => bail!("Cannot add track to folder"), ··· 158 158 #[napi(js_name = "playlist_filter_duplicates")] 159 159 #[allow(dead_code)] 160 160 pub fn filter_duplicates(playlist_id: TrackID, ids: Vec<String>, env: Env) -> Result<Vec<TrackID>> { 161 - let data: &mut Data = get_data(&env)?; 161 + let data: &mut Data = get_data(&env); 162 162 let mut track_ids: HashSet<String> = HashSet::from_iter(ids); 163 163 let playlist = match data.library.get_tracklist_mut(&playlist_id)? { 164 164 TrackList::Playlist(playlist) => playlist, ··· 176 176 #[napi(js_name = "remove_from_playlist")] 177 177 #[allow(dead_code)] 178 178 pub fn remove_from_playlist(playlist_id: TrackID, item_ids: Vec<ItemId>, env: Env) -> Result<()> { 179 - let data: &mut Data = get_data(&env)?; 179 + let data: &mut Data = get_data(&env); 180 180 let playlist = match data.library.get_tracklist_mut(&playlist_id)? { 181 181 TrackList::Playlist(playlist) => playlist, 182 182 _ => bail!("Cannot remove track from non-playlist"), ··· 218 218 #[napi(js_name = "delete_tracks_with_item_ids")] 219 219 #[allow(dead_code)] 220 220 pub fn delete_tracks_with_item_ids(item_ids: Vec<ItemId>, env: Env) -> Result<()> { 221 - let data: &mut Data = get_data(&env)?; 221 + let data: &mut Data = get_data(&env); 222 222 let library = &mut data.library; 223 223 let track_ids = get_track_ids_from_item_ids(&item_ids); 224 224 for track_id in &track_ids { ··· 236 236 parent_id: String, 237 237 env: Env, 238 238 ) -> Result<()> { 239 - let data: &mut Data = get_data(&env)?; 239 + let data: &mut Data = get_data(&env); 240 240 let library = &mut data.library; 241 241 242 242 let list = match is_folder { ··· 275 275 #[napi(js_name = "update_playlist")] 276 276 #[allow(dead_code)] 277 277 pub fn update_playlist(id: String, name: String, description: String, env: Env) -> Result<()> { 278 - let data: &mut Data = get_data(&env)?; 278 + let data: &mut Data = get_data(&env); 279 279 280 280 match data.library.trackLists.get_mut(&id) { 281 281 Some(TrackList::Special(_)) => bail!("Cannot edit special playlists"), ··· 339 339 to_index: u32, 340 340 env: Env, 341 341 ) -> Result<()> { 342 - let data: &mut Data = get_data(&env)?; 342 + let data: &mut Data = get_data(&env); 343 343 344 344 match data.library.trackLists.get(&id) { 345 345 Some(TrackList::Special(_)) => bail!("Cannot move special playlist"), ··· 386 386 to_index: u32, 387 387 env: Env, 388 388 ) -> Result<()> { 389 - let data: &mut Data = get_data(&env)?; 389 + let data: &mut Data = get_data(&env); 390 390 let playlist = match data.library.get_tracklist_mut(&playlist_id)? { 391 391 TrackList::Playlist(playlist) => playlist, 392 392 _ => bail!("Cannot rearrange tracks in non-playlist"),
+1 -1
src-native/tracks/cover.rs
··· 250 250 )] 251 251 #[allow(dead_code)] 252 252 pub fn read_cover_async(track_id: String, index: u16, env: Env) -> napi::Result<JsObject> { 253 - let data: &mut Data = get_data(&env)?; 253 + let data: &mut Data = get_data(&env); 254 254 let track = id_to_track(&env, &track_id)?; 255 255 let tracks_dir = &data.paths.tracks_dir; 256 256 let file_path = tracks_dir.join(&track.file);
+14 -14
src-native/tracks/mod.rs
··· 1 1 use crate::data::Data; 2 2 use crate::data_js::get_data; 3 3 use crate::get_now_timestamp; 4 - use crate::library_types::{ItemId, MsSinceUnixEpoch, Track, TrackID, TRACK_ID_MAP}; 5 - use anyhow::{bail, Context, Result}; 4 + use crate::library_types::{ItemId, MsSinceUnixEpoch, TRACK_ID_MAP, Track, TrackID}; 5 + use anyhow::{Context, Result, bail}; 6 6 use napi::{Env, JsArrayBuffer, JsBuffer}; 7 7 use std::fs; 8 8 use std::path::Path; ··· 15 15 pub use tag::Tag; 16 16 17 17 fn id_to_track<'a>(env: &'a Env, id: &String) -> Result<&'a mut Track> { 18 - let data: &mut Data = get_data(env)?; 18 + let data: &mut Data = get_data(env); 19 19 let track = data.library.get_track_mut(id)?; 20 20 return Ok(track); 21 21 } ··· 23 23 #[napi(js_name = "get_track")] 24 24 #[allow(dead_code)] 25 25 pub fn get_track(id: String, env: Env) -> Result<Track> { 26 - let data: &mut Data = get_data(&env)?; 26 + let data: &mut Data = get_data(&env); 27 27 let track = data.library.get_track(&id)?; 28 28 Ok(track.clone()) 29 29 } ··· 37 37 #[napi(js_name = "get_track_by_item_id")] 38 38 #[allow(dead_code)] 39 39 pub fn get_track_by_item_id(item_id: ItemId, env: Env) -> Result<KeyedTrack> { 40 - let data: &mut Data = get_data(&env)?; 40 + let data: &mut Data = get_data(&env); 41 41 let id_map = TRACK_ID_MAP.read().unwrap(); 42 42 let track_id = &id_map[item_id as usize]; 43 43 let track = data.library.get_track(&track_id)?; ··· 61 61 #[napi(js_name = "track_exists")] 62 62 #[allow(dead_code)] 63 63 pub fn track_exists(id: String, env: Env) -> Result<bool> { 64 - let data: &mut Data = get_data(&env)?; 64 + let data: &mut Data = get_data(&env); 65 65 let tracks = &data.library.get_tracks(); 66 66 Ok(tracks.contains_key(&id)) 67 67 } ··· 101 101 #[napi(js_name = "add_play_time")] 102 102 #[allow(dead_code)] 103 103 pub fn add_play_time(id: TrackID, start: MsSinceUnixEpoch, dur_ms: i64, env: Env) -> Result<()> { 104 - let data: &mut Data = get_data(&env)?; 104 + let data: &mut Data = get_data(&env); 105 105 let tracks = data.library.get_tracks(); 106 106 tracks.get(&id).context("Track ID not found")?; 107 107 data.library.playTime.push((id, start, dur_ms)); ··· 149 149 #[napi(js_name = "import_file")] 150 150 #[allow(dead_code)] 151 151 pub fn import_file(path: String, now: MsSinceUnixEpoch, env: Env) -> Result<()> { 152 - let data: &mut Data = get_data(&env)?; 152 + let data: &mut Data = get_data(&env); 153 153 let id = data.library.generate_id(); 154 154 let track = import::import(&data, Path::new(&path), now)?; 155 155 data.library.insert_track(id, track); ··· 159 159 #[napi(js_name = "load_tags")] 160 160 #[allow(dead_code)] 161 161 pub fn load_tags(track_id: String, env: Env) -> Result<()> { 162 - let data: &mut Data = get_data(&env)?; 162 + let data: &mut Data = get_data(&env); 163 163 data.current_tag = None; 164 164 let track = id_to_track(&env, &track_id)?; 165 165 ··· 180 180 #[napi(js_name = "get_image")] 181 181 #[allow(dead_code)] 182 182 pub fn get_image(index: u32, env: Env) -> Result<Option<JsImage>> { 183 - let data: &Data = get_data(&env)?; 183 + let data: &Data = get_data(&env); 184 184 185 185 let tag = match &data.current_tag { 186 186 Some(tag) => tag, ··· 203 203 #[napi(js_name = "set_image")] 204 204 #[allow(dead_code)] 205 205 pub fn set_image(index: u32, path_str: String, env: Env) -> Result<()> { 206 - let data: &mut Data = get_data(&env)?; 206 + let data: &mut Data = get_data(&env); 207 207 let path = data.paths.tracks_dir.join(path_str); 208 208 let tag = match &mut data.current_tag { 209 209 Some(tag) => tag, ··· 218 218 #[allow(dead_code)] 219 219 pub fn set_image_data(index: u32, bytes: JsArrayBuffer, env: Env) -> Result<()> { 220 220 let bytes: Vec<u8> = bytes.into_value()?.to_vec(); 221 - let data: &mut Data = get_data(&env)?; 221 + let data: &mut Data = get_data(&env); 222 222 let tag = match &mut data.current_tag { 223 223 Some(tag) => tag, 224 224 None => bail!("No tag loaded"), ··· 230 230 #[napi(js_name = "remove_image")] 231 231 #[allow(dead_code)] 232 232 pub fn remove_image(index: u32, env: Env) -> Result<()> { 233 - let data: &mut Data = get_data(&env)?; 233 + let data: &mut Data = get_data(&env); 234 234 match data.current_tag { 235 235 Some(ref mut tag) => { 236 236 tag.remove_image(index as usize); ··· 243 243 #[napi(js_name = "update_track_info")] 244 244 #[allow(dead_code)] 245 245 pub fn update_track_info(track_id: String, info: md::TrackMD, env: Env) -> Result<()> { 246 - let data: &mut Data = get_data(&env)?; 246 + let data: &mut Data = get_data(&env); 247 247 let track = id_to_track(&env, &track_id)?; 248 248 249 249 let tag = match &mut data.current_tag {
+2 -2
src-native/view_options.rs
··· 45 45 #[napi(js_name = "load_view_options")] 46 46 #[allow(dead_code)] 47 47 pub fn load_view_options(env: Env) -> Result<ViewOptions> { 48 - let data: &Data = get_data(&env)?; 48 + let data: &Data = get_data(&env); 49 49 Ok(ViewOptions::load(&data.paths)) 50 50 } 51 51 #[napi(js_name = "save_view_options")] 52 52 #[allow(dead_code)] 53 53 pub fn save_view_options(view_options: ViewOptions, env: Env) -> Result<()> { 54 - let data: &mut Data = get_data(&env)?; 54 + let data: &mut Data = get_data(&env); 55 55 view_options.save(&data.paths)?; 56 56 Ok(()) 57 57 }