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

Remove open playlist concept from Rust

Kasper (Sep 26, 2024, 3:20 AM +0200) 40c08185 b547d2cd

+153 -348
+18 -17
ferrum-addon/addon.d.ts
··· 14 14 } 15 15 export declare function get_paths(): PathsJs 16 16 export declare function save(): void 17 - export declare function filter_open_playlist(query: string): void 18 17 export interface ImportStatus { 19 18 errors: Array<string> 20 19 tracksCount: number ··· 82 81 originalId?: string 83 82 dateImported?: MsSinceUnixEpoch 84 83 dateCreated?: MsSinceUnixEpoch 85 - tracks: Array<PlaylistItemId> 84 + tracks: Array<ItemId> 86 85 } 87 86 export interface Folder { 88 87 id: TrackListID ··· 107 106 export const enum SpecialTrackListName { 108 107 Root = 0 109 108 } 110 - export declare function open_playlist(openPlaylistId: string, viewAs?: ViewAs | undefined | null): void 111 - export declare function get_page_track(index: number): Track 112 - export declare function get_page_track_id(index: number): string 113 - export declare function refresh_page(): void 114 - export declare function get_page_track_ids(): Array<TrackID> 109 + export interface TracksPageOptions { 110 + playlistId: string 111 + sortKey: string 112 + sortDesc: boolean 113 + filterQuery: string 114 + groupAlbumTracks: boolean 115 + } 116 + export interface TracksPage { 117 + playlistName: string 118 + playlistDescription?: string 119 + playlistLength: number 120 + trackIds: Array<TrackID> 121 + } 122 + export declare function get_tracks_page(options: TracksPageOptions): TracksPage 115 123 export const enum ViewAs { 116 124 Songs = 0, 117 125 Artists = 1 ··· 124 132 sortDesc: boolean 125 133 length: number 126 134 } 127 - export declare function get_page_info(): PageInfo 128 - export declare function set_group_album_tracks(value: boolean): void 129 - export declare function sort(sortKey: string, keepFilter: boolean): void 130 - export interface SelectionInfo { 131 - from: number 132 - to: number 133 - } 134 - export declare function move_tracks(indexesToMove: Array<number>, toIndex: number): SelectionInfo 135 + export declare function move_tracks(playlistId: string, itemIds: Array<ItemId>, toIndex: number): void 135 136 export interface TrackListDetails { 136 137 id: string 137 138 name: string ··· 145 146 export declare function delete_track_list(id: string): void 146 147 export declare function add_tracks_to_playlist(playlistId: string, trackIds: Array<string>): void 147 148 export declare function playlist_filter_duplicates(playlistId: string, ids: Array<string>): Array<TrackID> 148 - export declare function remove_from_open_playlist(indexesToRemove: Array<number>): void 149 - export declare function delete_tracks_in_open(indexesToDelete: Array<number>): void 149 + export declare function remove_from_playlist(playlistId: string, itemIds: Array<ItemId>): void 150 + export declare function delete_tracks(itemIds: Array<ItemId>): void 150 151 export declare function new_playlist(name: string, description: string, isFolder: boolean, parentId: string): void 151 152 export declare function update_playlist(id: string, name: string, description: string): void 152 153 export declare function move_playlist(id: string, fromId: string, toId: string, toIndex: number): void
+3 -47
src-native/data.rs
··· 1 1 use crate::artists::load_artists; 2 2 use crate::library::{load_library, Paths}; 3 - use crate::library_types::{Library, TrackID, TrackList, TrackListID}; 4 - use crate::page::{get_track_ids, ViewAs}; 5 - use crate::sort::sort; 3 + use crate::library_types::Library; 6 4 use crate::tracks::Tag; 7 5 use crate::view_options::ViewOptions; 8 - use crate::{page, UniResult}; 6 + use crate::UniResult; 9 7 use atomicwrites::{AllowOverwrite, AtomicFile}; 10 8 use dirs_next; 11 9 use napi::Result; ··· 20 18 pub paths: Paths, 21 19 pub library: Library, 22 20 pub view_options: ViewOptions, 23 - /// All tracks on the current page, even if they are filtered out 24 - pub open_playlist_track_ids: Vec<TrackID>, 25 - /// The visible tracks on the current page 26 - pub page_track_ids: Option<Vec<TrackID>>, 27 - pub open_playlist_id: TrackListID, 28 - pub view_as: ViewAs, 29 - pub filter: String, 30 - pub sort_key: String, 31 - pub sort_desc: bool, 32 - pub group_album_tracks: bool, 33 21 /// Current tag being edited 34 22 pub current_tag: Option<Tag>, 35 23 pub artists: HashSet<String>, ··· 55 43 } 56 44 println!("Write: {}ms", now.elapsed().as_millis()); 57 45 Ok(()) 58 - } 59 - pub fn get_page_tracks(&self) -> &Vec<String> { 60 - match &self.page_track_ids { 61 - Some(ids) => ids, 62 - None => &self.open_playlist_track_ids, 63 - } 64 46 } 65 47 pub fn load( 66 48 is_dev: bool, ··· 109 91 let loaded_cache = ViewOptions::load(&paths); 110 92 let artists = load_artists(&loaded_library); 111 93 112 - let mut data = Data { 94 + let data = Data { 113 95 paths, 114 96 library: loaded_library, 115 97 artists, 116 98 view_options: loaded_cache, 117 - open_playlist_id: "root".to_string(), 118 - open_playlist_track_ids: vec![], 119 - view_as: ViewAs::Songs, 120 - page_track_ids: None, 121 - filter: "".to_string(), 122 - sort_key: "index".to_string(), 123 - sort_desc: true, 124 - group_album_tracks: true, 125 99 current_tag: None, 126 100 }; 127 - data.open_playlist_track_ids = page::get_track_ids(&data)?; 128 - sort(&mut data, "dateAdded", true)?; 129 101 return Ok(data); 130 - } 131 - pub fn open_playlist(&mut self, playlist_id: TrackID, view_as: Option<ViewAs>) -> Result<()> { 132 - self.open_playlist_id = playlist_id; 133 - self.view_as = view_as.unwrap_or_default(); 134 - self.open_playlist_track_ids = get_track_ids(self)?; 135 - self.page_track_ids = None; 136 - match self.library.get_tracklist(&self.open_playlist_id)? { 137 - TrackList::Special(_) => { 138 - sort(self, "dateAdded", true)?; 139 - } 140 - _ => { 141 - self.sort_key = "index".to_string(); 142 - self.sort_desc = true; 143 - } 144 - }; 145 - Ok(()) 146 102 } 147 103 }
+16 -30
src-native/filter.rs
··· 1 - use crate::data::Data; 2 - use crate::data_js::get_data; 3 - use crate::library_types::TrackID; 4 - use napi::{Env, Result}; 1 + use crate::library_types::{Library, TrackID}; 5 2 use rayon::prelude::*; 6 3 use std::str::Chars; 7 4 use std::time::Instant; 8 5 use unicode_normalization::{Recompositions, UnicodeNormalization}; 9 - 10 - #[napi(js_name = "filter_open_playlist")] 11 - #[allow(dead_code)] 12 - pub fn filter_js(query: String, env: Env) -> Result<()> { 13 - let data: &mut Data = get_data(&env)?; 14 - filter(data, query); 15 - return Ok(()); 16 - } 17 6 18 7 fn match_at_start(mut text: Recompositions<Chars>, keyword: Chars) -> bool { 19 8 for keyword_char in keyword { ··· 68 57 } 69 58 } 70 59 71 - fn filter_keyword(data: &Data, ids: &Vec<TrackID>, keyword: &str) -> Vec<TrackID> { 72 - let tracks = &data.library.tracks; 60 + fn filter_keyword(ids: Vec<TrackID>, keyword: &str, library: &Library) -> Vec<TrackID> { 61 + let tracks = &library.tracks; 73 62 let filtered_tracks: Vec<_> = ids 74 63 .into_par_iter() 75 64 .with_min_len(2000) 76 65 .filter(|id| { 77 - let track = tracks.get(*id).expect("Track ID not found"); 66 + let track = tracks.get(id).expect("Track ID not found"); 78 67 let is_match = find_match(&track.name, keyword) 79 68 || find_match(&track.artist, keyword) 80 69 || find_match_opt(&track.albumName, keyword) ··· 87 76 filtered_tracks 88 77 } 89 78 90 - pub fn filter(data: &mut Data, query: String) { 79 + pub fn filter(mut track_ids: Vec<TrackID>, query: String, library: &Library) -> Vec<TrackID> { 91 80 let now = Instant::now(); 92 - data.page_track_ids = if query == "" { 93 - None 94 - } else { 95 - let query: String = query.nfc().collect(); 96 - let mut keywords_iter = query.split(' '); 97 - let mut filtered_tracks = match keywords_iter.next() { 98 - Some(keyword) => filter_keyword(data, &data.open_playlist_track_ids, keyword), 99 - None => return, 100 - }; 101 - for keyword in keywords_iter { 102 - filtered_tracks = filter_keyword(data, &filtered_tracks, keyword); 81 + if query == "" { 82 + return track_ids; 83 + } 84 + let query: String = query.nfc().collect(); 85 + 86 + for keyword in query.split(' ') { 87 + if keyword == "" { 88 + continue; 103 89 } 104 - Some(filtered_tracks) 105 - }; 106 - data.filter = query; 90 + track_ids = filter_keyword(track_ids, keyword, &library); 91 + } 107 92 println!("Filter: {}ms", now.elapsed().as_millis()); 93 + track_ids 108 94 } 109 95 110 96 enum Eq {
+1 -1
src-native/library_types.rs
··· 282 282 } 283 283 284 284 // 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. 285 - type ItemId = u32; 285 + pub type ItemId = u32; 286 286 pub static PLAYLIST_TRACK_ID_MAP: RwLock<Vec<String>> = RwLock::new(Vec::new()); 287 287 288 288 pub fn new_item_ids_from_track_ids(track_ids: &[TrackID]) -> Vec<ItemId> {
+73 -159
src-native/page.rs
··· 1 1 use crate::data::Data; 2 2 use crate::data_js::get_data; 3 - use crate::js::nerr; 4 - use crate::library::{get_track_field_type, TrackField}; 3 + use crate::filter::filter; 5 4 use crate::library_types::{ 6 - SpecialTrackListName, Track, TrackID, TrackList, PLAYLIST_TRACK_ID_MAP, 5 + ItemId, Library, SpecialTrackListName, TrackID, TrackList, PLAYLIST_TRACK_ID_MAP, 7 6 }; 8 7 use crate::sort::sort; 9 - use crate::{filter, UniResult}; 10 - use napi::{Env, JsString, JsUndefined, JsUnknown, Result}; 8 + use crate::UniResult; 9 + use napi::{Env, JsUnknown, Result}; 11 10 use std::collections::HashSet; 12 - use std::time::Instant; 13 11 14 - #[napi(js_name = "open_playlist")] 15 - #[allow(dead_code)] 16 - pub fn open_playlist(open_playlist_id: String, view_as: Option<ViewAs>, env: Env) -> Result<()> { 17 - let data: &mut Data = get_data(&env)?; 18 - data.open_playlist(open_playlist_id, view_as) 19 - } 20 - 21 - #[napi(js_name = "get_page_track")] 22 - #[allow(dead_code)] 23 - pub fn get_page_track(index: i64, env: Env) -> Result<Track> { 24 - let data: &mut Data = get_data(&env)?; 25 - let page_track_ids = data.get_page_tracks(); 26 - let track_id = page_track_ids.get(index as usize).ok_or(nerr!( 27 - "Track index {} not found in open playlist", 28 - index.to_string() 29 - ))?; 30 - let track = data 31 - .library 32 - .tracks 33 - .get(track_id) 34 - .ok_or(nerr("Track ID not found"))?; 35 - return Ok(track.clone()); 12 + #[napi(object)] 13 + #[derive(Clone)] 14 + pub struct TracksPageOptions { 15 + pub playlist_id: String, 16 + pub sort_key: String, 17 + pub sort_desc: bool, 18 + pub filter_query: String, 19 + pub group_album_tracks: bool, 36 20 } 37 21 38 - #[napi(js_name = "get_page_track_id")] 39 - #[allow(dead_code)] 40 - pub fn get_page_track_id(index: i64, env: Env) -> Result<JsString> { 41 - let data: &mut Data = get_data(&env)?; 42 - let page_track_ids = data.get_page_tracks(); 43 - let track_id = page_track_ids.get(index as usize).ok_or(nerr!( 44 - "Track index {} not found in open playlist", 45 - index.to_string() 46 - ))?; 47 - return env.create_string(track_id); 48 - } 49 - 50 - #[napi(js_name = "refresh_page")] 51 - #[allow(dead_code)] 52 - pub fn refresh(env: Env) -> Result<JsUndefined> { 53 - let data: &mut Data = get_data(&env)?; 54 - let sort_key = data.sort_key.clone(); 55 - let sort_desc = data.sort_desc; 56 - sort(data, &sort_key, sort_desc)?; 57 - filter::filter(data, data.filter.clone()); 58 - return env.get_undefined(); 22 + #[napi(object)] 23 + pub struct TracksPage { 24 + pub playlist_name: String, 25 + pub playlist_description: Option<String>, 26 + pub playlist_length: u32, 27 + pub track_ids: Vec<TrackID>, 59 28 } 60 29 61 - #[napi(js_name = "get_page_track_ids")] 30 + #[napi(js_name = "get_tracks_page")] 62 31 #[allow(dead_code)] 63 - pub fn get_page_track_ids(env: Env) -> Result<Vec<TrackID>> { 32 + pub fn get_tracks_page(options: TracksPageOptions, env: Env) -> Result<TracksPage> { 64 33 let data: &mut Data = get_data(&env)?; 65 - Ok(data.get_page_tracks().to_owned()) 34 + let tracklist = data.library.get_tracklist(&options.playlist_id)?; 35 + let track_ids = sort(options.clone(), &data.library)?; 36 + let tracklist_length = track_ids.len(); 37 + let track_ids = filter(track_ids, options.filter_query, &data.library); 38 + let track_page = match tracklist { 39 + TrackList::Playlist(playlist) => TracksPage { 40 + playlist_name: playlist.name.clone(), 41 + playlist_description: playlist.description.clone(), 42 + playlist_length: tracklist_length as u32, 43 + track_ids, 44 + }, 45 + TrackList::Folder(folder) => TracksPage { 46 + playlist_name: folder.name.clone(), 47 + playlist_description: folder.description.clone(), 48 + playlist_length: tracklist_length as u32, 49 + track_ids, 50 + }, 51 + TrackList::Special(special) => TracksPage { 52 + playlist_name: special.name.to_string(), 53 + playlist_description: None, 54 + playlist_length: tracklist_length as u32, 55 + track_ids, 56 + }, 57 + }; 58 + Ok(track_page) 66 59 } 67 60 68 - fn get_tracklist_track_ids(data: &Data, playlist_id: &str) -> UniResult<Vec<TrackID>> { 69 - match data.library.get_tracklist(playlist_id)? { 61 + pub fn get_tracklist_track_ids(library: &Library, playlist_id: &str) -> UniResult<Vec<TrackID>> { 62 + match library.get_tracklist(playlist_id)? { 70 63 TrackList::Playlist(playlist) => { 71 64 let id_map = PLAYLIST_TRACK_ID_MAP.read().unwrap(); 72 65 let ids = playlist ··· 79 72 TrackList::Folder(folder) => { 80 73 let mut ids: HashSet<TrackID> = HashSet::new(); 81 74 for child in &folder.children { 82 - let child_ids = get_tracklist_track_ids(data, &child)?; 75 + let child_ids = get_tracklist_track_ids(library, &child)?; 83 76 ids.extend(child_ids); 84 77 } 85 78 Ok(ids.into_iter().collect()) 86 79 } 87 80 TrackList::Special(special) => match special.name { 88 81 SpecialTrackListName::Root => { 89 - let track_keys = data.library.tracks.keys(); 82 + let track_keys = library.tracks.keys(); 90 83 let ids = track_keys.map(|track| track.to_string()).collect(); 91 84 Ok(ids) 92 85 } 93 86 }, 94 87 } 95 - } 96 - pub fn get_track_ids(data: &Data) -> UniResult<Vec<TrackID>> { 97 - let now = Instant::now(); 98 - let ids = get_tracklist_track_ids(data, &data.open_playlist_id)?; 99 - println!("Load page tracks: {}ms", now.elapsed().as_millis()); 100 - Ok(ids) 101 88 } 102 89 103 90 #[napi] ··· 122 109 pub length: i64, 123 110 } 124 111 125 - #[allow(dead_code)] 126 - #[napi(js_name = "get_page_info")] 127 - pub fn get_page_info(env: Env) -> Result<PageInfo> { 128 - let data = get_data(&env)?; 129 - let tracklist = data.library.get_tracklist(&data.open_playlist_id)?; 130 - 131 - Ok(PageInfo { 132 - id: data.open_playlist_id.clone(), 133 - view_as: data.view_as, 134 - tracklist: env.to_js_value(tracklist)?, 135 - sort_key: data.sort_key.clone(), 136 - sort_desc: data.sort_desc, 137 - length: data.get_page_tracks().len().try_into().expect("Too long"), 138 - }) 139 - } 140 - 141 - #[napi(js_name = "set_group_album_tracks")] 142 - #[allow(dead_code)] 143 - pub fn set_group_album_tracks(value: bool, env: Env) -> Result<JsUndefined> { 144 - let data: &mut Data = get_data(&env)?; 145 - data.group_album_tracks = value; 146 - env.get_undefined() 147 - } 148 - 149 - #[napi(js_name = "sort")] 150 - #[allow(dead_code)] 151 - pub fn sort_js(sort_key: String, keep_filter: bool, env: Env) -> Result<JsUndefined> { 152 - let data: &mut Data = get_data(&env)?; 153 - 154 - let old_sort_key = &data.sort_key; 155 - if &sort_key == old_sort_key { 156 - data.open_playlist_track_ids.reverse(); 157 - data.sort_desc = !data.sort_desc; 158 - } else { 159 - let field = get_track_field_type(&sort_key); 160 - let desc = match field { 161 - Some(TrackField::String) => false, 162 - _ => true, 163 - }; 164 - 165 - sort(data, &sort_key, desc)?; 166 - } 167 - 168 - if keep_filter { 169 - filter::filter(data, data.filter.clone()); 170 - } 171 - return env.get_undefined(); 172 - } 173 - 174 - #[napi(object)] 175 - pub struct SelectionInfo { 176 - pub from: u32, 177 - pub to: u32, 178 - } 179 - 180 112 #[napi(js_name = "move_tracks")] 181 113 #[allow(dead_code)] 182 114 pub fn move_tracks( 183 - mut indexes_to_move: Vec<u32>, 115 + playlist_id: String, 116 + mut item_ids: Vec<ItemId>, 184 117 to_index: u32, 185 118 env: Env, 186 - ) -> Result<SelectionInfo> { 119 + ) -> Result<()> { 187 120 let data: &mut Data = get_data(&env)?; 188 - indexes_to_move.sort_unstable(); 189 - indexes_to_move.dedup(); 190 - let tracklist = data 191 - .library 192 - .trackLists 193 - .get_mut(&data.open_playlist_id) 194 - .ok_or(nerr!("Playlist ID not found"))?; 195 - let playlist = match tracklist { 121 + let playlist = match data.library.get_tracklist_mut(&playlist_id)? { 196 122 TrackList::Playlist(playlist) => playlist, 197 - TrackList::Folder(_) => return Err(nerr!("Cannot rearrange tracks in folder")), 198 - TrackList::Special(_) => return Err(nerr!("Cannot rearrange tracks in special playlist")), 123 + _ => return Err(nerr!("Cannot rearrange tracks in non-playlist")), 199 124 }; 200 - if data.sort_key != "index" || !data.sort_desc { 201 - return Err(nerr!("Cannot rearrange when custom sorting is used")); 202 - } 203 - if data.filter != "" { 204 - return Err(nerr!("Cannot rearrange when filter is used")); 205 - } 206 - let mut start_ids = Vec::new(); 207 - let mut moved_ids = Vec::new(); 208 - let mut end_ids = Vec::new(); 209 125 210 - let mut indexes_to_move = indexes_to_move.iter(); 211 - let mut next_index = indexes_to_move.next().map(|n| *n as usize); 212 - for i in 0..playlist.tracks.len() { 213 - let id = playlist.tracks.remove(0); 214 - if Some(i) == next_index { 215 - next_index = indexes_to_move.next().map(|n| *n as usize); 216 - moved_ids.push(id); 217 - } else if i < to_index as usize { 218 - start_ids.push(id); 219 - } else { 220 - end_ids.push(id); 221 - } 126 + let item_ids_set: HashSet<ItemId> = item_ids.iter().cloned().collect(); 127 + assert_eq!(item_ids_set.len(), item_ids.len()); 128 + 129 + let playlist_item_ids_set: HashSet<ItemId> = playlist.tracks.iter().cloned().collect(); 130 + for item_id in &item_ids { 131 + assert!(playlist_item_ids_set.contains(item_id)); 222 132 } 223 - let new_from = start_ids.len() as u32; 224 - let new_to = new_from + moved_ids.len() as u32 - 1; 225 - start_ids.append(&mut moved_ids); 226 - start_ids.append(&mut end_ids); 227 - playlist.tracks = start_ids; 228 - Ok(SelectionInfo { 229 - from: new_from, 230 - to: new_to, 231 - }) 133 + 134 + let mut start_items = playlist.tracks.clone(); 135 + start_items.retain(|item_id| !item_ids_set.contains(item_id)); 136 + 137 + let mut end_items = start_items.split_off(to_index as usize); 138 + end_items.retain(|item_id| !item_ids_set.contains(item_id)); 139 + 140 + start_items.append(&mut item_ids); 141 + 142 + start_items.append(&mut end_items); 143 + 144 + playlist.tracks = start_items; 145 + Ok(()) 232 146 }
+21 -59
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 - new_item_ids_from_track_ids, Library, SpecialTrackListName, TrackID, TrackList, 5 - PLAYLIST_TRACK_ID_MAP, 4 + get_track_ids_from_item_ids, new_item_ids_from_track_ids, ItemId, Library, 5 + SpecialTrackListName, TrackID, TrackList, PLAYLIST_TRACK_ID_MAP, 6 6 }; 7 7 use crate::{str_to_option, UniResult}; 8 8 use napi::{Env, JsUnknown, Result}; ··· 118 118 Ok(()) 119 119 } 120 120 121 - fn delete_track_list(data: &mut Data, id: String) -> Result<()> { 121 + /// Returns the deleted track lists, including folder children 122 + #[napi(js_name = "delete_track_list")] 123 + #[allow(dead_code)] 124 + pub fn delete_track_list_js(id: String, env: Env) -> Result<()> { 125 + let data: &mut Data = get_data(&env)?; 122 126 let parent_id = data 123 127 .library 124 128 .get_parent_id(&id) ··· 135 139 for id in &ids { 136 140 data.library.trackLists.remove(id); 137 141 } 138 - if ids.contains(&data.open_playlist_id) { 139 - data.open_playlist("root".to_string(), None)?; 140 - } 141 142 Ok(()) 142 - } 143 - 144 - /// Returns the deleted track lists, including folder children 145 - #[napi(js_name = "delete_track_list")] 146 - #[allow(dead_code)] 147 - pub fn delete_track_list_js(id: String, env: Env) -> Result<()> { 148 - let data: &mut Data = get_data(&env)?; 149 - delete_track_list(data, id) 150 143 } 151 144 152 145 #[napi(js_name = "add_tracks_to_playlist")] ··· 181 174 Ok(track_ids) 182 175 } 183 176 184 - #[napi(js_name = "remove_from_open_playlist")] 177 + #[napi(js_name = "remove_from_playlist")] 185 178 #[allow(dead_code)] 186 - pub fn remove_from_open(mut indexes_to_remove: Vec<u32>, env: Env) -> Result<()> { 179 + pub fn remove_from_playlist(playlist_id: String, item_ids: Vec<ItemId>, env: Env) -> Result<()> { 187 180 let data: &mut Data = get_data(&env)?; 188 - indexes_to_remove.sort_unstable(); 189 - indexes_to_remove.dedup(); 190 - let playlist = match data.library.get_tracklist_mut(&data.open_playlist_id)? { 181 + let playlist = match data.library.get_tracklist_mut(&playlist_id)? { 191 182 TrackList::Playlist(playlist) => playlist, 192 - TrackList::Folder(_) => throw!("Cannot remove track from folder"), 193 - TrackList::Special(_) => throw!("Cannot remove track from special playlist"), 183 + _ => throw!("Cannot remove track from non-playlist"), 194 184 }; 195 - if data.sort_key != "index" || !data.sort_desc { 196 - throw!("Cannot remove track when custom sorting is used"); 197 - } 198 - if data.filter != "" { 199 - throw!("Cannot remove track when filter is used"); 200 - } 201 - let mut new_list = Vec::new(); 202 - let mut indexes_to_remove = indexes_to_remove.iter(); 203 - let mut next_index = indexes_to_remove.next().map(|n| *n as usize); 204 - for i in 0..playlist.tracks.len() { 205 - let id = playlist.tracks.remove(0); 206 - if Some(i) == next_index { 207 - next_index = indexes_to_remove.next().map(|n| *n as usize); 208 - } else { 209 - new_list.push(id); 210 - } 211 - } 212 - playlist.tracks = new_list; 185 + let items_to_remove: HashSet<ItemId> = item_ids.into_iter().collect(); 186 + 187 + playlist 188 + .tracks 189 + .retain(|item_id| !items_to_remove.contains(item_id)); 190 + 213 191 return Ok(()); 214 192 } 215 193 ··· 226 204 } 227 205 } 228 206 229 - fn get_page_ids(data: &mut Data, indexes: Vec<u32>) -> UniResult<Vec<String>> { 230 - let mut ids = Vec::new(); 231 - let page_track_ids = data.get_page_tracks(); 232 - for index in indexes { 233 - let id = match page_track_ids.get(index as usize) { 234 - Some(id) => id, 235 - None => throw!("Track index not found"), 236 - }; 237 - ids.push(id.clone()); 238 - } 239 - Ok(ids) 240 - } 241 - 242 207 fn delete_file(path: &PathBuf) -> UniResult<()> { 243 208 #[allow(unused_mut)] 244 209 let mut trash_context = trash::TrashContext::new(); ··· 252 217 } 253 218 } 254 219 255 - #[napi(js_name = "delete_tracks_in_open")] 220 + #[napi(js_name = "delete_tracks")] 256 221 #[allow(dead_code)] 257 - pub fn delete_tracks_in_open(mut indexes_to_delete: Vec<u32>, env: Env) -> Result<()> { 222 + pub fn delete_tracks_with_item_id(item_ids: Vec<ItemId>, env: Env) -> Result<()> { 258 223 let data: &mut Data = get_data(&env)?; 259 - indexes_to_delete.sort_unstable(); 260 - indexes_to_delete.dedup(); 261 - let ids_to_delete = get_page_ids(data, indexes_to_delete)?; 262 224 let library = &mut data.library; 263 - 264 - for id_to_delete in &ids_to_delete { 225 + let track_ids = get_track_ids_from_item_ids(&item_ids); 226 + for id_to_delete in &track_ids { 265 227 let file_path = { 266 228 let track = library.get_track(id_to_delete)?; 267 229 data.paths.tracks_dir.join(&track.file)
+21 -35
src-native/sort.rs
··· 1 - use crate::data::Data; 2 1 use crate::library::{get_track_field_type, TrackField}; 3 - use crate::library_types::{Track, TrackList}; 4 - use crate::{page, UniResult}; 2 + use crate::library_types::{Library, Track, TrackID}; 3 + use crate::page::{get_tracklist_track_ids, TracksPageOptions}; 4 + use crate::UniResult; 5 5 use alphanumeric_sort::compare_str; 6 6 use std::cmp::Ordering; 7 7 use std::time::Instant; ··· 85 85 } 86 86 } 87 87 88 - pub fn sort(data: &mut Data, sort_key: &str, desc: bool) -> UniResult<()> { 88 + pub fn sort(options: TracksPageOptions, library: &Library) -> UniResult<Vec<TrackID>> { 89 89 let now = Instant::now(); 90 90 91 - if sort_key == "index" { 92 - // No need to sort for index. Indexes descend from "first to last" 93 - // instead of "high to low", so it needs to be reversed 94 - let playlist = data 95 - .library 96 - .trackLists 97 - .get(&data.open_playlist_id) 98 - .ok_or("Playlist ID not found")?; 99 - match playlist { 100 - TrackList::Playlist(_) => { 101 - data.open_playlist_track_ids = page::get_track_ids(&data)?; 102 - data.sort_key = sort_key.to_string(); 103 - data.sort_desc = true; 104 - println!("Sort: {}ms", now.elapsed().as_millis()); 105 - return Ok(()); 106 - } 107 - TrackList::Folder(_) | TrackList::Special(_) => return Ok(()), 108 - } 109 - } 91 + let mut track_ids = get_tracklist_track_ids(library, &options.playlist_id)?; 110 92 111 - data.open_playlist_track_ids = page::get_track_ids(&data)?; 93 + if options.sort_key == "index" { 94 + // No need to sort for index. Note: Indexes descend from "first to last", 95 + // unlike other numbers which ascend from "high to low" 96 + println!("Sort: {}ms", now.elapsed().as_millis()); 97 + track_ids.reverse(); 98 + return Ok(track_ids); 99 + } 112 100 113 - let tracks = &data.library.tracks; 114 - let field = match get_track_field_type(sort_key) { 101 + let tracks = &library.tracks; 102 + let field = match get_track_field_type(&options.sort_key) { 115 103 Some(field) => field, 116 - None => throw!("Field type not found for {sort_key}"), 104 + None => throw!("Field type not found for {}", options.sort_key), 117 105 }; 118 - let subsort_field = data.group_album_tracks 119 - && match sort_key { 106 + let subsort_field = options.group_album_tracks 107 + && match options.sort_key.as_str() { 120 108 "dateAdded" | "albumName" | "comments" | "genre" | "year" | "artist" => true, 121 109 _ => false, 122 110 }; 123 - data.open_playlist_track_ids.sort_by(|id_a, id_b| { 111 + track_ids.sort_by(|id_a, id_b| { 124 112 let track_a = tracks.get(id_a).expect("Track ID non-existant (1)"); 125 113 let track_b = tracks.get(id_b).expect("Track ID non-existant (2)"); 126 114 if subsort_field ··· 138 126 order => return order, 139 127 }; 140 128 } 141 - let order = compare_track_field(track_a, track_b, sort_key, &field); 142 - return match desc { 129 + let order = compare_track_field(track_a, track_b, &options.sort_key, &field); 130 + return match options.sort_desc { 143 131 true => order.reverse(), 144 132 false => order, 145 133 }; 146 134 }); 147 - data.sort_key = sort_key.to_string(); 148 - data.sort_desc = desc; 149 135 println!("Sort: {}ms", now.elapsed().as_millis()); 150 - return Ok(()); 136 + return Ok(track_ids); 151 137 } 152 138 153 139 pub fn compare_track_field(a: &Track, b: &Track, sort_key: &str, field: &TrackField) -> Ordering {