#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" // Preserved browsing season that survives G memset across external app launch/return // This fixes season reset to 1 when returning from player/downloader where file read may race static int s_preserved_season = -1; void dm_preserve_season(int season){ if(season>0) s_preserved_season = season; } int dm_consume_preserved_season(void){ int s = s_preserved_season; s_preserved_season = -1; return s; } static void fetch_task_fn(void* arg){ (void)arg; G.fetching=true; G.fetch_cancel_req=false; ESP_LOGI(TAG,"fetch task start (manual refresh)"); tt_lvgl_lock(portMAX_DELAY); if(G.lbl_status) lv_label_set_text(G.lbl_status,"Fetching from network..."); tt_lvgl_unlock(); // Preserve user's current season selection across fetch int preserved_season = G.cur_season; char preserved_slug[96]={0}; if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt){ strncpy(preserved_slug, G.eps[G.cur_ep_idx].slug, sizeof(preserved_slug)-1); } else if(strlen(G.last_slug)>0){ strncpy(preserved_slug, G.last_slug, sizeof(preserved_slug)-1); } bool fetch_ok = fetch_data(); if(G.fetch_cancel_req){ ESP_LOGI(TAG,"fetch task canceled"); 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 { ESP_LOGW(TAG,"Network fetch failed"); // Do not overwrite current data if fetch fails – keep existing cache in memory // If we have no data at all, fetch_data already created fallback } ESP_LOGI(TAG,"fetch done seasons=%d eps=%d fetch_ok=%d", G.season_cnt, G.ep_cnt, fetch_ok); // Restore preserved season if it still exists if(preserved_season>0){ 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 { if(G.cur_ep_idx<0 && G.ep_cnt>0){ G.cur_ep_idx=0; G.cur_season=G.eps[0].season; } } 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, "Refreshed – select episode"); else lv_label_set_text(G.lbl_status, "Refresh failed – using cache"); } else { lv_label_set_text(G.lbl_status,"No data – check network"); } } tt_lvgl_unlock(); G.fetch_handle=NULL; save_state(); vTaskDelete(NULL); } void trigger_manual_refresh(void){ if(G.fetching){ ESP_LOGI(TAG,"refresh already in progress"); return; } if(G.fetch_handle){ ESP_LOGW(TAG,"fetch handle still present, not starting new"); return; } G.fetch_cancel_req=false; 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,"trigger_manual_refresh heap: 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 manual refresh 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 start refresh"); tt_lvgl_unlock(); G.fetch_handle=NULL; } } static void onShow(AppHandle app, void* data, lv_obj_t* parent){ (void)app; (void)data; (void)parent; // Check for preserved season from external app launch (survives G memset) int preserved = dm_consume_preserved_season(); if(preserved>0){ ESP_LOGI(TAG,"onShow preserved season S%02d from previous launch", preserved); } 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(); // If we have a preserved season from launching player/downloader, it overrides file if(preserved>0){ G.cur_season = preserved; ESP_LOGI(TAG,"onShow overriding cur_season with preserved S%02d", preserved); } build_ui(parent); // Instant cache load – no network, fully synchronous for fast startup // NOTE: load_state() already loaded last_season (browsing season) and last_slug (last played) // We want to preserve browsing season, NOT overwrite it with last_slug's season. bool cache_loaded = load_cache(); if(cache_loaded){ ESP_LOGI(TAG,"Cache loaded instantly: %d seasons, %d eps browsing S%02d last_slug=%s preserved=%d", G.season_cnt, G.ep_cnt, G.cur_season, G.last_slug, preserved); // Validate browsing season still exists, otherwise fallback to first season bool season_exists=false; for(int i=0;i0){ for(int i=0;i0){ ESP_LOGW(TAG,"Browsing season S%02d not found in cache, fallback to S%02d", G.cur_season, G.seasons[0].num); G.cur_season = G.seasons[0].num; season_exists=true; } } // Resolve cur_ep_idx: prefer last_slug if it is within browsing season, otherwise first ep in browsing season G.cur_ep_idx=-1; if(strlen(G.last_slug)>0 && season_exists){ for(int i=0;i0){ G.cur_ep_idx=0; G.cur_season=G.eps[0].season; } ui_rebuild_episode_list(); tt_lvgl_lock(portMAX_DELAY); if(G.lbl_status) lv_label_set_text(G.lbl_status,"Loaded from cache – tap Refresh to update"); tt_lvgl_unlock(); } else { ESP_LOGI(TAG,"No cache at startup, cur_season S%02d preserved %d", G.cur_season, preserved); tt_lvgl_lock(portMAX_DELAY); if(G.lbl_status) lv_label_set_text(G.lbl_status,"No cache – tap Refresh"); tt_lvgl_unlock(); ui_rebuild_episode_list(); } // Final safeguard: if preserved was set, ensure it is saved back to state file so next open keeps it if(preserved>0){ save_state(); } 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 after cache load: internal=%d psram=%d cache=%d cur_season=%d", free_internal, free_psram, cache_loaded, G.cur_season); } 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.btn_refresh = 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; }