mirror of
https://gitverse.ru/ot/DownOnSpot
synced 2025-12-19 09:54:19 +00:00
295 lines
6.6 KiB
Rust
295 lines
6.6 KiB
Rust
#[macro_use]
|
|
extern crate log;
|
|
|
|
mod arg;
|
|
mod converter;
|
|
mod downloader;
|
|
mod error;
|
|
mod settings;
|
|
mod spotify;
|
|
mod tag;
|
|
|
|
use arg::Args;
|
|
use async_std::task;
|
|
use colored::Colorize;
|
|
use downloader::{DownloadState, Downloader};
|
|
use error::SpotifyError;
|
|
use settings::Settings;
|
|
use spotify::Spotify;
|
|
use std::time::{Duration, Instant};
|
|
|
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
|
|
#[cfg(not(windows))]
|
|
#[tokio::main]
|
|
async fn main() {
|
|
start().await;
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[tokio::main]
|
|
async fn main() {
|
|
use colored::control;
|
|
|
|
//backwards compatibility.
|
|
if control::set_virtual_terminal(true).is_ok() {};
|
|
start().await;
|
|
}
|
|
|
|
async fn start() {
|
|
env_logger::init();
|
|
|
|
let args = Args::from_cli();
|
|
|
|
let settings = match Settings::load().await {
|
|
Ok(settings) => {
|
|
println!(
|
|
"{} {}.",
|
|
"Settings successfully loaded.\nContinuing with spotify account:".green(),
|
|
settings.username
|
|
);
|
|
settings
|
|
}
|
|
Err(e) => {
|
|
println!(
|
|
"{} {}...",
|
|
"Settings could not be loaded, because of the following error:".red(),
|
|
e
|
|
);
|
|
let default_settings = Settings::new("username", "password", "client_id", "secret");
|
|
match default_settings.save().await {
|
|
Ok(path) => {
|
|
println!(
|
|
"{}{}",
|
|
"..but default settings have been created successfully. Edit them and run the program again.\nFind the settings file at: ".green(),
|
|
path.to_string_lossy()
|
|
);
|
|
}
|
|
Err(e) => {
|
|
println!(
|
|
"{} {}",
|
|
"..and default settings could not be written:".red(),
|
|
e
|
|
);
|
|
}
|
|
};
|
|
return;
|
|
}
|
|
};
|
|
|
|
let spotify = match Spotify::new(
|
|
&settings.username,
|
|
&settings.password,
|
|
&settings.client_id,
|
|
&settings.client_secret,
|
|
settings.market_country_code,
|
|
)
|
|
.await
|
|
{
|
|
Ok(spotify) => {
|
|
println!("{}", "Login succeeded.".green());
|
|
spotify
|
|
}
|
|
Err(e) => {
|
|
println!(
|
|
"{} {}",
|
|
"Login failed, possibly due to invalid credentials or settings:".red(),
|
|
e
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let downloader = Downloader::new(settings.downloader, spotify);
|
|
|
|
let bold = "\x1b[1m";
|
|
let bold_off = "\x1b[0m";
|
|
|
|
match downloader.handle_input(&args.input).await {
|
|
Ok(search_results) => {
|
|
if let Some(search_results) = search_results {
|
|
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
|
|
|
|
for (i, track) in search_results.iter().enumerate() {
|
|
println!("{}: {} - {}", i + 1, track.author, track.title);
|
|
}
|
|
println!("{}", "Select the track (default: 1): ".green());
|
|
|
|
let mut selection;
|
|
loop {
|
|
let mut input = String::new();
|
|
std::io::stdin()
|
|
.read_line(&mut input)
|
|
.expect("Failed to read line");
|
|
|
|
selection = input.trim().parse::<usize>().unwrap_or(1) - 1;
|
|
|
|
if selection < search_results.len() {
|
|
break;
|
|
}
|
|
println!("{}", "Invalid selection. Try again or quit (CTRL+C):".red());
|
|
}
|
|
|
|
let track = &search_results[selection];
|
|
|
|
if let Err(e) = downloader
|
|
.add_uri(&format!("spotify:track:{}", track.track_id))
|
|
.await
|
|
{
|
|
error!(
|
|
"{}",
|
|
format!(
|
|
"{}: {}",
|
|
"Track could not be added to download queue.".red(),
|
|
e
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let refresh = Duration::from_secs(settings.refresh_ui_seconds);
|
|
let now = Instant::now();
|
|
let mut time_elapsed: u64 = 0;
|
|
|
|
let mut download_states =
|
|
vec![DownloadState::None; downloader.get_downloads().await.len()];
|
|
let mut messages = vec![];
|
|
|
|
'outer: loop {
|
|
print!("\x1b[2J\x1b[1;1H");
|
|
let mut exit_flag: i8 = 1;
|
|
|
|
let mut num_completed = 0;
|
|
let mut num_err = 0;
|
|
let mut num_downloading = 0;
|
|
let mut num_waiting = 0;
|
|
|
|
let mut current_download_view = String::new();
|
|
|
|
let mut progress_sum = 0.;
|
|
|
|
for (i, download) in (&downloader.get_downloads().await).iter().enumerate() {
|
|
let state = &download.state;
|
|
|
|
if state != &download_states[i] {
|
|
download_states[i] = state.clone();
|
|
match state {
|
|
DownloadState::None => (),
|
|
DownloadState::Lock => (),
|
|
DownloadState::Downloading(_, _) => (),
|
|
DownloadState::Post => (),
|
|
DownloadState::Done => messages.push(format!(
|
|
"{time_elapsed: >5} | {}: {}",
|
|
"Downloaded".green(),
|
|
download.title
|
|
)),
|
|
DownloadState::Error(e) => messages.push(format!(
|
|
"{time_elapsed: >5} | {}: {}",
|
|
if e == &SpotifyError::AlreadyDownloaded {
|
|
e.to_string().yellow()
|
|
} else {
|
|
e.to_string().red()
|
|
},
|
|
download.title
|
|
)),
|
|
};
|
|
}
|
|
|
|
if let Some(msg) = match state {
|
|
DownloadState::Downloading(r, t) => {
|
|
exit_flag &= 0;
|
|
let p = *r as f32 / *t as f32;
|
|
progress_sum += p;
|
|
num_downloading += 1;
|
|
if p > 1. {
|
|
Some("100%".to_string())
|
|
} else {
|
|
Some(format!("{}%", (p * 100.) as i8))
|
|
}
|
|
}
|
|
DownloadState::Post => {
|
|
exit_flag &= 0;
|
|
Some("Postprocessing... ".to_string())
|
|
}
|
|
DownloadState::None | DownloadState::Lock => {
|
|
exit_flag &= 0;
|
|
num_waiting += 1;
|
|
None
|
|
}
|
|
DownloadState::Error(_) => {
|
|
num_err += 1;
|
|
None
|
|
}
|
|
DownloadState::Done => {
|
|
num_completed += 1;
|
|
None
|
|
}
|
|
} {
|
|
current_download_view
|
|
.push_str(&format!("{: >4} | {}\n", msg, download.title));
|
|
}
|
|
}
|
|
|
|
while messages.len() > 8 {
|
|
messages.remove(0);
|
|
}
|
|
|
|
println!(" {bold}\x1b[0;34m- DownOnSpot v{VERSION} -\x1b[0m{bold_off}\n");
|
|
|
|
println!("Time elapsed: {}", secs_to_min_sec(time_elapsed as i32));
|
|
println!(
|
|
"Time remaining: {}\n",
|
|
secs_to_min_sec(
|
|
(time_elapsed as f32
|
|
/ (progress_sum + num_completed as f32 + num_err as f32)
|
|
* (num_waiting as f32 + num_downloading as f32 - progress_sum))
|
|
.round() as i32
|
|
)
|
|
);
|
|
|
|
println!(
|
|
" {bold}{} {}{bold_off}",
|
|
"Time".underline(),
|
|
"Event".underline()
|
|
);
|
|
for message in &messages {
|
|
println!("{}", message);
|
|
}
|
|
println!("\n\n {}", "Current downloads:".underline().bold());
|
|
println!("{}", current_download_view);
|
|
|
|
println!(
|
|
"\n {bold}Waiting | {} | {} | Total{bold_off}",
|
|
"Err/Skip".red(),
|
|
"Done".green()
|
|
);
|
|
println!(
|
|
" {: <8}| {: <9}| {: <5}| {}",
|
|
num_waiting,
|
|
num_err,
|
|
num_completed,
|
|
download_states.len()
|
|
);
|
|
|
|
time_elapsed = now.elapsed().as_secs();
|
|
if exit_flag == 1 {
|
|
break 'outer;
|
|
}
|
|
|
|
task::sleep(refresh).await
|
|
}
|
|
println!(
|
|
"Finished download(s) in {}.",
|
|
secs_to_min_sec(time_elapsed as i32)
|
|
);
|
|
}
|
|
Err(e) => {
|
|
error!("{} {}", "Handling input failed:".red(), e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn secs_to_min_sec(secs: i32) -> String {
|
|
format!("{:0>2}m{:0>2}s", secs / 60, secs % 60)
|
|
}
|