3778cb3399
- DiscoveryMountain: new UI optimized for episode list (not player anymore) - Shows episodes for current season, indicates if on SD card (green=On SD, gray=Not DL) - Per-episode action: download single via SdDownloader or play via Mp3Player - DL Missing button for missing eps in season (currently single first missing, batch reverted) - Season selector screen with cached/total counts - Top-right X close button - Seasons/episodes cached to cache.json to avoid network fetch every launch - Fixes: overlay deletion race (deferred timer), ui_timer leak causing crash on close, season selection overwritten by fetch task - Fixes: unicode ellipsis squares - SdDownloader: revert to stable single-file mode (batch array caused crashes) - Remove batch fields and array allocation, keep single url/path download - Result bundle simple success/path/bytes - Mp3Player: add resume position support (pos_sec from bundle, total_sec estimate, total_decoded_samples), return position on exit via bundle for DM to save Tested on 192.168.68.133: opens, shows S10 2/6, Play/DL buttons, close works, no crash on open/close cycles, cache shows 'Loaded from cache, refreshing...'
236 lines
6.8 KiB
C
236 lines
6.8 KiB
C
#include "player.h"
|
||
#include "storage.h"
|
||
#include "ui.h"
|
||
#include "download.h"
|
||
|
||
#include <tt_lvgl.h>
|
||
#include <tt_app.h>
|
||
#include <tt_bundle.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
#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;
|
||
}
|
||
|
||
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;
|
||
for(int i=0;i<G.ep_cnt;i++){
|
||
if(G.eps[i].season == season && !G.eps[i].seen && strlen(G.eps[i].audio_url)>=8){
|
||
first_missing = i;
|
||
break;
|
||
}
|
||
}
|
||
if(first_missing==-1){
|
||
tt_lvgl_lock(portMAX_DELAY);
|
||
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Season already cached");
|
||
tt_lvgl_unlock();
|
||
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);
|
||
}
|
||
|
||
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);
|
||
}
|