gamelist: always use /api/v1/images (always has data); enrich with title_name from games API

This commit is contained in:
KarnerF committed 2026-08-30 15:55:47 +02:00
1 parent b250018f99
commit 871fac5bf4
2 files changed
+64 -47

No files matched your search

+60 -44
View File
@@ -1777,7 +1777,6 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
"fetch('/api/sm/gamelist').then(function(r){return r.text();})"
".then(function(html){"
"if(gl)gl.innerHTML=html;"
"var noData=gl&&gl.querySelector('p')&&gl.textContent.indexOf('Keine')<0&&gl.textContent.indexOf('nicht')<0;"
"if(_gpR===0&&gl&&!gl.querySelector('button')){"
"setTimeout(function(){if(_gpR===0)loadGamePanel(1);},8000);}"
"}).catch(function(){"
@@ -2899,53 +2898,59 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
#undef SM_MANUAL
}
else if(mg_match(hm->uri,mg_str("/api/sm/gamelist"),NULL)){
/* server-side game list HTML using /api/v1/games (has title_name) */
char *games_json=sm_api_req("POST","/api/v1/games","{}");
char *out=malloc(131072); if(!out){mg_http_reply(c,500,"","");if(games_json)free(games_json);return;}
/* images always available; enrich with title_name from games API when mounted */
char *img_json=sm_api_req("POST","/api/v1/images","{}");
char *gam_json=sm_api_req("POST","/api/v1/games","{}");
char *out=malloc(131072); if(!out){if(img_json)free(img_json);if(gam_json)free(gam_json);mg_http_reply(c,500,"","");return;}
int pos=0;
#define GL(fmt,...) pos+=snprintf(out+pos,131071-pos,fmt,##__VA_ARGS__)
if(!games_json){
if(!img_json){
GL("<p class=\"hint\" style=\"color:#f87171;\">SM API nicht erreichbar</p>");
} else {
const char *gstart=strstr(games_json,"\"games\"");
if(!gstart||!strstr(gstart,"\"title_id\"")){GL("<p class=\"hint\">Keine Spiele gefunden</p>");}
const char *istart=strstr(img_json,"\"images\"");
if(!istart){GL("<p class=\"hint\">Keine Spiele gefunden</p>");}
else {
int n=0;
const char *cur=gstart;
const char *cur=istart;
while((cur=strstr(cur,"{\"path\""))!=NULL&&pos<120000){
char tid[16]={0},name[128]={0},imgpath[512]={0};
int mounted=0;
/* extract title_id */
const char *tv=strstr(cur,"\"title_id\":\"");
if(tv){tv+=12;const char *te=strchr(tv,'"');if(te&&te-tv<(int)sizeof(tid)){memcpy(tid,tv,te-tv);}}
/* extract title_name */
const char *nv=strstr(cur,"\"title_name\":\"");
if(nv){nv+=14;const char *ne=strchr(nv,'"');if(ne&&ne-nv<(int)sizeof(name)){memcpy(name,nv,ne-nv);}}
/* extract image_path for size */
const char *iv=strstr(cur,"\"image_path\":\"");
if(iv){iv+=14;const char *ie=strchr(iv,'"');if(ie&&ie-iv<(int)sizeof(imgpath)){memcpy(imgpath,iv,ie-iv);}}
/* extract mounted */
const char *mv=strstr(cur,"\"mounted\":");
if(mv){mv+=10;mounted=(mv[0]=='t');}
/* get file size from image_path */
char szbuf[32]="?";
if(imgpath[0]){
/* unescape JSON \/ */
char rp[512]={0};int ri=0;
for(int i=0;imgpath[i]&&ri<(int)sizeof(rp)-1;i++){
if(imgpath[i]=='\\'&&imgpath[i+1]=='/'){rp[ri++]='/';i++;}
else rp[ri++]=imgpath[i];}
struct stat ist; if(stat(rp,&ist)==0){
if(ist.st_size>1073741824) snprintf(szbuf,sizeof(szbuf),"%.2f GB",(double)ist.st_size/1073741824.0);
else snprintf(szbuf,sizeof(szbuf),"%lld MB",(long long)ist.st_size/1048576);}
char imgpath[512]={0},mp[256]={0};
long long sz=0; int mounted=0;
/* extract path, mount_point, size, mounted from images API */
const char *pv=strstr(cur,"\"path\":\""); if(pv){pv+=8;const char *pe=strchr(pv,'"');if(pe&&pe-pv<(int)sizeof(imgpath)-1)memcpy(imgpath,pv,pe-pv);}
const char *mv2=strstr(cur,"\"mount_point\":\""); if(mv2){mv2+=15;const char *me=strchr(mv2,'"');if(me&&me-mv2<(int)sizeof(mp)-1)memcpy(mp,mv2,me-mv2);}
const char *sv=strstr(cur,"\"size\":"); if(sv){sv+=7;sz=strtoll(sv,NULL,10);}
const char *mv=strstr(cur,"\"mounted\":"); if(mv){mv+=10;mounted=(mv[0]=='t');}
/* extract title_id from mount_point: /mnt/shadowmnt/PPSA20515_name_hash */
char tid[16]={0};
const char *bn=strrchr(mp,'/'); bn=bn?bn+1:mp;
char tmp[64]={0}; strncpy(tmp,bn,sizeof(tmp)-1);
char *us=strchr(tmp,'_'); if(us)*us=0;
if(strlen(tmp)==9){int ok=1;
for(int i=0;i<4;i++) if(!isalpha((unsigned char)tmp[i])){ok=0;break;}
for(int i=4;i<9;i++) if(!isdigit((unsigned char)tmp[i])){ok=0;break;}
if(ok) strncpy(tid,tmp,sizeof(tid)-1);}
/* look up title_name from games JSON if available */
char dname[128]={0};
if(tid[0]&&gam_json){
char srch[32]; snprintf(srch,sizeof(srch),"\"title_id\":\"%s\"",tid);
const char *gp=strstr(gam_json,srch);
if(gp){const char *nn=strstr(gp,"\"title_name\":\"");
if(nn){nn+=14;const char *ne=strchr(nn,'"');if(ne&&ne-nn<(int)sizeof(dname)-1)memcpy(dname,nn,ne-nn);}}
}
/* unescape name */
char dname[128]={0};{int di=0;
for(int i=0;name[i]&&di<(int)sizeof(dname)-1;i++){
if(name[i]=='\\'&&name[i+1]=='/'){ dname[di++]='/';i++;}
else dname[di++]=name[i];}
}
if(!dname[0]) strncpy(dname,tid,sizeof(dname)-1);
if(!dname[0]&&tid[0]) strncpy(dname,tid,sizeof(dname)-1);
/* unescape path for stat */
char rp[512]={0};{int ri=0;for(int i=0;imgpath[i]&&ri<(int)sizeof(rp)-1;i++){
if(imgpath[i]=='\\'&&imgpath[i+1]=='/'){rp[ri++]='/';i++;}else rp[ri++]=imgpath[i];}}
/* size from stat (more reliable than JSON size field) */
char szbuf[32]={0};
{struct stat ist;
if(rp[0]&&stat(rp,&ist)==0){
if(ist.st_size>1073741824) snprintf(szbuf,sizeof(szbuf),"%.2f GB",(double)ist.st_size/1073741824.0);
else snprintf(szbuf,sizeof(szbuf),"%lld MB",(long long)ist.st_size/1048576);}
else if(sz>0){
if(sz>1073741824) snprintf(szbuf,sizeof(szbuf),"%.2f GB",(double)sz/1073741824.0);
else snprintf(szbuf,sizeof(szbuf),"%lld MB",sz/1048576);}
else strncpy(szbuf,"?",sizeof(szbuf)-1);}
const char *mntcol=mounted?"color:#10b981":"color:var(--dim)";
const char *mnttxt=mounted?"&#9679; aktiv":"&#9675; inaktiv";
const char *bs_um="background:transparent;border:1px solid #f87171;border-radius:4px;color:#f87171;padding:3px 10px;cursor:pointer;font-size:.75rem;";
@@ -2953,23 +2958,24 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
GL("<div style=\"display:flex;align-items:center;gap:10px;padding:8px 4px;border-bottom:1px solid var(--border);\">");
GL("<div style=\"position:relative;width:48px;height:48px;border-radius:8px;overflow:hidden;background:#1e2a42;flex-shrink:0;\">"
"<img src=\"/api/icon/%s\" style=\"position:absolute;top:0;left:0;width:100%%;height:100%%;object-fit:cover;\" onerror=\"this.remove()\">"
"<span style=\"position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:.55rem;color:#64748b;font-family:monospace;\">%.4s</span></div>",tid,tid);
"<span style=\"position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:.55rem;color:#64748b;font-family:monospace;\">%.4s</span></div>",tid,tid[0]?tid:"?");
GL("<div style=\"flex:1;min-width:0;\">"
"<div style=\"font-size:.83rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;\">%s</div>"
"<div style=\"font-family:var(--mono);font-size:.7rem;color:var(--dim);\">%s &mdash; %s</div></div>",
dname, tid, szbuf);
dname[0]?dname:rp, tid[0]?tid:"–", szbuf);
GL("<div style=\"display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0;\">"
"<span style=\"font-size:.75rem;%s;\">%s</span>"
"<button type=\"button\" data-tid=\"%s\" data-path=\"%s\" onclick=\"%s(this.dataset.tid,this.dataset.path)\" style=\"%s\">%s</button>"
"</div></div>",
mntcol,mnttxt, tid,imgpath, mounted?"_smUnmount":"_smMount",
mntcol,mnttxt, tid,rp, mounted?"_smUnmount":"_smMount",
mounted?bs_um:bs_mt, mounted?"Unmount":"Mount");
n++; cur++;
}
if(n==0) GL("<p class=\"hint\">Keine Spiele gefunden</p>");
}
free(games_json);
free(img_json);
}
if(gam_json) free(gam_json);
out[pos]=0;
#undef GL
mg_http_reply(c,200,"Content-Type: text/html; charset=utf-8\r\nCache-Control: no-store\r\n","%s",out);
@@ -3009,6 +3015,16 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
if(stat(src,&sst)!=0) src[0]=0;}
closedir(sd);}
}
/* fallback: use GC's icon cache (already resized PNGs from Game Compressor) */
if(!src[0]){
DIR *gcd=opendir("/data/GameCompressor/icon-cache");
if(gcd){struct dirent *de;size_t tl=strlen(tid);
while(!src[0]&&(de=readdir(gcd))!=NULL)
if(strncmp(de->d_name,tid,tl)==0&&de->d_name[tl]=='-'){
snprintf(src,sizeof(src),"/data/GameCompressor/icon-cache/%s",de->d_name);
if(stat(src,&sst)!=0) src[0]=0;}
closedir(gcd);}
}
if(src[0]){
/* copy source to cache using cp via raw read/write */
FILE *sf=fopen(src,"rb");
+4 -3
View File
@@ -3501,9 +3501,10 @@ static void *p_open(const char *path, int flags) {
MultiByteToWideChar(CP_UTF8, 0, mode, -1, b2, sizeof(b2) / sizeof(b2[0]));
return (void *) _wfopen(b1, b2);
#else
const char *mode = flags == MG_FS_READ ? "rbe"
: (flags & MG_FS_EXCL) ? "wxbe"
: "a+be"; // e for CLOSEXEC
/* PS5/FreeBSD: 'e' flag (O_CLOEXEC) may not be supported; use plain modes */
const char *mode = flags == MG_FS_READ ? "rb"
: (flags & MG_FS_EXCL) ? "wxb"
: "a+b";
return (void *) fopen(path, mode);
#endif
}