From eaccebc95da1273b70cc73085d5458b2f4027ee1 Mon Sep 17 00:00:00 2001 From: Adolfo Date: Sat, 25 Jul 2026 22:49:28 -0400 Subject: [PATCH] feat(DM+SdDDL): sequential full-season download queue, fix close crash, return true - DM: add dl_missing_* fields to AppCtx for sequential queue - DM player: count_missing_in_season, find_next_missing, download_episode, download_missing_season_start/next - DM main onResult: auto-continue queue when dl_missing_active, updates status DL X/Y, finishes with done ok/fail - SdDownloader: file exists returns true (Already on SD) with total=file size, not false - SdDownloader: set_result_and_close minimal bundle (only success bool) to avoid internal heap OOM crash on close - SdDownloader: Cancel button becomes Close after done, calls tt_app_stop() directly (manual close per request, auto-close unreliable) - Fixes crash on close observed in demo mode (file exists) and after download done Tested on 192.168.68.133: SdDownloader shows Already on SD + Close, DM shows S10 5/6 after sequential, no crash on open/close cycles --- .../main/Source/app_context.h | 7 ++ Apps/DiscoveryMountain/main/Source/main.c | 50 ++++++++++++ Apps/DiscoveryMountain/main/Source/player.c | 79 ++++++++++++++++--- Apps/DiscoveryMountain/main/Source/player.h | 7 ++ Apps/SdDownloader/main/Source/main.c | 41 ++++++---- 5 files changed, 157 insertions(+), 27 deletions(-) diff --git a/Apps/DiscoveryMountain/main/Source/app_context.h b/Apps/DiscoveryMountain/main/Source/app_context.h index 2e5b5b2..1763a3b 100644 --- a/Apps/DiscoveryMountain/main/Source/app_context.h +++ b/Apps/DiscoveryMountain/main/Source/app_context.h @@ -90,6 +90,13 @@ typedef struct { // External downloader integration (SdDownloader) int pending_dl_idx; + + // Full season download queue (sequential single-file to avoid batch crash) + bool dl_missing_active; + int dl_missing_season; + int dl_missing_total; + int dl_missing_done; + int dl_missing_failed; } AppCtx; extern AppCtx G; diff --git a/Apps/DiscoveryMountain/main/Source/main.c b/Apps/DiscoveryMountain/main/Source/main.c index 4951480..42648ec 100644 --- a/Apps/DiscoveryMountain/main/Source/main.c +++ b/Apps/DiscoveryMountain/main/Source/main.c @@ -297,6 +297,55 @@ static void onResult(AppHandle app, void* data, AppLaunchId launchId, AppResult save_state(); ui_rebuild_episode_list(); + // Handle sequential full-season download queue (safe single-file mode) + if(G.dl_missing_active){ + if(success) G.dl_missing_done++; + else G.dl_missing_failed++; + + int next = find_next_missing_in_season(G.dl_missing_season); + if(next!=-1){ + ESP_LOGI(TAG, "Batch queue continuing: %d/%d done, next idx %d", G.dl_missing_done, G.dl_missing_total, next); + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status){ + char msg[64]; + snprintf(msg,sizeof(msg),"DL %d/%d S%02d...", G.dl_missing_done+1, G.dl_missing_total, G.dl_missing_season); + lv_label_set_text(G.lbl_status, msg); + } + tt_lvgl_unlock(); + // Small delay to let UI update before launching next downloader + vTaskDelay(pdMS_TO_TICKS(300)); + if(download_episode(next)){ + // started next + return; + } else { + // failed to start next, abort queue + G.dl_missing_active=false; + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status){ + char msg[64]; + snprintf(msg,sizeof(msg),"DL stopped %d ok %d fail", G.dl_missing_done, G.dl_missing_failed); + lv_label_set_text(G.lbl_status, msg); + } + tt_lvgl_unlock(); + return; + } + } else { + // No more missing – queue finished + ESP_LOGI(TAG, "Batch queue finished: %d ok %d fail", G.dl_missing_done, G.dl_missing_failed); + G.dl_missing_active=false; + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status){ + char msg[64]; + snprintf(msg,sizeof(msg),"DL S%02d done %d ok %d fail", G.dl_missing_season, G.dl_missing_done, G.dl_missing_failed); + lv_label_set_text(G.lbl_status, msg); + } + tt_lvgl_unlock(); + if(success && idx>=0) ESP_LOGI(TAG, "Single DL success idx %d", idx); + else ESP_LOGW(TAG, "External DL failed for idx %d", idx); + return; + } + } + tt_lvgl_lock(portMAX_DELAY); if (G.lbl_status) { if (success) lv_label_set_text(G.lbl_status, "DL done – tap to play"); @@ -316,6 +365,7 @@ static void onHide(AppHandle app, void* data){ save_current_ep_pos(); G.fetch_cancel_req=true; G.dl_cancel_req=true; + G.dl_missing_active=false; if(G.fetch_handle){ ESP_LOGI(TAG,"onHide: waiting fetch to exit"); diff --git a/Apps/DiscoveryMountain/main/Source/player.c b/Apps/DiscoveryMountain/main/Source/player.c index b19c2ff..8d1a419 100644 --- a/Apps/DiscoveryMountain/main/Source/player.c +++ b/Apps/DiscoveryMountain/main/Source/player.c @@ -62,24 +62,81 @@ static bool try_external_downloader(int idx) { return true; } -static bool try_external_downloader_batch(int season) { - // Reverted to single-file mode – batch array caused crashes in SdDownloader - // Now just download first missing episode in season - int first_missing = -1; +int count_missing_in_season(int season){ + int cnt=0; for(int i=0;i=8){ - first_missing = i; - break; - } + if(G.eps[i].season==season && !G.eps[i].seen && strlen(G.eps[i].audio_url)>=8) cnt++; } - if(first_missing==-1){ + return cnt; +} +int find_next_missing_in_season(int season){ + for(int i=0;i=8) return i; + } + return -1; +} +bool download_episode(int idx){ + return try_external_downloader(idx); +} +bool download_missing_next(void){ + if(!G.dl_missing_active) return false; + int next = find_next_missing_in_season(G.dl_missing_season); + if(next==-1){ + // No more missing – done + G.dl_missing_active=false; + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status){ + char msg[64]; + snprintf(msg,sizeof(msg),"DL S%02d done %d ok %d fail", G.dl_missing_season, G.dl_missing_done, G.dl_missing_failed); + lv_label_set_text(G.lbl_status, msg); + } + tt_lvgl_unlock(); + return false; + } + // Update UI for next file + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status){ + char msg[64]; + snprintf(msg,sizeof(msg),"DL %d/%d S%02d...", G.dl_missing_done+1, G.dl_missing_total, G.dl_missing_season); + lv_label_set_text(G.lbl_status, msg); + } + tt_lvgl_unlock(); + return try_external_downloader(next); +} +bool download_missing_season_start(int season){ + int missing = count_missing_in_season(season); + if(missing==0){ tt_lvgl_lock(portMAX_DELAY); if(G.lbl_status) lv_label_set_text(G.lbl_status,"Season already cached"); tt_lvgl_unlock(); + G.dl_missing_active=false; return true; } - ESP_LOGI(TAG, "DL Missing (single-file revert) – downloading first missing S%02d idx %d", season, first_missing); - return try_external_downloader(first_missing); + ESP_LOGI(TAG, "Starting sequential DL for S%02d, %d missing", season, missing); + G.dl_missing_active=true; + G.dl_missing_season=season; + G.dl_missing_total=missing; + G.dl_missing_done=0; + G.dl_missing_failed=0; + int first = find_next_missing_in_season(season); + if(first==-1){ + G.dl_missing_active=false; + return true; + } + // Update status + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status){ + char msg[64]; + snprintf(msg,sizeof(msg),"DL 1/%d S%02d...", missing, season); + lv_label_set_text(G.lbl_status, msg); + } + tt_lvgl_unlock(); + return try_external_downloader(first); +} + +static bool try_external_downloader_batch(int season) { + // Sequential queue – safe single-file mode + return download_missing_season_start(season); } void start_idx_internal(int idx){ diff --git a/Apps/DiscoveryMountain/main/Source/player.h b/Apps/DiscoveryMountain/main/Source/player.h index 275745b..1a82a87 100644 --- a/Apps/DiscoveryMountain/main/Source/player.h +++ b/Apps/DiscoveryMountain/main/Source/player.h @@ -18,6 +18,13 @@ void go_next(void); void go_prev(void); bool download_season(int season); +// New single-file download API +bool download_episode(int idx); +bool download_missing_season_start(int season); +bool download_missing_next(void); // continue queue, returns false if done +int count_missing_in_season(int season); +int find_next_missing_in_season(int season); + #ifdef __cplusplus } #endif diff --git a/Apps/SdDownloader/main/Source/main.c b/Apps/SdDownloader/main/Source/main.c index 2206847..8db5bc5 100644 --- a/Apps/SdDownloader/main/Source/main.c +++ b/Apps/SdDownloader/main/Source/main.c @@ -90,24 +90,29 @@ static void update_ui_status(const char *txt) { static void set_result_and_close(bool success) { G.result_success = success; - BundleHandle result_bundle = tt_bundle_alloc(); + size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + BundleHandle result_bundle = NULL; + if (free_internal > 8000) { + result_bundle = tt_bundle_alloc(); + } if (result_bundle) { tt_bundle_put_bool(result_bundle, "success", success); - tt_bundle_put_string(result_bundle, "path", G.path); - tt_bundle_put_int32(result_bundle, "bytes", G.total); - if (!success) { - tt_lvgl_lock(portMAX_DELAY); - const char *status = G.lbl_status ? lv_label_get_text(G.lbl_status) : "failed"; - char status_copy[128]; - strncpy(status_copy, status, sizeof(status_copy)-1); - status_copy[sizeof(status_copy)-1]=0; - tt_lvgl_unlock(); - tt_bundle_put_string(result_bundle, "error", status_copy); - } + // Keep bundle minimal to avoid low heap crash (previous put_string path 512B caused OOM) tt_app_set_result(G.app_handle, success ? APP_RESULT_OK : APP_RESULT_ERROR, result_bundle); } else { + // Low heap – return OK without bundle (DM will still refresh via has_slug) + if (free_internal <= 8000) { + ESP_LOGW(TAG, "Low heap %d, returning result without bundle", free_internal); + } tt_app_set_result(G.app_handle, success ? APP_RESULT_OK : APP_RESULT_ERROR, NULL); } + // Change Cancel button to Close once done (manual close, auto-close unreliable) + tt_lvgl_lock(portMAX_DELAY); + if (G.btn_cancel) { + lv_obj_t* lbl = lv_obj_get_child(G.btn_cancel, 0); + if (lbl) lv_label_set_text(lbl, "Close"); + } + tt_lvgl_unlock(); } static bool download_one_file(const char *url, const char *path) { @@ -116,9 +121,10 @@ static bool download_one_file(const char *url, const char *path) { struct stat st; if (stat(path, &st)==0) { if (!G.override_existing) { - ESP_LOGW(TAG, "File exists and override=false: %s", path); - update_ui_status("File exists"); - return false; + ESP_LOGW(TAG, "File exists and override=false: %s (returning true as cached)", path); + G.total = st.st_size; + update_ui_status("Already on SD"); + return true; } else { ESP_LOGI(TAG, "File exists but override=true, overwriting %s", path); unlink(path); @@ -294,7 +300,10 @@ static void download_task(void *arg) { static void btn_cancel_cb(lv_event_t *e) { (void)e; - if (!G.downloading) { tt_app_stop(); return; } + if (!G.downloading) { + tt_app_stop(); + return; + } G.cancel_req = true; tt_lvgl_lock(portMAX_DELAY); if (G.lbl_status) lv_label_set_text(G.lbl_status, "Canceling...");