Files

447 lines
22 KiB
C

// Omega — "La mia libreria": i backup dei giochi che l'utente possiede, con link,
// copertina e dati. Resta solo sulla console (OMEGA_DIR/library.json) e il demone
// è l'unico a scriverla: la UI e il telecomando passano dalle sue rotte.
// Si riempie a mano (dalla UI o dal telefono) oppure da un JSON: incollato o
// caricato dal telefono, o da un link che il demone scarica e risincronizza.
// I nomi dei campi accettati sono gli stessi del server (api/src/endpoints/store.js).
#include "lib.h"
#include "json.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>
#include <ctype.h>
#include <unistd.h>
#define MAX_ITEMS 2000
#define MAX_JSON (8 * 1024 * 1024)
void player_log(const char *fmt, ...);
typedef struct {
char id[40], title[128], url[1024], cover[1024], platform[8], version[24], title_id[16], kind[8], origin[8]; // origin: manual, import, json, upload
char desc[1200];
char shots[4][512]; int nshots;
double size;
} Item;
static pthread_mutex_t mx = PTHREAD_MUTEX_INITIALIZER;
static Item *items; static int n;
static char src_url[1024]; static long src_sync; static char src_status[64];
static char path[300];
static lib_fetch_fn fetch_fn;
static unsigned seq;
static unsigned fnv(const char *s) { unsigned h = 2166136261u; for (; *s; s++) { h ^= (unsigned char)*s; h *= 16777619u; } return h; }
static int is_http(const char *u) { return !strncasecmp(u, "http://", 7) || !strncasecmp(u, "https://", 8); }
// file caricati dal telefono o dal PC (ctl.c): restano sulla console
#ifdef PS5
static int is_local(const char *u) { return !strncmp(u, "file:///data/", 13) && !strstr(u, "/../"); }
#else
static int is_local(const char *u) { return !strncmp(u, "file:///", 8) && !strstr(u, "/../"); } // prova sul Mac: cartella di prova
#endif
static void kind_of(const char *hint, const char *url, char *out, size_t on) {
const char *k = hint && *hint ? hint : NULL;
if (k && (!strcasecmp(k, "pkg") || !strcasecmp(k, "zip") || !strcasecmp(k, "elf") || !strcasecmp(k, "folder"))) { snprintf(out, on, "%s", k); for (char *c = out; *c; c++) *c = (char)(*c | 0x20); return; }
const char *q = strchr(url, '?'); size_t L = q ? (size_t)(q - url) : strlen(url);
if (L >= 4 && !strncasecmp(url + L - 4, ".pkg", 4)) snprintf(out, on, "pkg");
else if (L >= 4 && !strncasecmp(url + L - 4, ".zip", 4)) snprintf(out, on, "zip");
else if (L >= 4 && !strncasecmp(url + L - 4, ".elf", 4)) snprintf(out, on, "elf");
else snprintf(out, on, "auto");
}
static const char *first(JVal *o, const char *const *keys) {
for (int i = 0; keys[i]; i++) { const char *v = jstr(o, keys[i], NULL); if (v && *v) return v; }
return "";
}
static int has_any(JVal *o, const char *const *keys) {
for (int i = 0; keys[i]; i++) if (jget(o, keys[i])) return 1;
return 0;
}
// "45 GB", "1.2GB", "700 MB", "123456" o un numero → byte
static double size_of(JVal *o) {
static const char *const K[] = { "size", NULL };
for (int i = 0; K[i]; i++) {
JVal *v = jget(o, K[i]); if (!v) continue;
if (v->t == J_NUM) return v->n > 0 ? v->n : 0;
if (v->t == J_STR && v->s) {
char *end; double x = strtod(v->s, &end);
if (x <= 0) continue;
while (*end == ' ') end++;
char u = (char)toupper((unsigned char)*end);
return u == 'T' ? x * 1e12 : u == 'G' ? x * 1e9 : u == 'M' ? x * 1e6 : u == 'K' ? x * 1e3 : x;
}
}
return 0;
}
// CUSA12345
static int tid_ok(const char *t) {
for (int i = 0; i < 9; i++) if (!(i < 4 ? isalpha((unsigned char)t[i]) : isdigit((unsigned char)t[i]))) return 0;
return 1;
}
static void tid_from(JVal *o, char *out, size_t on) {
static const char *const K_TID[] = { "title_id", NULL };
out[0] = 0;
const char *t = first(o, K_TID);
if (strlen(t) >= 9 && tid_ok(t)) snprintf(out, on, "%.9s", t);
for (char *p = out; *p; p++) *p = (char)toupper((unsigned char)*p);
}
// un elemento da JSON (manuale o importato); 0 = valido. Solo il formato di
// Omega (README del demone): la libreria è per i propri backup, non legge le
// liste di cataloghi che girano nella scena.
static const char *const K_TITLE[] = { "title", "name", NULL };
static const char *const K_URL[] = { "url", "download", "link", NULL };
static const char *const K_COVER[] = { "cover", "image", NULL };
static const char *const K_DESC[] = { "description", NULL };
static const char *const K_VER[] = { "version", NULL };
static const char *const K_PLAT[] = { "platform", NULL };
static const char *const K_KIND[] = { "type", NULL };
static const char *const K_SIZE[] = { "size", NULL };
static const char *const K_SHOTS[] = { "screenshots", "images", NULL };
static const char *const K_TIDS[] = { "title_id", NULL };
static int item_from(Item *it, JVal *o, const char *origin, int idx) {
memset(it, 0, sizeof *it);
snprintf(it->title, sizeof it->title, "%s", first(o, K_TITLE));
snprintf(it->url, sizeof it->url, "%s", first(o, K_URL));
if (!it->title[0] || !(is_http(it->url) || (!strcmp(origin, "upload") && is_local(it->url)))) return -1;
const char *cv = first(o, K_COVER); if (is_http(cv)) snprintf(it->cover, sizeof it->cover, "%s", cv);
snprintf(it->desc, sizeof it->desc, "%s", first(o, K_DESC));
snprintf(it->version, sizeof it->version, "%s", first(o, K_VER));
tid_from(o, it->title_id, sizeof it->title_id);
char pl[8]; snprintf(pl, sizeof pl, "%s", first(o, K_PLAT)); for (char *c = pl; *c; c++) *c = (char)toupper((unsigned char)*c);
if (!strcmp(pl, "PS4") || !strcmp(pl, "PS5")) snprintf(it->platform, sizeof it->platform, "%s", pl);
else if (it->title_id[0]) snprintf(it->platform, sizeof it->platform, "%s", !strncmp(it->title_id, "PPSA", 4) ? "PS5" : "PS4"); // PPSA = PS5, il resto (CUSA...) = PS4
kind_of(first(o, K_KIND), it->url, it->kind, sizeof it->kind);
it->size = size_of(o);
JVal *sh = jget(o, "screenshots"); if (!sh) sh = jget(o, "images");
JFOR(s, sh) { if (it->nshots >= 4) break; if (s->s && is_http(s->s)) snprintf(it->shots[it->nshots++], 512, "%s", s->s); }
snprintf(it->origin, sizeof it->origin, "%s", origin);
const char *ext = jstr(o, "id", NULL);
if (ext && *ext) snprintf(it->id, sizeof it->id, "%c%.30s", origin[0], ext);
else snprintf(it->id, sizeof it->id, "%c%08x", origin[0], fnv(it->url)); // stabile: non cambia se la lista cambia ordine
(void)idx;
return 0;
}
// ---------------------------------------------------------------- file --
static void jw(FILE *f, const char *k, const char *v, int comma) {
fprintf(f, "%s\"%s\":\"", comma ? "," : "", k);
for (const unsigned char *p = (const unsigned char *)v; *p; p++) {
if (*p == '"' || *p == '\\') fprintf(f, "\\%c", *p);
else if (*p < 0x20) fprintf(f, "\\u%04x", *p);
else fputc(*p, f);
}
fputc('"', f);
}
static void save_locked(void) {
char tmp[320]; snprintf(tmp, sizeof tmp, "%s.tmp", path);
FILE *f = fopen(tmp, "w"); if (!f) return;
fputs("{\"source\":{", f); jw(f, "url", src_url, 0); fprintf(f, ",\"last_sync\":%ld", src_sync); jw(f, "status", src_status, 1); fputs("},\"items\":[", f);
for (int i = 0; i < n; i++) {
Item *it = &items[i];
fputs(i ? ",{" : "{", f);
jw(f, "id", it->id, 0); jw(f, "title", it->title, 1); jw(f, "url", it->url, 1); jw(f, "cover", it->cover, 1);
jw(f, "platform", it->platform, 1); jw(f, "version", it->version, 1); jw(f, "title_id", it->title_id, 1);
jw(f, "kind", it->kind, 1); jw(f, "origin", it->origin, 1); jw(f, "description", it->desc, 1);
fprintf(f, ",\"size\":%.0f,\"screenshots\":[", it->size);
for (int k = 0; k < it->nshots; k++) { if (k) fputc(',', f); fputc('"', f); fputs(it->shots[k], f); fputc('"', f); }
fputs("]}", f);
}
fputs("]}\n", f);
// su disco prima del rename: una console spenta a metà non lascia la libreria vuota
int ok = fflush(f) == 0 && fsync(fileno(f)) == 0;
if (fclose(f) != 0) ok = 0;
if (ok) rename(tmp, path); else unlink(tmp);
seq++;
}
void lib_init(const char *file, lib_fetch_fn fn) {
snprintf(path, sizeof path, "%s", file); fetch_fn = fn;
items = calloc(MAX_ITEMS, sizeof *items);
FILE *f = fopen(path, "r"); if (!f) return;
fseek(f, 0, SEEK_END); long L = ftell(f); fseek(f, 0, SEEK_SET);
char *b = L > 0 && L < MAX_JSON ? malloc((size_t)L + 1) : NULL;
if (b && fread(b, 1, (size_t)L, f) == (size_t)L) {
b[L] = 0;
JVal *j = json_parse(b);
JVal *s = jget(j, "source");
jcpy(src_url, sizeof src_url, s, "url"); src_sync = (long)jnum(s, "last_sync", 0); jcpy(src_status, sizeof src_status, s, "status");
JFOR(o, jget(j, "items")) {
if (n >= MAX_ITEMS) break;
Item it; const char *origin = jstr(o, "origin", "manual");
if (item_from(&it, o, origin[0] == 'j' ? "json" : origin[0] == 'i' ? "import" : origin[0] == 'u' ? "upload" : "manual", n) == 0) { const char *id = jstr(o, "id", ""); if (*id) snprintf(it.id, sizeof it.id, "%s", id); items[n++] = it; }
}
json_free(j);
}
free(b); fclose(f);
player_log("libreria: %d giochi", n);
}
// ------------------------------------------------------------------ JSON --
// "chiave":"valore" con escape (k NULL = solo la stringa, per gli array)
static size_t put(char *o, size_t cap, size_t at, const char *k, const char *v, int comma) {
if (at + 16 >= cap) return at;
if (k) at += (size_t)snprintf(o + at, cap - at, "%s\"%s\":\"", comma ? "," : "", k);
else at += (size_t)snprintf(o + at, cap - at, "%s\"", comma ? "," : "");
for (const unsigned char *p = (const unsigned char *)v; *p && at + 8 < cap; p++) {
if (*p == '"' || *p == '\\') { o[at++] = '\\'; o[at++] = (char)*p; }
else if (*p < 0x20) at += (size_t)snprintf(o + at, cap - at, "\\u%04x", *p);
else o[at++] = (char)*p;
}
if (at + 2 < cap) o[at++] = '"';
o[at] = 0;
return at;
}
static size_t item_json(char *o, size_t cap, size_t at, const Item *it, int lite) {
o[at++] = '{';
at = put(o, cap, at, "id", it->id, 0); at = put(o, cap, at, "title", it->title, 1); at = put(o, cap, at, "url", it->url, 1);
at = put(o, cap, at, "cover", it->cover, 1); at = put(o, cap, at, "platform", it->platform, 1); at = put(o, cap, at, "version", it->version, 1);
at = put(o, cap, at, "title_id", it->title_id, 1); at = put(o, cap, at, "kind", it->kind, 1); at = put(o, cap, at, "origin", it->origin, 1);
if (!lite) at = put(o, cap, at, "description", it->desc, 1);
at += (size_t)snprintf(o + at, cap - at, ",\"size\":%.0f", it->size);
if (!lite) {
at += (size_t)snprintf(o + at, cap - at, ",\"screenshots\":[");
for (int k = 0; k < it->nshots; k++) at = put(o, cap, at, NULL, it->shots[k], k > 0);
at += (size_t)snprintf(o + at, cap - at, "]");
}
at += (size_t)snprintf(o + at, cap - at, "}");
return at;
}
// Un gioco solo, completo (la UI chiede l'elenco leggero e la scheda quando la apri).
size_t lib_item_json(const char *id, char *o, size_t cap) {
pthread_mutex_lock(&mx);
size_t at = 0;
for (int i = 0; i < n; i++) if (!strcmp(items[i].id, id)) { at = item_json(o, cap, 0, &items[i], 0); break; }
pthread_mutex_unlock(&mx);
if (at < cap) o[at] = 0;
return at;
}
size_t lib_list_json_lite(char *o, size_t cap, int lite);
size_t lib_list_json(char *o, size_t cap) { return lib_list_json_lite(o, cap, 0); }
size_t lib_list_json_lite(char *o, size_t cap, int lite) {
pthread_mutex_lock(&mx);
size_t at = (size_t)snprintf(o, cap, "{\"seq\":%u,\"source\":{", seq);
at = put(o, cap, at, "url", src_url, 0);
at += (size_t)snprintf(o + at, cap - at, ",\"last_sync\":%ld", src_sync);
at = put(o, cap, at, "status", src_status, 1);
at += (size_t)snprintf(o + at, cap - at, "},\"items\":[");
for (int i = 0; i < n && at + 8192 < cap; i++) {
o[at++] = i ? ',' : ' ';
at = item_json(o, cap, at, &items[i], lite);
}
at += (size_t)snprintf(o + at, cap - at, "]}");
pthread_mutex_unlock(&mx);
return at < cap ? at : cap - 1;
}
// ------------------------------------------------------------- modifiche --
int lib_add(JVal *o, char *err, size_t en) {
if (!items) { snprintf(err, en, "starting"); return -1; }
Item it;
const char *id = jstr(o, "id", NULL);
// un gioco caricato si può rinominare o completare, ma file e tipo restano quelli
char up_url[1024] = "", up_kind[8] = "";
pthread_mutex_lock(&mx);
if (id && *id) for (int i = 0; i < n; i++) if (!strcmp(items[i].id, id) && !strcmp(items[i].origin, "upload")) { snprintf(up_url, sizeof up_url, "%s", items[i].url); snprintf(up_kind, sizeof up_kind, "%s", items[i].kind); }
pthread_mutex_unlock(&mx);
if (up_url[0]) {
if (item_from(&it, o, "upload", 0) != 0 && !it.title[0]) { snprintf(err, en, "title_and_url_required"); return -1; }
snprintf(it.url, sizeof it.url, "%s", up_url); snprintf(it.kind, sizeof it.kind, "%s", up_kind);
} else if (item_from(&it, o, "manual", (int)time(NULL)) != 0) { snprintf(err, en, "title_and_url_required"); return -1; }
pthread_mutex_lock(&mx);
int at = -1;
if (id && *id) for (int i = 0; i < n; i++) if (!strcmp(items[i].id, id)) { at = i; break; }
if (at >= 0) {
// modifica: i campi che la richiesta non manda restano com'erano (la UI ne
// manda solo alcuni, e prima descrizione, dimensione e screenshot sparivano)
Item *old = &items[at];
if (!has_any(o, K_COVER)) snprintf(it.cover, sizeof it.cover, "%s", old->cover);
if (!has_any(o, K_DESC)) snprintf(it.desc, sizeof it.desc, "%s", old->desc);
if (!has_any(o, K_VER)) snprintf(it.version, sizeof it.version, "%s", old->version);
if (!has_any(o, K_TIDS)) snprintf(it.title_id, sizeof it.title_id, "%s", old->title_id);
if (!has_any(o, K_PLAT) && !it.platform[0]) snprintf(it.platform, sizeof it.platform, "%s", old->platform);
if (!has_any(o, K_KIND) && !strcmp(it.url, old->url)) snprintf(it.kind, sizeof it.kind, "%s", old->kind);
if (!has_any(o, K_SIZE)) it.size = old->size;
if (!has_any(o, K_SHOTS)) { it.nshots = old->nshots; memcpy(it.shots, old->shots, sizeof it.shots); }
snprintf(it.id, sizeof it.id, "%s", old->id); snprintf(it.origin, sizeof it.origin, "%s", old->origin);
items[at] = it;
}
else if (n >= MAX_ITEMS) { pthread_mutex_unlock(&mx); snprintf(err, en, "library_full"); return -1; }
else { snprintf(it.id, sizeof it.id, "m%08x%04x", fnv(it.url) ^ (unsigned)time(NULL), (unsigned)(rand() & 0xffff)); items[n++] = it; }
save_locked();
pthread_mutex_unlock(&mx);
return 0;
}
int lib_remove(const char *id, char *url_out, size_t un) {
if (!items) return -1;
pthread_mutex_lock(&mx);
int rc = -1;
if (url_out && un) url_out[0] = 0;
for (int i = 0; i < n; i++) if (!strcmp(items[i].id, id)) { if (url_out && !strcmp(items[i].origin, "upload")) snprintf(url_out, un, "%s", items[i].url); memmove(&items[i], &items[i + 1], (size_t)(n - i - 1) * sizeof *items); n--; rc = 0; break; }
if (!rc) save_locked();
pthread_mutex_unlock(&mx);
return rc;
}
// Un gioco caricato dal telefono o dal PC (file:///data/...): cartella, pkg, zip o elf.
int lib_add_upload(const char *title, const char *url, const char *kind, const char *cover,
const char *title_id, const char *version, const char *platform, char *id_out, size_t idn) {
if (!items || !title[0] || !is_local(url)) return -1;
Item it; memset(&it, 0, sizeof it);
snprintf(it.title, sizeof it.title, "%s", title); snprintf(it.url, sizeof it.url, "%s", url);
snprintf(it.kind, sizeof it.kind, "%s", kind); snprintf(it.origin, sizeof it.origin, "upload");
if (cover && is_http(cover)) snprintf(it.cover, sizeof it.cover, "%s", cover);
if (title_id) snprintf(it.title_id, sizeof it.title_id, "%s", title_id);
if (version) snprintf(it.version, sizeof it.version, "%s", version);
if (platform) snprintf(it.platform, sizeof it.platform, "%s", platform);
pthread_mutex_lock(&mx);
if (n >= MAX_ITEMS) { pthread_mutex_unlock(&mx); return -1; }
snprintf(it.id, sizeof it.id, "u%08x%04x", fnv(url) ^ (unsigned)time(NULL), (unsigned)(rand() & 0xffff));
items[n++] = it;
if (id_out) snprintf(id_out, idn, "%s", it.id);
save_locked();
pthread_mutex_unlock(&mx);
return 0;
}
// Esito dell'ultima importazione, per la risposta (ctl.c) e lo stato del link.
static int last_added, last_updated, last_skipped;
void lib_last_import(int *added, int *updated, int *skipped) {
pthread_mutex_lock(&mx); *added = last_added; *updated = last_updated; *skipped = last_skipped; pthread_mutex_unlock(&mx);
}
// L'elenco dei giochi: un array, {"items":[...]} ({"games"}) o un gioco solo.
static JVal *list_of(JVal *j) {
if (!j) return NULL;
if (j->t == J_ARR) return j;
if (j->t != J_OBJ) return NULL;
JVal *v = jget(j, "items"); if (!v) v = jget(j, "games");
if (v && v->t == J_ARR) return v;
if (*first(j, K_URL) || *first(j, K_TITLE)) return j; // un gioco solo
return NULL;
}
// Importa un JSON. replace_json: toglie prima le voci arrivate dal link
// (sincronizzazione), ma solo se il JSON nuovo ha almeno un gioco valido: un
// link che oggi risponde male non deve svuotare la libreria.
// origin: "json" = dal link collegato (rimpiazzato a ogni sincronizzazione),
// "import" = importato una volta dal telefono (resta finché non lo togli tu)
static int import_val(JVal *j, int replace_json, const char *origin, char *err, size_t en);
static int import_buf(const char *buf, int replace_json, const char *origin, char *err, size_t en) {
if (!items) { snprintf(err, en, "starting"); return -1; }
if (!strncmp(buf, "\xEF\xBB\xBF", 3)) buf += 3; // BOM UTF-8 (file salvati da Windows)
while (*buf == ' ' || *buf == '\n' || *buf == '\r' || *buf == '\t') buf++;
if (*buf == '<') { snprintf(err, en, "html_not_json"); return -1; } // pagina web al posto del file (Drive, GitHub "blob"...)
JVal *j = json_parse(buf);
if (!j) { snprintf(err, en, "invalid_json"); return -1; }
int r = import_val(j, replace_json, origin, err, en);
json_free(j);
return r;
}
static int import_val(JVal *j, int replace_json, const char *origin, char *err, size_t en) {
if (!items) { snprintf(err, en, "starting"); return -1; }
JVal *arr = list_of(j);
if (!arr) { snprintf(err, en, "no_items"); return -1; }
// prima si leggono tutte le voci, poi si tocca la libreria
int cap = MAX_ITEMS, nv = 0, skipped = 0, idx = 0;
Item *val = calloc((size_t)cap, sizeof *val);
if (!val) { snprintf(err, en, "memory"); return -1; }
if (arr == j && j->t == J_OBJ) { if (item_from(&val[0], j, origin, 0) == 0) nv = 1; else skipped = 1; }
else JFOR(o, arr) {
if (nv >= cap) { skipped++; continue; }
Item *it = &val[nv];
int ok = item_from(it, o, origin, idx) == 0;
idx++;
if (ok) nv++; else skipped++;
}
if (!nv) { free(val); snprintf(err, en, skipped ? "no_valid_items" : "no_items"); pthread_mutex_lock(&mx); last_added = last_updated = 0; last_skipped = skipped; pthread_mutex_unlock(&mx); return -1; }
pthread_mutex_lock(&mx);
if (replace_json) { int w = 0; for (int i = 0; i < n; i++) if (strcmp(items[i].origin, "json")) items[w++] = items[i]; n = w; }
int added = 0, updated = 0;
for (int v = 0; v < nv; v++) {
Item *it = &val[v];
int dup = -1; for (int i = 0; i < n; i++) if (!strcmp(items[i].url, it->url)) { dup = i; break; }
if (dup >= 0) { if (!strcmp(items[dup].origin, origin)) { items[dup] = *it; updated++; } else skipped++; continue; }
if (n >= MAX_ITEMS) { skipped++; continue; }
items[n++] = *it; added++;
}
last_added = added; last_updated = updated; last_skipped = skipped;
save_locked();
pthread_mutex_unlock(&mx);
free(val);
player_log("libreria: importati %d nuovi, %d aggiornati, %d scartati", added, updated, skipped);
return added;
}
int lib_import(JVal *body, char *err, size_t en) {
// {"json":"<testo>"} dal telefono, oppure il JSON stesso ({"items":[...]})
const char *txt = jstr(body, "json", NULL);
if (txt && *txt) return import_buf(txt, 0, "import", err, en);
return import_val(body, 0, "import", err, en);
}
// Collega (o scollega, url vuoto) il link a un JSON e lo sincronizza subito.
int lib_set_source(const char *url, char *err, size_t en) {
if (!items) { snprintf(err, en, "starting"); return -1; }
pthread_mutex_lock(&mx);
snprintf(src_url, sizeof src_url, "%s", url ? url : "");
if (!src_url[0]) { int w = 0; for (int i = 0; i < n; i++) if (strcmp(items[i].origin, "json")) items[w++] = items[i]; n = w; src_status[0] = 0; src_sync = 0; }
save_locked();
pthread_mutex_unlock(&mx);
return src_url[0] ? lib_sync(err, en) : 0;
}
// Link "da browser" che non portano al file: GitHub blob, Dropbox, Google Drive.
static void direct_link(const char *u, char *out, size_t on) {
const char *p;
if ((p = strstr(u, "://github.com/")) && strstr(u, "/blob/")) {
// https://github.com/<utente>/<repo>/blob/<ramo>/<file> → raw.githubusercontent.com/<utente>/<repo>/<ramo>/<file>
const char *rest = p + 14, *blob = strstr(rest, "/blob/");
snprintf(out, on, "https://raw.githubusercontent.com/%.*s/%s", (int)(blob - rest), rest, blob + 6);
return;
}
if (strstr(u, "dropbox.com/") && (p = strstr(u, "dl=0"))) { snprintf(out, on, "%.*sdl=1%s", (int)(p - u), u, p + 4); return; }
if ((p = strstr(u, "drive.google.com/file/d/"))) {
const char *id = p + 24; size_t L = strcspn(id, "/?");
snprintf(out, on, "https://drive.google.com/uc?export=download&id=%.*s", (int)L, id);
return;
}
snprintf(out, on, "%s", u);
}
int lib_sync(char *err, size_t en) {
char url[1024];
pthread_mutex_lock(&mx); snprintf(url, sizeof url, "%s", src_url); pthread_mutex_unlock(&mx);
if (!url[0]) { snprintf(err, en, "no_source"); return -1; }
if (!is_http(url)) { snprintf(err, en, "invalid_url"); return -1; }
if (!fetch_fn) { snprintf(err, en, "no_network"); return -1; }
char fetch_url[1100]; direct_link(url, fetch_url, sizeof fetch_url);
char *buf = malloc(MAX_JSON + 1); if (!buf) { snprintf(err, en, "memory"); return -1; }
long got = fetch_fn(fetch_url, buf, MAX_JSON);
int r;
if (got <= 0) { snprintf(err, en, got == -2 ? "too_large" : "fetch_failed"); r = -1; }
else { buf[got] = 0; r = import_buf(buf, 1, "json", err, en); }
free(buf);
pthread_mutex_lock(&mx);
src_sync = (long)time(NULL);
// "ok 12 3" = giochi dal link e scartati; altrimenti il codice d'errore
if (r >= 0) { int tot = 0; for (int i = 0; i < n; i++) tot += !strcmp(items[i].origin, "json"); snprintf(src_status, sizeof src_status, "ok %d %d", tot, last_skipped); }
else snprintf(src_status, sizeof src_status, "%s", err);
save_locked();
pthread_mutex_unlock(&mx);
player_log("libreria: sincronizzazione %s -> %d", url, r);
return r;
}