#include "storage.h" #include "cJSON.h" #include #include #include #include #include #include #include "esp_heap_caps.h" void make_sd_path(char* out, size_t out_len, const char* slug){ if(!out || !slug) return; snprintf(out, out_len, "%s/%s.mp3", DM_DIR, slug); } void ensure_dir(){ mkdir("/sdcard/apps",0777); mkdir("/sdcard/apps/one.tactility.discoverymountain",0777); mkdir("/sdcard/apps/one.tactility.discoverymountain/userdata",0777); mkdir(DM_DIR,0777); } bool has_slug(const char* slug){ char p[300]; make_sd_path(p, sizeof(p), slug); struct stat st; return stat(p,&st)==0; } void fmt_time(char* o,int s){ snprintf(o,16,"%d:%02d",s/60,s%60); } void sort_seasons(){ for(int i=0;iseason != b->season) should_swap = (b->season < a->season); else should_swap = (b->ep_num < a->ep_num); if(should_swap){ *t=*a; *a=*b; *b=*t; } } } heap_caps_free(t); } // ── Per-episode position helpers ── int find_saved_pos_idx(const char* slug){ if(!slug) return -1; for(int i=0;i=0) return G.saved_pos[idx].pos; return 0; } void set_saved_pos_for_slug(const char* slug, int pos){ if(!slug || strlen(slug)==0) return; if(pos<0) pos=0; if(G.total_sec>0 && pos>0 && pos >= G.total_sec-10){ pos=0; } int idx=find_saved_pos_idx(slug); if(idx>=0){ G.saved_pos[idx].pos=pos; return; } if(G.saved_pos_cnt < MAX_EPS){ strncpy(G.saved_pos[G.saved_pos_cnt].slug, slug, sizeof(G.saved_pos[G.saved_pos_cnt].slug)-1); G.saved_pos[G.saved_pos_cnt].slug[sizeof(G.saved_pos[G.saved_pos_cnt].slug)-1]=0; G.saved_pos[G.saved_pos_cnt].pos=pos; G.saved_pos_cnt++; } } void save_current_ep_pos(){ if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt){ const char* slug=G.eps[G.cur_ep_idx].slug; set_saved_pos_for_slug(slug, G.pos_sec); } else if(strlen(G.last_slug)>0){ set_saved_pos_for_slug(G.last_slug, G.pos_sec); } } void save_state(){ if(G.cur_ep_idx>=0 && G.ep_cnt>0){ if(G.pos_sec>=0){ set_saved_pos_for_slug(G.eps[G.cur_ep_idx].slug, G.pos_sec); } } cJSON* r=cJSON_CreateObject(); if(G.ep_cnt>0 && G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt) cJSON_AddStringToObject(r,"last_slug",G.eps[G.cur_ep_idx].slug); else if(strlen(G.last_slug)>0) cJSON_AddStringToObject(r,"last_slug",G.last_slug); cJSON_AddNumberToObject(r,"last_season",G.cur_season); cJSON_AddNumberToObject(r,"pos_sec",G.pos_sec); if(G.saved_pos_cnt>0){ cJSON* pos_obj=cJSON_CreateObject(); for(int i=0;i0){ cJSON_AddNumberToObject(pos_obj, G.saved_pos[i].slug, G.saved_pos[i].pos); } } cJSON_AddItemToObject(r,"positions",pos_obj); } char* s=cJSON_PrintUnformatted(r); if(s){ FILE* f=fopen(STATE_PATH,"w"); if(f){ fwrite(s,1,strlen(s),f); fclose(f);} free(s); } cJSON_Delete(r); } void load_state(){ G.cur_season=1; G.cur_ep_idx=-1; G.pos_sec=0; G.saved_pos_cnt=0; G.last_save_tick=0; memset(G.last_slug,0,sizeof(G.last_slug)); memset(G.saved_pos,0,sizeof(G.saved_pos)); FILE* f=fopen(STATE_PATH,"r"); if(!f) return; fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET); if(sz<=0||sz>16384){ fclose(f); return;} char* buf=malloc(sz+1); if(!buf){ fclose(f); return; } size_t got=fread(buf,1,sz,f); buf[got]=0; fclose(f); cJSON* r=cJSON_Parse(buf); free(buf); if(!r) return; cJSON* ls=cJSON_GetObjectItem(r,"last_slug"); cJSON* lsn=cJSON_GetObjectItem(r,"last_season"); cJSON* ps=cJSON_GetObjectItem(r,"pos_sec"); if(ps&&cJSON_IsNumber(ps)) G.pos_sec=ps->valueint; if(lsn&&cJSON_IsNumber(lsn)) G.cur_season=lsn->valueint; if(ls&&cJSON_IsString(ls)){ strncpy(G.last_slug, ls->valuestring, sizeof(G.last_slug)-1); for(int i=0;ivaluestring)==0){ G.cur_ep_idx=i; G.cur_season=G.eps[i].season; break; } } cJSON* poss=cJSON_GetObjectItem(r,"positions"); if(poss && cJSON_IsObject(poss)){ cJSON* child=NULL; cJSON_ArrayForEach(child, poss){ if(cJSON_IsNumber(child) && child->string && G.saved_pos_cnt < MAX_EPS){ strncpy(G.saved_pos[G.saved_pos_cnt].slug, child->string, sizeof(G.saved_pos[G.saved_pos_cnt].slug)-1); G.saved_pos[G.saved_pos_cnt].pos=child->valueint; G.saved_pos_cnt++; } } if(strlen(G.last_slug)>0){ int sp=get_saved_pos_for_slug(G.last_slug); if(sp>0) G.pos_sec=sp; } } cJSON_Delete(r); } bool save_cache(void){ if(G.ep_cnt==0 || G.season_cnt==0) return false; cJSON* root=cJSON_CreateObject(); if(!root) return false; cJSON* seasons_arr=cJSON_CreateArray(); if(!seasons_arr){ cJSON_Delete(root); return false; } for(int i=0;islug[0]==0) continue; cJSON* obj=cJSON_CreateObject(); if(!obj) continue; cJSON_AddStringToObject(obj,"title", e->title); cJSON_AddStringToObject(obj,"slug", e->slug); cJSON_AddNumberToObject(obj,"season_num", e->season); cJSON_AddNumberToObject(obj,"episode_num", e->ep_num); cJSON_AddStringToObject(obj,"audio_url", e->audio_url); cJSON_AddItemToArray(eps_arr, obj); } cJSON_AddItemToObject(root,"episodes",eps_arr); char* s=cJSON_PrintUnformatted(root); cJSON_Delete(root); if(!s) return false; FILE* f=fopen(CACHE_PATH,"w"); bool ok=false; if(f){ size_t len=strlen(s); size_t wrote=fwrite(s,1,len,f); fclose(f); ok = (wrote==len); } free(s); return ok; } bool load_cache(void){ FILE* f=fopen(CACHE_PATH,"r"); if(!f) return false; fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET); if(sz<=0 || sz>200000){ fclose(f); return false; } // Use PSRAM for large json to avoid internal heap pressure char* buf=heap_caps_malloc(sz+1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if(!buf){ buf=malloc(sz+1); if(!buf){ fclose(f); return false; } } size_t got=fread(buf,1,sz,f); buf[got]=0; fclose(f); cJSON* root=cJSON_Parse(buf); heap_caps_free(buf); if(!root) return false; cJSON* seasons_arr=cJSON_GetObjectItem(root,"seasons"); cJSON* eps_arr=cJSON_GetObjectItem(root,"episodes"); if(!cJSON_IsArray(seasons_arr) || !cJSON_IsArray(eps_arr)){ cJSON_Delete(root); return false; } int scnt=cJSON_GetArraySize(seasons_arr); G.season_cnt=0; for(int i=0;ivalueint; G.season_cnt++; } } int ecnt=cJSON_GetArraySize(eps_arr); G.ep_cnt=0; for(int i=0;ititle, t->valuestring, sizeof(e->title)-1); if(slug && cJSON_IsString(slug)) strncpy(e->slug, slug->valuestring, sizeof(e->slug)-1); else if(t && cJSON_IsString(t)) strncpy(e->slug, t->valuestring, sizeof(e->slug)-1); e->season=sn->valueint; e->ep_num=en->valueint; if(au && cJSON_IsString(au)) strncpy(e->audio_url, au->valuestring, sizeof(e->audio_url)-1); e->seen=has_slug(e->slug); G.ep_cnt++; } cJSON_Delete(root); if(G.season_cnt>0) sort_seasons(); if(G.ep_cnt>0) sort_eps(); // Refresh seen in case SD changed for(int i=0;i0 && G.ep_cnt>0); } void lru_check(){ typedef struct{ char path[300]; time_t mt; } FE; // Allocate from PSRAM to avoid stack overflow (200*~304=60KB on stack would overflow 12KB task) FE *files = heap_caps_malloc(200 * sizeof(FE), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if(!files) return; int fc=0; DIR* d=opendir(DM_DIR); if(!d){ heap_caps_free(files); return; } struct dirent* de; char cur_path[300]={0}; if(G.cur_ep_idx>=0 && G.cur_ep_idxd_name[0]=='.') continue; size_t l=strlen(de->d_name); if(l<5) continue; if(strcmp(de->d_name+l-4,".mp3")!=0) continue; char full[300]; snprintf(full,sizeof(full),"%s/%.200s",DM_DIR,de->d_name); struct stat st; if(stat(full,&st)!=0) continue; if(cur_path[0] && strcmp(full,cur_path)==0) continue; strncpy(files[fc].path,full,sizeof(files[fc].path)-1); files[fc].mt=st.st_mtime; fc++; } closedir(d); if(fc<12){ heap_caps_free(files); return; } for(int i=0;i