use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use std::path::Path;
use std::rc::Rc;
use futures_util::StreamExt;
use indicatif::MultiProgress;
use regex::Regex;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::{Url, Response};
use serde_json::json;
use tokio_dl_stream_to_disk::{AsyncDownload, error::ErrorKind as TDSTDErrorKind};
use tokio::time::{sleep, Duration as TokioDuration};
use crate::util::{OutputFormat, progress_bar::progress_wrapper};
fn http_headers(options: &HashMap<&str, &str>) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("x-cv", HeaderValue::from_static("3172501"));
headers.insert("x-sv", HeaderValue::from_static("29"));
let arch = match options.get("arch"){
Some(arch) => {
arch.split(";").collect::<Vec<_>>().join(",")
},
None => "arm64-v8a,armeabi-v7a,armeabi,x86,x86_64".to_string()
};
match HeaderValue::from_str(&arch) {
Ok(abis_header) => {
headers.insert(
"x-abis",
abis_header
);
},
Err(_) => {
println!("Invalid options specified, excluding device arch specification.");
}
}
headers.insert("x-gp", HeaderValue::from_static("1"));
headers
}
pub async fn download_apps(
apps: Vec<(String, Option<String>)>,
parallel: usize,
sleep_duration: u64,
outpath: &Path,
options: HashMap<&str, &str>,
) {
let mp = Rc::new(MultiProgress::new());
let http_client = Rc::new(reqwest::Client::new());
let app_arch = options.get("arch").cloned();
let headers = http_headers(&options);
let re = Rc::new(Regex::new(crate::consts::APKPURE_DOWNLOAD_URL_REGEX).unwrap());
futures_util::stream::iter(
apps.into_iter().map(|app| {
let (app_id, app_version) = app;
let http_client = Rc::clone(&http_client);
let re = Rc::clone(&re);
let headers = headers.clone();
let mp = Rc::clone(&mp);
let mp_log = Rc::clone(&mp);
async move {
let app_string = match (&app_version, app_arch) {
(None, None) => {
mp_log.suspend(|| println!("Downloading {}...", app_id));
app_id.to_string()
},
(None, Some(ref arch)) => {
mp_log.suspend(|| println!("Downloading {} arch {}...", app_id, arch));
format!("{}@{}", app_id, arch)
},
(Some(ref version), None) => {
mp_log.suspend(|| println!("Downloading {} version {}...", app_id, version));
format!("{}@{}", app_id, version)
},
(Some(ref version), Some(ref arch)) => {
mp_log.suspend(|| println!("Downloading {} version {} arch {}...", app_id, version, arch));
format!("{}@{}@{}", app_id, version, arch)
},
};
if sleep_duration > 0 {
sleep(TokioDuration::from_millis(sleep_duration)).await;
}
let versions_url = Url::parse(&format!("{}{}", crate::consts::APKPURE_VERSIONS_URL_FORMAT, app_id)).unwrap();
let versions_response = http_client
.get(versions_url)
.headers(headers)
.send().await.unwrap();
if let Some(app_version) = app_version {
let regex_string = format!("[[:^digit:]]{}:(?s:.)+?{}", regex::escape(&app_version), crate::consts::APKPURE_DOWNLOAD_URL_REGEX);
let re = Regex::new(®ex_string).unwrap();
download_from_response(versions_response, Box::new(Box::new(re)), app_string, outpath, mp).await;
} else {
download_from_response(versions_response, Box::new(re), app_string, outpath, mp).await;
}
}
})
).buffer_unordered(parallel).collect::<Vec<()>>().await;
}
async fn download_from_response(response: Response, re: Box<dyn Deref<Target=Regex>>, app_string: String, outpath: &Path, mp: Rc<MultiProgress>) {
let mp_log = Rc::clone(&mp);
let mp = Rc::clone(&mp);
match response.status() {
reqwest::StatusCode::OK => {
let body = response.text().await.unwrap();
match re.captures(&body) {
Some(caps) if caps.len() >= 2 => {
let apk_xapk = caps.get(1).unwrap().as_str();
let download_url = caps.get(2).unwrap().as_str();
let fname = match apk_xapk {
"XAPKJ" => format!("{}.xapk", app_string),
_ => format!("{}.apk", app_string),
};
match AsyncDownload::new(download_url, Path::new(outpath), &fname).get().await {
Ok(mut dl) => {
let length = dl.length();
let cb = match length {
Some(length) => Some(progress_wrapper(mp)(fname.clone(), length)),
None => None,
};
match dl.download(&cb).await {
Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)),
Err(err) if matches!(err.kind(), TDSTDErrorKind::FileExists) => {
mp_log.println(format!("File already exists for {}. Skipping...", app_string)).unwrap();
},
Err(err) if matches!(err.kind(), TDSTDErrorKind::PermissionDenied) => {
mp_log.println(format!("Permission denied when attempting to write file for {}. Skipping...", app_string)).unwrap();
},
Err(_) => {
mp_log.println(format!("An error has occurred attempting to download {}. Retry #1...", app_string)).unwrap();
match AsyncDownload::new(download_url, Path::new(outpath), &fname).download(&cb).await {
Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)),
Err(_) => {
mp_log.println(format!("An error has occurred attempting to download {}. Retry #2...", app_string)).unwrap();
match AsyncDownload::new(download_url, Path::new(outpath), &fname).download(&cb).await {
Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)),
Err(_) => {
mp_log.println(format!("An error has occurred attempting to download {}. Skipping...", app_string)).unwrap();
}
}
}
}
}
}
},
Err(_) => {
mp_log.println(format!("Invalid response for {}. Skipping...", app_string)).unwrap();
}
}
},
_ => {
mp_log.println(format!("Could not get download URL for {}. Skipping...", app_string)).unwrap();
}
}
},
_ => {
mp_log.println(format!("Invalid app response for {}. Skipping...", app_string)).unwrap();
}
}
}
pub async fn list_versions(apps: Vec<(String, Option<String>)>, options: HashMap<&str, &str>) {
let http_client = Rc::new(reqwest::Client::new());
let re = Rc::new(Regex::new(r"([[:alnum:]\.-]+):\([[:xdigit:]]{40,}").unwrap());
let headers = http_headers(&options);
let output_format = match options.get("output_format") {
Some(val) if val.to_lowercase() == "json" => OutputFormat::Json,
_ => OutputFormat::Plaintext,
};
let json_root = Rc::new(RefCell::new(match output_format {
OutputFormat::Json => Some(HashMap::new()),
_ => None,
}));
for app in apps {
let (app_id, _) = app;
let http_client = Rc::clone(&http_client);
let re = Rc::clone(&re);
let json_root = Rc::clone(&json_root);
let output_format = output_format.clone();
let headers = headers.clone();
async move {
if output_format.is_plaintext() {
println!("Versions available for {} on APKPure:", app_id);
}
let versions_url = Url::parse(&format!("{}{}", crate::consts::APKPURE_VERSIONS_URL_FORMAT, app_id)).unwrap();
let versions_response = http_client
.get(versions_url)
.headers(headers)
.send().await.unwrap();
match versions_response.status() {
reqwest::StatusCode::OK => {
let body = versions_response.text().await.unwrap();
let mut versions = HashSet::new();
for caps in re.captures_iter(&body) {
if caps.len() >= 2 {
versions.insert(caps.get(1).unwrap().as_str().to_string());
}
}
let mut versions = versions.drain().collect::<Vec<String>>();
versions.sort();
match output_format {
OutputFormat::Plaintext => {
println!("| {}", versions.join(", "));
},
OutputFormat::Json => {
let mut app_root: HashMap<String, Vec<HashMap<String, String>>> = HashMap::new();
app_root.insert("available_versions".to_string(), versions.into_iter().map(|v| {
let mut version_map = HashMap::new();
version_map.insert("version".to_string(), v);
version_map
}).collect());
json_root.borrow_mut().as_mut().unwrap().insert(app_id.to_string(), json!(app_root));
},
}
}
_ => {
match output_format {
OutputFormat::Plaintext => {
eprintln!("| Invalid app response for {}. Skipping...", app_id);
},
OutputFormat::Json => {
let mut app_root = HashMap::new();
app_root.insert("error".to_string(), "Invalid app response.".to_string());
json_root.borrow_mut().as_mut().unwrap().insert(app_id.to_string(), json!(app_root));
},
}
}
}
}.await;
}
if output_format.is_json() {
println!("{{\"source\":\"APKPure\",\"apps\":{}}}", json!(*json_root));
};
}