be9229b80d
- 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
204 lines
6.5 KiB
C
204 lines
6.5 KiB
C
#include "storage.h"
|
|
#include "cJSON.h"
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <dirent.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.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;i<G.season_cnt;i++){
|
|
for(int j=i+1;j<G.season_cnt;j++){
|
|
if(G.seasons[j].num < G.seasons[i].num){
|
|
SeasonInfo t=G.seasons[i]; G.seasons[i]=G.seasons[j]; G.seasons[j]=t;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
void sort_eps(){
|
|
for(int i=0;i<G.ep_cnt;i++){
|
|
for(int j=i+1;j<G.ep_cnt;j++){
|
|
EpInfo *a=&G.eps[i], *b=&G.eps[j];
|
|
bool should_swap=false;
|
|
if(a->season != b->season) should_swap = (b->season < a->season);
|
|
else should_swap = (b->ep_num < a->ep_num);
|
|
if(should_swap){
|
|
EpInfo t=*a; *a=*b; *b=t;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Per-episode position helpers ──
|
|
int find_saved_pos_idx(const char* slug){
|
|
if(!slug) return -1;
|
|
for(int i=0;i<G.saved_pos_cnt;i++){
|
|
if(strcmp(G.saved_pos[i].slug, slug)==0) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
int get_saved_pos_for_slug(const char* slug){
|
|
int idx=find_saved_pos_idx(slug);
|
|
if(idx>=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;i<G.saved_pos_cnt;i++){
|
|
if(G.saved_pos[i].slug[0]==0) continue;
|
|
if(G.saved_pos[i].pos>0){
|
|
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;i<G.ep_cnt;i++) if(strcmp(G.eps[i].slug,ls->valuestring)==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);
|
|
}
|
|
|
|
void lru_check(){
|
|
typedef struct{ char path[300]; time_t mt; } FE;
|
|
FE files[200];
|
|
int fc=0;
|
|
DIR* d=opendir(DM_DIR);
|
|
if(!d) return;
|
|
struct dirent* de;
|
|
char cur_path[300]={0};
|
|
if(G.cur_ep_idx>=0 && G.cur_ep_idx<G.ep_cnt){
|
|
make_sd_path(cur_path, sizeof(cur_path), G.eps[G.cur_ep_idx].slug);
|
|
}
|
|
while((de=readdir(d))!=NULL && fc<200){
|
|
if(de->d_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) return;
|
|
for(int i=0;i<fc;i++){
|
|
for(int j=i+1;j<fc;j++){
|
|
if(files[j].mt<files[i].mt){ FE t=files[i]; files[i]=files[j]; files[j]=t; }
|
|
}
|
|
}
|
|
int todel=fc-8;
|
|
for(int i=0;i<todel;i++) unlink(files[i].path);
|
|
}
|