diff --git a/Cargo.lock b/Cargo.lock index 00d0e97..a4a6a07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -139,6 +154,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "color-eyre" version = "0.6.3" @@ -624,6 +653,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -923,6 +975,7 @@ dependencies = [ name = "lyrica" version = "0.1.0" dependencies = [ + "chrono", "color-eyre", "crossterm", "dirs", @@ -994,6 +1047,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -1563,8 +1625,8 @@ dependencies = [ [[package]] name = "soundcloud" -version = "0.1.1" -source = "git+https://gitea.awain.net/alterwain/soundcloud_api.git#87614c2a10c30f7d3e4b3cb0f8973d62ffec7916" +version = "0.1.4" +source = "git+https://gitea.awain.net/alterwain/soundcloud_api.git#22f02cfa43bb91370211b64c9c6240496bd44515" dependencies = [ "hyper-util", "regex", @@ -2166,6 +2228,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-registry" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 76993be..1a14b07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ license = "AGPLv3" authors = ["Michael Wain "] [dependencies] +chrono = "0.4.39" rusb = "0.9.4" dirs = "6.0.0" toml = "0.8.20" @@ -17,5 +18,5 @@ crossterm = "0.28.1" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7.12", features = ["codec"] } strum = { version = "0.27", features = ["derive"] } -soundcloud = { version = "0.1.1", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" } +soundcloud = { version = "0.1.4", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" } itunesdb = { version = "0.1.1", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index b5739e2..8021ce0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; pub fn get_configs_dir() -> PathBuf { let mut p = dirs::home_dir().unwrap(); @@ -8,22 +8,47 @@ pub fn get_configs_dir() -> PathBuf { p } -#[derive(Debug, Deserialize)] +pub fn get_temp_dl_dir() -> PathBuf { + let mut p = get_configs_dir(); + p.push("tmp"); + p +} + +pub fn get_config_path() -> PathBuf { + let mut p = get_configs_dir(); + p.push("config"); + p.set_extension(".toml"); + p +} + +pub fn get_temp_itunesdb() -> PathBuf { + let mut p = get_configs_dir(); + p.push("idb"); + p +} + +#[derive(Debug, Deserialize, Serialize)] pub struct YouTubeConfiguration { pub user_id: u64 } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct SoundCloudConfiguration { pub user_id: u64 } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct LyricaConfiguration { soundcloud: SoundCloudConfiguration, youtube: YouTubeConfiguration } +impl Default for LyricaConfiguration { + fn default() -> Self { + Self { soundcloud: SoundCloudConfiguration { user_id: 0 }, youtube: YouTubeConfiguration { user_id: 0 } } + } +} + impl LyricaConfiguration { pub fn get_soundcloud(&self) -> &SoundCloudConfiguration { &self.soundcloud diff --git a/src/dlp.rs b/src/dlp.rs new file mode 100644 index 0000000..6d9fecf --- /dev/null +++ b/src/dlp.rs @@ -0,0 +1,28 @@ +use std::path::PathBuf; + +use tokio::process::Command; + +pub async fn download_from_soundcloud(playlist_url: &str, download_dir: &PathBuf) -> std::result::Result<(), Box> { + let args = &[ + "--ignore-errors", + "--newline", + "--progress-template", + "{\"progressPercentage\":\"%(progress._percent_str)s\",\"progressTotal\":\"%(progress._total_bytes_str)s\",\"speed\":\"%(progress._speed_str)s\",\"ETA\":\"%(progress._eta_str)s\"}", + "-o", + "%(id)i.%(ext)s", + "--write-thumbnail", + playlist_url + ]; + + let mut command = Command::new("yt-dlp"); + command.args(args); + command.current_dir(download_dir); + + let mut child = command.spawn()?; + + let mut stdout = Vec::new(); + let child_stdout = child.stdout.take(); + tokio::io::copy(&mut child_stdout.unwrap(), &mut stdout).await.unwrap(); + + Ok(()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ef7c9e8..fee6ad5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use tokio_util::sync::CancellationToken; use ratatui::prelude::Constraint::{Length, Min}; use wait_screen::WaitScreen; +mod dlp; mod util; mod config; mod screen; diff --git a/src/main_screen.rs b/src/main_screen.rs index a6055bc..5e069a6 100644 --- a/src/main_screen.rs +++ b/src/main_screen.rs @@ -1,14 +1,17 @@ +use chrono::{DateTime, Utc}; use color_eyre::owo_colors::OwoColorize; use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders, Paragraph, Tabs, Widget}, Frame}; +use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, Paragraph, Row, Table, Tabs, Widget}, Frame}; use soundcloud::sobjects::CloudPlaylists; use strum::IntoEnumIterator; -use crate::screen::AppScreen; +use crate::{config::get_temp_dl_dir, dlp, screen::AppScreen}; #[derive(Debug, Clone)] pub struct MainScreen { selected_tab: i8, + selected_row: i32, + max_rows: i32, tab_titles: Vec, pub soundcloud: Option } @@ -16,8 +19,11 @@ pub struct MainScreen { impl AppScreen for MainScreen { fn handle_key_event(&mut self, key_event: KeyEvent) { match key_event.code { - KeyCode::Char('l') | KeyCode::Right => self.next_tab(), - KeyCode::Char('h') | KeyCode::Left => self.previous_tab(), + KeyCode::Right => self.next_tab(), + KeyCode::Left => self.previous_tab(), + KeyCode::Up => self.previous_row(), + KeyCode::Down => self.next_row(), + KeyCode::F(6) => self.download_row(), _ => {} } } @@ -36,15 +42,15 @@ impl AppScreen for MainScreen { self.tab_titles.iter().map(|t| Span::raw(t.clone())).collect::>(), ) .block(Block::default().borders(Borders::ALL)) - .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) .select(self.selected_tab as usize) .style(Style::default().fg(Color::White)); frame.render_widget(tabs, chunks[0]); - let main_content = Paragraph::new("Main content goes here!") - .block(Block::default().borders(Borders::ALL).title("Main")); - frame.render_widget(main_content, chunks[1]); // Render into second chunk + /*let main_content = Paragraph::new("Main content goes here!") + .block(Block::default().borders(Borders::ALL).title("Main")); */ + frame.render_widget(self.render_tab(), chunks[1]); // Render into second chunk // Render Status Bar let status_bar = Paragraph::new( @@ -63,7 +69,7 @@ impl AppScreen for MainScreen { impl MainScreen { pub fn new() -> Self { - MainScreen { soundcloud: None, selected_tab: 0, tab_titles: vec!["YouTube".to_string(), "SoundCloud".to_string(), "Local Playlists".to_string(), "Settings".to_string()] } + MainScreen { selected_row: -1, max_rows: 0, soundcloud: None, selected_tab: 0, tab_titles: vec!["YouTube".to_string(), "SoundCloud".to_string(), "Local Playlists".to_string(), "Settings".to_string()] } } fn next_tab(&mut self) { @@ -73,4 +79,62 @@ impl MainScreen { fn previous_tab(&mut self) { self.selected_tab = std::cmp::max(0, self.selected_tab-1); } + + fn previous_row(&mut self) { + self.selected_row = std::cmp::max(0, self.selected_row-1); + } + + fn next_row(&mut self) { + self.selected_row = std::cmp::min(self.selected_row + 1, self.max_rows); + } + + fn download_row(&mut self) { + match self.selected_tab { + 1 => {// SC + let playlist_url = self.soundcloud.as_ref().unwrap().collection.get(self.selected_row as usize).unwrap().permalink_url.clone(); + dlp::download_from_soundcloud(&playlist_url, &get_temp_dl_dir()); + }, + _ => {} + } + } + + fn render_tab(&self) -> Table<'_> { + let rows = match self.selected_tab { + 1 => { // SC + let mut v = Vec::new(); + v.push(Row::new(vec!["Id", "Title", "Songs Count", "Date", "IS"]).style(Style::default().fg(Color::Gray))); + if let Some(s) = &self.soundcloud { + for (i, playlist) in (&s.collection).iter().enumerate() { + let date: DateTime = playlist.created_at.parse().unwrap(); + let mut row = Row::new( + vec![ + playlist.id.to_string(), + playlist.title.clone(), + [playlist.track_count.to_string(), " songs".to_string()].concat(), + format!("{}", date.format("%Y-%m-%d %H:%M")), + "NO".to_string() + ] + ); + if self.selected_row == i as i32 { + row = row.style(Style::default().bg(Color::Yellow)); + } + v.push(row); + } + } + v + } + _ => Vec::new() + }; + + // Create the table + Table::new(rows, &[ + Constraint::Length(3), // ID column + Constraint::Percentage(50), // Playlist name column + Constraint::Percentage(20), // Song count column + Constraint::Percentage(30), + Constraint::Length(2) + ]) + .block(Block::default().borders(Borders::ALL).title(" Playlists ")) + .style(Style::default().fg(Color::White)) + } } \ No newline at end of file diff --git a/src/sync.rs b/src/sync.rs index d49acf6..f997b77 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -2,10 +2,10 @@ use std::path::{Path, PathBuf}; use itunesdb::xobjects::XDatabase; use soundcloud::sobjects::CloudPlaylists; -use tokio::{fs::File, io::AsyncReadExt, sync::mpsc::{Sender, UnboundedReceiver}}; +use tokio::{fs::File, io::{AsyncReadExt, AsyncWriteExt}, sync::mpsc::{Sender, UnboundedReceiver}}; use tokio_util::sync::CancellationToken; -use crate::config::{get_configs_dir, LyricaConfiguration}; +use crate::config::{get_config_path, get_configs_dir, get_temp_itunesdb, LyricaConfiguration}; pub enum AppEvent { SearchIPod, @@ -36,8 +36,7 @@ pub fn initialize_async_service(sender: Sender, receiver: UnboundedRec AppEvent::ParseItunes(path) => { // todo: parse itunes let _ = std::fs::create_dir_all(get_configs_dir()); - let mut cd = get_configs_dir(); - cd.push("idb"); + let cd = get_temp_itunesdb(); let mut p: PathBuf = Path::new(&path).into(); // p.push("iPod_Control"); // p.push("iTunes"); @@ -49,10 +48,13 @@ pub fn initialize_async_service(sender: Sender, receiver: UnboundedRec let xdb = itunesdb::deserializer::parse_bytes(&contents); let _ = sender.send(AppEvent::ITunesParsed(xdb)).await; - let mut p = get_configs_dir(); - p.push("config"); - p.set_extension(".toml"); - if !p.exists() { return; } + let p = get_config_path(); + if !p.exists() { + let config = LyricaConfiguration::default(); + let cfg_str = toml::to_string_pretty(&config).unwrap(); + let mut file = File::create(&p).await.unwrap(); + file.write(cfg_str.as_bytes()).await; + } let mut file = File::open(p).await.unwrap(); let mut content = String::new(); file.read_to_string(&mut content).await.unwrap();