modified: Cargo.lock

modified:   Cargo.toml
	modified:   src/config.rs
	new file:   src/dlp.rs
	modified:   src/main.rs
	modified:   src/main_screen.rs
	modified:   src/sync.rs
This commit is contained in:
Michael Wain 2025-02-10 12:58:17 +03:00
parent 34cf8c0a15
commit f016bd754b
7 changed files with 216 additions and 24 deletions

75
Cargo.lock generated
View File

@ -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"

View File

@ -6,6 +6,7 @@ license = "AGPLv3"
authors = ["Michael Wain <alterwain@protonmail.com>"]
[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" }

View File

@ -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

28
src/dlp.rs Normal file
View File

@ -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<dyn std::error::Error>> {
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(())
}

View File

@ -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;

View File

@ -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<String>,
pub soundcloud: Option<CloudPlaylists>
}
@ -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::<Vec<Span>>(),
)
.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<Utc> = 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))
}
}

View File

@ -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<AppEvent>, 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<AppEvent>, 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();