modified: Cargo.lock

modified:   Cargo.toml
	modified:   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 15:17:24 +03:00
parent f016bd754b
commit 30422c28eb
6 changed files with 133 additions and 33 deletions

1
Cargo.lock generated
View File

@ -984,6 +984,7 @@ dependencies = [
"regex",
"rusb",
"serde",
"serde_json",
"soundcloud",
"strum 0.27.0",
"tokio",

View File

@ -11,6 +11,7 @@ rusb = "0.9.4"
dirs = "6.0.0"
toml = "0.8.20"
serde = "1.0.217"
serde_json = "1.0"
regex = "1.11.1"
ratatui = { version = "0.29.0", features = ["all-widgets"] }
color-eyre = "0.6.3"

View File

@ -1,13 +1,32 @@
use std::path::PathBuf;
use std::{path::PathBuf, process::Stdio};
use tokio::process::Command;
use regex::Regex;
use serde::Deserialize;
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::Sender};
use crate::sync::AppEvent;
#[derive(Debug, Deserialize)]
pub struct DownloadProgress {
progress_percentage: String,
progress_total: String,
speed: String,
eta: String
}
pub async fn download_from_soundcloud(playlist_url: &str, download_dir: &PathBuf, sender: Sender<AppEvent>) -> std::result::Result<(), Box<dyn std::error::Error>> {
let dl_rx: Regex = Regex::new(r"\[download\] Downloading item \d+ of \d+").unwrap();
if download_dir.exists() {
let _ = std::fs::remove_dir_all(download_dir);
}
let _ = std::fs::create_dir_all(download_dir);
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\"}",
"{\"progress_percentage\":\"%(progress._percent_str)s\",\"progress_total\":\"%(progress._total_bytes_str)s\",\"speed\":\"%(progress._speed_str)s\",\"eta\":\"%(progress._eta_str)s\"}",
"-o",
"%(id)i.%(ext)s",
"--write-thumbnail",
@ -16,13 +35,34 @@ pub async fn download_from_soundcloud(playlist_url: &str, download_dir: &PathBuf
let mut command = Command::new("yt-dlp");
command.args(args);
command.stdout(Stdio::piped());
command.stderr(Stdio::null());
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();
let stdout = child.stdout.take().unwrap();
let mut reader = BufReader::new(stdout).lines();
while let Some(line) = reader.next_line().await? {
match dl_rx.find(&line) {
Some(m) => {
let mut s = m.as_str();
s = s.split("Downloading item ").last().unwrap();
let s: Vec<&str> = s.split(' ').collect();
let cur = s.first().unwrap().trim().parse().unwrap();
let max = s.last().unwrap().trim().parse().unwrap();
let _ = sender.send(AppEvent::OverallProgress((cur, max))).await;
},
None => {
if line.starts_with("{") {
let progress: DownloadProgress = serde_json::from_str(&line).unwrap();
let _ = sender.send(AppEvent::CurrentProgress(progress)).await;
}
}
}
}
Ok(())
}

View File

@ -35,7 +35,7 @@ pub struct App {
impl Default for App {
fn default() -> Self {
let (tx, mut rx) = mpsc::channel(1);
let (tx, mut rx) = mpsc::channel(10);
let (jx, mut jr) = mpsc::unbounded_channel();
let token = CancellationToken::new();
@ -45,7 +45,7 @@ impl Default for App {
let mut screens: HashMap<AppState, Box<dyn AppScreen>> = HashMap::new();
screens.insert(AppState::IPodWait, Box::new(WaitScreen::default()));
screens.insert(AppState::MainScreen, Box::new(MainScreen::new()));
screens.insert(AppState::MainScreen, Box::new(MainScreen::new(jx.clone())));
Self { receiver: rx, sender: jx, token, state: AppState::IPodWait, screens }
}
@ -65,12 +65,6 @@ impl App {
}
fn handle_events(&mut self) -> io::Result<()> {
match event::read()? {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_event(key_event)
}
_ => {}
};
if let Ok(event) = self.receiver.try_recv() {
match event {
AppEvent::IPodFound(path) => {
@ -85,15 +79,24 @@ impl App {
},
AppEvent::SoundcloudGot(playlists) => {
let a = self.screens.get_mut(&AppState::MainScreen).unwrap();
let screen: &mut MainScreen = match a.as_any().downcast_mut::<MainScreen>() {
Some(b) => b,
None => panic!("&a isn't a B!"),
};
let screen: &mut MainScreen = a.as_any().downcast_mut::<MainScreen>().unwrap();
screen.soundcloud = Some(playlists);
},
AppEvent::OverallProgress((c, max)) => {
let a = self.screens.get_mut(&AppState::MainScreen).unwrap();
let screen: &mut MainScreen = a.as_any().downcast_mut::<MainScreen>().unwrap();
screen.progress = Some((c, max));
screen.download_screen();
}
_ => {}
}
}
};
match event::read()? {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_event(key_event)
}
_ => {}
};
Ok(())
}

View File

@ -1,11 +1,12 @@
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, List, ListItem, Paragraph, Row, Table, Tabs, Widget}, Frame};
use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, Row, Table, Tabs, Widget}, Frame};
use soundcloud::sobjects::CloudPlaylists;
use strum::IntoEnumIterator;
use tokio::sync::mpsc::UnboundedSender;
use crate::{config::get_temp_dl_dir, dlp, screen::AppScreen};
use crate::{config::get_temp_dl_dir, dlp, screen::AppScreen, sync::AppEvent};
#[derive(Debug, Clone)]
pub struct MainScreen {
@ -13,7 +14,9 @@ pub struct MainScreen {
selected_row: i32,
max_rows: i32,
tab_titles: Vec<String>,
pub soundcloud: Option<CloudPlaylists>
pub soundcloud: Option<CloudPlaylists>,
pub progress: Option<(u32, u32)>,
sender: UnboundedSender<AppEvent>
}
impl AppScreen for MainScreen {
@ -48,9 +51,11 @@ impl AppScreen for MainScreen {
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(self.render_tab(), chunks[1]); // Render into second chunk
if self.selected_tab != -1 {
frame.render_widget(self.render_tab(), chunks[1]); // Render into second chunk
} else {
self.render_progress(frame, chunks[1]);
}
// Render Status Bar
let status_bar = Paragraph::new(
@ -68,16 +73,37 @@ impl AppScreen for MainScreen {
}
impl MainScreen {
pub fn new() -> Self {
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()] }
pub fn new( sender: UnboundedSender<AppEvent> ) -> Self {
MainScreen {
selected_row: -1,
max_rows: 0,
soundcloud: None,
progress: None,
selected_tab: 0,
tab_titles: vec!["YouTube".to_string(), "SoundCloud".to_string(), "Local Playlists".to_string(), "Settings".to_string()],
sender
}
}
pub fn download_screen(&mut self) {
self.selected_tab = -1;
}
fn update_max_rows(&mut self) {
self.max_rows = match self.selected_tab {
1 => self.soundcloud.as_ref().unwrap_or( &CloudPlaylists { collection: Vec::new() }).collection.len(),
_ => 0
}.try_into().unwrap();
}
fn next_tab(&mut self) {
self.selected_tab = std::cmp::min(self.selected_tab+1, (self.tab_titles.len()-1).try_into().unwrap())
self.selected_tab = std::cmp::min(self.selected_tab+1, (self.tab_titles.len()-1).try_into().unwrap());
self.update_max_rows();
}
fn previous_tab(&mut self) {
self.selected_tab = std::cmp::max(0, self.selected_tab-1);
self.update_max_rows();
}
fn previous_row(&mut self) {
@ -85,19 +111,42 @@ impl MainScreen {
}
fn next_row(&mut self) {
self.selected_row = std::cmp::min(self.selected_row + 1, self.max_rows);
self.selected_row = std::cmp::min(self.selected_row + 1, self.max_rows - 1);
}
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());
let _ = self.sender.send(AppEvent::DownloadPlaylist(playlist_url));
},
_ => {}
}
}
fn render_progress(&self, frame: &mut Frame, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(0), // Main content
Constraint::Length(3), // Progress bar
])
.split(area);
let main_content = Paragraph::new("Main content goes here!")
.block(Block::default().borders(Borders::ALL).title("Main"));
frame.render_widget(main_content, chunks[0]);
let gauge = Gauge::default()
.block(Block::default().borders(Borders::ALL).title(" Downloading Playlist "))
.gauge_style(Style::default().fg(Color::Green))
.ratio(self.progress.unwrap().0 as f64 / self.progress.unwrap().1 as f64)
.label(format!("{:}/{:}", self.progress.unwrap().0, self.progress.unwrap().1));
frame.render_widget(gauge, chunks[1]);
}
fn render_tab(&self) -> Table<'_> {
let rows = match self.selected_tab {
1 => { // SC

View File

@ -5,7 +5,7 @@ use soundcloud::sobjects::CloudPlaylists;
use tokio::{fs::File, io::{AsyncReadExt, AsyncWriteExt}, sync::mpsc::{Sender, UnboundedReceiver}};
use tokio_util::sync::CancellationToken;
use crate::config::{get_config_path, get_configs_dir, get_temp_itunesdb, LyricaConfiguration};
use crate::{config::{get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration}, dlp::{self, DownloadProgress}};
pub enum AppEvent {
SearchIPod,
@ -13,7 +13,10 @@ pub enum AppEvent {
IPodNotFound,
ParseItunes(String),
ITunesParsed(XDatabase),
SoundcloudGot(CloudPlaylists)
SoundcloudGot(CloudPlaylists),
DownloadPlaylist(String),
CurrentProgress(DownloadProgress),
OverallProgress((u32, u32))
}
pub fn initialize_async_service(sender: Sender<AppEvent>, receiver: UnboundedReceiver<AppEvent>, token: CancellationToken) {
@ -66,6 +69,9 @@ pub fn initialize_async_service(sender: Sender<AppEvent>, receiver: UnboundedRec
let _ = sender.send(AppEvent::SoundcloudGot(playlists)).await;
},
AppEvent::DownloadPlaylist(playlist_url) => {
let _ = dlp::download_from_soundcloud(&playlist_url, &get_temp_dl_dir(), sender.clone()).await;
}
_ => {}
}
}