Rewrite it in Rust

This commit is contained in:
ByteDream 2022-10-20 18:52:08 +02:00
parent d4bef511cb
commit 039d7cfb81
51 changed files with 4018 additions and 3208 deletions

196
crunchy-cli-core/src/lib.rs Normal file
View file

@ -0,0 +1,196 @@
use crate::cli::log::CliLogger;
use crate::utils::context::Context;
use crate::utils::locale::system_locale;
use crate::utils::log::progress;
use anyhow::bail;
use anyhow::Result;
use clap::{Parser, Subcommand};
use crunchyroll_rs::{Crunchyroll, Locale};
use log::{debug, error, info, LevelFilter};
use std::{env, fs};
mod cli;
mod utils;
pub use cli::{archive::Archive, download::Download, login::Login};
#[async_trait::async_trait(?Send)]
trait Execute {
async fn execute(self, ctx: Context) -> Result<()>;
}
#[derive(Debug, Parser)]
#[clap(author, version, about)]
#[clap(name = "crunchy-cli")]
pub struct Cli {
#[clap(flatten)]
verbosity: Option<Verbosity>,
#[arg(help = "Overwrite the language in which results are returned. Default is your system language")]
#[arg(long)]
lang: Option<Locale>,
#[clap(flatten)]
login_method: LoginMethod,
#[clap(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Archive(Archive),
Download(Download),
Login(Login),
}
#[derive(Debug, Parser)]
struct Verbosity {
#[arg(help = "Verbose output")]
#[arg(short)]
v: bool,
#[arg(help = "Quiet output. Does not print anything unless it's a error")]
#[arg(long_help = "Quiet output. Does not print anything unless it's a error. Can be helpful if you pipe the output to stdout")]
#[arg(short)]
q: bool,
}
#[derive(Debug, Parser)]
struct LoginMethod {
#[arg(help = "Login with credentials (username or email and password)")]
#[arg(long_help = "Login with credentials (username or email and password). Must be provided as user:password")]
#[arg(long)]
credentials: Option<String>,
#[arg(help = "Login with the etp-rt cookie")]
#[arg(long_help = "Login with the etp-rt cookie. This can be obtained when you login on crunchyroll.com and extract it from there")]
#[arg(long)]
etp_rt: Option<String>,
}
pub async fn cli_entrypoint() {
let cli: Cli = Cli::parse();
if let Some(verbosity) = &cli.verbosity {
if verbosity.v && verbosity.q {
eprintln!("Output cannot be verbose ('-v') and quiet ('-q') at the same time");
std::process::exit(1)
} else if verbosity.v {
CliLogger::init(LevelFilter::Debug).unwrap()
} else if verbosity.q {
CliLogger::init(LevelFilter::Error).unwrap()
}
} else {
CliLogger::init(LevelFilter::Info).unwrap()
}
debug!("cli input: {:?}", cli);
let ctx = match create_ctx(&cli).await {
Ok(ctx) => ctx,
Err(e) => {
error!("{}", e);
std::process::exit(1)
}
};
debug!("Created context");
ctrlc::set_handler(move || {
debug!("Ctrl-c detected");
if let Ok(dir) = fs::read_dir(&env::temp_dir()) {
for file in dir.flatten() {
if file
.path()
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.starts_with(".crunchy-cli_")
{
let result = fs::remove_file(file.path());
debug!(
"Ctrl-c removed temporary file {} {}",
file.path().to_string_lossy(),
if result.is_ok() {
"successfully"
} else {
"not successfully"
}
)
}
}
}
std::process::exit(1)
})
.unwrap();
debug!("Created ctrl-c handler");
let result = match cli.command {
Command::Archive(archive) => archive.execute(ctx).await,
Command::Download(download) => download.execute(ctx).await,
Command::Login(login) => {
if login.remove {
Ok(())
} else {
login.execute(ctx).await
}
}
};
if let Err(err) = result {
error!("{}", err);
std::process::exit(1)
}
}
async fn create_ctx(cli: &Cli) -> Result<Context> {
let crunchy = crunchyroll_session(cli).await?;
// TODO: Use crunchy.client() when it's possible
// currently crunchy.client() has a cloudflare bypass built-in to access crunchyroll. the servers
// where crunchy stores their videos can't handle this bypass and simply refuses to connect
let client = isahc::HttpClient::new().unwrap();
Ok(Context { crunchy, client })
}
async fn crunchyroll_session(cli: &Cli) -> Result<Crunchyroll> {
let mut builder = Crunchyroll::builder();
builder.locale(cli.lang.clone().unwrap_or_else(system_locale));
let _progress_handler = progress!("Logging in");
if cli.login_method.credentials.is_none() && cli.login_method.etp_rt.is_none() {
if let Some(login_file_path) = cli::login::login_file_path() {
if login_file_path.exists() {
let session = fs::read_to_string(login_file_path)?;
if let Some((token_type, token)) = session.split_once(':') {
match token_type {
"refresh_token" => {
return Ok(builder.login_with_refresh_token(token).await?)
}
"etp_rt" => return Ok(builder.login_with_etp_rt(token).await?),
_ => (),
}
}
bail!("Could not read stored session ('{}')", session)
}
}
bail!("Please use a login method ('--credentials' or '--etp_rt')")
} else if cli.login_method.credentials.is_some() && cli.login_method.etp_rt.is_some() {
bail!("Please use only one login method ('--credentials' or '--etp_rt')")
}
let crunchy = if let Some(credentials) = &cli.login_method.credentials {
if let Some((user, password)) = credentials.split_once(':') {
builder.login_with_credentials(user, password).await?
} else {
bail!("Invalid credentials format. Please provide your credentials as user:password")
}
} else if let Some(etp_rt) = &cli.login_method.etp_rt {
builder.login_with_etp_rt(etp_rt).await?
} else {
bail!("should never happen")
};
info!("Logged in");
Ok(crunchy)
}