#include #include #include #include "app_context.h" #include "storage.h" #include "network.h" #include "download.h" #include "player.h" #include "ui.h" #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #include "esp_heap_caps.h" static void fetch_task_fn(void* arg){ (void)arg; G.fetching=true; G.fetch_cancel_req=false; ESP_LOGI(TAG,"fetch task start"); // Try load cached seasons/episodes – instant offline support bool cache_loaded = load_cache(); if(cache_loaded){ ESP_LOGI(TAG,"Cache loaded: %d seasons, %d eps", G.season_cnt, G.ep_cnt); // Restore cur idx from last_slug if(strlen(G.last_slug)>0){ for(int i=0;i0){ s1=0; G.cur_season=G.eps[0].season; } G.cur_ep_idx=s1; } // Show cached data immediately G.fetching=false; ui_rebuild_episode_list(); tt_lvgl_lock(portMAX_DELAY); if(G.lbl_status) lv_label_set_text(G.lbl_status,"Loaded from cache, refreshing..."); tt_lvgl_unlock(); G.fetching=true; vTaskDelay(pdMS_TO_TICKS(200)); } else { ESP_LOGI(TAG,"No cache found, will fetch from network"); } // Network fetch – updates if possible, otherwise keeps cache bool fetch_ok = fetch_data(); if(G.fetch_cancel_req){ ESP_LOGI(TAG,"fetch task canceled, exiting"); G.fetching=false; G.fetch_handle=NULL; vTaskDelete(NULL); return; } if(fetch_ok){ ESP_LOGI(TAG,"Network fetch ok: %d seasons, %d eps – saving cache", G.season_cnt, G.ep_cnt); save_cache(); } else { if(cache_loaded){ ESP_LOGW(TAG,"Network fetch failed, restoring cache"); // fetch_data overwrote with fallback, restore cache load_cache(); } else { ESP_LOGW(TAG,"Network fetch failed and no cache – using fallback"); } } ESP_LOGI(TAG,"fetch done seasons=%d eps=%d cache=%d fetch_ok=%d",G.season_cnt,G.ep_cnt,cache_loaded,fetch_ok); vTaskDelay(pdMS_TO_TICKS(300)); // Resolve current episode index – preserve user season selection if cache was already shown if(cache_loaded){ // User may have changed season while network fetch was in progress – keep their selection if it still exists int cur_s = G.cur_season; bool season_exists=false; for(int i=0;i0){ for(int i=0;i0){ for(int i=0;i0){ G.cur_ep_idx=0; G.cur_season=G.eps[0].season; } } } else { // First launch – use last_slug to restore position if(strlen(G.last_slug)>0){ for(int i=0;i0){ s1=0; G.cur_season=G.eps[0].season; } G.cur_ep_idx=s1; } } G.fetching=false; tt_lvgl_lock(portMAX_DELAY); if(G.fetch_cancel_req){ tt_lvgl_unlock(); G.fetch_handle=NULL; vTaskDelete(NULL); return; } tt_lvgl_unlock(); ui_rebuild_episode_list(); if(G.ep_cnt>0 && G.cur_ep_idx>=0){ EpInfo* ep=&G.eps[G.cur_ep_idx]; int saved = get_saved_pos_for_slug(ep->slug); if(saved==0) saved = G.pos_sec; if(saved>0) G.pos_sec=saved; } tt_lvgl_lock(portMAX_DELAY); if(G.lbl_status){ if(G.ep_cnt>0){ if(fetch_ok) lv_label_set_text(G.lbl_status, "Select episode to play / download"); else if(cache_loaded) lv_label_set_text(G.lbl_status, "Offline, using cached data"); else lv_label_set_text(G.lbl_status,"No network – limited data"); } else { lv_label_set_text(G.lbl_status,"No data – check network"); } } tt_lvgl_unlock(); G.fetch_handle=NULL; save_state(); vTaskDelete(NULL); } static void onShow(AppHandle app, void* data, lv_obj_t* parent){ (void)app; (void)data; (void)parent; memset(&G,0,sizeof(G)); G.pos_sec=0; G.total_sec=0; G.cur_season=1; G.cur_ep_idx=-1; G.pending_dl_idx=-1; G.volume=80; G.last_pct=-1; G.dl_last_pct=-1; G.dl_expected=-1; G.fetch_cancel_req=false; G.dl_cancel_req=false; ensure_dir(); load_state(); build_ui(parent); size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); size_t free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); ESP_LOGI(TAG, "onShow heap before fetch task: internal=%d psram=%d", free_internal, free_psram); BaseType_t res = xTaskCreate(fetch_task_fn,"dm_fetch",12288,NULL,5,&G.fetch_handle); ESP_LOGI(TAG, "xTaskCreate fetch_task res=%d handle=%p", res, G.fetch_handle); if (res != pdPASS) { tt_lvgl_lock(portMAX_DELAY); if (G.lbl_status) lv_label_set_text(G.lbl_status, "Failed to create fetch task"); tt_lvgl_unlock(); } } static void onResult(AppHandle app, void* data, AppLaunchId launchId, AppResult result, BundleHandle resultData){ (void)app; (void)data; (void)launchId; ESP_LOGI(TAG, "onResult: result=%d pending_idx=%d cur_idx=%d", result, G.pending_dl_idx, G.cur_ep_idx); // Check if result contains timestamp from Mp3Player if (resultData) { int32_t pos = 0; bool has_pos = false; if (tt_bundle_opt_int32(resultData, "pos_sec", &pos) || tt_bundle_opt_int32(resultData, "timestamp", &pos) || tt_bundle_opt_int32(resultData, "position", &pos)) { has_pos = true; } char file_path[512]={0}; bool has_file = tt_bundle_opt_string(resultData, "file", file_path, sizeof(file_path)) || tt_bundle_opt_string(resultData, "path", file_path, sizeof(file_path)); if (has_pos) { ESP_LOGI(TAG, "Mp3Player returned pos %d sec for file %s", pos, has_file?file_path:"unknown"); int target_idx = -1; if (has_file && strlen(file_path)>0) { for(int i=0;i=0 && G.pending_dl_idx < G.ep_cnt) target_idx = G.pending_dl_idx; if (target_idx<0 && G.cur_ep_idx>=0) target_idx = G.cur_ep_idx; if (target_idx>=0) { bool finished = false; tt_bundle_opt_bool(resultData, "finished", &finished); int32_t total_sec = 0; tt_bundle_opt_int32(resultData, "total_sec", &total_sec); if (finished || (total_sec>0 && pos >= total_sec-10)) { set_saved_pos_for_slug(G.eps[target_idx].slug, 0); G.pos_sec = 0; ESP_LOGI(TAG, "Episode %s finished, clearing pos", G.eps[target_idx].slug); } else { set_saved_pos_for_slug(G.eps[target_idx].slug, pos); G.pos_sec = pos; ESP_LOGI(TAG, "Saved pos %d for %s", pos, G.eps[target_idx].slug); } save_state(); ui_rebuild_episode_list(); tt_lvgl_lock(portMAX_DELAY); if (G.lbl_status) { if (pos>5) lv_label_set_text(G.lbl_status,"Progress saved – tap episode to resume"); else lv_label_set_text(G.lbl_status,"Select episode to play / download"); } tt_lvgl_unlock(); } G.pending_dl_idx = -1; return; } } // SdDownloader batch result handling if (G.pending_dl_idx <0 || G.pending_dl_idx >= G.ep_cnt) { if (resultData) { int32_t succ=0, fail=0, total=0; if (tt_bundle_opt_int32(resultData, "success_count", &succ) || tt_bundle_opt_int32(resultData, "total_count", &total)) { tt_bundle_opt_int32(resultData, "fail_count", &fail); ESP_LOGI(TAG, "Batch DL result: success %ld fail %ld total %ld", (long)succ, (long)fail, (long)total); for(int i=0;i=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"); else lv_label_set_text(G.lbl_status, "DL failed – tap to retry"); } 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); } } static void onHide(AppHandle app, void* data){ (void)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"); tt_lvgl_lock(portMAX_DELAY); for(int i=0;i<20 && G.fetch_handle!=NULL; i++){ tt_lvgl_unlock(); vTaskDelay(pdMS_TO_TICKS(100)); tt_lvgl_lock(portMAX_DELAY); } if(G.fetch_handle){ ESP_LOGW(TAG,"onHide: force deleting fetch task (leak risk)"); vTaskDelete(G.fetch_handle); G.fetch_handle=NULL; } G.fetching=false; tt_lvgl_unlock(); } if(G.dl_handle){ ESP_LOGI(TAG,"onHide: waiting dl to exit"); tt_lvgl_lock(portMAX_DELAY); for(int i=0;i<30 && G.dl_handle!=NULL; i++){ tt_lvgl_unlock(); vTaskDelay(pdMS_TO_TICKS(100)); tt_lvgl_lock(portMAX_DELAY); } if(G.dl_handle){ ESP_LOGW(TAG,"onHide: force deleting dl task after timeout"); vTaskDelete(G.dl_handle); G.dl_handle=NULL; G.downloading=false; if(G.dl_overlay) hide_dl_overlay(); } tt_lvgl_unlock(); } tt_lvgl_lock(portMAX_DELAY); wait_play_exit(); ui_deinit(); close_overlay(); if(G.dl_overlay) hide_dl_overlay(); if(G.root){ lv_obj_delete(G.root); G.root = NULL; G.lbl_title = NULL; G.lbl_meta = NULL; G.lbl_season = NULL; G.ep_list_cont = NULL; G.lbl_time = NULL; G.lbl_status = NULL; G.btn_play = NULL; G.btn_seasons = NULL; G.btn_dl_missing = NULL; G.season_list_cont = NULL; G.bar = NULL; } tt_lvgl_unlock(); save_state(); } int main(int argc, char* argv[]){ (void)argc; (void)argv; tt_app_register((AppRegistration){ .onShow=onShow, .onHide=onHide, .onResult=onResult }); return 0; }