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

Complete iTunes import

Kasper (Oct 20, 2022, 2:26 AM +0200) 268504c3 f624036e

+538 -277
+1 -1
.gitignore
··· 1 1 node_modules 2 2 build 3 3 target 4 - appdata 4 + src-native/appdata 5 5 ferrum-addon/addon.node
+2
CHANGELOG.md
··· 3 3 ## Next 4 4 - Add drag-and-drop, selection and context menus in queue panel 5 5 - Add warning when adding duplicate songs to a playlist 6 + - Rewritten iTunes import, should be much more reliabile 7 + - iTunes import no longer overwrites the existing library 6 8 - Scroll sidebar to opened playlist 7 9 - Improve queue panel performance 8 10 - Don't clear selection unnecessarily
+8 -1
ferrum-addon/addon.d.ts
··· 18 18 tracksCount: number 19 19 playlistsCount: number 20 20 } 21 - export function import_itunes(path: string, tracksDir: string): Promise<ImportStatus> 22 21 export function copyFile(from: string, to: string): void 23 22 export function atomicFileSave(filePath: string, content: string): void 23 + export const enum Version { 24 + V1 = 1 25 + } 24 26 export interface Track { 25 27 size: number 26 28 duration: number ··· 169 171 export function set_image_data(index: number, bytes: JsArrayBuffer, mimeType: string): void 170 172 export function remove_image(index: number): void 171 173 export function update_track_info(trackId: string, info: TrackMd): void 174 + export class ItunesImport { 175 + static new(): ItunesImport 176 + start(path: string, tracksDir: string): Promise<ImportStatus> 177 + finish(): void 178 + }
+4 -1
src-native/data.rs
··· 61 61 } 62 62 pub fn load(is_dev: bool) -> UniResult<Data> { 63 63 let paths = if is_dev { 64 - let appdata_dir = env::current_dir().unwrap().join("appdata"); 64 + let appdata_dir = env::current_dir() 65 + .unwrap() 66 + .join("src-native") 67 + .join("appdata"); 65 68 let library_dir = appdata_dir.join("Library"); 66 69 Paths { 67 70 library_dir: library_dir.clone(),
+62 -13
src-native/itunes_import.rs
··· 1 + use crate::data_js::get_data; 1 2 use crate::get_now_timestamp; 2 3 use crate::library_types::{CountObject, Folder, Library, Playlist, Track, TrackList}; 3 4 use crate::tracks::generate_filename; 4 5 use crate::tracks::import::{read_file_metadata, FileType}; 5 6 use lofty::AudioFile; 6 - use napi::Result; 7 + use napi::{Env, Result}; 7 8 use serde::{Deserialize, Serialize}; 8 9 use std::collections::HashMap; 10 + use std::fs; 9 11 use std::path::{Path, PathBuf}; 10 12 use time::serde::iso8601; 11 13 use time::serde::iso8601::option as iso8601_opt; ··· 320 322 } 321 323 322 324 /// Parses track but does not move it to `tracks_dir` 323 - fn parse_track(xml_track: XmlTrack, start_time: i64, tracks_dir: &Path) -> Result<Track> { 325 + fn parse_track( 326 + xml_track: XmlTrack, 327 + start_time: i64, 328 + tracks_dir: &Path, 329 + ) -> Result<(PathBuf, Track)> { 324 330 let xml_location = match xml_track.location { 325 331 Some(ref location) => location, 326 332 None => throw!("Missing track location"), ··· 434 440 }, 435 441 }; 436 442 437 - Ok(track) 443 + Ok((xml_track_path, track)) 438 444 } 439 445 440 446 #[derive(Serialize, Deserialize, Clone, Debug)] ··· 577 583 #[napi(object)] 578 584 pub struct ImportStatus { 579 585 pub errors: Vec<String>, 580 - pub tracks_count: u32, 581 - pub playlists_count: u32, 586 + pub tracks_count: i64, 587 + pub playlists_count: i64, 588 + } 589 + 590 + #[napi] 591 + pub struct ItunesImport { 592 + new_library: Option<Library>, 593 + /// iTunes path -> Ferrum file 594 + itunes_track_paths: HashMap<PathBuf, String>, 595 + } 596 + #[napi] 597 + impl ItunesImport { 598 + #[napi(factory)] 599 + pub fn new(env: Env) -> Result<Self> { 600 + let data = get_data(&env)?; 601 + Ok(Self { 602 + new_library: Some(data.library.clone()), 603 + itunes_track_paths: HashMap::new(), 604 + }) 605 + } 606 + #[napi] 607 + pub async fn start(&mut self, path: String, tracks_dir: String) -> Result<ImportStatus> { 608 + import_itunes(self, path, tracks_dir).await 609 + } 610 + #[napi] 611 + pub fn finish(&mut self, env: Env) -> Result<()> { 612 + let mut data = get_data(&env)?; 613 + for (itunes_path, ferrum_file) in &self.itunes_track_paths { 614 + let new_path = data.paths.tracks_dir.join(ferrum_file); 615 + fs::copy(itunes_path, new_path).or(Err(nerr!("Error copying file")))?; 616 + } 617 + data.library = self.new_library.take().ok_or(nerr!("Not initialized"))?; 618 + Ok(()) 619 + } 582 620 } 583 621 584 - #[napi(js_name = "import_itunes")] 585 - #[allow(dead_code)] 586 - pub async fn import_itunes(path: String, tracks_dir: String) -> Result<ImportStatus> { 587 - let mut library = Library::new(); 622 + pub async fn import_itunes( 623 + itunes_import: &mut ItunesImport, 624 + path: String, 625 + tracks_dir: String, 626 + ) -> Result<ImportStatus> { 627 + let mut library = match &mut itunes_import.new_library { 628 + Some(library) => library, 629 + None => throw!("Not initialized"), 630 + }; 631 + let itunes_track_paths = &mut itunes_import.itunes_track_paths; 632 + let original_tracks_count = library.tracks.len(); 633 + let original_tracklists_count = library.trackLists.len(); 588 634 let xml_lib: XmlLibrary = match plist::from_file(path) { 589 635 Ok(book) => book, 590 636 Err(e) => throw!("Unable to parse: {e}"), ··· 626 672 } 627 673 628 674 match parse_track(xml_track, start_time, Path::new(&tracks_dir)) { 629 - Ok(track) => { 675 + Ok((xml_track_path, track)) => { 630 676 let generated_id = library.generate_id(); 631 677 // immediately insert into library so new generated ids are unique 678 + itunes_track_paths.insert(xml_track_path, track.file.clone()); 632 679 library.tracks.insert(generated_id.clone(), track); 633 680 if xml_track_id_map.contains_key(&xml_id) { 634 681 errors.push(format!("[{artist_title}] Duplicate track ids {}", xml_id)); ··· 702 749 703 750 // recursively import playlists 704 751 for i in root_child_indexes { 705 - import_playlist( 752 + let playlist_id = import_playlist( 706 753 &xml_playlist_infos, 707 754 i, 708 755 &mut library, ··· 710 757 &mut errors, 711 758 &xml_track_id_map, 712 759 ); 760 + let root = library.get_root_tracklist_mut()?; 761 + root.children.push(playlist_id); 713 762 } 714 763 715 764 Ok(ImportStatus { 716 765 errors, 717 - tracks_count: library.tracks.len() as u32, 718 - playlists_count: library.trackLists.len() as u32, 766 + tracks_count: (library.tracks.len() - original_tracks_count) as i64, 767 + playlists_count: (library.trackLists.len() - original_tracklists_count) as i64, 719 768 }) 720 769 } 721 770
+11 -1
src-native/library_types.rs
··· 7 7 use serde::{Deserialize, Serialize}; 8 8 use serde_repr::{Deserialize_repr, Serialize_repr}; 9 9 10 - #[derive(Serialize, Deserialize, Debug)] 10 + #[derive(Serialize, Deserialize, Clone, Debug)] 11 11 #[serde(deny_unknown_fields)] 12 12 pub struct Library { 13 13 pub version: Version, ··· 88 88 let tracklist = self.trackLists.get_mut(id); 89 89 Ok(tracklist.ok_or("Playlist ID not found")?) 90 90 } 91 + pub fn get_root_tracklist_mut(&mut self) -> UniResult<&mut Special> { 92 + let tracklist = self.trackLists.get_mut("root"); 93 + match tracklist { 94 + Some(TrackList::Special(special)) => match special.name { 95 + SpecialTrackListName::Root => Ok(special), 96 + }, 97 + _ => throw!("Root playlist not found"), 98 + } 99 + } 91 100 // pub fn get_parent_id(&self, id: &str) -> Option<String> { 92 101 // for (parent_id, tracklist) in &self.trackLists { 93 102 // let children = match tracklist { ··· 114 123 115 124 #[derive(Serialize_repr, Deserialize_repr, Debug)] 116 125 #[repr(u8)] 126 + #[napi] 117 127 pub enum Version { 118 128 V1 = 1, 119 129 }
+325 -206
src-native/tests/Library.xml
··· 1 1 <?xml version="1.0" encoding="UTF-8"?> 2 2 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 3 <plist version="1.0"> 4 - <dict> 5 - <key>Major Version</key><integer>1</integer> 6 - <key>Minor Version</key><integer>1</integer> 7 - <key>Date</key><date>2022-10-15T10:56:55Z</date> 8 - <key>Application Version</key><string>1.0.6.10</string> 9 - <key>Features</key><integer>5</integer> 10 - <key>Show Content Ratings</key><true/> 11 - <key>Music Folder</key><string>file:///Users/k/Music/Music/Media.localized/</string> 12 - <key>Library Persistent ID</key><string>9003D1E43533D0C6</string> 13 - <key>Tracks</key> 14 4 <dict> 15 - <key>144</key> 5 + <key>Major Version</key> 6 + <integer>1</integer> 7 + <key>Minor Version</key> 8 + <integer>1</integer> 9 + <key>Date</key> 10 + <date>2022-10-15T10:56:55Z</date> 11 + <key>Application Version</key> 12 + <string>1.0.6.10</string> 13 + <key>Features</key> 14 + <integer>5</integer> 15 + <key>Show Content Ratings</key> 16 + <true/> 17 + <key>Music Folder</key> 18 + <string>file:///Users/k/Music/Music/Media.localized/</string> 19 + <key>Library Persistent ID</key> 20 + <string>9003D1E43533D0C6</string> 21 + <key>Tracks</key> 16 22 <dict> 17 - <key>Track ID</key><integer>144</integer> 18 - <key>Name</key><string>Crescendo (feat. MYLK)</string> 19 - <key>Artist</key><string>Muzzy</string> 20 - <key>Album Artist</key><string>MuzzyAlb</string> 21 - <key>Composer</key><string>MrMuzzMan</string> 22 - <key>Album</key><string>Some album</string> 23 - <key>Grouping</key><string>MonstercatGroup</string> 24 - <key>Genre</key><string>Drum &#38; Bass</string> 25 - <key>Kind</key><string>Matched AAC audio file</string> 26 - <key>Size</key><integer>10573312</integer> 27 - <key>Total Time</key><integer>298600</integer> 28 - <key>Start Time</key><integer>1000</integer> 29 - <key>Stop Time</key><integer>20000</integer> 30 - <key>Disc Number</key><integer>2</integer> 31 - <key>Disc Count</key><integer>3</integer> 32 - <key>Track Number</key><integer>1</integer> 33 - <key>Track Count</key><integer>10</integer> 34 - <key>Year</key><integer>2016</integer> 35 - <key>BPM</key><integer>126</integer> 36 - <key>Date Modified</key><date>2022-10-14T07:36:08Z</date> 37 - <key>Date Added</key><date>2022-07-25T17:03:37Z</date> 38 - <key>Bit Rate</key><integer>256</integer> 39 - <key>Sample Rate</key><integer>44100</integer> 40 - <key>Volume Adjustment</key><integer>255</integer> 41 - <key>Equalizer</key><string>Electronic</string> 42 - <key>Comments</key><string>Monstercat</string> 43 - <key>Play Count</key><integer>1</integer> 44 - <key>Play Date</key><integer>3748677813</integer> 45 - <key>Play Date UTC</key><date>2022-10-15T09:23:33Z</date> 46 - <key>Skip Count</key><integer>5</integer> 47 - <key>Skip Date</key><date>2022-10-11T10:16:59Z</date> 48 - <key>Rating</key><integer>60</integer> 49 - <key>Album Rating</key><integer>60</integer> 50 - <key>Album Rating Computed</key><true/> 51 - <key>Loved</key><true/> 52 - <key>Normalization</key><integer>11079</integer> 53 - <key>Artwork Count</key><integer>1</integer> 54 - <key>Sort Album</key><string>B</string> 55 - <key>Sort Album Artist</key><string>C</string> 56 - <key>Sort Artist</key><string>D</string> 57 - <key>Sort Composer</key><string>E</string> 58 - <key>Sort Name</key><string>A</string> 59 - <key>Persistent ID</key><string>5F7ACC3AEC98302A</string> 60 - <key>Track Type</key><string>File</string> 61 - <key>Matched</key><true/> 62 - <key>Location</key><string>file:///Users/k/Music/Music/Media.localized/MuzzyAlb/Some%20album/2-01%20Crescendo%20(feat.%20MYLK).m4a</string> 63 - <key>File Folder Count</key><integer>4</integer> 64 - <key>Library Folder Count</key><integer>1</integer> 23 + <key>144</key> 24 + <dict> 25 + <key>Track ID</key> 26 + <integer>144</integer> 27 + <key>Name</key> 28 + <string>Crescendo (feat. MYLK)</string> 29 + <key>Artist</key> 30 + <string>Muzzy</string> 31 + <key>Album Artist</key> 32 + <string>MuzzyAlb</string> 33 + <key>Composer</key> 34 + <string>MrMuzzMan</string> 35 + <key>Album</key> 36 + <string>Some album</string> 37 + <key>Grouping</key> 38 + <string>MonstercatGroup</string> 39 + <key>Genre</key> 40 + <string>Drum &#38; Bass</string> 41 + <key>Kind</key> 42 + <string>Matched AAC audio file</string> 43 + <key>Size</key> 44 + <integer>10573312</integer> 45 + <key>Total Time</key> 46 + <integer>298600</integer> 47 + <key>Start Time</key> 48 + <integer>1000</integer> 49 + <key>Stop Time</key> 50 + <integer>20000</integer> 51 + <key>Disc Number</key> 52 + <integer>2</integer> 53 + <key>Disc Count</key> 54 + <integer>3</integer> 55 + <key>Track Number</key> 56 + <integer>1</integer> 57 + <key>Track Count</key> 58 + <integer>10</integer> 59 + <key>Year</key> 60 + <integer>2016</integer> 61 + <key>BPM</key> 62 + <integer>126</integer> 63 + <key>Date Modified</key> 64 + <date>2022-10-14T07:36:08Z</date> 65 + <key>Date Added</key> 66 + <date>2022-07-25T17:03:37Z</date> 67 + <key>Bit Rate</key> 68 + <integer>256</integer> 69 + <key>Sample Rate</key> 70 + <integer>44100</integer> 71 + <key>Volume Adjustment</key> 72 + <integer>255</integer> 73 + <key>Equalizer</key> 74 + <string>Electronic</string> 75 + <key>Comments</key> 76 + <string>Monstercat</string> 77 + <key>Play Count</key> 78 + <integer>1</integer> 79 + <key>Play Date</key> 80 + <integer>3748677813</integer> 81 + <key>Play Date UTC</key> 82 + <date>2022-10-15T09:23:33Z</date> 83 + <key>Skip Count</key> 84 + <integer>5</integer> 85 + <key>Skip Date</key> 86 + <date>2022-10-11T10:16:59Z</date> 87 + <key>Rating</key> 88 + <integer>60</integer> 89 + <key>Album Rating</key> 90 + <integer>60</integer> 91 + <key>Album Rating Computed</key> 92 + <true/> 93 + <key>Loved</key> 94 + <true/> 95 + <key>Normalization</key> 96 + <integer>11079</integer> 97 + <key>Artwork Count</key> 98 + <integer>1</integer> 99 + <key>Sort Album</key> 100 + <string>B</string> 101 + <key>Sort Album Artist</key> 102 + <string>C</string> 103 + <key>Sort Artist</key> 104 + <string>D</string> 105 + <key>Sort Composer</key> 106 + <string>E</string> 107 + <key>Sort Name</key> 108 + <string>A</string> 109 + <key>Persistent ID</key> 110 + <string>5F7ACC3AEC98302A</string> 111 + <key>Track Type</key> 112 + <string>File</string> 113 + <key>Matched</key> 114 + <true/> 115 + <key>Location</key> 116 + <string>file:///Users/k/Music/Music/Media.localized/MuzzyAlb/Some%20album/2-01%20Crescendo%20(feat.%20MYLK).m4a</string> 117 + <key>File Folder Count</key> 118 + <integer>4</integer> 119 + <key>Library Folder Count</key> 120 + <integer>1</integer> 121 + </dict> 122 + <key>350</key> 123 + <dict> 124 + <key>Track ID</key> 125 + <integer>350</integer> 126 + <key>Name</key> 127 + <string>inhuman</string> 128 + <key>Artist</key> 129 + <string>ISOxo</string> 130 + <key>Kind</key> 131 + <string>MPEG audio file</string> 132 + <key>Size</key> 133 + <integer>8424597</integer> 134 + <key>Total Time</key> 135 + <integer>197712</integer> 136 + <key>Date Modified</key> 137 + <date>2022-10-15T10:54:46Z</date> 138 + <key>Date Added</key> 139 + <date>2022-10-15T10:54:00Z</date> 140 + <key>Bit Rate</key> 141 + <integer>320</integer> 142 + <key>Sample Rate</key> 143 + <integer>48000</integer> 144 + <key>Volume Adjustment</key> 145 + <integer>-255</integer> 146 + <key>Artwork Count</key> 147 + <integer>1</integer> 148 + <key>Persistent ID</key> 149 + <string>728BFAEAF7C0C8D7</string> 150 + <key>Track Type</key> 151 + <string>File</string> 152 + <key>Location</key> 153 + <string>file:///Users/k/Music/Music/Media.localized/ISOxo/Unknown%20Album/inhuman.mp3</string> 154 + <key>File Folder Count</key> 155 + <integer>4</integer> 156 + <key>Library Folder Count</key> 157 + <integer>1</integer> 158 + </dict> 65 159 </dict> 66 - <key>350</key> 67 - <dict> 68 - <key>Track ID</key><integer>350</integer> 69 - <key>Name</key><string>inhuman</string> 70 - <key>Artist</key><string>ISOxo</string> 71 - <key>Kind</key><string>MPEG audio file</string> 72 - <key>Size</key><integer>8424597</integer> 73 - <key>Total Time</key><integer>197712</integer> 74 - <key>Date Modified</key><date>2022-10-15T10:54:46Z</date> 75 - <key>Date Added</key><date>2022-10-15T10:54:00Z</date> 76 - <key>Bit Rate</key><integer>320</integer> 77 - <key>Sample Rate</key><integer>48000</integer> 78 - <key>Volume Adjustment</key><integer>-255</integer> 79 - <key>Artwork Count</key><integer>1</integer> 80 - <key>Persistent ID</key><string>728BFAEAF7C0C8D7</string> 81 - <key>Track Type</key><string>File</string> 82 - <key>Location</key><string>file:///Users/k/Music/Music/Media.localized/ISOxo/Unknown%20Album/inhuman.mp3</string> 83 - <key>File Folder Count</key><integer>4</integer> 84 - <key>Library Folder Count</key><integer>1</integer> 85 - </dict> 160 + <key>Playlists</key> 161 + <array> 162 + <dict> 163 + <key>Name</key> 164 + <string>Library</string> 165 + <key>Description</key> 166 + <string></string> 167 + <key>Master</key> 168 + <true/> 169 + <key>Playlist ID</key> 170 + <integer>64</integer> 171 + <key>Playlist Persistent ID</key> 172 + <string>0000000000000005</string> 173 + <key>Visible</key> 174 + <false/> 175 + <key>All Items</key> 176 + <true/> 177 + <key>Playlist Items</key> 178 + <array> 179 + <dict> 180 + <key>Track ID</key> 181 + <integer>144</integer> 182 + </dict> 183 + <dict> 184 + <key>Track ID</key> 185 + <integer>350</integer> 186 + </dict> 187 + </array> 188 + </dict> 189 + <dict> 190 + <key>Name</key> 191 + <string>Downloaded</string> 192 + <key>Description</key> 193 + <string></string> 194 + <key>Playlist ID</key> 195 + <integer>147</integer> 196 + <key>Playlist Persistent ID</key> 197 + <string>533817C18A096868</string> 198 + <key>Distinguished Kind</key> 199 + <integer>65</integer> 200 + <key>All Items</key> 201 + <true/> 202 + <key>Playlist Items</key> 203 + <array> 204 + <dict> 205 + <key>Track ID</key> 206 + <integer>144</integer> 207 + </dict> 208 + <dict> 209 + <key>Track ID</key> 210 + <integer>350</integer> 211 + </dict> 212 + </array> 213 + </dict> 214 + <dict> 215 + <key>Name</key> 216 + <string>Music</string> 217 + <key>Description</key> 218 + <string></string> 219 + <key>Playlist ID</key> 220 + <integer>151</integer> 221 + <key>Playlist Persistent ID</key> 222 + <string>D613EC438867A085</string> 223 + <key>Distinguished Kind</key> 224 + <integer>4</integer> 225 + <key>Music</key> 226 + <true/> 227 + <key>All Items</key> 228 + <true/> 229 + <key>Playlist Items</key> 230 + <array> 231 + <dict> 232 + <key>Track ID</key> 233 + <integer>144</integer> 234 + </dict> 235 + <dict> 236 + <key>Track ID</key> 237 + <integer>350</integer> 238 + </dict> 239 + </array> 240 + </dict> 241 + <dict> 242 + <key>Name</key> 243 + <string>cool</string> 244 + <key>Description</key> 245 + <string></string> 246 + <key>Playlist ID</key> 247 + <integer>161</integer> 248 + <key>Playlist Persistent ID</key> 249 + <string>1176ABF0C527C1D4</string> 250 + <key>All Items</key> 251 + <true/> 252 + <key>Folder</key> 253 + <true/> 254 + <key>Playlist Items</key> 255 + <array> 256 + <dict> 257 + <key>Track ID</key> 258 + <integer>144</integer> 259 + </dict> 260 + </array> 261 + </dict> 262 + <dict> 263 + <key>Name</key> 264 + <string>k</string> 265 + <key>Description</key> 266 + <string>sample text</string> 267 + <key>Playlist ID</key> 268 + <integer>165</integer> 269 + <key>Playlist Persistent ID</key> 270 + <string>BC4E3B8BEF50A180</string> 271 + <key>Parent Persistent ID</key> 272 + <string>1176ABF0C527C1D4</string> 273 + <key>All Items</key> 274 + <true/> 275 + <key>Playlist Items</key> 276 + <array> 277 + <dict> 278 + <key>Track ID</key> 279 + <integer>144</integer> 280 + </dict> 281 + </array> 282 + </dict> 283 + <dict> 284 + <key>Name</key> 285 + <string>MySmartPlaylist</string> 286 + <key>Description</key> 287 + <string></string> 288 + <key>Playlist ID</key> 289 + <integer>169</integer> 290 + <key>Playlist Persistent ID</key> 291 + <string>C7121C245F83DF41</string> 292 + <key>All Items</key> 293 + <true/> 294 + <key>Smart Info</key> 295 + <data> AQEBAwAAAAIAAAAZAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAA== 296 + </data> 297 + <key>Smart Criteria</key> 298 + <data> U0xzdAABAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQBAAACAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAE0= 299 + </data> 300 + <key>Playlist Items</key> 301 + <array> 302 + <dict> 303 + <key>Track ID</key> 304 + <integer>144</integer> 305 + </dict> 306 + </array> 307 + </dict> 308 + <dict> 309 + <key>Name</key> 310 + <string>xplaylist</string> 311 + <key>Description</key> 312 + <string></string> 313 + <key>Playlist ID</key> 314 + <integer>173</integer> 315 + <key>Playlist Persistent ID</key> 316 + <string>E718DE8281D556DC</string> 317 + <key>All Items</key> 318 + <true/> 319 + <key>Playlist Items</key> 320 + <array> 321 + <dict> 322 + <key>Track ID</key> 323 + <integer>144</integer> 324 + </dict> 325 + <dict> 326 + <key>Track ID</key> 327 + <integer>144</integer> 328 + </dict> 329 + </array> 330 + </dict> 331 + </array> 86 332 </dict> 87 - <key>Playlists</key> 88 - <array> 89 - <dict> 90 - <key>Name</key><string>Library</string> 91 - <key>Description</key><string></string> 92 - <key>Master</key><true/> 93 - <key>Playlist ID</key><integer>64</integer> 94 - <key>Playlist Persistent ID</key><string>0000000000000005</string> 95 - <key>Visible</key><false/> 96 - <key>All Items</key><true/> 97 - <key>Playlist Items</key> 98 - <array> 99 - <dict> 100 - <key>Track ID</key><integer>144</integer> 101 - </dict> 102 - <dict> 103 - <key>Track ID</key><integer>350</integer> 104 - </dict> 105 - </array> 106 - </dict> 107 - <dict> 108 - <key>Name</key><string>Downloaded</string> 109 - <key>Description</key><string></string> 110 - <key>Playlist ID</key><integer>147</integer> 111 - <key>Playlist Persistent ID</key><string>533817C18A096868</string> 112 - <key>Distinguished Kind</key><integer>65</integer> 113 - <key>All Items</key><true/> 114 - <key>Playlist Items</key> 115 - <array> 116 - <dict> 117 - <key>Track ID</key><integer>144</integer> 118 - </dict> 119 - <dict> 120 - <key>Track ID</key><integer>350</integer> 121 - </dict> 122 - </array> 123 - </dict> 124 - <dict> 125 - <key>Name</key><string>Music</string> 126 - <key>Description</key><string></string> 127 - <key>Playlist ID</key><integer>151</integer> 128 - <key>Playlist Persistent ID</key><string>D613EC438867A085</string> 129 - <key>Distinguished Kind</key><integer>4</integer> 130 - <key>Music</key><true/> 131 - <key>All Items</key><true/> 132 - <key>Playlist Items</key> 133 - <array> 134 - <dict> 135 - <key>Track ID</key><integer>144</integer> 136 - </dict> 137 - <dict> 138 - <key>Track ID</key><integer>350</integer> 139 - </dict> 140 - </array> 141 - </dict> 142 - <dict> 143 - <key>Name</key><string>cool</string> 144 - <key>Description</key><string></string> 145 - <key>Playlist ID</key><integer>161</integer> 146 - <key>Playlist Persistent ID</key><string>1176ABF0C527C1D4</string> 147 - <key>All Items</key><true/> 148 - <key>Folder</key><true/> 149 - <key>Playlist Items</key> 150 - <array> 151 - <dict> 152 - <key>Track ID</key><integer>144</integer> 153 - </dict> 154 - </array> 155 - </dict> 156 - <dict> 157 - <key>Name</key><string>k</string> 158 - <key>Description</key><string>sample text</string> 159 - <key>Playlist ID</key><integer>165</integer> 160 - <key>Playlist Persistent ID</key><string>BC4E3B8BEF50A180</string> 161 - <key>Parent Persistent ID</key><string>1176ABF0C527C1D4</string> 162 - <key>All Items</key><true/> 163 - <key>Playlist Items</key> 164 - <array> 165 - <dict> 166 - <key>Track ID</key><integer>144</integer> 167 - </dict> 168 - </array> 169 - </dict> 170 - <dict> 171 - <key>Name</key><string>MySmartPlaylist</string> 172 - <key>Description</key><string></string> 173 - <key>Playlist ID</key><integer>169</integer> 174 - <key>Playlist Persistent ID</key><string>C7121C245F83DF41</string> 175 - <key>All Items</key><true/> 176 - <key>Smart Info</key> 177 - <data> 178 - AQEBAwAAAAIAAAAZAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 179 - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 180 - AAAAAA== 181 - </data> 182 - <key>Smart Criteria</key> 183 - <data> 184 - U0xzdAABAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 185 - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 186 - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQBAAACAAAAAAAAAAAAAAAAAAAAAAAA 187 - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAE0= 188 - </data> 189 - <key>Playlist Items</key> 190 - <array> 191 - <dict> 192 - <key>Track ID</key><integer>144</integer> 193 - </dict> 194 - </array> 195 - </dict> 196 - <dict> 197 - <key>Name</key><string>x</string> 198 - <key>Description</key><string></string> 199 - <key>Playlist ID</key><integer>173</integer> 200 - <key>Playlist Persistent ID</key><string>E718DE8281D556DC</string> 201 - <key>All Items</key><true/> 202 - <key>Playlist Items</key> 203 - <array> 204 - <dict> 205 - <key>Track ID</key><integer>144</integer> 206 - </dict> 207 - <dict> 208 - <key>Track ID</key><integer>144</integer> 209 - </dict> 210 - </array> 211 - </dict> 212 - </array> 213 - </dict> 214 333 </plist>
+4 -1
src/App.svelte
··· 95 95 } 96 96 } 97 97 98 - let showItunesImport = true 98 + let showItunesImport = false 99 99 onDestroy( 100 100 ipcListen('itunesImport', () => { 101 101 if ($modalCount === 0) { ··· 212 212 user-select: none 213 213 :global(h1), :global(h2), :global(h3) 214 214 font-weight: 400 215 + margin: 0px 216 + :global(h4), :global(h5), :global(h6) 217 + font-weight: 600 215 218 margin: 0px 216 219 .dropzone, .drag-overlay 217 220 position: fixed
+1
src/components/Button.svelte
··· 23 23 position: relative 24 24 outline: none 25 25 z-index: 1 26 + user-select: none 26 27 &::before 27 28 content: '' 28 29 position: absolute
+69 -31
src/components/ItunesImport.svelte
··· 1 1 <script lang="ts"> 2 - import { importItunes } from '@/lib/data' 2 + import { ItunesImport, paths, call, methods, page, trackLists } from '@/lib/data' 3 3 import { ipcRenderer } from '@/lib/window' 4 4 import type { ImportStatus } from 'ferrum-addon/addon' 5 5 import Button from './Button.svelte' 6 6 import Modal from './Modal.svelte' 7 7 8 8 export let cancel: () => void 9 - let filePath = '' 10 - let locked = false 11 - let status: ImportStatus | null = null 9 + let itunesImport = ItunesImport.new() 10 + 11 + type Stage = 'select' | 'fileSelect' | 'scanning' | ImportStatus 12 + let stage: Stage = 'select' 12 13 13 14 function cancelHandler() { 14 - if (!locked) { 15 - cancel() 15 + if (stage === 'fileSelect' || stage === 'scanning') { 16 + return 16 17 } 18 + cancel() 17 19 } 18 20 19 21 async function selectFile() { 20 - locked = true 22 + stage = 'fileSelect' 21 23 const open = await ipcRenderer.invoke('showOpenDialog', true, { 22 24 properties: ['openFile'], 23 - // filters: [{ name: 'Audio', extensions: ['mp3', 'm4a', 'opus'] }], 25 + filters: [{ name: 'iTunes Library File', extensions: ['xml'] }], 24 26 }) 25 27 if (!open.canceled && open.filePaths[0]) { 26 - filePath = open.filePaths[0] 27 - try { 28 - status = await importItunes(filePath) 29 - } catch (e) { 30 - console.error(e) 31 - } 28 + stage = 'scanning' 29 + const filePath = open.filePaths[0] 30 + stage = await call(() => itunesImport.start(filePath, paths.tracksDir)) 31 + console.log(stage) 32 + } else { 33 + stage = 'select' 32 34 } 33 - locked = false 35 + } 36 + async function finish() { 37 + itunesImport.finish() 38 + methods.save() 39 + page.refresh() 40 + trackLists.refreshTrackIdList() 41 + cancel() 42 + } 43 + async function submit() { 44 + if (stage === 'select') { 45 + selectFile() 46 + } else if (typeof stage === 'object' && 'tracksCount' in stage) { 47 + finish() 48 + } 34 49 } 35 50 </script> 36 51 37 - <Modal onCancel={cancelHandler} cancelOnEscape form={selectFile}> 52 + <Modal onCancel={cancelHandler} cancelOnEscape form={submit} title="Import iTunes Library"> 38 53 <main> 39 - {#if filePath === ''} 40 - <h3>Import iTunes Library</h3> 54 + {#if stage === 'select' || stage === 'fileSelect'} 41 55 <p> 42 56 Select an iTunes <strong>Library.xml</strong> file. To get that file, open iTunes and click 43 57 on ··· 62 76 <Button secondary on:click={cancelHandler}>Cancel</Button> 63 77 <Button type="submit">Select File</Button> 64 78 </div> 65 - {:else if status} 66 - <div class="error-box"> 67 - <h4>Errors</h4> 68 - {#each status.errors as error} 69 - <p>{error}</p> 70 - {/each} 71 - </div> 72 - <p>Playlists: {status.playlistsCount}</p> 73 - <p>Tracks: {status.tracksCount}</p> 74 - {:else if locked} 79 + {:else if stage === 'scanning'} 75 80 Scanning... 76 - {:else} 77 - Done 81 + {:else if 'tracksCount' in stage} 82 + {#if stage.errors.length > 0} 83 + <div class="error-box"> 84 + <h4>{stage.errors.length} Errors</h4> 85 + {#each stage.errors as error} 86 + <p>{error}</p> 87 + {/each} 88 + </div> 89 + <p>The following will be imported:</p> 90 + {:else} 91 + <p>Success, no errors! The following will be imported:</p> 92 + {/if} 93 + <ul> 94 + <li>Playlists: {stage.playlistsCount}</li> 95 + <li>Tracks: {stage.tracksCount}</li> 96 + </ul> 97 + <div class="buttons"> 98 + <Button secondary on:click={cancelHandler}>Cancel</Button> 99 + <Button type="submit">Continue</Button> 100 + </div> 78 101 {/if} 79 102 </main> 80 103 </Modal> 81 104 82 105 <style lang="sass"> 83 106 main 84 - font-size: 0.95rem 85 107 width: 530px 86 108 line-height: 1.5 109 + display: flex 110 + flex-direction: column 111 + p, ul 112 + font-size: 0.95rem 113 + margin-top: 0px 114 + h4 115 + margin-block: 1em 87 116 strong 88 117 font-weight: normal 89 118 background-color: hsl(0, 0%, 100%, 0.1) ··· 95 124 border: 1px solid hsl(0, 100%, 49%) 96 125 border-radius: 5px 97 126 padding: 0px 10px 127 + max-height: 500px 128 + overflow-y: scroll 129 + margin-bottom: 15px 130 + @media screen and (max-height: 800px) 131 + .error-box 132 + max-height: calc(100vh - 280px) 133 + @media screen and (max-height: 400px) 134 + .error-box 135 + max-height: 150px 98 136 .buttons 99 137 display: flex 100 138 justify-content: flex-end
+6
src/components/Modal.svelte
··· 12 12 export let cancelOnEscape = false 13 13 export let form: (() => void) | undefined = undefined 14 14 $: tag = form === undefined ? 'div' : 'form' 15 + export let title: string | null = null 15 16 16 17 $modalCount += 1 17 18 onDestroy(() => { ··· 94 95 tabindex="-1" 95 96 on:keydown|self={boxKeydown} 96 97 > 98 + {#if title !== null} 99 + <h3>{title}</h3> 100 + {/if} 97 101 <slot {focus} /> 98 102 {#if $$slots.buttons} 99 103 <div class="buttons"> ··· 107 111 .backdrop 108 112 background-color: rgba(#000000, 0.5) 109 113 outline: none 114 + h3 115 + margin-bottom: 15px 110 116 .backdrop, .container 111 117 position: fixed 112 118 user-select: text
+8 -8
src/components/PlaylistInfo.svelte
··· 37 37 38 38 <svelte:window on:keydown={keydown} /> 39 39 40 - <Modal onCancel={cancel} let:focus form={save} on:keydown={formKeydown}> 40 + <Modal 41 + onCancel={cancel} 42 + let:focus 43 + form={save} 44 + on:keydown={formKeydown} 45 + title={(info.editMode ? 'Edit' : 'New') + ' Playlist' + (info.isFolder ? ' Folder' : '')} 46 + > 41 47 <main> 42 - <h3> 43 - {info.editMode ? 'Edit' : 'New'} Playlist {#if info.isFolder}Folder{/if} 44 - </h3> 45 - <div class="spacer" /> 46 48 <input type="text" bind:value={info.name} placeholder="Title" use:focus /> 47 49 <textarea 48 50 rows={rows(info.description)} 49 51 bind:value={info.description} 50 52 placeholder="Description" 51 53 /> 52 - <div class="spacer" /> 53 54 </main> 54 55 <svelte:fragment slot="buttons"> 55 56 <Button secondary on:click={cancel}>Cancel</Button> ··· 62 63 display: flex 63 64 flex-direction: column 64 65 background-color: inherit 65 - .spacer 66 - height: 15px 66 + margin-bottom: 15px 67 67 input, textarea 68 68 resize: none 69 69 width: 300px
+1 -3
src/components/Queue.svelte
··· 280 280 align-items: center 281 281 padding: 0px 10px 282 282 box-sizing: border-box 283 - h4.row 284 - font-weight: 600 283 + h4 285 284 padding-left: 18px 286 - margin: 0px 287 285 .drag-line 288 286 position: absolute 289 287 width: 100%
+17 -1
src/components/TrackList.svelte
··· 24 24 import * as dragGhost from './DragGhost.svelte' 25 25 import { showTrackMenu } from '@/lib/menus' 26 26 import type { TrackID } from 'ferrum-addon' 27 + import { modalCount } from './Modal.svelte' 27 28 28 29 export let onTrackInfo: (allIds: TrackID[], index: number) => void 29 30 ··· 219 220 $: if ($softRefreshPage && virtualList) { 220 221 virtualList.refresh() 221 222 } 223 + 224 + function electronDragRegion(el: HTMLElement) { 225 + const unsubscribe = modalCount.subscribe((count) => { 226 + if (count === 0) { 227 + el.style.setProperty('-webkit-app-region', 'drag') 228 + } else { 229 + el.style.setProperty('-webkit-app-region', 'no-drag') 230 + } 231 + }) 232 + return { 233 + destroy() { 234 + unsubscribe() 235 + }, 236 + } 237 + } 222 238 </script> 223 239 224 240 <div ··· 228 244 class:no-selection={$selection.count === 0} 229 245 > 230 246 <div class="header"> 231 - <div class="dragbar" /> 247 + <div class="dragbar" use:electronDragRegion /> 232 248 <h3 class="title"> 233 249 {#if $page.tracklist.id === 'root'} 234 250 Songs
+19 -10
src/lib/data.ts
··· 6 6 export const isMac = window.isMac 7 7 export const isWindows = window.isWindows 8 8 const innerAddon = window.addon 9 + export const ItunesImport = innerAddon.ItunesImport 9 10 10 11 call((addon) => addon.load_data(isDev)) 11 12 ··· 29 30 } 30 31 return '' 31 32 } 33 + function errorPopup(err: unknown) { 34 + ipcRenderer.invoke('showMessageBox', false, { 35 + type: 'error', 36 + message: getErrorMessage(err), 37 + detail: getErrorStack(err), 38 + }) 39 + } 32 40 33 - export function call<T>(cb: (addon: typeof innerAddon) => T): T { 41 + export function call<T, P extends T | Promise<T>>(cb: (addon: typeof innerAddon) => P): P { 34 42 try { 35 - return cb(innerAddon) 43 + const result = cb(innerAddon) 44 + if (result instanceof Promise) { 45 + return result.catch((err) => { 46 + errorPopup(err) 47 + throw err 48 + }) as P 49 + } else { 50 + return result 51 + } 36 52 } catch (err) { 37 - ipcRenderer.invoke('showMessageBox', false, { 38 - type: 'error', 39 - message: getErrorMessage(err), 40 - detail: getErrorStack(err), 41 - }) 53 + errorPopup(err) 42 54 throw err 43 55 } 44 56 } ··· 121 133 call((addon) => addon.move_playlist(id, fromParent, toParent)) 122 134 methods.save() 123 135 trackLists.refreshTrackIdList() 124 - } 125 - export function importItunes(path: string) { 126 - return call((addon) => addon.import_itunes(path, paths.tracksDir)) 127 136 } 128 137 129 138 export const paths = call((addon) => addon.get_paths())