Compare commits

..

No commits in common. "16d0edbbb67baf61d1fe610bc2f39ad1cc279c69" and "59de02d34dd425eb67fb209d3cbdfca79b95480f" have entirely different histories.

8 changed files with 162 additions and 182 deletions

28
Cargo.lock generated
View File

@ -11,12 +11,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anyhow"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -176,7 +170,6 @@ dependencies = [
name = "ffdl" name = "ffdl"
version = "0.1.2" version = "0.1.2"
dependencies = [ dependencies = [
"anyhow",
"chrono", "chrono",
"clap", "clap",
"crossterm", "crossterm",
@ -184,7 +177,6 @@ dependencies = [
"percent-encoding", "percent-encoding",
"regex", "regex",
"reqwest", "reqwest",
"thiserror",
"tokio", "tokio",
] ]
@ -991,26 +983,6 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.44" version = "0.1.44"

View File

@ -14,5 +14,3 @@ regex = "1.5.5"
crossterm = "0.23.1" crossterm = "0.23.1"
clap = { version = "3.1.6", features = [ "derive" ] } clap = { version = "3.1.6", features = [ "derive" ] }
chrono = "0.4.19" chrono = "0.4.19"
thiserror = "1.0.30"
anyhow = "1.0.56"

View File

@ -18,6 +18,14 @@ pub struct CLIArgs {
)] )]
pub outdir: PathBuf, pub outdir: PathBuf,
#[clap(
short = 'i',
long = "into-file",
value_name = "FILENAME",
help = "Force filename. This only works for single file downloads",
)]
pub into_file: Option<PathBuf>,
#[clap( #[clap(
short = 'n', short = 'n',
long = "num-files", long = "num-files",
@ -39,6 +47,14 @@ pub struct CLIArgs {
)] )]
pub conn_count: NonZeroU32, pub conn_count: NonZeroU32,
#[clap(
short = 'z',
long = "zippy",
help = "The provided URLs are zippyshare URLs and need to be \
resolved to direct download urls",
)]
pub zippy: bool,
#[clap( #[clap(
short = 'l', short = 'l',
long = "listfile", long = "listfile",

View File

