11use std:: cell:: RefCell ;
22use std:: collections:: { HashMap , HashSet } ;
3- use std:: error :: Error ;
3+ use std:: ops :: Deref ;
44use std:: path:: Path ;
55use std:: rc:: Rc ;
66
77use futures_util:: StreamExt ;
88use indicatif:: MultiProgress ;
9+ use regex:: Regex ;
910use reqwest:: header:: { HeaderMap , HeaderValue } ;
1011use reqwest:: { Url , Response } ;
11- use serde_json:: { json, value:: Value } ;
12- use simple_error:: SimpleError ;
12+ use serde_json:: json;
1313use tokio_dl_stream_to_disk:: { AsyncDownload , error:: ErrorKind as TDSTDErrorKind } ;
1414use tokio:: time:: { sleep, Duration as TokioDuration } ;
1515
1616use crate :: util:: { OutputFormat , progress_bar:: progress_wrapper} ;
1717
1818fn 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
191168pub 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