Skip to content

Commit 8277422

Browse files
committed
Reverting to old APKPure endpoint but preserve ability to specify architecture (fixes #219)
1 parent 7cb029f commit 8277422

3 files changed

Lines changed: 86 additions & 132 deletions

File tree

USAGE-apkpure.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,3 @@ You can also list the versions available, either specifying a specific architect
2828
apkeep -l -a com.instagram.android -o 'arch=x86'
2929
apkeep -l -a com.instagram.android
3030
```
31-
32-
To specify a preferred language or Android API version, use the `language=` or `os_ver` options. The following shows the defaults:
33-
34-
```shell
35-
apkeep -a com.instagram.android -o 'language=en-US,os_ver=35' .
36-
```

src/consts.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
pub const APKPURE_VERSIONS_URL_FORMAT: &str = "https://tapi.pureapk.com/v3/get_app_his_version?hl=en&package_name=";
1+
pub const APKPURE_VERSIONS_URL_FORMAT: &str = "https://api.pureapk.com/m/v3/cms/app_version?hl=en-US&package_name=";
2+
pub const APKPURE_DOWNLOAD_URL_REGEX: &str = r"(X?APKJ)..(https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))";
23
pub const FDROID_REPO: &str = "https://f-droid.org/repo";
34
pub const FDROID_INDEX_FINGERPRINT: &[u8] = &[67, 35, 141, 81, 44, 30, 94, 178, 214, 86, 159, 74, 58, 251, 245, 82, 52, 24, 184, 46, 10, 62, 209, 85, 39, 112, 171, 185, 169, 201, 204, 171];
45
pub const FDROID_SIGNATURE_BLOCK_FILE_REGEX: &str = r"^META-INF/.*\.(DSA|EC|RSA)$";

src/download_sources/apkpure.rs

Lines changed: 84 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,42 @@
11
use std::cell::RefCell;
22
use std::collections::{HashMap, HashSet};
3-
use std::error::Error;
3+
use std::ops::Deref;
44
use std::path::Path;
55
use std::rc::Rc;
66

77
use futures_util::StreamExt;
88
use indicatif::MultiProgress;
9+
use regex::Regex;
910
use reqwest::header::{HeaderMap, HeaderValue};
1011
use reqwest::{Url, Response};
11-
use serde_json::{json, value::Value};
12-
use simple_error::SimpleError;
12+
use serde_json::json;
1313
use tokio_dl_stream_to_disk::{AsyncDownload, error::ErrorKind as TDSTDErrorKind};
1414
use tokio::time::{sleep, Duration as TokioDuration};
1515

1616
use crate::util::{OutputFormat, progress_bar::progress_wrapper};
1717

1818
fn http_headers(options: &HashMap<&str, &str>) -> HeaderMap {
1919
let mut headers = HeaderMap::new();
20-
headers.insert(
21-
"user-agent",
22-
HeaderValue::from_static("Dalvik/2.1.0 (Linux; U; Android 15; Pixel 4a (5G) Build/BP1A.250505.005); APKPure/3.20.53 (Aegon)")
23-
);
24-
headers.insert("ual-access-businessid", HeaderValue::from_static("projecta"));
25-
let abis = match options.get("arch"){
20+
headers.insert("x-cv", HeaderValue::from_static("3172501"));
21+
headers.insert("x-sv", HeaderValue::from_static("29"));
22+
let arch = match options.get("arch"){
2623
Some(arch) => {
27-
let arch_vec: Vec<&str> = arch.split(";").collect();
28-
json!(arch_vec).to_string()
24+
arch.split(";").collect::<Vec<_>>().join(",")
2925
},
30-
None => "[\"arm64-v8a\",\"armeabi-v7a\",\"armeabi\",\"x86\",\"x86_64\"]".to_string()
26+
None => "arm64-v8a,armeabi-v7a,armeabi,x86,x86_64".to_string()
3127
};
32-
let language = match options.get("language") {
33-
Some(language) => json!(language).to_string(),
34-
None => "\"en-US\"".to_string()
35-
};
36-
let os_ver = match options.get("os_ver") {
37-
Some(os_ver) => json!(os_ver).to_string(),
38-
None => "\"35\"".to_string()
39-
};
40-
let device_info = format!("{{\"device_info\":{{\"abis\":{},\"language\":{},\"os_ver\":{}}}", abis, language, os_ver);
41-
match HeaderValue::from_str(&device_info) {
42-
Ok(device_info_header) => {
28+
match HeaderValue::from_str(&arch) {
29+
Ok(abis_header) => {
4330
headers.insert(
44-
"ual-access-projecta",
45-
device_info_header
31+
"x-abis",
32+
abis_header
4633
);
4734
},
4835
Err(_) => {
4936
println!("Invalid options specified, excluding device arch specification.");
5037
}
5138
}
39+
headers.insert("x-gp", HeaderValue::from_static("1"));
5240
headers
5341
}
5442

@@ -63,11 +51,13 @@ pub async fn download_apps(
6351
let http_client = Rc::new(reqwest::Client::new());
6452
let app_arch = options.get("arch").cloned();
6553
let headers = http_headers(&options);
54+
let re = Rc::new(Regex::new(crate::consts::APKPURE_DOWNLOAD_URL_REGEX).unwrap());
6655

6756
futures_util::stream::iter(
6857
apps.into_iter().map(|app| {
6958
let (app_id, app_version) = app;
7059
let http_client = Rc::clone(&http_client);
60+
let re = Rc::clone(&re);
7161
let headers = headers.clone();
7262
let mp = Rc::clone(&mp);
7363
let mp_log = Rc::clone(&mp);
@@ -98,98 +88,86 @@ pub async fn download_apps(
9888
.get(versions_url)
9989
.headers(headers)
10090
.send().await.unwrap();
101-
if let Err(err) = download_from_response(versions_response, app_string, app_version, outpath, mp).await {
102-
mp_log.println(format!("{}", err)).unwrap();
91+
if let Some(app_version) = app_version {
92+
let regex_string = format!("[[:^digit:]]{}:(?s:.)+?{}", regex::escape(&app_version), crate::consts::APKPURE_DOWNLOAD_URL_REGEX);
93+
let re = Regex::new(&regex_string).unwrap();
94+
download_from_response(versions_response, Box::new(Box::new(re)), app_string, outpath, mp).await;
95+
} else {
96+
download_from_response(versions_response, Box::new(re), app_string, outpath, mp).await;
10397
}
10498
}
10599
})
106100
).buffer_unordered(parallel).collect::<Vec<()>>().await;
107101
}
108102

109-
async fn download_from_response(response: Response, app_string: String, app_version: Option<String>, outpath: &Path, mp: Rc<MultiProgress>) -> Result<(), Box<dyn Error>> {
103+
async fn download_from_response(response: Response, re: Box<dyn Deref<Target=Regex>>, app_string: String, outpath: &Path, mp: Rc<MultiProgress>) {
110104
let mp_log = Rc::clone(&mp);
111105
let mp = Rc::clone(&mp);
112106
match response.status() {
113107
reqwest::StatusCode::OK => {
114-
let body_json = response.text().await?;
115-
let mut download_url = String::new();
116-
let mut fname = String::new();
117-
match serde_json::from_str::<Value>(&body_json){
118-
Ok(body) => {
119-
if let Some(Value::Array(version_list)) = body.get("version_list"){
120-
for version in version_list {
121-
let app_version = app_version.clone();
122-
if app_version.is_some() && app_version != version.get("version_name").map(|v| v.as_str().unwrap().to_string()) {
123-
continue
124-
}
125-
if let Some(Value::Object(asset)) = version.get("asset"){
126-
if let (Some(Value::String(url)), Some(Value::String(apk_type))) = (asset.get("url"), asset.get("type")) {
127-
download_url = url.to_string();
128-
if apk_type == "XAPK" {
129-
fname = format!("{}.xapk", app_string);
130-
} else {
131-
fname = format!("{}.apk", app_string);
132-
}
133-
break
134-
}
135-
}
136-
}
137-
}
138-
},
139-
Err(_) => {
140-
return Err(Box::new(SimpleError::new(format!("Invalid app JSON response for {}. Skipping...", app_string))));
141-
}
142-
}
143-
if download_url.is_empty(){
144-
return Err(Box::new(SimpleError::new(format!("No valid versions for {}. Skipping...", app_string))));
145-
}
146-
match AsyncDownload::new(&download_url, Path::new(outpath), &fname).get().await {
147-
Ok(mut dl) => {
148-
let length = dl.length();
149-
let cb = match length {
150-
Some(length) => Some(progress_wrapper(mp)(fname.clone(), length)),
151-
None => None,
108+
let body = response.text().await.unwrap();
109+
match re.captures(&body) {
110+
Some(caps) if caps.len() >= 2 => {
111+
let apk_xapk = caps.get(1).unwrap().as_str();
112+
let download_url = caps.get(2).unwrap().as_str();
113+
let fname = match apk_xapk {
114+
"XAPKJ" => format!("{}.xapk", app_string),
115+
_ => format!("{}.apk", app_string),
152116
};
153117

154-
match dl.download(&cb).await {
155-
Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)),
156-
Err(err) if matches!(err.kind(), TDSTDErrorKind::FileExists) => {
157-
mp_log.println(format!("File already exists for {}. Skipping...", app_string)).unwrap();
158-
},
159-
Err(err) if matches!(err.kind(), TDSTDErrorKind::PermissionDenied) => {
160-
mp_log.println(format!("Permission denied when attempting to write file for {}. Skipping...", app_string)).unwrap();
161-
},
162-
Err(_) => {
163-
mp_log.println(format!("An error has occurred attempting to download {}. Retry #1...", app_string)).unwrap();
164-
match AsyncDownload::new(&download_url, Path::new(outpath), &fname).download(&cb).await {
118+
match AsyncDownload::new(download_url, Path::new(outpath), &fname).get().await {
119+
Ok(mut dl) => {
120+
let length = dl.length();
121+
let cb = match length {
122+
Some(length) => Some(progress_wrapper(mp)(fname.clone(), length)),
123+
None => None,
124+
};
125+
126+
match dl.download(&cb).await {
165127
Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)),
128+
Err(err) if matches!(err.kind(), TDSTDErrorKind::FileExists) => {
129+
mp_log.println(format!("File already exists for {}. Skipping...", app_string)).unwrap();
130+
},
131+
Err(err) if matches!(err.kind(), TDSTDErrorKind::PermissionDenied) => {
132+
mp_log.println(format!("Permission denied when attempting to write file for {}. Skipping...", app_string)).unwrap();
133+
},
166134
Err(_) => {
167-
mp_log.println(format!("An error has occurred attempting to download {}. Retry #2...", app_string)).unwrap();
168-
match AsyncDownload::new(&download_url, Path::new(outpath), &fname).download(&cb).await {
135+
mp_log.println(format!("An error has occurred attempting to download {}. Retry #1...", app_string)).unwrap();
136+
match AsyncDownload::new(download_url, Path::new(outpath), &fname).download(&cb).await {
169137
Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)),
170138
Err(_) => {
171-
mp_log.println(format!("An error has occurred attempting to download {}. Skipping...", app_string)).unwrap();
139+
mp_log.println(format!("An error has occurred attempting to download {}. Retry #2...", app_string)).unwrap();
140+
match AsyncDownload::new(download_url, Path::new(outpath), &fname).download(&cb).await {
141+
Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)),
142+
Err(_) => {
143+
mp_log.println(format!("An error has occurred attempting to download {}. Skipping...", app_string)).unwrap();
144+
}
145+
}
172146
}
173147
}
174148
}
175149
}
150+
},
151+
Err(_) => {
152+
mp_log.println(format!("Invalid response for {}. Skipping...", app_string)).unwrap();
176153
}
177154
}
178-
Ok(())
179155
},
180-
Err(_) => {
181-
Err(Box::new(SimpleError::new(format!("Invalid response for {}. Skipping...", app_string))))
156+
_ => {
157+
mp_log.println(format!("Could not get download URL for {}. Skipping...", app_string)).unwrap();
182158
}
183159
}
160+
184161
},
185162
_ => {
186-
return Err(Box::new(SimpleError::new(format!("Invalid app response for {}. Skipping...", app_string))));
163+
mp_log.println(format!("Invalid app response for {}. Skipping...", app_string)).unwrap();
187164
}
188165
}
189166
}
190167

191168
pub async fn list_versions(apps: Vec<(String, Option<String>)>, options: HashMap<&str, &str>) {
192169
let http_client = Rc::new(reqwest::Client::new());
170+
let re = Rc::new(Regex::new(r"([[:alnum:]\.-]+):\([[:xdigit:]]{40,}").unwrap());
193171
let headers = http_headers(&options);
194172
let output_format = match options.get("output_format") {
195173
Some(val) if val.to_lowercase() == "json" => OutputFormat::Json,
@@ -203,6 +181,7 @@ pub async fn list_versions(apps: Vec<(String, Option<String>)>, options: HashMap
203181
for app in apps {
204182
let (app_id, _) = app;
205183
let http_client = Rc::clone(&http_client);
184+
let re = Rc::clone(&re);
206185
let json_root = Rc::clone(&json_root);
207186
let output_format = output_format.clone();
208187
let headers = headers.clone();
@@ -218,49 +197,29 @@ pub async fn list_versions(apps: Vec<(String, Option<String>)>, options: HashMap
218197

219198
match versions_response.status() {
220199
reqwest::StatusCode::OK => {
221-
let body_json = versions_response.text().await.unwrap();
222-
223-
match serde_json::from_str::<Value>(&body_json){
224-
Ok(body) => {
225-
let mut versions = HashSet::new();
226-
if let Some(Value::Array(version_list)) = body.get("version_list"){
227-
for version in version_list {
228-
if let Some(Value::String(version_name)) = version.get("version_name") {
229-
versions.insert(version_name.to_string());
230-
}
231-
}
232-
}
233-
let mut versions = versions.drain().collect::<Vec<String>>();
234-
versions.sort();
235-
versions.reverse();
236-
match output_format {
237-
OutputFormat::Plaintext => {
238-
println!("| {}", versions.join(", "));
239-
},
240-
OutputFormat::Json => {
241-
let mut app_root: HashMap<String, Vec<HashMap<String, String>>> = HashMap::new();
242-
app_root.insert("available_versions".to_string(), versions.into_iter().map(|v| {
243-
let mut version_map = HashMap::new();
244-
version_map.insert("version".to_string(), v);
245-
version_map
246-
}).collect());
247-
json_root.borrow_mut().as_mut().unwrap().insert(app_id.to_string(), json!(app_root));
248-
},
249-
}
250-
},
251-
Err(_) => {
252-
match output_format {
253-
OutputFormat::Plaintext => {
254-
eprintln!("| Invalid app JSON response for {}. Skipping...", app_id);
255-
},
256-
OutputFormat::Json => {
257-
let mut app_root = HashMap::new();
258-
app_root.insert("error".to_string(), "Invalid app JSON response.".to_string());
259-
json_root.borrow_mut().as_mut().unwrap().insert(app_id.to_string(), json!(app_root));
260-
}
261-
}
200+
let body = versions_response.text().await.unwrap();
201+
let mut versions = HashSet::new();
202+
for caps in re.captures_iter(&body) {
203+
if caps.len() >= 2 {
204+
versions.insert(caps.get(1).unwrap().as_str().to_string());
262205
}
263206
}
207+
let mut versions = versions.drain().collect::<Vec<String>>();
208+
versions.sort();
209+
match output_format {
210+
OutputFormat::Plaintext => {
211+
println!("| {}", versions.join(", "));
212+
},
213+
OutputFormat::Json => {
214+
let mut app_root: HashMap<String, Vec<HashMap<String, String>>> = HashMap::new();
215+
app_root.insert("available_versions".to_string(), versions.into_iter().map(|v| {
216+
let mut version_map = HashMap::new();
217+
version_map.insert("version".to_string(), v);
218+
version_map
219+
}).collect());
220+
json_root.borrow_mut().as_mut().unwrap().insert(app_id.to_string(), json!(app_root));
221+
},
222+
}
264223
}
265224
_ => {
266225
match output_format {

0 commit comments

Comments
 (0)