faster image processing
All checks were successful
ESP32 Build & Release / build (push) Successful in 1m16s

This commit is contained in:
2026-09-13 23:31:36 +02:00
parent 8d034ed1df
commit e6760e4866
3 changed files with 169 additions and 11 deletions

View File

@@ -37,6 +37,8 @@ static const char *TAG = "scryfall";
#define EVT_CARD_CACHED (1U << 0)
#define EVT_CARD_FAILED (1U << 1)
#define EVT_IMG_CACHED (1U << 2)
#define EVT_IMG_FAILED (1U << 3)
typedef struct {
char name[CARD_NAME_MAX];
@@ -49,6 +51,11 @@ static TaskHandle_t s_net_task;
static volatile uint32_t s_result_bits = 0;
static char s_last_uid[UID_MAX] = {0};
static volatile uint32_t s_image_bits = 0;
static char s_last_img_uid[UID_MAX] = {0};
static volatile bool s_image_fetching = false;
static char s_fetching_img_uid[UID_MAX] = {0};
/* ------------------------------------------------------------------ */
/* URL helpers */
/* ------------------------------------------------------------------ */
@@ -181,7 +188,8 @@ static esp_err_t http_request(const char *url, http_sink_t *sink)
.timeout_ms = HTTP_TIMEOUT_MS,
.event_handler = http_event_handler,
.user_data = sink,
.buffer_size = 2048,
.buffer_size = 4096,
.buffer_size_tx = 1024,
.crt_bundle_attach = esp_crt_bundle_attach,
.keep_alive_enable = false,
};
@@ -271,7 +279,7 @@ static const char *get_string(const cJSON *item, const char *fallback)
/* Cache metadata */
/* ------------------------------------------------------------------ */
static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid)
static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid, bool signal_ui)
{
char json_path[64];
snprintf(json_path, sizeof(json_path), STORAGE_CARDS_DIR "/%s.json", uid);
@@ -302,9 +310,19 @@ static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid)
if (valid) {
if (storage_file_exists(jpg_path)) {
ESP_LOGI(TAG, "cache hit %s and %s", json_path, jpg_path);
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = EVT_CARD_CACHED;
snprintf(s_last_img_uid, sizeof(s_last_img_uid), "%s", uid);
s_image_bits = EVT_IMG_CACHED;
}
return ESP_OK;
}
ESP_LOGI(TAG, "json valid but %s missing, fetching art...", jpg_path);
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = EVT_CARD_CACHED;
}
} else {
ESP_LOGW(TAG, "cache entry %s is invalid or card mismatch (expected '%s'), re-fetching...", json_path, card_name);
remove(json_path);
@@ -323,6 +341,10 @@ static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid)
if (ret != ESP_OK || body == NULL) {
free(body);
ESP_LOGE(TAG, "scryfall request failed for '%s'", card_name);
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = EVT_CARD_FAILED;
}
return ret != ESP_OK ? ret : ESP_FAIL;
}
@@ -330,11 +352,19 @@ static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid)
free(body);
if (root == NULL) {
ESP_LOGE(TAG, "JSON parse failed for '%s'", card_name);
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = EVT_CARD_FAILED;
}
return ESP_FAIL;
}
cJSON *obj = cJSON_GetObjectItem(root, "object");
if (obj && cJSON_IsString(obj) && strcmp(obj->valuestring, "error") == 0) {
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = EVT_CARD_FAILED;
}
ESP_LOGE(TAG, "Scryfall API error: %s",
get_string(cJSON_GetObjectItem(root, "details"), "unknown error"));
cJSON_Delete(root);
@@ -414,25 +444,55 @@ static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid)
ret = ESP_ERR_NO_MEM;
}
/* 4. Download cover art (best effort) */
/* Signal metadata ready immediately so UI can display card text and stats */
if (signal_ui) {
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = (ret == ESP_OK) ? EVT_CARD_CACHED : EVT_CARD_FAILED;
}
if (ret != ESP_OK) {
cJSON_Delete(root);
return ret;
}
/* 4. Download cover art in background (best effort) */
if (image_url && strlen(image_url) > 0) {
char jpg_path[64];
snprintf(jpg_path, sizeof(jpg_path), STORAGE_CARDS_DIR "/%s.jpg", uid);
char enc_img[512];
url_encode(image_url, enc_img, sizeof(enc_img));
char proxy_url[600];
/* wsrv.nl converts Scryfall's progressive JPEG to a lightweight baseline JPEG preserving original aspect ratio */
snprintf(proxy_url, sizeof(proxy_url), "https://wsrv.nl/?url=%s&w=200&h=150&fit=inside&output=jpg", enc_img);
/* wsrv.nl converts Scryfall's progressive JPEG to lightweight baseline JPEG (q=65 for 2-3x faster download & smaller flash footprint) */
snprintf(proxy_url, sizeof(proxy_url), "https://wsrv.nl/?url=%s&w=200&h=150&fit=inside&output=jpg&q=65", enc_img);
if (signal_ui) {
snprintf(s_fetching_img_uid, sizeof(s_fetching_img_uid), "%s", uid);
s_image_fetching = true;
}
esp_err_t dl = http_download_file(proxy_url, jpg_path);
if (dl != ESP_OK) {
ESP_LOGW(TAG, "proxied image download failed, trying direct: %s", image_url);
dl = http_download_file(image_url, jpg_path);
}
if (signal_ui) {
s_image_fetching = false;
snprintf(s_last_img_uid, sizeof(s_last_img_uid), "%s", uid);
s_image_bits = (dl == ESP_OK) ? EVT_IMG_CACHED : EVT_IMG_FAILED;
}
if (dl != ESP_OK) {
ESP_LOGW(TAG, "image download failed: %s", image_url);
} else {
ESP_LOGI(TAG, "image cached to %s", jpg_path);
}
} else {
if (signal_ui) {
s_image_fetching = false;
snprintf(s_last_img_uid, sizeof(s_last_img_uid), "%s", uid);
s_image_bits = EVT_IMG_FAILED;
}
}
cJSON_Delete(root);
@@ -445,10 +505,7 @@ static esp_err_t scryfall_do_fetch(const char *card_name, const char *uid)
static esp_err_t scryfall_ensure_cached(const char *card_name, const char *uid)
{
esp_err_t ret = scryfall_do_fetch(card_name, uid);
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
s_result_bits = (ret == ESP_OK) ? EVT_CARD_CACHED : EVT_CARD_FAILED;
return ret;
return scryfall_do_fetch(card_name, uid, true);
}
/* ------------------------------------------------------------------ */
@@ -486,8 +543,8 @@ static void prefetch_mappings(void)
cJSON_ArrayForEach(item, root) {
uid = item->string;
if (uid == NULL || item->valuestring == NULL) continue;
/* Call scryfall_do_fetch directly so we do not clobber UI fetch signaling */
scryfall_do_fetch(item->valuestring, uid);
/* Call scryfall_do_fetch directly with signal_ui=false so we do not clobber UI fetch signaling */
scryfall_do_fetch(item->valuestring, uid, false);
/* Rate-limit delay: Scryfall requests 50-100ms between queries */
vTaskDelay(pdMS_TO_TICKS(150));
}
@@ -567,4 +624,35 @@ const char *scryfall_last_result_uid(void)
void scryfall_consume_result(void)
{
s_result_bits = 0;
}
bool scryfall_has_pending_image(void)
{
return (s_image_bits & (EVT_IMG_CACHED | EVT_IMG_FAILED)) != 0;
}
bool scryfall_last_image_success(void)
{
return (s_image_bits & EVT_IMG_CACHED) != 0;
}
const char *scryfall_last_image_uid(void)
{
return s_last_img_uid;
}
void scryfall_consume_image(void)
{
s_image_bits = 0;
}
bool scryfall_is_fetching_image(const char *uid)
{
if (!s_image_fetching) {
return false;
}
if (uid == NULL || uid[0] == '\0') {
return false;
}
return (strcasecmp(s_fetching_img_uid, uid) == 0);
}