eaccebc95d
- 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
434 lines
15 KiB
C
434 lines
15 KiB
C
#include <tt_app.h>
|
||
#include <tt_lvgl.h>
|
||
#include <tt_bundle.h>
|
||
#include "app_context.h"
|
||
#include "storage.h"
|
||
#include "network.h"
|
||
#include "download.h"
|
||
#include "player.h"
|
||
#include "ui.h"
|
||
|
||
#include <string.h>
|
||
#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;i<G.ep_cnt;i++) if(strcmp(G.eps[i].slug,G.last_slug)==0){ G.cur_ep_idx=i; G.cur_season=G.eps[i].season; break; }
|
||
}
|
||
if(G.cur_ep_idx<0){
|
||
int s1=-1;
|
||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==G.cur_season){ s1=i; break; }
|
||
if(s1==-1 && G.ep_cnt>0){
|
||
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;i<G.season_cnt;i++) if(G.seasons[i].num==cur_s) { season_exists=true; break; }
|
||
if(!season_exists){
|
||
// Fallback to last_slug or first season
|
||
if(strlen(G.last_slug)>0){
|
||
for(int i=0;i<G.ep_cnt;i++) if(strcmp(G.eps[i].slug,G.last_slug)==0){ G.cur_ep_idx=i; G.cur_season=G.eps[i].season; season_exists=true; break; }
|
||
}
|
||
}
|
||
if(season_exists){
|
||
// Keep cur_season, find best idx within it
|
||
int best=-1;
|
||
if(strlen(G.last_slug)>0){
|
||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==cur_s && strcmp(G.eps[i].slug,G.last_slug)==0){ best=i; break; }
|
||
}
|
||
if(best==-1){
|
||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==cur_s){ best=i; break; }
|
||
}
|
||
if(best!=-1) G.cur_ep_idx=best;
|
||
} else {
|
||
// Season no longer exists, fallback to first
|
||
if(G.ep_cnt>0){
|
||
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;i<G.ep_cnt;i++) if(strcmp(G.eps[i].slug,G.last_slug)==0){ G.cur_ep_idx=i; G.cur_season=G.eps[i].season; break; }
|
||
}
|
||
if(G.cur_ep_idx<0){
|
||
int s1=-1;
|
||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==G.cur_season){ s1=i; break; }
|
||
if(s1==-1 && G.ep_cnt>0){
|
||
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<G.ep_cnt;i++){
|
||
char ep_path[300];
|
||
make_sd_path(ep_path, sizeof(ep_path), G.eps[i].slug);
|
||
if (strcmp(ep_path, file_path)==0) { target_idx = i; break; }
|
||
}
|
||
}
|
||
if (target_idx<0 && G.pending_dl_idx>=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<G.ep_cnt;i++) G.eps[i].seen = has_slug(G.eps[i].slug);
|
||
save_state();
|
||
ui_rebuild_episode_list();
|
||
tt_lvgl_lock(portMAX_DELAY);
|
||
if (G.lbl_status) {
|
||
char msg[64];
|
||
snprintf(msg,sizeof(msg),"DL: %ld ok, %ld fail", (long)succ, (long)fail);
|
||
lv_label_set_text(G.lbl_status, msg);
|
||
}
|
||
tt_lvgl_unlock();
|
||
G.pending_dl_idx = -1;
|
||
return;
|
||
}
|
||
}
|
||
G.pending_dl_idx = -1;
|
||
// Still refresh list in case files appeared
|
||
for(int i=0;i<G.ep_cnt;i++) G.eps[i].seen = has_slug(G.eps[i].slug);
|
||
ui_rebuild_episode_list();
|
||
return;
|
||
}
|
||
|
||
// Single download result
|
||
bool success = false;
|
||
if (resultData) {
|
||
tt_bundle_opt_bool(resultData, "success", &success);
|
||
}
|
||
if (result == APP_RESULT_OK) {
|
||
if (resultData) {
|
||
bool b=false;
|
||
if (tt_bundle_opt_bool(resultData, "success", &b)) success = b;
|
||
else success = true;
|
||
} else {
|
||
success = true;
|
||
}
|
||
}
|
||
|
||
int idx = G.pending_dl_idx;
|
||
G.pending_dl_idx = -1;
|
||
|
||
for(int i=0;i<G.ep_cnt;i++) G.eps[i].seen = has_slug(G.eps[i].slug);
|
||
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");
|
||
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;
|
||
}
|