refactor(DiscoveryMountain): split monolithic main.c into modular files

- Extracted app_context (global AppCtx G definition)
- storage: path helpers, LRU, sort, fmt_time, per-episode resume map, state persistence
- network: resolve_host, normalize URL, http_get_raw with growing PSRAM buffer, fetch_data
- download: dl_ep_raw with progress tracking, cancel, overlay UI (show/hide/update)
- player: audio device, stream, play_task with resume seek, start/select/next/prev with position save/restore
- ui: build_ui, timers, season picker, button callbacks, periodic save
- main: minimal glue, fetch_task, onShow/onHide, app registration
- Preserves all previous fixes: download progress overlay, per-episode resume, robust download handling
- Build verified: 111 undef, all exported
This commit is contained in:
Adolfo
2026-07-22 17:11:59 -04:00
parent 6bfe514ff1
commit be9229b80d
13 changed files with 1662 additions and 1519 deletions
+344
View File
@@ -0,0 +1,344 @@
#include "player.h"
#include "storage.h"
#include "download.h"
#include "ui.h"
#include <tt_lvgl.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <lwip/sockets.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include <unistd.h>
#define MINIMP3_IMPLEMENTATION
#define MINIMP3_NO_SIMD
#include "minimp3.h"
bool find_audio_device(){
struct Device* dev=device_find_by_name("audio-stream");
if(dev){ G.stream_dev=dev; return true; }
dev=device_find_first_by_type(&AUDIO_STREAM_TYPE);
if(dev){ G.stream_dev=dev; return true; }
return false;
}
void close_stream(){
if(G.stream_handle){
audio_stream_close(G.stream_handle);
G.stream_handle=NULL;
}
}
bool open_stream(uint32_t sr, uint8_t ch){
close_stream();
if(!G.stream_dev && !find_audio_device()) return false;
struct AudioStreamConfig cfg={ .sample_rate=sr, .bits_per_sample=16, .channels=ch };
error_t err=audio_stream_open_output(G.stream_dev,&cfg,&G.stream_handle);
return err==ERROR_NONE;
}
void wait_play_exit(){
if(G.play_handle){
G.need_stop=true;
while(G.play_handle!=NULL){
tt_lvgl_unlock();
vTaskDelay(pdMS_TO_TICKS(10));
tt_lvgl_lock(portMAX_DELAY);
}
}
close_stream();
}
void stop_playback_sync(){
if(G.play_handle){
G.need_stop=true;
int tries=200;
while(G.play_handle!=NULL && tries-- >0){
vTaskDelay(pdMS_TO_TICKS(10));
}
}
close_stream();
G.is_playing=false;
G.is_paused=false;
}
void play_task(void* arg){
(void)arg;
int idx=G.cur_ep_idx;
if(idx<0||idx>=G.ep_cnt){ G.play_handle=NULL; vTaskDelete(NULL); return; }
EpInfo* ep=&G.eps[idx];
char path[300];
make_sd_path(path,sizeof(path),ep->slug);
FILE* file=fopen(path,"rb");
if(!file){ ESP_LOGE(TAG,"open fail %s",path); G.is_playing=false; G.play_handle=NULL; vTaskDelete(NULL); return; }
fseek(file,0,SEEK_END);
long fsize=ftell(file);
fseek(file,0,SEEK_SET);
if(fsize>0) G.total_sec=(int)(fsize/16000);
if(!find_audio_device()){ ESP_LOGE(TAG,"no audio-stream dev"); fclose(file); G.is_playing=false; G.play_handle=NULL; vTaskDelete(NULL); return; }
uint8_t id3hdr[10];
long id3_skip=0;
if(fread(id3hdr,1,10,file)==10 && memcmp(id3hdr,"ID3",3)==0){
int id3size = (id3hdr[6]&0x7F)<<21 | (id3hdr[7]&0x7F)<<14 | (id3hdr[8]&0x7F)<<7 | (id3hdr[9]&0x7F);
ESP_LOGI(TAG,"ID3 found %d, skip",id3size);
id3_skip=id3size+10;
fseek(file, id3_skip, SEEK_SET);
fsize -= id3_skip;
if(fsize>0) G.total_sec=(int)(fsize/16000);
} else {
fseek(file,0,SEEK_SET);
}
int resume_sec = G.pos_sec;
if(resume_sec>5){
long seek_bytes = (long)resume_sec * 16000;
if(seek_bytes < fsize){
fseek(file, id3_skip + seek_bytes, SEEK_SET);
ESP_LOGI(TAG,"Resuming %s from %d sec (~%ld bytes)", ep->slug, resume_sec, seek_bytes);
}
}
mp3dec_t* dec = heap_caps_malloc(sizeof(mp3dec_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if(!dec){ ESP_LOGE(TAG,"dec malloc fail"); fclose(file); G.play_handle=NULL; vTaskDelete(NULL); return; }
mp3dec_init(dec);
uint8_t* inbuf=malloc(MP3_BUF);
int16_t* pcm=malloc(1152*2*2);
if(!inbuf||!pcm){ if(inbuf) free(inbuf); if(pcm) free(pcm); fclose(file); heap_caps_free(dec); G.play_handle=NULL; vTaskDelete(NULL); return; }
size_t buffered=0;
mp3dec_frame_info_t info;
memset(&info,0,sizeof(info));
bool configured=false;
int sr=0, ch=0;
size_t r=fread(inbuf,1,MP3_BUF,file);
buffered=r;
ESP_LOGI(TAG,"play task start %s size %ld resume=%d",path,fsize,resume_sec);
long total_decoded_samples = 0;
if(resume_sec>5){
total_decoded_samples = (long)resume_sec * 16000;
}
while(!G.need_stop){
if(G.is_paused){
if(configured){
close_stream();
configured=false;
sr=0; ch=0;
}
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
int samples=mp3dec_decode_frame(dec,inbuf,(int)buffered,pcm,&info);
if(samples>0){
total_decoded_samples += samples;
if(info.hz>0) G.pos_sec = total_decoded_samples / info.hz;
else G.pos_sec = total_decoded_samples / 16000;
if(!configured || info.hz!=sr || info.channels!=ch){
ESP_LOGI(TAG,"open_stream sr=%d ch=%d",info.hz,info.channels);
if(!open_stream((uint32_t)info.hz,(uint8_t)info.channels)){
ESP_LOGE(TAG,"open_stream fail %d %d",info.hz,info.channels);
break;
}
ESP_LOGI(TAG,"open_stream ok");
sr=info.hz; ch=info.channels;
configured=true;
if(resume_sec>5 && total_decoded_samples < (long)resume_sec * info.hz + info.hz){
total_decoded_samples = (long)resume_sec * info.hz;
G.pos_sec = resume_sec;
}
}
float vol=G.volume/100.0f;
if(vol<0) vol=0;
if(vol>1) vol=1;
int total_samples = samples * (info.channels>0?info.channels:1);
for(int i=0;i<total_samples;i++){
int32_t s=pcm[i];
s=(int32_t)(s*vol);
if(s>32767) s=32767;
if(s<-32768) s=-32768;
pcm[i]=(int16_t)s;
}
size_t to_write = total_samples * sizeof(int16_t);
size_t off=0;
while(off<to_write && !G.need_stop){
if(G.is_paused){
if(configured){
close_stream();
configured=false;
sr=0; ch=0;
}
break;
}
size_t w=0;
error_t e=audio_stream_write(G.stream_handle,(uint8_t*)pcm+off,to_write-off,&w,pdMS_TO_TICKS(500));
if(e==ERROR_NONE){
off+=w;
} else {
ESP_LOGW(TAG,"audio_write err %d",e);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
if(G.is_paused) continue;
}
if(info.frame_bytes>0 && info.frame_bytes <= (int)buffered){
memmove(inbuf,inbuf+info.frame_bytes,buffered-info.frame_bytes);
buffered-=info.frame_bytes;
} else {
buffered=0;
}
if(buffered < MP3_BUF/2){
size_t nr=fread(inbuf+buffered,1,MP3_BUF-buffered,file);
if(nr>0) buffered+=nr;
else { if(samples==0) break; }
}
if(buffered==0) break;
}
fclose(file);
free(inbuf);
free(pcm);
heap_caps_free(dec);
close_stream();
bool finished_naturally = !G.need_stop;
if(finished_naturally){
ESP_LOGI(TAG,"playback finished naturally for %s, clearing saved pos", ep->slug);
set_saved_pos_for_slug(ep->slug, 0);
G.pos_sec=0;
save_state();
} else {
set_saved_pos_for_slug(ep->slug, G.pos_sec);
save_state();
}
ESP_LOGI(TAG,"play task end finished_naturally=%d", finished_naturally);
G.is_playing=false;
G.play_handle=NULL;
vTaskDelete(NULL);
}
void start_idx_internal(int idx){
if(idx<0||idx>=G.ep_cnt) return;
EpInfo* ep=&G.eps[idx];
if(G.cur_ep_idx!=idx){
int saved = get_saved_pos_for_slug(ep->slug);
if(saved>5){
G.pos_sec=saved;
ESP_LOGI(TAG,"Restoring saved pos %d for %s", saved, ep->slug);
}
G.last_pct=-1;
}
tt_lvgl_lock(portMAX_DELAY);
wait_play_exit();
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);
G.need_stop=false; G.is_paused=false; G.is_playing=true;
save_state();
tt_lvgl_unlock();
xTaskCreate(play_task,"dm_play",16384,NULL,5,&G.play_handle);
}
void start_idx(int idx){
if(idx<0||idx>=G.ep_cnt) return;
EpInfo* ep=&G.eps[idx];
if(!has_slug(ep->slug)){
stop_playback_sync();
tt_lvgl_lock(portMAX_DELAY);
show_dl_overlay_locked(ep->title, ep->slug, ep->season, ep->ep_num);
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Downloading…");
tt_lvgl_unlock();
if(G.dl_handle){ return; }
xTaskCreate(dl_task,"dm_dl",16384,(void*)(intptr_t)idx,5,&G.dl_handle);
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);
if(saved>5){
G.pos_sec=saved;
ESP_LOGI(TAG,"select_ep restore %s pos=%d", ep->slug, saved);
} else {
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();
stop_playback_sync();
int n=G.cur_ep_idx+1;
if(n>=G.ep_cnt) n=0;
select_ep(n);
start_idx(n);
}
void go_prev(){
if(G.ep_cnt==0) return;
if(G.pos_sec>5){
ESP_LOGI(TAG,"go_prev restart current %d sec",G.pos_sec);
if(G.cur_ep_idx>=0){
set_saved_pos_for_slug(G.eps[G.cur_ep_idx].slug, 0);
save_state();
}
stop_playback_sync();
G.pos_sec=0;
int cur=G.cur_ep_idx;
if(cur<0) cur=0;
select_ep(cur);
start_idx(cur);
return;
}
save_current_ep_pos();
save_state();
stop_playback_sync();
int p=G.cur_ep_idx-1;
if(p<0) p=G.ep_cnt-1;
select_ep(p);
start_idx(p);
}