From 41ef6bcbb18057e383986a2e962692ee4bea7b4a Mon Sep 17 00:00:00 2001 From: "alterwain@protonmail.com" Date: Fri, 21 Feb 2025 03:19:38 +0300 Subject: [PATCH] upd --- Cargo.lock | 12 +- Cargo.toml | 2 +- README.md | 6 +- src/file_system.rs | 96 ++++++++-- src/main_screen.rs | 79 ++++++++- src/sync.rs | 426 +++++++++++++++++++++++++++++++++++---------- 6 files changed, 500 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f22ea80..0918bb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -434,7 +434,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1038,8 +1038,8 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "itunesdb" -version = "0.1.57" -source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#ea14aaf6c3284b2cd83f4628b21f652099a79815" +version = "0.1.69" +source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#03263e0d02f45062238f020e2c58faa91a4a59ab" dependencies = [ "bincode", "env_logger", @@ -1614,7 +1614,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1969,7 +1969,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2457,7 +2457,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 32df9fc..601d602 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ futures = "0.3" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7.12", features = ["codec"] } soundcloud = { version = "0.1.8", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" } -itunesdb = { version = "0.1.57", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } +itunesdb = { version = "0.1.69", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } rand = "0.8.5" tui-big-text = "0.7.1" throbber-widgets-tui = "0.8.0" diff --git a/README.md b/README.md index d7033b6..22adc15 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Lightweight iPod manager, batteries included. -# +#
@@ -37,10 +37,10 @@ Lightweight iPod manager, batteries included. ### Manually +## Usage + ## Todos -- Implement import from file system - Implement artwork integration -- Implement addition to fresh ipod (without any data) - Implement youtube api - Implement albums generation \ No newline at end of file diff --git a/src/file_system.rs b/src/file_system.rs index 7a63791..b7f889e 100644 --- a/src/file_system.rs +++ b/src/file_system.rs @@ -16,17 +16,39 @@ use tokio::sync::mpsc::UnboundedSender; pub struct FileSystem { files: Vec, + current_path: PathBuf, table: SmartTable, sender: UnboundedSender, } -fn check_extension_comptability(ext: &OsStr) -> bool { +fn check_extension_compatibility(ext: &OsStr) -> bool { matches!( ext.to_str().unwrap().to_lowercase().as_str(), - "mp3" | "m4a" | "wav" | "aiff" | "aif" | "aac" + "mp3" | "m4a" | "wav" | "aiff" | "aif" ) } +fn list_files_recursively(p: PathBuf) -> Vec { + let mut files = Vec::new(); + + let paths = std::fs::read_dir(p).unwrap(); + + for path in paths { + if path.is_err() { + continue; + } + let a = path.unwrap().path(); + if a.is_file() && check_extension_compatibility(a.extension().unwrap()) { + files.push(a.clone()); + } + if a.is_dir() { + files.append(&mut list_files_recursively(a)); + } + } + + files +} + impl AppScreen for FileSystem { fn handle_key_event(&mut self, key_event: crossterm::event::KeyEvent) { match key_event.code { @@ -37,9 +59,10 @@ impl AppScreen for FileSystem { .sender .send(AppEvent::SwitchScreen(AppState::MainScreen)); } - KeyCode::F(5) => { - self.download_as_is(); - } + KeyCode::F(5) => self.download_as_is(), + KeyCode::F(6) => self.download_as_playlist(), + KeyCode::Tab => self.move_up(), + KeyCode::Enter => self.enter_directory(), _ => {} } } @@ -57,16 +80,14 @@ impl AppScreen for FileSystem { // Render Status Bar let status_bar = Paragraph::new(Line::from(vec![ + " MOVE UP".bold(), + " | ".dark_gray(), " SWITCH TO NORMAL".bold(), " | ".dark_gray(), " SAVE AS IS".bold(), " | ".dark_gray(), " SAVE AS PLAYLIST".bold(), " | ".dark_gray(), - " SELECT".bold(), - " | ".dark_gray(), - " DESELECT".bold(), - " | ".dark_gray(), " QUIT".bold(), ])) .centered(); @@ -97,23 +118,32 @@ impl FileSystem { table, sender, files: Vec::new(), + current_path: dirs::document_dir().unwrap(), }; a.get_path(dirs::document_dir().unwrap()); a } fn get_path(&mut self, p: PathBuf) { + self.current_path = p.clone(); let paths = std::fs::read_dir(&p).unwrap(); let mut dir = paths .filter_map(|res| res.ok()) .filter(|p| { p.path() .extension() - .map_or(false, check_extension_comptability) + .map_or(false, check_extension_compatibility) || p.path().is_dir() }) .collect::>(); - dir.sort_by(|a, _b| { + dir.sort_by(|a, b| { + if a.file_type().unwrap().is_dir() == b.file_type().unwrap().is_dir() { + let af = a.file_name(); + let bf = b.file_name(); + let ac = af.to_str().unwrap_or("a"); + let bc = bf.to_str().unwrap_or("a"); + return ac.cmp(bc); + } if a.file_type().unwrap().is_dir() { Ordering::Less } else { @@ -127,7 +157,12 @@ impl FileSystem { let datetime: DateTime = entry.metadata().unwrap().modified().unwrap().into(); let datetime = datetime.format("%d/%m/%Y %T").to_string(); let size = entry.metadata().unwrap().size().to_string(); - let file_type = entry.file_type().unwrap().is_file().to_string(); + let file_type = if entry.file_type().unwrap().is_file() { + "FILE" + } else { + "DIR" + } + .to_string(); vec![ entry.file_name().to_str().unwrap().to_string(), file_type, @@ -147,12 +182,47 @@ impl FileSystem { fn download_as_is(&self) { let entry = self.files.get(self.table.selected_row()).unwrap(); if entry.path().is_dir() { - todo!("Implement that later"); + let files = list_files_recursively(entry.path()); + let _ = self.sender.send(AppEvent::LoadFromFSVec(files)); } else { let _ = self.sender.send(AppEvent::LoadFromFS(entry.path())); } } + fn download_as_playlist(&self) { + let entry = self.files.get(self.table.selected_row()).unwrap(); + if entry.path().is_dir() { + let files = list_files_recursively(entry.path()); + let _ = self.sender.send(AppEvent::LoadFromFSPL(( + files, + entry + .path() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(), + ))); + } + } + + fn move_up(&mut self) { + let p = self.current_path.parent(); + if p.is_none() { + return; + } + let p: PathBuf = p.unwrap().to_path_buf(); + self.get_path(p); + } + + fn enter_directory(&mut self) { + let entry = self.files.get(self.table.selected_row()).unwrap(); + if !entry.path().is_dir() { + return; + } + self.get_path(entry.path()); + } + fn render_main(&self, frame: &mut Frame, area: Rect) { self.table.render(frame, area); } diff --git a/src/main_screen.rs b/src/main_screen.rs index d539c17..9abc567 100644 --- a/src/main_screen.rs +++ b/src/main_screen.rs @@ -33,6 +33,8 @@ impl AppScreen for MainScreen { KeyCode::Up => self.previous_row(), KeyCode::Down => self.next_row(), KeyCode::F(5) => self.download_row(), + KeyCode::F(8) => self.remove_row(), + KeyCode::F(9) => self.remove_completely(), KeyCode::Tab => self.switch_mode(), KeyCode::F(4) => { let _ = self @@ -74,8 +76,6 @@ impl AppScreen for MainScreen { // Render Status Bar let status_bar = Paragraph::new(Line::from(vec![ - "◄ ► to change tab".bold(), - " | ".dark_gray(), " SWITCH PANEL".bold(), " | ".dark_gray(), " FS MODE".bold(), @@ -84,6 +84,8 @@ impl AppScreen for MainScreen { " | ".dark_gray(), " DEL".bold(), " | ".dark_gray(), + " DEL REC".bold(), + " | ".dark_gray(), " QUIT".bold(), ])) .centered(); @@ -157,6 +159,75 @@ impl MainScreen { } } + fn remove_row(&mut self) { + if self.selected_tab != 2 { + return; + } + let pl_id = self + .playlists + .as_ref() + .unwrap() + .get(self.pl_table.selected_row()) + .unwrap() + .id; + match self.mode { + false => { + let _ = self.sender.send(AppEvent::RemovePlaylist((pl_id, false))); + } + true => { + let track_id = self + .playlists + .as_ref() + .unwrap() + .get(self.pl_table.selected_row()) + .unwrap() + .tracks + .get(self.song_table.selected_row()) + .unwrap() + .data + .unique_id; + + let _ = self + .sender + .send(AppEvent::RemoveTrackFromPlaylist((track_id, pl_id))); + } + } + } + + fn remove_completely(&mut self) { + if self.selected_tab != 2 { + return; + } + match self.mode { + false => { + let pl_id = self + .playlists + .as_ref() + .unwrap() + .get(self.pl_table.selected_row()) + .unwrap() + .id; + + let _ = self.sender.send(AppEvent::RemovePlaylist((pl_id, true))); + } + true => { + let track = self + .playlists + .as_ref() + .unwrap() + .get(self.pl_table.selected_row()) + .unwrap() + .tracks + .get(self.song_table.selected_row()) + .unwrap() + .clone(); + let _ = self + .sender + .send(AppEvent::RemoveTrack(track.data.unique_id)); + } + } + } + fn download_row(&mut self) { if self.selected_tab == 1 { // SC @@ -246,7 +317,7 @@ impl MainScreen { let date = Utc.timestamp_millis_opt(playlist.timestamp as i64).unwrap(); vec![ playlist.id.to_string(), - "".to_string(), + playlist.title.clone(), playlist.tracks.len().to_string(), format!("{}", date.format("%Y-%m-%d %H:%M")), "YES".to_string(), @@ -330,7 +401,7 @@ impl MainScreen { vec![ track.data.unique_id.to_string(), track.get_title(), - track.get_location(), + track.get_artist(), track.data.bitrate.to_string(), track.get_genre(), ] diff --git a/src/sync.rs b/src/sync.rs index afa0a6a..26ba79c 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,9 +1,9 @@ use audiotags::Tag; +use color_eyre::owo_colors::OwoColorize; use itunesdb::objects::{ListSortOrder, PlaylistItem}; use itunesdb::serializer; use itunesdb::xobjects::{XDatabase, XPlArgument, XPlaylist, XTrackItem}; use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists, CloudTrack}; -use std::error::Error; use std::io::Write; use std::path::{Path, PathBuf}; use tokio::{ @@ -32,6 +32,11 @@ pub enum AppEvent { OverallProgress((u32, u32)), SwitchScreen(AppState), LoadFromFS(PathBuf), + LoadFromFSVec(Vec), + LoadFromFSPL((Vec, String)), + RemoveTrack(u32), + RemovePlaylist((u64, bool)), + RemoveTrackFromPlaylist((u32, u64)), } pub struct DBPlaylist { @@ -45,23 +50,24 @@ async fn track_from_soundcloud(value: &CloudTrack) -> Option { let mut track_path = get_temp_dl_dir(); track_path.push(value.id.to_string()); track_path.set_extension("mp3"); - let f = std::fs::File::open(&track_path).unwrap(); - let mut data = &std::fs::read(&track_path).unwrap()[..]; - let audio_info = audio_file_info::from_path(track_path.to_str().unwrap()) + let audio_file = audio_file_info::from_path(track_path.to_str().unwrap()) .await .unwrap(); - let audio_info = audio_info.audio_file.tracks.get(0).unwrap(); + let audio_info = &audio_file.audio_file.tracks.track; let mut track = XTrackItem::new( value.id as u32, - f.metadata().unwrap().len() as u32, + audio_info.audio_bytes as u32, (audio_info.duration * 1000.0) as u32, 0, - audio_info.bit_rate as u32, + (audio_info.bit_rate / 1000) as u32, audio_info.sample_rate as u32, hash(), 0, ); + + audio_file.modify_xtrack(&mut track); + track.set_title(value.title.clone().unwrap()); track.set_artist( value @@ -70,7 +76,6 @@ async fn track_from_soundcloud(value: &CloudTrack) -> Option { .map_or(String::new(), |a| a.username.unwrap_or(a.permalink)), ); track.set_genre(value.genre.clone().unwrap()); - track.update_arg(6, String::from("MPEG audio file")); Some(track) } @@ -79,12 +84,42 @@ fn hash() -> u64 { rand::random::() } -fn overwrite_database(database: &mut XDatabase, ipod_path: &String) { - let data = serializer::to_bytes(database); - let mut p: PathBuf = Path::new(ipod_path).into(); +fn get_track_location(unique_id: u32, extension: &str) -> String { + let mut tp = PathBuf::new(); + tp.push(":iPod_Control"); + tp.push("Music"); + tp.push(["F", &format!("{:02}", &(unique_id % 100))].concat()); + tp.push(format!("{:X}", unique_id)); + tp.set_extension(extension); + tp.to_str() + .unwrap() + .to_string() + .replace("/", ":") + .to_string() +} + +fn get_full_track_location(p: PathBuf, unique_id: u32, extension: &str) -> PathBuf { + let mut dest = p.clone(); + dest.push("iPod_Control"); + dest.push("Music"); + dest.push(["F", &format!("{:02}", &(unique_id % 100))].concat()); + let _ = std::fs::create_dir_all(dest.to_str().unwrap()); + dest.push(format!("{:X}", unique_id)); + dest.set_extension(extension); + dest +} + +fn get_itunesdb_location(path: &str) -> PathBuf { + let mut p: PathBuf = Path::new(path).into(); p.push("iPod_Control"); p.push("iTunes"); p.push("iTunesDB"); + p +} + +fn overwrite_database(database: &mut XDatabase, ipod_path: &String) { + let data = serializer::to_bytes(database); + let p: PathBuf = get_itunesdb_location(ipod_path); let mut file = std::fs::File::create(p).unwrap(); let _ = file.write(&data); } @@ -121,7 +156,12 @@ pub fn initialize_async_service( AppEvent::DownloadPlaylist(playlist) => { download_playlist(playlist, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await; }, AppEvent::DownloadTrack(track) => { download_track(track, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await; }, AppEvent::SwitchScreen(state) => { let _ = sender.send(AppEvent::SwitchScreen(state)).await;}, - AppEvent::LoadFromFS(path) => load_from_fs(path, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()), + AppEvent::LoadFromFS(path) => { load_from_fs(path, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await; }, + AppEvent::LoadFromFSVec(files) => load_files_from_fs(files, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await, + AppEvent::LoadFromFSPL((files, title)) => load_files_from_fs_as_playlist(files, title, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await, + AppEvent::RemoveTrack(id) => remove_track(id, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await, + AppEvent::RemovePlaylist((pl_id, is_hard)) => remove_playlist(pl_id, is_hard, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await, + AppEvent::RemoveTrackFromPlaylist((track_id, pl_id)) => remove_track_from_playlist(track_id, pl_id, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await, _ => {} } } @@ -131,49 +171,220 @@ pub fn initialize_async_service( }); } -fn load_from_fs( - path: PathBuf, +async fn remove_track_from_playlist( + track_id: u32, + pl_id: u64, database: &mut XDatabase, sender: &Sender, ipod_path: String, ) { - let mut tag = Tag::new().read_from_path(path).unwrap(); + let _ = sender.send(AppEvent::OverallProgress((0, 1))).await; + + database.remove_track_from_playlist(track_id, pl_id); + + let _ = sender.send(AppEvent::OverallProgress((1, 1))).await; + + let _ = sender + .send(AppEvent::SwitchScreen(AppState::MainScreen)) + .await; + + let _ = sender + .send(AppEvent::ITunesParsed(get_playlists(database))) + .await; + + overwrite_database(database, &ipod_path); +} + +async fn remove_playlist( + pl_id: u64, + is_hard: bool, + database: &mut XDatabase, + sender: &Sender, + ipod_path: String, +) { + if is_hard { + let pls = database.get_playlists(); + let pl = pls.iter().find(|p| p.data.persistent_playlist_id == pl_id); + if pl.is_none() { + return; + } + let pl = pl.unwrap(); + let max = pl.elems.len(); + let mut i = 1; + for (item, args) in pl.elems.iter() { + let _ = sender + .send(AppEvent::OverallProgress((i, max as u32))) + .await; + remove_track(item.track_id, database, sender, ipod_path.clone()).await; + i += 1; + } + } + + let _ = sender.send(AppEvent::OverallProgress((0, 1))).await; + + database.remove_playlist(pl_id); + + let _ = sender.send(AppEvent::OverallProgress((1, 1))).await; + + let _ = sender + .send(AppEvent::SwitchScreen(AppState::MainScreen)) + .await; + + let _ = sender + .send(AppEvent::ITunesParsed(get_playlists(database))) + .await; + + overwrite_database(database, &ipod_path); +} + +async fn remove_track( + id: u32, + database: &mut XDatabase, + sender: &Sender, + ipod_path: String, +) { + let _ = sender.send(AppEvent::OverallProgress((0, 1))).await; + database.remove_track_completely(id); + for ext in ["mp3", "m4a", "wav", "aif"].iter() { + let dest = get_full_track_location(PathBuf::from(ipod_path.clone()), id, ext); + let _ = std::fs::remove_file(dest); + } + + let _ = sender.send(AppEvent::OverallProgress((1, 1))).await; + + let _ = sender + .send(AppEvent::SwitchScreen(AppState::MainScreen)) + .await; + + let _ = sender + .send(AppEvent::ITunesParsed(get_playlists(database))) + .await; + + overwrite_database(database, &ipod_path); +} + +async fn load_files_from_fs_as_playlist( + files: Vec, + title: String, + database: &mut XDatabase, + sender: &Sender, + ipod_path: String, +) { + let mut new_playlist = XPlaylist::new(rand::random(), ListSortOrder::SongTitle); + + new_playlist.set_title(title); + + for (i, file) in files.iter().enumerate() { + let _ = sender + .send(AppEvent::OverallProgress((i as u32, files.len() as u32))) + .await; + let id = load_from_fs(file.clone(), database, sender, ipod_path.clone()).await; + + new_playlist.add_elem(id); + } + + database.add_playlist(new_playlist); + + let _ = sender + .send(AppEvent::SwitchScreen(AppState::MainScreen)) + .await; + + let _ = sender + .send(AppEvent::ITunesParsed(get_playlists(database))) + .await; + + overwrite_database(database, &ipod_path); +} + +async fn load_files_from_fs( + files: Vec, + database: &mut XDatabase, + sender: &Sender, + ipod_path: String, +) { + for (i, file) in files.iter().enumerate() { + let _ = sender + .send(AppEvent::OverallProgress((i as u32, files.len() as u32))) + .await; + load_from_fs(file.clone(), database, sender, ipod_path.clone()).await; + } +} + +async fn load_from_fs( + path: PathBuf, + database: &mut XDatabase, + sender: &Sender, + ipod_path: String, +) -> u32 { + let tag = Tag::new().read_from_path(&path).unwrap(); let id = database.get_unique_id(); + let audio_file = audio_file_info::from_path(path.to_str().unwrap()) + .await + .unwrap(); + let audio_info = &audio_file.audio_file.tracks.track; + let mut track = XTrackItem::new( id, - 0, - (tag.duration().unwrap() / 1000.0) as u32, - tag.year().unwrap() as u32, - 0, - 0, + audio_info.audio_bytes as u32, + (audio_info.duration * 1000.0) as u32, + tag.year().unwrap_or(0) as u32, + (audio_info.bit_rate / 1000) as u32, + audio_info.sample_rate as u32, hash(), 0, ); - // TODO: implement check for every property - track.set_title(tag.title().unwrap().to_string()); - let mut tp = PathBuf::new(); - tp.push(":iPod_Control"); - tp.push("Music"); - tp.push(["F", &format!("{:02}", &(track.data.unique_id % 100))].concat()); - tp.push(format!("{:X}", track.data.unique_id)); - tp.set_extension("mp3"); - track.set_location( - tp.to_str() - .unwrap() - .to_string() - .replace("/", ":") - .to_string(), + audio_file.modify_xtrack(&mut track); + + if let Some(title) = tag.title() { + track.set_title(title.to_string()); + } else { + track.set_title(path.file_name().unwrap().to_str().unwrap().to_string()); + } + + if let Some(genre) = tag.genre() { + track.set_genre(genre.to_string()); + } + + if let Some(artist) = tag.artist() { + track.set_artist(artist.to_string()); + } + + if let Some(album) = tag.album() { + track.set_album(album.title.to_string()); + // TODO: Add new album into iTunesDB + } + + track.set_location(get_track_location( + track.data.unique_id, + audio_file.get_audio_extension(), + )); + + //let cover = tag.album().unwrap().cover.unwrap(); + + let dest = get_full_track_location( + PathBuf::from(ipod_path.clone()), + track.data.unique_id, + audio_file.get_audio_extension(), ); - //track.update_arg(6); - track.set_genre(tag.genre().unwrap().to_string()); - track.set_artist(tag.artist().unwrap().to_string()); - track.set_album(tag.album().unwrap().title.to_string()); + let _ = std::fs::copy(path.to_str().unwrap(), dest.to_str().unwrap()); - let cover = tag.album().unwrap().cover.unwrap(); + database.add_track(track); + + let _ = sender + .send(AppEvent::SwitchScreen(AppState::MainScreen)) + .await; + + let _ = sender + .send(AppEvent::ITunesParsed(get_playlists(database))) + .await; + + overwrite_database(database, &ipod_path); + + id } async fn download_track( @@ -193,26 +404,8 @@ async fn download_track( if let Some(mut t) = track_from_soundcloud(&track).await { t.data.unique_id = database.get_unique_id(); - let mut tp = PathBuf::new(); - tp.push(":iPod_Control"); - tp.push("Music"); - tp.push(["F", &format!("{:02}", &(t.data.unique_id % 100))].concat()); - tp.push(format!("{:X}", t.data.unique_id)); - tp.set_extension("mp3"); - t.set_location( - tp.to_str() - .unwrap() - .to_string() - .replace("/", ":") - .to_string(), - ); - let mut dest = p.clone(); - dest.push("iPod_Control"); - dest.push("Music"); - dest.push(["F", &format!("{:02}", &(t.data.unique_id % 100))].concat()); - let _ = std::fs::create_dir_all(dest.to_str().unwrap()); - dest.push(format!("{:X}", t.data.unique_id)); - dest.set_extension("mp3"); + t.set_location(get_track_location(t.data.unique_id, "mp3")); + let dest = get_full_track_location(p.clone(), t.data.unique_id, "mp3"); let mut track_path = get_temp_dl_dir(); track_path.push(track.id.to_string()); @@ -260,27 +453,8 @@ async fn download_playlist( if let Some(mut t) = track_from_soundcloud(&track).await { t.data.unique_id = database.get_unique_id(); new_playlist.add_elem(t.data.unique_id); - let mut tp = PathBuf::new(); - tp.push(":iPod_Control"); - tp.push("Music"); - tp.push(["F", &format!("{:02}", &(t.data.unique_id % 100))].concat()); - tp.push(format!("{:X}", t.data.unique_id)); - tp.set_extension("mp3"); - t.set_location( - tp.to_str() - .unwrap() - .to_string() - .replace("/", ":") - .to_string(), - ); - let mut dest = p.clone(); - dest.push("iPod_Control"); - dest.push("Music"); - dest.push(["F", &format!("{:02}", &(t.data.unique_id % 100))].concat()); - let _ = std::fs::create_dir_all(dest.to_str().unwrap()); - dest.push(format!("{:X}", t.data.unique_id)); - dest.set_extension("mp3"); - + t.set_location(get_track_location(t.data.unique_id, "mp3")); + let dest = get_full_track_location(p.clone(), t.data.unique_id, "mp3"); let mut track_path = get_temp_dl_dir(); track_path.push(track.id.to_string()); track_path.set_extension("mp3"); @@ -329,10 +503,7 @@ fn to_tracks(db: &mut XDatabase, elems: Vec<(PlaylistItem, Vec)>) - async fn parse_itunes(sender: &Sender, path: String) -> XDatabase { let cd = get_temp_itunesdb(); - let mut p: PathBuf = Path::new(&path).into(); - p.push("iPod_Control"); - p.push("iTunes"); - p.push("iTunesDB"); + let p = get_itunesdb_location(&path); let _ = std::fs::copy(p, &cd); let mut file = File::open(cd).await.unwrap(); let mut contents = vec![]; @@ -389,24 +560,84 @@ async fn parse_itunes(sender: &Sender, path: String) -> XDatabase { } mod audio_file_info { - use serde::{Deserialize, Serialize}; + use itunesdb::xobjects::XTrackItem; + use serde::Deserialize; use std::process::Stdio; use tokio::io::{AsyncReadExt, BufReader}; use tokio::process::Command; - #[derive(Debug, Serialize, Deserialize, PartialEq)] + #[derive(Debug, Deserialize, PartialEq)] pub struct AudioInfo { pub audio_file: AudioFileInfo, } - #[derive(Debug, Serialize, Deserialize, PartialEq)] + #[derive(Debug, Deserialize, PartialEq)] pub struct AudioFileInfo { pub file_name: String, pub file_type: String, - pub tracks: Vec, + pub tracks: AudioFileTracks, } - #[derive(Debug, Serialize, Deserialize, PartialEq)] + impl AudioInfo { + pub fn get_audio_extension(&self) -> &str { + match self.audio_file.file_type.as_str() { + "'WAVE'" => "wav", + "'AIFF'" => "aif", + "'m4af'" => "m4a", + _ => "mp3", + } + } + + fn get_audio_codec(&self) -> String { + match self.audio_file.file_type.as_str() { + "'WAVE'" => "WAV audio file", + "'AIFF'" => "AIFF audio file", + "'m4af'" => match self.audio_file.tracks.track.format_type.as_str() { + "alac" => "Apple Lossless audio file", + _ => "AAC audio file", + }, + _ => "MPEG audio file", + } + .to_string() + } + + pub fn modify_xtrack(&self, track: &mut XTrackItem) { + track.data.type1 = 0; + track.data.type2 = if self.audio_file.file_type == "'MPG3'" { + 1 + } else { + 0 + }; + + let bytes = match self.audio_file.file_type.as_str() { + "'WAVE'" => "WAV", + "'AIFF'" => "AIF", + "'m4af'" => match self.audio_file.tracks.track.format_type.as_str() { + "alac" => "M4A ", + _ => "M4A", + }, + _ => "MP3", + } + .as_bytes(); + + let file_type = u32::from_le_bytes(if bytes.len() == 4 { + [bytes[0], bytes[1], bytes[2], bytes[3]] + } else { + [bytes[0], bytes[1], bytes[2], 0u8] + }); + + track.data.filetype = file_type; + + track.update_arg(6, self.get_audio_codec()); + } + } + + #[derive(Deserialize, Debug, PartialEq)] + pub struct AudioFileTracks { + pub track: AudioFileTrack, + } + + #[derive(Debug, Deserialize, PartialEq)] pub struct AudioFileTrack { pub num_channels: u32, pub sample_rate: u64, @@ -418,18 +649,25 @@ mod audio_file_info { pub async fn from_path(p: &str) -> Option { let mut command = Command::new("afinfo"); - command.args(vec!["-x", p]); + command.arg("-x"); + command.arg(p); command.stdout(Stdio::piped()); command.stderr(Stdio::null()); let mut child = command.spawn().unwrap(); - let mut str = String::new(); + let mut vec = Vec::new(); let stdout = child.stdout.take().unwrap(); let size = BufReader::new(stdout) - .read_to_string(&mut str) + .read_to_end(&mut vec) .await - .unwrap(); - Some(serde_xml_rs::from_str(&str).unwrap()) + .unwrap_or(0); + if size == 0 { + return None; + } + + /*let mut f = File::create("afinfo_out.xml").unwrap(); + let _ = f.write(str.as_bytes());*/ + Some(serde_xml_rs::from_str(String::from_utf8_lossy(vec.as_slice()).as_ref()).unwrap()) } }