#include "player.h" #include "storage.h" #include "ui.h" #include "download.h" #include #include #include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #include "esp_heap_caps.h" void wait_play_exit(void) { // No internal playback anymore, nothing to wait } void stop_playback_sync(void) { // No internal playback, nothing to stop G.is_playing = false; G.is_paused = false; } static bool try_external_downloader(int idx) { if(idx<0||idx>=G.ep_cnt) return false; EpInfo* ep=&G.eps[idx]; if (strlen(ep->audio_url) < 8) { ESP_LOGW(TAG, "Skipping DL for %s: empty URL", ep->slug); tt_lvgl_lock(portMAX_DELAY); if (G.lbl_status) lv_label_set_text(G.lbl_status, "No URL – fetch failed?"); tt_lvgl_unlock(); return false; } char fpath[300]; make_sd_path(fpath, sizeof(fpath), ep->slug); tt_lvgl_lock(portMAX_DELAY); close_overlay(); if(G.dl_overlay) hide_dl_overlay(); tt_lvgl_unlock(); BundleHandle b = tt_bundle_alloc(); if (!b) { ESP_LOGW(TAG, "Failed to alloc bundle for external DL"); return false; } tt_bundle_put_string(b, "url", ep->audio_url); tt_bundle_put_string(b, "path", fpath); tt_bundle_put_bool(b, "override", false); ESP_LOGI(TAG, "Starting external SdDownloader for %s -> %s", ep->slug, fpath); G.pending_dl_idx = idx; tt_lvgl_lock(portMAX_DELAY); if (G.lbl_status) lv_label_set_text(G.lbl_status, "Starting downloader..."); tt_lvgl_unlock(); tt_app_start_with_bundle("one.tactility.sddownloader", b); return true; } int count_missing_in_season(int season){ int cnt=0; for(int i=0;i=8) cnt++; } 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, "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){ if(idx<0||idx>=G.ep_cnt) return; EpInfo* ep=&G.eps[idx]; char fpath[300]; make_sd_path(fpath, sizeof(fpath), ep->slug); if(!has_slug(ep->slug)){ ESP_LOGW(TAG, "start_idx_internal called but file missing %s", ep->slug); start_idx(idx); return; } int saved = 0; if(G.cur_ep_idx!=idx){ saved = get_saved_pos_for_slug(ep->slug); if(saved>5){ ESP_LOGI(TAG,"Restoring saved pos %d for %s", saved, ep->slug); } } else { saved = G.pos_sec; } tt_lvgl_lock(portMAX_DELAY); close_overlay(); if(G.dl_overlay) hide_dl_overlay(); tt_lvgl_unlock(); BundleHandle b = tt_bundle_alloc(); if(b){ tt_bundle_put_string(b, "file", fpath); tt_bundle_put_string(b, "path", fpath); tt_bundle_put_int32(b, "pos_sec", saved); tt_bundle_put_int32(b, "position", saved); tt_bundle_put_int32(b, "volume", G.volume); ESP_LOGI(TAG, "Launching Mp3Player for %s pos %d", fpath, saved); G.pending_dl_idx = idx; G.cur_ep_idx=idx; G.cur_season=ep->season; strncpy(G.cur_title,ep->title,sizeof(G.cur_title)-1); strncpy(G.last_slug,ep->slug,sizeof(G.last_slug)-1); save_state(); tt_app_start_with_bundle("one.tactility.mp3player", b); return; } ESP_LOGW(TAG, "Failed to alloc bundle for Mp3Player"); tt_lvgl_lock(portMAX_DELAY); if(G.lbl_status) lv_label_set_text(G.lbl_status,"Failed to launch player"); tt_lvgl_unlock(); } void start_idx(int idx){ if(idx<0||idx>=G.ep_cnt) return; EpInfo* ep=&G.eps[idx]; if(!has_slug(ep->slug)){ // File not on SD, download via external downloader if (try_external_downloader(idx)) { return; } // Fallback: show message, cannot download internally anymore tt_lvgl_lock(portMAX_DELAY); if(G.lbl_status) lv_label_set_text(G.lbl_status,"Install SdDownloader"); tt_lvgl_unlock(); return; } start_idx_internal(idx); } void select_ep(int idx){ if(idx<0||idx>=G.ep_cnt) return; if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt && G.cur_ep_idx!=idx){ save_current_ep_pos(); } G.cur_ep_idx=idx; EpInfo* ep=&G.eps[idx]; G.cur_season=ep->season; strncpy(G.last_slug, ep->slug, sizeof(G.last_slug)-1); int saved = get_saved_pos_for_slug(ep->slug); G.pos_sec=saved; G.last_pct=-1; G.total_sec=0; tt_lvgl_lock(portMAX_DELAY); if(G.lbl_title) lv_label_set_text(G.lbl_title,ep->title); if(G.lbl_meta){ char b[64]; if(saved>5){ char tbuf[16]; fmt_time(tbuf,saved); snprintf(b,sizeof(b),"S%02d E%02d %s • %s",ep->season,ep->ep_num,ep->seen?"*":"",tbuf); } else { snprintf(b,sizeof(b),"S%02d E%02d %s",ep->season,ep->ep_num,ep->seen?"*":""); } lv_label_set_text(G.lbl_meta,b); } if(G.lbl_status){ if(saved>5) lv_label_set_text(G.lbl_status,"Resume – tap Play"); else lv_label_set_text(G.lbl_status,ep->seen?"On SD – tap Play":"Tap Play to download"); } if(G.bar){ lv_bar_set_value(G.bar,0,LV_ANIM_OFF); } if(G.lbl_time){ if(saved>5){ char a[16]; fmt_time(a,saved); char buf[40]; snprintf(buf,sizeof(buf),"%s / --:--",a); lv_label_set_text(G.lbl_time,buf); } else { lv_label_set_text(G.lbl_time,"0:00 / --:--"); } } tt_lvgl_unlock(); save_state(); } void go_next(){ if(G.ep_cnt==0) return; save_current_ep_pos(); save_state(); int n=G.cur_ep_idx+1; if(n>=G.ep_cnt) n=0; select_ep(n); // Do not auto-play next, per user request: no autoplay } void go_prev(){ if(G.ep_cnt==0) return; if(G.pos_sec>5){ if(G.cur_ep_idx>=0){ set_saved_pos_for_slug(G.eps[G.cur_ep_idx].slug, 0); save_state(); } G.pos_sec=0; int cur=G.cur_ep_idx; if(cur<0) cur=0; select_ep(cur); return; } save_current_ep_pos(); save_state(); int p=G.cur_ep_idx-1; if(p<0) p=G.ep_cnt-1; select_ep(p); } // Called from UI for season batch download bool download_season(int season) { return try_external_downloader_batch(season); }