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:
parent
f016bd754b
commit
30422c28eb
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -984,6 +984,7 @@ dependencies = [
|
||||
"regex",
|
||||
"rusb",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"soundcloud",
|
||||
"strum 0.27.0",
|
||||
"tokio",
|
||||
|
@ -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"
|
||||
|
54
src/dlp.rs
54
src/dlp.rs
@ -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(())
|
||||
}
|
29
src/main.rs
29
src/main.rs
@ -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(())
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
10
src/sync.rs
10
src/sync.rs
@ -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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user