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

Implement `ItemId`s for library.tracks

Kasper (Sep 26, 2024, 3:20 AM +0200) ca236d9c b0edd28d

+73 -47
+1 -1
src-native/artists.rs
··· 8 8 pub fn load_artists(library: &Library) -> HashSet<String> { 9 9 let now = Instant::now(); 10 10 let mut artists = HashSet::new(); 11 - for (_, track) in &library.tracks { 11 + for (_, track) in library.get_tracks() { 12 12 if !artists.contains(&track.artist) { 13 13 artists.insert(track.artist.clone()); 14 14 }
+1 -1
src-native/filter.rs
··· 58 58 } 59 59 60 60 fn filter_keyword(ids: Vec<TrackID>, keyword: &str, library: &Library) -> Vec<TrackID> { 61 - let tracks = &library.tracks; 61 + let tracks = library.get_tracks(); 62 62 let filtered_tracks: Vec<_> = ids 63 63 .into_par_iter() 64 64 .with_min_len(2000)
+3 -3
src-native/itunes_import.rs
··· 635 635 None => throw!("Not initialized"), 636 636 }; 637 637 let mut itunes_track_paths = itunes_import.itunes_track_paths.lock().unwrap(); 638 - let original_tracks_count = library.tracks.len(); 638 + let original_tracks_count = library.get_tracks().len(); 639 639 let original_tracklists_count = library.trackLists.len(); 640 640 let xml_lib: XmlLibrary = match plist::from_file(path) { 641 641 Ok(book) => book, ··· 682 682 let generated_id = library.generate_id(); 683 683 // immediately insert into library so new generated ids are unique 684 684 itunes_track_paths.insert(xml_track_path, track.file.clone()); 685 - library.tracks.insert(generated_id.clone(), track); 685 + library.insert_track(generated_id.clone(), track); 686 686 if xml_track_id_map.contains_key(&xml_id) { 687 687 errors.push(format!("Duplicate track ids \"{}\": artist_title", xml_id)); 688 688 } ··· 769 769 770 770 Ok(ImportStatus { 771 771 errors, 772 - tracks_count: (library.tracks.len() - original_tracks_count) as i64, 772 + tracks_count: (library.get_tracks().len() - original_tracks_count) as i64, 773 773 playlists_count: (library.trackLists.len() - original_tracklists_count) as i64, 774 774 }) 775 775 }
+50 -10
src-native/library_types.rs
··· 1 1 #![allow(non_snake_case)] 2 2 3 + use crate::playlists::{delete_file, remove_from_all_playlists}; 3 4 use crate::{get_now_timestamp, UniResult}; 4 - use linked_hash_map::LinkedHashMap; 5 + use linked_hash_map::{Entry, LinkedHashMap}; 5 6 use nanoid::nanoid; 6 7 use serde::{Deserialize, Serialize}; 7 8 use std::borrow::Cow; 9 + use std::path::PathBuf; 8 10 use std::sync::RwLock; 9 11 10 12 #[derive(Serialize, Deserialize, Clone, Debug)] 11 13 #[serde(deny_unknown_fields)] 12 14 pub struct Library { 13 - pub tracks: LinkedHashMap<TrackID, Track>, 15 + tracks: LinkedHashMap<TrackID, Track>, 14 16 pub trackLists: TrackLists, 15 17 /// v1 playtime has two issues: 16 18 /// - some durations are double counted (or triple, etc.) ··· 50 52 } 51 53 impl V1Library { 52 54 pub fn upgrade(self) -> Library { 53 - Library { 54 - tracks: self.tracks, 55 + let mut library = Library { 56 + tracks: LinkedHashMap::new(), 55 57 trackLists: self.trackLists, 56 58 v1PlayTime: self.playTime, 57 59 playTime: Vec::new(), 60 + }; 61 + for (id, track) in self.tracks { 62 + library.insert_track(id, track); 58 63 } 64 + library 59 65 } 60 66 } 61 67 ··· 76 82 trackLists: track_lists, 77 83 } 78 84 } 85 + #[inline] 86 + pub fn get_tracks(&self) -> &LinkedHashMap<TrackID, Track> { 87 + &self.tracks 88 + } 89 + pub fn insert_track(&mut self, id: TrackID, track: Track) { 90 + match self.tracks.entry(id.clone()) { 91 + Entry::Occupied(_) => panic!("Track ID already exists"), 92 + Entry::Vacant(entry) => entry.insert(track), 93 + }; 94 + let mut track_id_map = TRACK_ID_MAP.write().unwrap(); 95 + track_id_map.push(id); 96 + } 97 + pub fn delete_track_and_file(&mut self, id: &TrackID, tracks_dir: &PathBuf) -> UniResult<()> { 98 + let file_path = { 99 + let track = self.get_track(id)?; 100 + tracks_dir.join(&track.file) 101 + }; 102 + if !file_path.exists() { 103 + throw!("File does not exist: {}", file_path.to_string_lossy()); 104 + } 105 + 106 + remove_from_all_playlists(self, id); 107 + self.tracks 108 + .remove(id) 109 + .expect("Track ID not found when deleting"); 110 + delete_file(&file_path)?; 111 + Ok(()) 112 + } 79 113 pub fn generate_id(&self) -> String { 80 114 let alphabet: [char; 32] = [ 81 115 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', ··· 83 117 ]; 84 118 for _ in 0..1000 { 85 119 let id = nanoid!(7, &alphabet); 86 - if !self.tracks.contains_key(&id) && !self.trackLists.contains_key(&id) { 120 + if !self.get_tracks().contains_key(&id) && !self.trackLists.contains_key(&id) { 87 121 return id; 88 122 } 89 123 } ··· 117 151 children: Vec::new(), 118 152 } 119 153 } 120 - pub fn get_track(&self, id: &str) -> UniResult<&Track> { 121 - match self.tracks.get(id) { 154 + pub fn get_track(&self, id: &TrackID) -> UniResult<&Track> { 155 + match self.get_tracks().get(id) { 156 + Some(track) => Ok(track), 157 + None => throw!("Track with ID {} not found", id), 158 + } 159 + } 160 + pub fn get_track_mut(&mut self, id: &TrackID) -> UniResult<&mut Track> { 161 + match self.tracks.get_mut(id) { 122 162 Some(track) => Ok(track), 123 163 None => throw!("Track with ID {} not found", id), 124 164 } ··· 290 330 291 331 // These are used to give each playlist entry an ID. This is for example helpful to keep track of a user's selection. These IDs are unique across the entire library, so that it works for folders folders. 292 332 pub type ItemId = u32; 293 - pub static PLAYLIST_TRACK_ID_MAP: RwLock<Vec<String>> = RwLock::new(Vec::new()); 333 + pub static TRACK_ID_MAP: RwLock<Vec<String>> = RwLock::new(Vec::new()); 294 334 295 335 pub fn new_item_ids_from_track_ids(track_ids: &[TrackID]) -> Vec<ItemId> { 296 - let mut playlist_track_id_map = PLAYLIST_TRACK_ID_MAP.write().unwrap(); 336 + let mut playlist_track_id_map = TRACK_ID_MAP.write().unwrap(); 297 337 let playlist_track_ids: Vec<u32> = track_ids 298 338 .iter() 299 339 .map(|track_id| { ··· 308 348 } 309 349 310 350 pub fn get_track_ids_from_item_ids(playlist_item_ids: &[ItemId]) -> Vec<TrackID> { 311 - let playlist_track_id_map = PLAYLIST_TRACK_ID_MAP.read().unwrap(); 351 + let playlist_track_id_map = TRACK_ID_MAP.read().unwrap(); 312 352 playlist_item_ids 313 353 .iter() 314 354 .map(|playlist_item_id| playlist_track_id_map[*playlist_item_id as usize].clone())
+3 -3
src-native/page.rs
··· 2 2 use crate::data_js::get_data; 3 3 use crate::filter::filter; 4 4 use crate::library_types::{ 5 - ItemId, Library, SpecialTrackListName, TrackID, TrackList, PLAYLIST_TRACK_ID_MAP, 5 + ItemId, Library, SpecialTrackListName, TrackID, TrackList, TRACK_ID_MAP, 6 6 }; 7 7 use crate::sort::sort; 8 8 use crate::UniResult; ··· 65 65 pub fn get_tracklist_track_ids(library: &Library, playlist_id: &str) -> UniResult<Vec<TrackID>> { 66 66 match library.get_tracklist(playlist_id)? { 67 67 TrackList::Playlist(playlist) => { 68 - let id_map = PLAYLIST_TRACK_ID_MAP.read().unwrap(); 68 + let id_map = TRACK_ID_MAP.read().unwrap(); 69 69 let ids = playlist 70 70 .tracks 71 71 .iter() ··· 83 83 } 84 84 TrackList::Special(special) => match special.name { 85 85 SpecialTrackListName::Root => { 86 - let track_keys = library.tracks.keys(); 86 + let track_keys = library.get_tracks().keys(); 87 87 let ids = track_keys.map(|track| track.to_string()).collect(); 88 88 Ok(ids) 89 89 }
+7 -20
src-native/playlists.rs
··· 2 2 use crate::data_js::get_data; 3 3 use crate::library_types::{ 4 4 get_track_ids_from_item_ids, new_item_ids_from_track_ids, ItemId, Library, 5 - SpecialTrackListName, TrackID, TrackList, PLAYLIST_TRACK_ID_MAP, 5 + SpecialTrackListName, TrackID, TrackList, TRACK_ID_MAP, 6 6 }; 7 7 use crate::{str_to_option, UniResult}; 8 8 use napi::{Env, JsUnknown, Result}; ··· 191 191 return Ok(()); 192 192 } 193 193 194 - fn remove_from_all_playlists(library: &mut Library, id: &str) { 195 - let track_id_map = PLAYLIST_TRACK_ID_MAP.read().unwrap(); 194 + pub fn remove_from_all_playlists(library: &mut Library, id: &TrackID) { 195 + let track_id_map = TRACK_ID_MAP.read().unwrap(); 196 196 for (_, tracklist) in &mut library.trackLists { 197 197 let playlist = match tracklist { 198 198 TrackList::Playlist(playlist) => playlist, ··· 200 200 }; 201 201 playlist 202 202 .tracks 203 - .retain(|current_id| track_id_map[*current_id as usize] != id); 203 + .retain(|current_id| track_id_map[*current_id as usize] != *id); 204 204 } 205 205 } 206 206 207 - fn delete_file(path: &PathBuf) -> UniResult<()> { 207 + pub fn delete_file(path: &PathBuf) -> UniResult<()> { 208 208 #[allow(unused_mut)] 209 209 let mut trash_context = trash::TrashContext::new(); 210 210 ··· 223 223 let data: &mut Data = get_data(&env)?; 224 224 let library = &mut data.library; 225 225 let track_ids = get_track_ids_from_item_ids(&item_ids); 226 - for id_to_delete in &track_ids { 227 - let file_path = { 228 - let track = library.get_track(id_to_delete)?; 229 - data.paths.tracks_dir.join(&track.file) 230 - }; 231 - if !file_path.exists() { 232 - throw!("File does not exist: {}", file_path.to_string_lossy()); 233 - } 234 - 235 - remove_from_all_playlists(library, &id_to_delete); 236 - library 237 - .tracks 238 - .remove(id_to_delete) 239 - .expect("Track ID not found when deleting"); 240 - delete_file(&file_path)?; 226 + for track_id in &track_ids { 227 + library.delete_track_and_file(track_id, &data.paths.tracks_dir)?; 241 228 } 242 229 return Ok(()); 243 230 }
+1 -1
src-native/sort.rs
··· 98 98 return Ok(track_ids); 99 99 } 100 100 101 - let tracks = &library.tracks; 101 + let tracks = library.get_tracks(); 102 102 let field = match get_track_field_type(&options.sort_key) { 103 103 Some(field) => field, 104 104 None => throw!("Field type not found for {}", options.sort_key),
+6 -7
src-native/tracks/mod.rs
··· 16 16 17 17 fn id_to_track<'a>(env: &'a Env, id: &String) -> Result<&'a mut Track> { 18 18 let data: &mut Data = get_data(env)?; 19 - let tracks = &mut data.library.tracks; 20 - let track = tracks.get_mut(id).ok_or(nerr("Track ID not found"))?; 19 + let track = data.library.get_track_mut(id)?; 21 20 return Ok(track); 22 21 } 23 22 ··· 25 24 #[allow(dead_code)] 26 25 pub fn get_track(id: String, env: Env) -> Result<Track> { 27 26 let data: &mut Data = get_data(&env)?; 28 - let tracks = &data.library.tracks; 27 + let tracks = &data.library.get_tracks(); 29 28 let track = tracks.get(&id).ok_or(nerr("Track ID not found"))?; 30 29 Ok(track.clone()) 31 30 } ··· 34 33 #[allow(dead_code)] 35 34 pub fn track_exists(id: String, env: Env) -> Result<bool> { 36 35 let data: &mut Data = get_data(&env)?; 37 - let tracks = &data.library.tracks; 36 + let tracks = &data.library.get_tracks(); 38 37 Ok(tracks.contains_key(&id)) 39 38 } 40 39 ··· 74 73 #[allow(dead_code)] 75 74 pub fn add_play_time(id: TrackID, start: MsSinceUnixEpoch, dur_ms: i64, env: Env) -> Result<()> { 76 75 let data: &mut Data = get_data(&env)?; 77 - let tracks = &mut data.library.tracks; 78 - tracks.get_mut(&id).ok_or(nerr("Track ID not found"))?; 76 + let tracks = data.library.get_tracks(); 77 + tracks.get(&id).ok_or(nerr("Track ID not found"))?; 79 78 data.library.playTime.push((id, start, dur_ms)); 80 79 Ok(()) 81 80 } ··· 168 167 let data: &mut Data = get_data(&env)?; 169 168 let id = data.library.generate_id(); 170 169 let track = import::import(&data, Path::new(&path), now)?; 171 - data.library.tracks.insert(id, track); 170 + data.library.insert_track(id, track); 172 171 Ok(()) 173 172 } 174 173
+1 -1
src/components/TrackList.svelte
··· 91 91 } else if (action === 'Remove from Playlist') { 92 92 // remove_from_playlist(params.playlist_id, item_ids) 93 93 } else if (action === 'Delete from Library') { 94 - delete_indexes(selection.items) 94 + // delete_indexes(selection.items) 95 95 } else { 96 96 assert_unreachable(action) 97 97 }