diff --git a/Cargo.lock b/Cargo.lock index 57586e8..daf4a50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,15 +112,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614aa3f2bac03707e62a84d18a48dd3d9ea6171313fd5e6a53b5054d8ae74601" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "bumpalo" version = "3.17.0" @@ -266,16 +257,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "darling" version = "0.20.10" @@ -320,16 +301,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "dirs" version = "6.0.0" @@ -547,16 +518,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -975,8 +936,8 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "itunesdb" -version = "0.1.11" -source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#4204fdcda886438d815da7b70f736506e06a22e3" +version = "0.1.19" +source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#e1e8d0a12ca2c3825191ff1d815645a704e1b646" dependencies = [ "bincode", "env_logger", @@ -1071,15 +1032,15 @@ dependencies = [ name = "lyrica" version = "0.1.0" dependencies = [ - "bincode", "chrono", "color-eyre", "crossterm", "dirs", "futures", "itunesdb", - "md-5", + "mp3-duration", "puremp3", + "rand", "ratatui", "regex", "rusb", @@ -1091,16 +1052,6 @@ dependencies = [ "toml", ] -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - [[package]] name = "memchr" version = "2.7.4" @@ -1134,6 +1085,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mp3-duration" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348bdc7300502f0801e5b57c448815713cd843b744ef9bda252a2698fdf90a0f" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "native-tls" version = "0.2.13" @@ -1411,7 +1371,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 2.0.11", ] [[package]] @@ -1883,13 +1843,33 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.11", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2114,12 +2094,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - [[package]] name = "unicode-ident" version = "1.0.16" @@ -2196,12 +2170,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 1eb7def..0ed3241 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ dirs = "6.0.0" toml = "0.8.20" serde = "1.0.217" serde_json = "1.0" -bincode = "1.3.3" regex = "1.11.1" ratatui = { version = "0.29.0", features = ["all-widgets"] } color-eyre = "0.6.3" @@ -21,6 +20,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.11", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } -md-5 = "0.10.6" +itunesdb = { version = "0.1.19", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } puremp3 = "0.1.0" +mp3-duration = "0.1.10" +rand = "0.8.5" \ No newline at end of file diff --git a/src/file_system.rs b/src/file_system.rs index 9a1c7ab..d363c4b 100644 --- a/src/file_system.rs +++ b/src/file_system.rs @@ -1,15 +1,71 @@ use crate::{screen::AppScreen, theme::Theme}; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::prelude::{Color, Line, Style, Stylize}; +use ratatui::widgets::{Block, Borders, Paragraph, Row, Table}; +use ratatui::Frame; +#[derive(Default)] pub struct FileSystem {} impl AppScreen for FileSystem { fn handle_key_event(&mut self, key_event: crossterm::event::KeyEvent) {} fn render(&self, frame: &mut ratatui::Frame, theme: &Theme) { - todo!() + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Min(0), // Main content area + Constraint::Length(1), // Status bar + ]) + .split(frame.area()); + + self.render_main(frame, chunks[0]); + + // Render Status Bar + let status_bar = Paragraph::new(Line::from(vec![ + " SAVE AS PLAYLIST".bold(), + " | ".dark_gray(), + " SAVE AS IS".bold(), + " | ".dark_gray(), + " SELECT".bold(), + " | ".dark_gray(), + " DESELECT".bold(), + " | ".dark_gray(), + " QUIT".bold(), + ])) + .centered(); + frame.render_widget(status_bar, chunks[1]); // Render into third chunk } fn as_any(&mut self) -> &mut dyn std::any::Any { - todo!() + self + } +} + +impl FileSystem { + fn render_main(&self, frame: &mut Frame, area: Rect) { + let mut v = vec![Row::new(vec!["Name", "Type", "Size", "Modified"]) + .style(Style::default().fg(Color::Gray))]; + + // move this out to make hdd not suffer + let paths = std::fs::read_dir("~/Documents").unwrap(); + + for path in paths { + v.push(); + } + + let table = Table::new( + v, + &[ + Constraint::Percentage(50), + Constraint::Length(5), + Constraint::Percentage(20), + Constraint::Percentage(30), + ], + ) + .block(Block::default().borders(Borders::ALL).title(" Documents ")) + .style(Style::default().fg(Color::Black)); + + frame.render_widget(table, area); } } diff --git a/src/main.rs b/src/main.rs index 1d0c1af..bebbb5b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ -use std::{collections::HashMap, error::Error, io}; - +use crate::file_system::FileSystem; use crate::theme::Theme; use color_eyre::Result; use crossterm::{ @@ -19,6 +18,7 @@ use ratatui::{ Frame, Terminal, }; use screen::AppScreen; +use std::{collections::HashMap, error::Error, io}; use sync::AppEvent; use tokio::sync::mpsc::{self, Receiver, UnboundedSender}; use tokio_util::sync::CancellationToken; @@ -66,6 +66,7 @@ impl Default for App { screens.insert(AppState::IPodWait, Box::new(WaitScreen::default())); screens.insert(AppState::MainScreen, Box::new(MainScreen::new(jx.clone()))); screens.insert(AppState::LoadingScreen, Box::new(LoadingScreen::default())); + screens.insert(AppState::FileSystem, Box::new(FileSystem::default())); Self { receiver: rx, diff --git a/src/main_screen.rs b/src/main_screen.rs index e7bac45..11f384d 100644 --- a/src/main_screen.rs +++ b/src/main_screen.rs @@ -1,6 +1,5 @@ use chrono::{DateTime, TimeZone, Utc}; use crossterm::event::{KeyCode, KeyEvent}; -use itunesdb::xobjects::XPlaylist; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style, Stylize}, @@ -11,8 +10,8 @@ use ratatui::{ use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists}; use tokio::sync::mpsc::UnboundedSender; -use crate::{screen::AppScreen, sync::AppEvent, theme::Theme}; use crate::sync::DBPlaylist; +use crate::{screen::AppScreen, sync::AppEvent, theme::Theme, AppState}; pub struct MainScreen { mode: bool, @@ -36,6 +35,10 @@ impl AppScreen for MainScreen { KeyCode::Down => self.next_row(), KeyCode::F(6) => self.download_row(), KeyCode::Tab => self.switch_mode(), + KeyCode::F(2) => { + self.sender + .send(AppEvent::SwitchScreen(AppState::FileSystem)); + } _ => {} } } @@ -82,7 +85,7 @@ impl AppScreen for MainScreen { " QUIT".bold(), ])) .centered(); - frame.render_widget(status_bar, chunks[2]); // Render into third chunk + frame.render_widget(status_bar, chunks[2]); } fn as_any(&mut self) -> &mut dyn std::any::Any { @@ -203,7 +206,7 @@ impl MainScreen { self.update_max_rows(); } } - + pub fn set_itunes(&mut self, pl: Vec) { self.playlists = Some(pl); if self.selected_tab == 2 { @@ -247,11 +250,11 @@ impl MainScreen { ); if let Some(s) = &self.playlists { for (i, playlist) in s.iter().enumerate() { - let date = Utc.timestamp_millis_opt(playlist.data.timestamp as i64).unwrap(); + let date = Utc.timestamp_millis_opt(playlist.timestamp as i64).unwrap(); let mut row = Row::new(vec![ - playlist.data.persistent_playlist_id.to_string(), + playlist.id.to_string(), "".to_string(), - playlist.elems.len().to_string(), + playlist.tracks.len().to_string(), format!("{}", date.format("%Y-%m-%d %H:%M")), "YES".to_string(), ]); @@ -320,7 +323,7 @@ impl MainScreen { } } v - }, + } 2 => { // local let mut v = Vec::new(); @@ -329,14 +332,14 @@ impl MainScreen { .style(Style::default().fg(Color::Gray)), ); if let Some(pls) = &self.playlists { - let s = &pls.get(self.selected_playlist as usize).unwrap().elems; + let s = &pls.get(self.selected_playlist as usize).unwrap().tracks; for (i, track) in s.iter().enumerate() { let mut row = Row::new(vec![ - track.unique_id.to_string(), - track.title.clone(), - track.location.clone(), - track.bitrate.to_string(), - track.genre.clone(), + track.data.unique_id.to_string(), + track.get_title(), + track.get_location(), + track.data.bitrate.to_string(), + track.get_genre(), ]); if self.selected_song == i as i32 { row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White)); diff --git a/src/sync.rs b/src/sync.rs index d1dab5e..f26dfd0 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,7 +1,9 @@ +use itunesdb::objects::{ListSortOrder, PlaylistItem}; +use itunesdb::serializer; +use itunesdb::xobjects::{XDatabase, XPlArgument, XPlaylist, XTrackItem}; +use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists, CloudTrack}; +use std::io::Write; use std::path::{Path, PathBuf}; - -use itunesdb::xobjects::{XDatabase, XTrackItem}; -use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists}; use tokio::{ fs::File, io::{AsyncReadExt, AsyncWriteExt}, @@ -9,9 +11,13 @@ use tokio::{ }; use tokio_util::sync::CancellationToken; -use crate::{config::{ - get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration, -}, dlp::{self, DownloadProgress}, util, AppState}; +use crate::{ + config::{ + get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration, + }, + dlp::{self, DownloadProgress}, + util, AppState, +}; pub enum AppEvent { SearchIPod, @@ -28,7 +34,54 @@ pub struct DBPlaylist { pub id: u64, pub title: String, pub timestamp: u32, - pub tracks: Vec + pub tracks: Vec, +} + +fn track_from_soundcloud(value: &CloudTrack) -> XTrackItem { + 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 (header, _samples) = puremp3::read_mp3(data).unwrap(); + + let duration = mp3_duration::from_read(&mut data).unwrap(); + + let mut track = XTrackItem::new( + value.id as u32, + f.metadata().unwrap().len() as u32, + duration.as_millis() as u32, + 0, + header.bitrate.bps() / 1000, + header.sample_rate.hz(), + hash(), + 0, + ); + track.set_title(value.title.clone().unwrap()); + track.set_artist( + value + .user + .clone() + .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")); + track +} + +// note: this hash function is used to make unique ids for each track. It doesn't aim to generate secure ones. +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(); + p.push("iPod_Control"); + p.push("iTunes"); + p.push("iTunesDB"); + let mut file = std::fs::File::create(p).unwrap(); + let _ = file.write(&data); } pub fn initialize_async_service( @@ -60,7 +113,8 @@ pub fn initialize_async_service( let _ = sender.send(AppEvent::IPodNotFound).await; } }, - AppEvent::DownloadPlaylist(playlist) => download_playlist(playlist, &mut database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await, + AppEvent::DownloadPlaylist(playlist) => download_playlist(playlist, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await, + AppEvent::SwitchScreen(state) => { let _ = sender.send(AppEvent::SwitchScreen(state)).await;}, _ => {} } } @@ -74,7 +128,7 @@ async fn download_playlist( playlist: CloudPlaylist, database: &mut XDatabase, sender: &Sender, - ipod_path: String + ipod_path: String, ) { if let Ok(()) = dlp::download_from_soundcloud(&playlist.permalink_url, &get_temp_dl_dir(), sender.clone()) @@ -82,35 +136,61 @@ async fn download_playlist( { let tracks = playlist.tracks; - let mut p: PathBuf = Path::new(&ipod_path).into(); + let p: PathBuf = Path::new(&ipod_path).into(); + + let mut new_playlist = XPlaylist::new(rand::random(), ListSortOrder::SongTitle); + + new_playlist.set_title(playlist.title); + for track in tracks { if track.title.is_none() { continue; } - let mut t: XTrackItem = track.into(); + let mut t: XTrackItem = track_from_soundcloud(&track); 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(":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()); + t.set_location( + tp.to_str() + .unwrap() + .to_string() + .replace("/", ":") + .to_string(), + ); let mut dest = p.clone(); - dest.push(tp); + 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"); let mut track_path = get_temp_dl_dir(); track_path.push(track.id.to_string()); track_path.set_extension("mp3"); - let _ = std::fs::copy(track_path, dest); + let _ = std::fs::copy(track_path.to_str().unwrap(), dest.to_str().unwrap()); - let _ = database.add_track(t); + database.add_track(t); } + + 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); } fn get_playlists(db: &mut XDatabase) -> Vec { @@ -120,7 +200,19 @@ fn get_playlists(db: &mut XDatabase) -> Vec { id: t.data.persistent_playlist_id, title: t.get_title(), timestamp: t.data.timestamp, - tracks: t.elems.iter().map(|(i, _a)| db.get_track(i.track_id)).filter(|t| t.is_some()).map(|t| t.unwrap().clone()).collect()}).collect() + tracks: to_tracks(db, t.elems.clone()), + }) + .collect() +} + +fn to_tracks(db: &mut XDatabase, elems: Vec<(PlaylistItem, Vec)>) -> Vec { + elems + .iter() + .map(|(i, _a)| i.track_id) + .map(|id| db.get_track(id)) + .filter(|i| i.is_some()) + .map(|i| i.unwrap().clone()) + .collect() } async fn parse_itunes(sender: &Sender, path: String) -> XDatabase { @@ -129,7 +221,6 @@ async fn parse_itunes(sender: &Sender, path: String) -> XDatabase { p.push("iPod_Control"); p.push("iTunes"); p.push("iTunesDB"); - println!("{}", p.to_str().unwrap()); let _ = std::fs::copy(p, &cd); let mut file = File::open(cd).await.unwrap(); let mut contents = vec![]; @@ -137,9 +228,7 @@ async fn parse_itunes(sender: &Sender, path: String) -> XDatabase { let mut database = itunesdb::deserializer::parse_bytes(&contents); let _ = sender - .send(AppEvent::ITunesParsed( - get_playlists(&mut database), - )) + .send(AppEvent::ITunesParsed(get_playlists(&mut database))) .await; let p = get_config_path();