@ -9,7 +9,7 @@ use crossterm::execute;
use crossterm::style::Print; use crossterm::style::Print;
use crossterm::terminal::{Clear, ClearType}; use crossterm::terminal::{Clear, ClearType};
use anyhow::Result; use crate::errors::*;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum DlStatus { pub enum DlStatus {
@ -35,7 +35,10 @@ pub struct DlReporter {
impl DlReporter { impl DlReporter {
pub fn new(id: u32, transmitter: mpsc::UnboundedSender<DlReport>) -> DlReporter { pub fn new(id: u32, transmitter: mpsc::UnboundedSender<DlReport>) -> DlReporter {
DlReporter { id, transmitter } DlReporter {
id: id,
transmitter: transmitter,
}
} }
pub fn send(&self, status: DlStatus) { pub fn send(&self, status: DlStatus) {
@ -47,46 +50,6 @@ impl DlReporter {
}) })
.unwrap(); .unwrap();
} }
pub fn init(&self, bytes_total: u64, filename: String) {
self.send(DlStatus::Init {
bytes_total,
filename,
})
}
pub fn update(&self, speed_mbps: f32, bytes_curr: u64) {
self.send(DlStatus::Update {
speed_mbps,
bytes_curr,
})
}
pub fn done(&self, duration_ms: u64) {
self.send(DlStatus::Done { duration_ms })
}
pub fn done_err(&self, filename: String) {
self.send(DlStatus::DoneErr { filename })
}
pub fn skipped(&self) {
self.send(DlStatus::Skipped);
}
pub fn msg(&self, msg: String) {
self.send(DlStatus::Message(msg));
}
}
#[macro_export]
macro_rules! report_msg {
($rep:ident, $fmt:expr) => {
DlReporter::msg(&$rep, $fmt.to_string());
};
($rep:ident, $fmt:expr, $($fmt2:expr),+) => {
DlReporter::msg(&$rep, format!($fmt, $($fmt2,)+));
};
} }
struct InfoHolder { struct InfoHolder {
@ -113,7 +76,7 @@ fn print_accumulated_report(
moved_lines: u16, moved_lines: u16,
file_count_completed: i32, file_count_completed: i32,
file_count_total: i32, file_count_total: i32,
) -> Result<u16> { ) -> ResBE<u16> {
let mut dl_speed_sum = 0.0; let mut dl_speed_sum = 0.0;
execute!( execute!(
@ -135,12 +98,12 @@ fn print_accumulated_report(
execute!( execute!(
stdout(), stdout(),
Print("----------------------------------------".to_string()), Print(format!("----------------------------------------")),
Clear(ClearType::UntilNewLine), Clear(ClearType::UntilNewLine),
Print("\n") Print("\n")
)?; )?;
for v in statuses.values() { for (_k, v) in statuses {
let percent_complete = v.progress as f64 / v.total_size as f64 * 100.0; let percent_complete = v.progress as f64 / v.total_size as f64 * 100.0;
execute!( execute!(
@ -179,7 +142,7 @@ fn print_accumulated_report(
pub async fn watch_and_print_reports( pub async fn watch_and_print_reports(
mut receiver: mpsc::UnboundedReceiver<DlReport>, mut receiver: mpsc::UnboundedReceiver<DlReport>,
file_count_total: i32, file_count_total: i32,
) -> Result<()> { ) -> ResBE<()> {
let mut statuses: HashMap<u32, InfoHolder> = HashMap::new(); let mut statuses: HashMap<u32, InfoHolder> = HashMap::new();
let mut moved_lines = 0; let mut moved_lines = 0;
let mut msg_queue = VecDeque::new(); let mut msg_queue = VecDeque::new();

View File

@ -1,4 +1,3 @@
use anyhow::Result;
use futures::stream::FuturesUnordered; use futures::stream::FuturesUnordered;
use futures::StreamExt; use futures::StreamExt;
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
@ -25,7 +24,7 @@ impl RollingAverage {
} }
fn value(&self) -> f64 { fn value(&self) -> f64 {
if self.data.is_empty() { if self.data.len() == 0 {
0.0 0.0
} else { } else {
let mut max = self.data[0]; let mut max = self.data[0];
@ -64,7 +63,7 @@ impl RollingAverage {
/// Get the filename at the end of the given URL. This will decode the URL Encoding. /// Get the filename at the end of the given URL. This will decode the URL Encoding.
pub fn url_to_filename(url: &str) -> String { pub fn url_to_filename(url: &str) -> String {
let url_dec = percent_decode_str(url) let url_dec = percent_decode_str(&url)
.decode_utf8_lossy() .decode_utf8_lossy()
.to_owned() .to_owned()
.to_string(); .to_string();
@ -74,7 +73,7 @@ pub fn url_to_filename(url: &str) -> String {
.to_str() .to_str()
.unwrap(); .unwrap();
// Split at ? and return the first part. If no ? is present, this just returns the full string // Split at ? and return the first part. If no ? is present, this just returns the full string
file_name.split('?').next().unwrap().to_string() file_name.split("?").next().unwrap().to_string()
} }
pub async fn download_feedback( pub async fn download_feedback(
@ -82,7 +81,7 @@ pub async fn download_feedback(
into_file: &Path, into_file: &Path,
rep: DlReporter, rep: DlReporter,
content_length: Option<u64>, content_length: Option<u64>,
) -> Result<()> { ) -> ResBE<()> {
download_feedback_chunks(url, into_file, rep, None, content_length).await download_feedback_chunks(url, into_file, rep, None, content_length).await
} }
@ -92,10 +91,13 @@ pub async fn download_feedback_chunks(
rep: DlReporter, rep: DlReporter,
from_to: Option<(u64, u64)>, from_to: Option<(u64, u64)>,
content_length: Option<u64>, content_length: Option<u64>,
) -> Result<()> { ) -> ResBE<()> {
let mut content_length = match content_length { let mut content_length = match content_length {
Some(it) => it, Some(it) => it,
None => http_get_filesize_and_range_support(url).await?.filesize, None => {
let (content_length, _) = http_get_filesize_and_range_support(url).await?;
content_length
}
}; };
// Send the HTTP request to download the given link // Send the HTTP request to download the given link
@ -120,7 +122,7 @@ pub async fn download_feedback_chunks(
let mut ofile = opts let mut ofile = opts
.create(true) .create(true)
.write(true) .write(true)
.truncate(from_to.is_none()) .truncate(!from_to.is_some())
.open(into_file) .open(into_file)
.await?; .await?;
@ -131,7 +133,10 @@ pub async fn download_feedback_chunks(
let filename = into_file.file_name().unwrap().to_str().unwrap(); let filename = into_file.file_name().unwrap().to_str().unwrap();
// Report the download start // Report the download start
rep.init(content_length, filename.to_string()); rep.send(DlStatus::Init {
bytes_total: content_length,
filename: filename.to_string(),
});
let mut curr_progress = 0; let mut curr_progress = 0;
let mut speed_mbps = 0.0; let mut speed_mbps = 0.0;
@ -184,10 +189,13 @@ pub async fn download_feedback_chunks(
} }
// Send status update report // Send status update report
rep.update(speed_mbps, curr_progress); rep.send(DlStatus::Update {
speed_mbps,
bytes_curr: curr_progress,
});
} }
if !buff.is_empty() { if buff.len() > 0 {
ofile.write_all(&buff).await?; ofile.write_all(&buff).await?;
} }
@ -201,7 +209,7 @@ pub async fn download_feedback_chunks(
let duration_ms = t_start.elapsed()?.as_millis() as u64; let duration_ms = t_start.elapsed()?.as_millis() as u64;
// Send report that the download is finished // Send report that the download is finished
rep.done(duration_ms); rep.send(DlStatus::Done { duration_ms });
Ok(()) Ok(())
} }
@ -214,10 +222,10 @@ pub async fn download_feedback_multi(
rep: DlReporter, rep: DlReporter,
conn_count: u32, conn_count: u32,
content_length: Option<u64>, content_length: Option<u64>,
) -> Result<()> { ) -> ResBE<()> {
let content_length = match content_length { let content_length = match content_length {
Some(it) => it, Some(it) => it,
None => http_get_filesize_and_range_support(url).await?.filesize, None => http_get_filesize_and_range_support(url).await?.0,
}; };
// Create zeroed file with 1 byte too much. This will be truncated on download // Create zeroed file with 1 byte too much. This will be truncated on download
@ -234,8 +242,8 @@ pub async fn download_feedback_multi(
let t_start = SystemTime::now(); let t_start = SystemTime::now();
for index in 0..conn_count { for index in 0..conn_count {
let url = url.to_owned(); let url = url.clone().to_owned();
let into_file = into_file.to_owned(); let into_file = into_file.clone().to_owned();
let tx = tx.clone(); let tx = tx.clone();
@ -261,6 +269,7 @@ pub async fn download_feedback_multi(
Some(specific_content_length), Some(specific_content_length),
) )
.await .await
.map_err(|e| e.to_string())
})) }))
} }
@ -268,11 +277,14 @@ pub async fn download_feedback_multi(
let filename = Path::new(into_file).file_name().unwrap().to_str().unwrap(); let filename = Path::new(into_file).file_name().unwrap().to_str().unwrap();
rep.init(content_length, filename.to_string()); rep.send(DlStatus::Init {
bytes_total: content_length,
filename: filename.to_string(),
});
let rep_task = rep.clone(); let rep_task = rep.clone();
let mut t_last = t_start; let mut t_last = t_start.clone();
let manager_handle = tokio::task::spawn(async move { let manager_handle = tokio::task::spawn(async move {
let rep = rep_task; let rep = rep_task;
@ -310,7 +322,10 @@ pub async fn download_feedback_multi(
t_last = SystemTime::now(); t_last = SystemTime::now();
} }
rep.update(speed_mbps, progress_curr); rep.send(DlStatus::Update {
speed_mbps: speed_mbps,
bytes_curr: progress_curr,
});
} }
DlStatus::Done { duration_ms: _ } => { DlStatus::Done { duration_ms: _ } => {
@ -338,7 +353,7 @@ pub async fn download_feedback_multi(
tokio::fs::remove_file(&into_file).await?; tokio::fs::remove_file(&into_file).await?;
return Err(e); return Err(e.into());
} }
} }
@ -354,53 +369,50 @@ pub async fn download_feedback_multi(
ofile.set_len(content_length).await?; ofile.set_len(content_length).await?;
rep.done(t_start.elapsed()?.as_millis() as u64); rep.send(DlStatus::Done {
duration_ms: t_start.elapsed()?.as_millis() as u64,
});
Ok(()) Ok(())
} }
async fn create_zeroed_file(file: &Path, filesize: usize) -> Result<()> { async fn create_zeroed_file(file: &Path, filesize: usize) -> ResBE<()> {
let ofile = tokio::fs::OpenOptions::new() let ofile = tokio::fs::OpenOptions::new()
.create(true) .create(true)
// Open in write mode
.write(true) .write(true)
// Delete and overwrite the file
.truncate(true) .truncate(true)
.open(file) .open(file)
.await?; .await?;
ofile.set_len(filesize as u64).await?; ofile.set_len(filesize as u64).await?;
Ok(()) Ok(())
} }
pub struct HttpFileInfo { pub async fn http_get_filesize_and_range_support(url: &str) -> ResBE<(u64, bool)> {
pub filesize: u64,
pub range_support: bool,
pub filename: String,
}
pub async fn http_get_filesize_and_range_support(url: &str) -> Result<HttpFileInfo> {
let resp = reqwest::Client::new().head(url).send().await?; let resp = reqwest::Client::new().head(url).send().await?;
let filesize = resp if let Some(filesize) = resp.headers().get(reqwest::header::CONTENT_LENGTH) {
.headers() if let Ok(val_str) = filesize.to_str() {
.get(reqwest::header::CONTENT_LENGTH) if let Ok(val) = val_str.parse::<u64>() {
.and_then(|it| it.to_str().unwrap().parse::<u64>().ok()) let mut range_supported = false;
.ok_or(DlError::ContentLengthUnknown)?;
let range = resp if let Some(range) = resp.headers().get(reqwest::header::ACCEPT_RANGES) {
.headers() if let Ok(range) = range.to_str() {
.get(reqwest::header::ACCEPT_RANGES) if range == "bytes" {
.and_then(|it| it.to_str().ok()); range_supported = true;
let range_support = matches!(range, Some("bytes")); }
}
}
let filename = url_to_filename(url); return Ok((val, range_supported));
}
}
}
let info = HttpFileInfo { Err(DlError::ContentLengthUnknown.into())
filesize,
range_support,
filename,
};
Ok(info)
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,14 +1,27 @@
use thiserror::Error; use std::error::Error;
use std::fmt::{self, Display, Formatter};
/// Result Boxed Error
pub type ResBE<T> = Result<T, Box<dyn Error>>;
#[allow(unused)] #[allow(unused)]
#[derive(Error, Clone, Debug)] #[derive(Clone, Debug)]
pub enum DlError { pub enum DlError {
#[error("Bad http response status")]
BadHttpStatus, BadHttpStatus,
#[error("Content-Length is unknown")]
ContentLengthUnknown, ContentLengthUnknown,
#[error("Http server sent no more data")]
HttpNoData, HttpNoData,
#[error("Unknown download error: '{0}'")]
Other(String), Other(String),
} }
impl Error for DlError {}
impl Display for DlError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
DlError::BadHttpStatus => write!(f, "Bad http response status"),
DlError::ContentLengthUnknown => write!(f, "Content-Length is unknown"),
DlError::HttpNoData => write!(f, "Http server sent no more data"),
DlError::Other(s) => write!(f, "Unknown download error: '{}'", s),
}
}
}

View File

@ -7,6 +7,7 @@ use std::{
}; };
use clap::Parser; use clap::Parser;
use download::{download_feedback, download_feedback_multi, http_get_filesize_and_range_support};
use futures::future::join_all; use futures::future::join_all;
use tokio::{ use tokio::{
fs::create_dir_all, fs::create_dir_all,
@ -18,13 +19,10 @@ use tokio::{
use crate::{ use crate::{
args::CLIArgs, args::CLIArgs,
dlreport::{watch_and_print_reports, DlReport, DlReporter}, dlreport::{watch_and_print_reports, DlReport, DlReporter, DlStatus},
download::{download_feedback, download_feedback_multi, http_get_filesize_and_range_support}, errors::ResBE,
zippy::is_zippyshare_url,
}; };
use anyhow::Result;
mod args; mod args;
mod dlreport; mod dlreport;
mod download; mod download;
@ -39,7 +37,7 @@ struct DlRequest {
type SyncQueue = Arc<Mutex<VecDeque<DlRequest>>>; type SyncQueue = Arc<Mutex<VecDeque<DlRequest>>>;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> ResBE<()> {
let args = CLIArgs::parse(); let args = CLIArgs::parse();
// Combine all urls taken from files and the ones provided on the command line // Combine all urls taken from files and the ones provided on the command line
@ -63,7 +61,7 @@ async fn main() -> Result<()> {
} }
/// Parse a listfile and return all urls found in it /// Parse a listfile and return all urls found in it
async fn urls_from_listfile(listfile: &Path) -> Result<Vec<String>> { async fn urls_from_listfile(listfile: &Path) -> ResBE<Vec<String>> {
let text = tokio::fs::read_to_string(listfile).await?; let text = tokio::fs::read_to_string(listfile).await?;
let urls = text let urls = text
.lines() .lines()
@ -75,7 +73,7 @@ async fn urls_from_listfile(listfile: &Path) -> Result<Vec<String>> {
} }
// Download all files in parallel according to the provided CLI arguments // Download all files in parallel according to the provided CLI arguments
async fn download_multiple(args: CLIArgs, raw_urls: Vec<String>) -> Result<()> { async fn download_multiple(args: CLIArgs, raw_urls: Vec<String>) -> ResBE<()> {
let num_urls = raw_urls.len(); let num_urls = raw_urls.len();
let urls: SyncQueue = Default::default(); let urls: SyncQueue = Default::default();
@ -115,19 +113,24 @@ async fn download_multiple(args: CLIArgs, raw_urls: Vec<String>) -> Result<()> {
} }
async fn download_job(urls: SyncQueue, reporter: UnboundedSender<DlReport>, cli_args: CLIArgs) { async fn download_job(urls: SyncQueue, reporter: UnboundedSender<DlReport>, cli_args: CLIArgs) {
while let Some(dlreq) = urls.lock().await.pop_front() { loop {
// Get the next url to download or break if there are no more urls
let dlreq = match urls.lock().await.pop_front() {
Some(it) => it,
None => break,
};
let reporter = DlReporter::new(dlreq.id as u32, reporter.clone()); let reporter = DlReporter::new(dlreq.id as u32, reporter.clone());
// Resolve the zippy url to the direct download url if necessary // Resolve the zippy url to the direct download url if necessary
let url = if is_zippyshare_url(&dlreq.url) { let url = if cli_args.zippy {
match zippy::resolve_link(&dlreq.url).await { match zippy::resolve_link(&dlreq.url).await {
Ok(url) => url, Ok(url) => url,
Err(_e) => { Err(_e) => {
report_msg!( reporter.send(DlStatus::Message(format!(
reporter, "Zippyshare link could not be resolved: {}",
"Zippyshare link could not be resolved, skipping: {}",
dlreq.url dlreq.url
); )));
continue; continue;
} }
} }
@ -135,64 +138,72 @@ async fn download_job(urls: SyncQueue, reporter: UnboundedSender<DlReport>, cli_
dlreq.url.to_string() dlreq.url.to_string()
}; };
let info = match http_get_filesize_and_range_support(&url).await { let file_name = cli_args
Ok(it) => it, .into_file
Err(_e) => { .clone()
report_msg!(reporter, "Error while querying metadata: {url}"); .unwrap_or_else(|| download::url_to_filename(&url).into());
continue;
}
};
let into_file: PathBuf = cli_args let into_file: PathBuf = cli_args
.outdir .outdir
.join(Path::new(&info.filename)) .join(Path::new(&file_name))
.to_str() .to_str()
.unwrap() .unwrap()
.to_string() .to_string()
.into(); .into();
let (filesize, range_supported) = match http_get_filesize_and_range_support(&url).await {
Ok((filesize, range_supported)) => (filesize, range_supported),
Err(_e) => {
reporter.send(DlStatus::Message(format!(
"Error while querying metadata: {}",
url
)));
continue;
}
};
// If file with same name is present locally, check filesize // If file with same name is present locally, check filesize
if into_file.exists() { if into_file.exists() {
let local_filesize = std::fs::metadata(&into_file).unwrap().len(); let local_filesize = std::fs::metadata(&into_file).unwrap().len();
if info.filesize == local_filesize { if filesize == local_filesize {
report_msg!( reporter.send(DlStatus::Message(format!(
reporter,
"Skipping file '{}': already present", "Skipping file '{}': already present",
info.filename file_name.display()
); )));
reporter.skipped(); reporter.send(DlStatus::Skipped);
continue; continue;
} else { } else {
report_msg!( reporter.send(DlStatus::Message(format!(
reporter,
"Replacing file '{}': present but not completed", "Replacing file '{}': present but not completed",
&info.filename &file_name.display()
); )));
} }
} }
let dl_status = if cli_args.conn_count.get() == 1 { let dl_status = if cli_args.conn_count.get() == 1 {
download_feedback(&url, &into_file, reporter.clone(), Some(info.filesize)).await download_feedback(&url, &into_file, reporter.clone(), Some(filesize)).await
} else if !info.range_support { } else if !range_supported {
report_msg!( reporter.send(DlStatus::Message(format!(
reporter, "Server does not support range headers. Downloading with single connection: {}",
"Server does not support range headers. Downloading with single connection: {url}" url
); )));
download_feedback(&url, &into_file, reporter.clone(), Some(info.filesize)).await download_feedback(&url, &into_file, reporter.clone(), Some(filesize)).await
} else { } else {
download_feedback_multi( download_feedback_multi(
&url, &url,
&into_file, &into_file,
reporter.clone(), reporter.clone(),
cli_args.conn_count.get(), cli_args.conn_count.get(),
Some(info.filesize), Some(filesize),
) )
.await .await
}; };
if dl_status.is_err() { if dl_status.is_err() {
reporter.done_err(info.filename); reporter.send(DlStatus::DoneErr {
filename: file_name.to_str().unwrap().to_string(),
});
} }
} }
} }

View File

@ -1,12 +1,7 @@
use anyhow::Result;
use regex::Regex; use regex::Regex;
use std::io::{Error, ErrorKind}; use std::io::{Error, ErrorKind};
pub fn is_zippyshare_url(url: &str) -> bool { use crate::errors::ResBE;
Regex::new(r"^https?://(?:www\d*\.)?zippyshare\.com/v/[0-9a-zA-Z]+/file\.html$")
.unwrap()
.is_match(url)
}
/* /*
Updated: 07.03.2022 Updated: 07.03.2022
@ -22,15 +17,15 @@ Link generation code:
document.getElementById('dlbutton').href = "/d/0Ky7p1C6/" + (186549 % 51245 + 186549 % 913) + "/some-file-name.part1.rar"; document.getElementById('dlbutton').href = "/d/0Ky7p1C6/" + (186549 % 51245 + 186549 % 913) + "/some-file-name.part1.rar";
``` ```
*/ */
pub async fn resolve_link(url: &str) -> Result<String> { pub async fn resolve_link(url: &str) -> ResBE<String> {
// Regex to check if the provided url is a zippyshare download url // Regex to check if the provided url is a zippyshare download url
let re = Regex::new(r"(https://www\d*\.zippyshare\.com)")?; let re = Regex::new(r"(https://www\d*\.zippyshare\.com)")?;
if !re.is_match(url) { if !re.is_match(&url) {
return Err(Error::new(ErrorKind::Other, "URL is not a zippyshare url").into()); return Err(Error::new(ErrorKind::Other, "URL is not a zippyshare url").into());
} }
// Extract the hostname (with https:// prefix) for later // Extract the hostname (with https:// prefix) for later
let base_host = &re.captures(url).unwrap()[0]; let base_host = &re.captures(&url).unwrap()[0];
// Download the html body for the download page // Download the html body for the download page
let body = reqwest::get(url).await?.text().await?; let body = reqwest::get(url).await?.text().await?;
@ -47,10 +42,10 @@ pub async fn resolve_link(url: &str) -> Result<String> {
let url_start = &cap_link[1]; let url_start = &cap_link[1];
let url_end = &cap_link[5]; let url_end = &cap_link[5];
let n2: i32 = cap_link[2].parse()?; let n2: i32 = i32::from_str_radix(&cap_link[2], 10)?;
let n3: i32 = cap_link[3].parse()?; let n3: i32 = i32::from_str_radix(&cap_link[3], 10)?;
let n4 = n2; let n4 = n2;
let n5: i32 = cap_link[4].parse()?; let n5: i32 = i32::from_str_radix(&cap_link[4], 10)?;
let mixed = n2 % n3 + n4 % n5; let mixed = n2 % n3 + n4 % n5;