From 7631ef29fa35ca7021b2047a0dd54ee5066af3bb Mon Sep 17 00:00:00 2001 From: qfm_ky Date: Thu, 13 Aug 2026 15:12:36 +0800 Subject: [PATCH] Surface Google Play download errors and exit non-zero on failure `MultiProgress::println()` routes through indicatif's draw target, and `ProgressDrawTarget::term()` swaps in a hidden target whenever stderr is not a color-capable terminal. A hidden target drops the message and still returns `Ok`, so every Google Play error was discarded without a trace whenever output was piped, redirected, or run under CI, cron, or Docker without a TTY. The success messages were unaffected only because they go through `suspend()`, which runs its closure even when the draw target is hidden. Route the error messages through `suspend()` + `eprintln!` so they survive in non-interactive contexts while staying on stderr, where the draw target had been sending them all along. Also propagate a failure count out of `download_apps()` so that a failed run exits non-zero, which lets callers detect failures at all. An already-present file counts as success; unavailable apps, permission errors, exhausted retries, and version-pinned requests count as failures. Finally, reword the `InvalidApp` message: the API returns that same kind for several conditions that cannot be told apart from the response -- the app may be paid, nonexistent, restricted to particular accounts, region-locked, or incompatible with the selected device. Co-Authored-By: Claude Fable 5 --- src/download_sources/google_play.rs | 83 +++++++++++++++++------------ src/main.rs | 6 ++- 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/src/download_sources/google_play.rs b/src/download_sources/google_play.rs index 582238f..36f1d70 100644 --- a/src/download_sources/google_play.rs +++ b/src/download_sources/google_play.rs @@ -20,7 +20,7 @@ pub async fn download_apps( outpath: &Path, accept_tos: bool, mut options: HashMap<&str, &str>, -) { +) -> usize { let device = options.remove("device").unwrap_or("px_9a"); let split_apk = match options.remove("split_apk") { Some(val) if val == "1" || val.to_lowercase() == "true" => true, @@ -81,7 +81,7 @@ pub async fn download_apps( let mp = Rc::new(MultiProgress::new()); let gpa = Rc::new(gpa); - futures_util::stream::iter( + let results = futures_util::stream::iter( apps.into_iter().map(|app| { let (app_id, app_version) = app; let gpa = Rc::clone(&gpa); @@ -91,47 +91,62 @@ pub async fn download_apps( let mp_log = Rc::clone(&mp); async move { - if app_version.is_none() { - mp_log.suspend(|| println!("Downloading {}...", app_id)); - if sleep_duration > 0 { - sleep(TokioDuration::from_millis(sleep_duration)).await; + if let Some(app_version) = app_version { + mp_log.suspend(|| eprintln!("Specific versions can not be downloaded from Google Play ({}@{}). Skipping...", app_id, app_version)); + return false; + } + mp_log.suspend(|| println!("Downloading {}...", app_id)); + if sleep_duration > 0 { + sleep(TokioDuration::from_millis(sleep_duration)).await; + } + match gpa.download(&app_id, None, split_apk, include_dex_metadata, include_additional_files, Path::new(outpath), Some(&progress_wrapper(mp_dl1))).await { + Ok(_) => { + mp_log.suspend(|| println!("{} downloaded successfully!", app_id)); + true } - match gpa.download(&app_id, None, split_apk, include_dex_metadata, include_additional_files, Path::new(outpath), Some(&progress_wrapper(mp_dl1))).await { - Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_id)), - Err(err) if matches!(err.kind(), GpapiErrorKind::FileExists) => { - mp_log.println(format!("File already exists for {}. Skipping...", app_id)).unwrap(); - } - Err(err) if matches!(err.kind(), GpapiErrorKind::DirectoryExists) => { - mp_log.println(format!("Split APK directory already exists for {}. Skipping...", app_id)).unwrap(); - } - Err(err) if matches!(err.kind(), GpapiErrorKind::InvalidApp) => { - mp_log.println(format!("Invalid app response for {}. Skipping...", app_id)).unwrap(); - } - Err(err) if matches!(err.kind(), GpapiErrorKind::PermissionDenied) => { - mp_log.println(format!("Permission denied when attempting to write file for {}. Skipping...", app_id)).unwrap(); - } - Err(_) => { - mp_log.println(format!("An error has occurred attempting to download {}. Retry #1...", app_id)).unwrap(); - match gpa.download(&app_id, None, split_apk, include_dex_metadata, include_additional_files, Path::new(outpath), Some(&progress_wrapper(mp_dl2))).await { - Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_id)), - Err(_) => { - mp_log.println(format!("An error has occurred attempting to download {}. Retry #2...", app_id)).unwrap(); - match gpa.download(&app_id, None, split_apk, include_dex_metadata, include_additional_files, Path::new(outpath), Some(&progress_wrapper(mp_dl3))).await { - Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_id)), - Err(_) => { - mp_log.println(format!("An error has occurred attempting to download {}. Skipping...", app_id)).unwrap(); - } + Err(err) if matches!(err.kind(), GpapiErrorKind::FileExists) => { + mp_log.suspend(|| eprintln!("File already exists for {}. Skipping...", app_id)); + true + } + Err(err) if matches!(err.kind(), GpapiErrorKind::DirectoryExists) => { + mp_log.suspend(|| eprintln!("Split APK directory already exists for {}. Skipping...", app_id)); + true + } + Err(err) if matches!(err.kind(), GpapiErrorKind::InvalidApp) => { + mp_log.suspend(|| eprintln!("Could not download {}. The app may be paid, nonexistent, restricted to certain accounts, unavailable in this region, or incompatible with the selected device. Skipping...", app_id)); + false + } + Err(err) if matches!(err.kind(), GpapiErrorKind::PermissionDenied) => { + mp_log.suspend(|| eprintln!("Permission denied when attempting to write file for {}. Skipping...", app_id)); + false + } + Err(_) => { + mp_log.suspend(|| eprintln!("An error has occurred attempting to download {}. Retry #1...", app_id)); + match gpa.download(&app_id, None, split_apk, include_dex_metadata, include_additional_files, Path::new(outpath), Some(&progress_wrapper(mp_dl2))).await { + Ok(_) => { + mp_log.suspend(|| println!("{} downloaded successfully!", app_id)); + true + } + Err(_) => { + mp_log.suspend(|| eprintln!("An error has occurred attempting to download {}. Retry #2...", app_id)); + match gpa.download(&app_id, None, split_apk, include_dex_metadata, include_additional_files, Path::new(outpath), Some(&progress_wrapper(mp_dl3))).await { + Ok(_) => { + mp_log.suspend(|| println!("{} downloaded successfully!", app_id)); + true + } + Err(_) => { + mp_log.suspend(|| eprintln!("An error has occurred attempting to download {}. Skipping...", app_id)); + false } } } } } - } else { - mp_log.println(format!("Specific versions can not be downloaded from Google Play ({}@{}). Skipping...", app_id, app_version.unwrap())).unwrap(); } } }) - ).buffer_unordered(parallel).collect::>().await; + ).buffer_unordered(parallel).collect::>().await; + results.into_iter().filter(|&success| !success).count() } pub async fn request_aas_token( diff --git a/src/main.rs b/src/main.rs index 8889119..1098a1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -376,7 +376,7 @@ async fn main() { } } - google_play::download_apps( + let failures = google_play::download_apps( list, parallel, sleep_duration, @@ -388,6 +388,10 @@ async fn main() { options, ) .await; + if failures > 0 { + eprintln!("{} app(s) could not be downloaded.", failures); + std::process::exit(1); + } } } DownloadSource::FDroid => {