#include "network.h" #include "storage.h" #include "cJSON.h" #include #include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #include "esp_heap_caps.h" #include uint16_t my_htons(uint16_t v){ // Proper byte swap with masking to avoid overflow return (uint16_t)(((v & 0x00FF) << 8) | ((v & 0xFF00) >> 8)); } bool resolve_host(const char* host, uint32_t* out_ip){ if(!host || !out_ip) return false; uint32_t ip = ipaddr_addr(host); if(ip!=0 && ip!=0xFFFFFFFF){ *out_ip=ip; return true; } ESP_LOGE(TAG,"resolve fail (DNS not supported) %s",host); return false; } void normalize_audio_url(const char* in, char* out, size_t out_len){ if(!in || !out) return; if(strncmp(in,"http://",7)==0){ strncpy(out,in,out_len-1); out[out_len-1]=0; return; } if(in[0]=='/'){ snprintf(out,out_len,"%s%s",PB,in); return; } if(strstr(in,"://")==NULL){ if(in[0]=='/') snprintf(out,out_len,"%s%s",PB,in); else snprintf(out,out_len,"%s/%s",PB,in); return; } strncpy(out,in,out_len-1); out[out_len-1]=0; } int http_get_raw(const char* url, char** out_body){ if(!url || strncmp(url,"http://",7)!=0){ ESP_LOGE(TAG,"http_get url not http %s",url); return -1; } const char* p = url+7; const char* slash = strchr(p,'/'); if(!slash){ ESP_LOGE(TAG,"no slash %s",url); return -1; } char hostport[128]={0}; size_t hlen=slash-p; if(hlen>=sizeof(hostport)) return -1; memcpy(hostport,p,hlen); hostport[hlen]=0; const char* path=slash; char host[96]={0}; int port=80; char* colon=strchr(hostport,':'); if(colon){ *colon=0; strncpy(host,hostport,sizeof(host)-1); port=atoi(colon+1); if(port<=0) port=80; } else { strncpy(host,hostport,sizeof(host)-1); } ESP_LOGI(TAG,"GET %s host=%s port=%d",url,host,port); uint32_t ip=0; ESP_LOGI(TAG,"Resolving host %s",host); if(!resolve_host(host,&ip)){ ESP_LOGE(TAG,"ip fail %s",host); return -1; } ESP_LOGI(TAG,"Resolved %s -> ip 0x%08x", host, ip); int fd=lwip_socket(AF_INET,SOCK_STREAM,0); ESP_LOGI(TAG,"Socket fd=%d", fd); if(fd<0){ ESP_LOGE(TAG,"socket fail"); return -1; } struct sockaddr_in sa; ESP_LOGI(TAG,"Memset sa"); memset(&sa,0,sizeof(sa)); ESP_LOGI(TAG,"Set sa len/family/port/addr"); sa.sin_len = sizeof(sa); sa.sin_family=AF_INET; sa.sin_port=my_htons(port); sa.sin_addr.s_addr=ip; ESP_LOGI(TAG,"SetSockOpt RCV"); struct timeval tv={5,0}; int opt_res = lwip_setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)); ESP_LOGI(TAG,"SetSockOpt RCV res=%d", opt_res); ESP_LOGI(TAG,"SetSockOpt SND"); opt_res = lwip_setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv)); ESP_LOGI(TAG,"SetSockOpt SND res=%d", opt_res); ESP_LOGI(TAG,"Connecting to %s:%d",host,port); int conn_res = lwip_connect(fd,(struct sockaddr*)&sa,sizeof(sa)); ESP_LOGI(TAG,"Connect res=%d", conn_res); if(conn_res<0){ ESP_LOGE(TAG,"connect fail %s:%d",host,port); close(fd); return -1; } ESP_LOGI(TAG,"Connected, sending request"); char req[512]; int reqlen=snprintf(req,sizeof(req),"GET %s HTTP/1.0\r\nHost: %s:%d\r\nConnection: close\r\nUser-Agent: TactilityDM/0.1\r\nAccept: */*\r\n\r\n",path,host,port); if(lwip_send(fd,req,reqlen,0)<0){ ESP_LOGE(TAG,"send fail"); close(fd); return -1; } ESP_LOGI(TAG,"Request sent, waiting for response"); int capacity=8192; // further reduced to 8K internal to avoid PSRAM StoreProhibited crash (seen 0x8208775e) char* buf=malloc(capacity); if(!buf){ buf=heap_caps_malloc(capacity, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if(!buf){ ESP_LOGE(TAG,"buf 8k fail"); close(fd); return -1; } } int total=0; char* header_end=NULL; int body_start=0; int content_len=-1; bool is_chunked=false; while(1){ // Graceful cancel support to avoid force vTaskDelete leaking PSRAM/SD resources if(G.fetch_cancel_req){ ESP_LOGI(TAG,"http_get_raw canceled by user"); heap_caps_free(buf); close(fd); return -1; } if(total>=capacity-1){ int newcap=capacity*2; if(newcap>600000){ ESP_LOGE(TAG,"too large %d",newcap); heap_caps_free(buf); close(fd); return -1; } char* nb=heap_caps_malloc(newcap, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if(!nb){ heap_caps_free(buf); close(fd); return -1; } memcpy(nb,buf,total); heap_caps_free(buf); buf=nb; capacity=newcap; } int r=lwip_recv(fd,buf+total,capacity-total-1,0); if(r<=0){ if(r<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==ETIMEDOUT)){ ESP_LOGW(TAG,"http_get_raw timeout total=%d",total); // On timeout, if fetching canceled, abort if(G.fetch_cancel_req){ ESP_LOGI(TAG,"http_get_raw timeout + canceled"); heap_caps_free(buf); close(fd); return -1; } // Continue loop – LWIP will retry until EOF // But if we already have header and content_len satisfied, break if(header_end && content_len>=0 && !is_chunked && total-body_start >= content_len) break; // If no header yet and timeout, treat as failure to avoid 15s hang if(!header_end && total==0){ ESP_LOGE(TAG,"http_get_raw timeout before header"); heap_caps_free(buf); close(fd); return -1; } // Otherwise continue to try again continue; } break; } total+=r; buf[total]=0; if(!header_end){ header_end=strstr(buf,"\r\n\r\n"); if(header_end){ body_start=(header_end-buf)+4; if(strstr(buf,"chunked")) is_chunked=true; char* cl=strstr(buf,"Content-Length:"); if(!cl) cl=strstr(buf,"content-length:"); if(cl){ cl=strchr(cl,':'); if(cl){ cl++; while(*cl==' ') cl++; content_len=atoi(cl); } } ESP_LOGI(TAG,"header_end chunked=%d cl=%d total=%d",is_chunked,content_len,total); if(content_len>=0 && !is_chunked){ if(total-body_start >= content_len) break; } } } else { if(content_len>=0 && !is_chunked){ if(total-body_start >= content_len) break; } } } close(fd); if(!header_end){ ESP_LOGE(TAG,"no header end total=%d",total); heap_caps_free(buf); return -1; } if(strncmp(buf,"HTTP/1.1 200",12)!=0 && strncmp(buf,"HTTP/1.0 200",12)!=0){ ESP_LOGE(TAG,"not 200: %.60s",buf); heap_caps_free(buf); return -1; } int raw_len=total-body_start; char* raw=buf+body_start; char* final_body=NULL; int final_len=0; if(is_chunked){ final_body=heap_caps_malloc(200000, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if(!final_body){ heap_caps_free(buf); return -1; } int pos=0, out=0; while(pos16){ pos+=off+2; continue; } char hex[16]={0}; memcpy(hex,raw+pos,off); char* semi=strchr(hex,';'); if(semi) *semi=0; long sz=strtol(hex,NULL,16); if(sz==0) break; pos+=off+2; if(pos+sz>raw_len) sz=raw_len-pos; if(out+sz>=200000) break; memcpy(final_body+out,raw+pos,sz); out+=sz; pos+=sz; if(pos+1=0 && bl>content_len) bl=content_len; final_body=heap_caps_malloc(bl+1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if(!final_body){ heap_caps_free(buf); return -1; } memcpy(final_body,raw,bl); final_body[bl]=0; final_len=bl; } heap_caps_free(buf); *out_body=final_body; ESP_LOGI(TAG,"GET ok %d",final_len); return final_len; } bool fetch_data(){ char* js=NULL; char url[320]; bool seasons_ok=false; bool episodes_ok=false; ESP_LOGI(TAG,"fetch seasons"); // Reduce payload via fields filter – drastically lowers cJSON malloc pressure (internal heap low warning) snprintf(url,sizeof(url),"%s/api/collections/dm_seasons/records?perPage=100&sort=season_num&fields=season_num",PB); int len=http_get_raw(url,&js); if(len>0 && js){ cJSON* root=cJSON_Parse(js); free(js); js=NULL; if(root){ cJSON* items=cJSON_GetObjectItem(root,"items"); if(cJSON_IsArray(items)){ G.season_cnt=0; int n=cJSON_GetArraySize(items); for(int i=0;ivalueint; G.season_cnt++; } } sort_seasons(); if(G.season_cnt>0) seasons_ok=true; } cJSON_Delete(root); } } else { ESP_LOGW(TAG,"seasons fetch failed"); } if(G.season_cnt==0){ ESP_LOGW(TAG,"seasons fallback"); for(int i=1;i<=37;i++) G.seasons[i-1].num=i; G.season_cnt=37; } ESP_LOGI(TAG,"fetch episodes paginated to reduce internal heap (prev low 7KB)"); G.ep_cnt=0; for(int page=1; page<=10; page++){ if(G.fetch_cancel_req) break; if(G.ep_cnt>=MAX_EPS) break; snprintf(url,sizeof(url),"%s/api/collections/dm_episodes/records?perPage=25&page=%d&sort=season_num,episode_num&fields=title,slug,season_num,episode_num,audio_url",PB,page); len=http_get_raw(url,&js); if(len<=0 || !js){ ESP_LOGW(TAG,"episodes page %d fetch failed",page); if(js){ free(js); js=NULL; } break; } cJSON* root=cJSON_Parse(js); free(js); js=NULL; if(!root){ ESP_LOGW(TAG,"episodes page %d JSON parse fail",page); break; } cJSON* items=cJSON_GetObjectItem(root,"items"); if(!cJSON_IsArray(items)){ cJSON_Delete(root); break; } int n=cJSON_GetArraySize(items); if(n==0){ cJSON_Delete(root); break; } ESP_LOGI(TAG,"episodes page %d got %d items (total so far %d)",page,n,G.ep_cnt); 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) 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++; } episodes_ok=true; cJSON_Delete(root); // Yield between pages to let MemoryChecker recover vTaskDelay(pdMS_TO_TICKS(400)); if(n<25) break; // last page } // Final sort after all pages if(G.ep_cnt>0) sort_eps(); if(G.ep_cnt==0){ ESP_LOGW(TAG,"episodes fallback"); for(int i=0;i<6;i++){ EpInfo* e=&G.eps[i]; snprintf(e->slug,sizeof(e->slug),"S01E%02d",i+1); snprintf(e->title,sizeof(e->title),"Episode %d",i+1); e->season=1; e->ep_num=i+1; } G.ep_cnt=6; } for(int i=0;i