/* logic/cover.c — 封面下载(HTTPS)+ 限幅 + 解码缓存(单张,URL 变更即换;后台线程)。 * * 内存纪律:下载限幅 NCM_COVER_MAX(~288KB);解码后的 RGBA 保持单张(换封面先 free 旧再解码)。 * 线程模型:一个持久的 cover 工作线程负责「目标 URL → 下载 → 解码 → 换入」;UI 线程只经 * cover_request(url)(提交目标)与 cover_get()/cover_changed()(读取/判变)+ cover_ack()(消费)。 * 工作线程不碰 meow_ui(绘制在 UI 线程 tick 判 cover_changed 后触发 invalidate)。 */ #include "../ncm_app_internal.h" #include #include #include #include #include static pthread_mutex_t g_cm = PTHREAD_MUTEX_INITIALIZER; static meow_img *g_cover_img; static char g_cover_url[512]; /* g_cover_img 对应的 URL */ static char g_tgt_url[512]; /* 目标 URL(worker 拉取) */ static pthread_t g_thr; static int g_thr_on; static int g_quit; static volatile int g_gen; /* 每次换入 +1 */ static int g_acked; /* UI 已消费的版本 */ static char g_cover_path[512]; /* 封面原始文件路径(供 launcher) */ static unsigned char g_buf[NCM_COVER_MAX]; /* 单张下载缓冲(常驻) */ static meow_img *decode_cover(const char *url, size_t *out_len) { if (!url || !url[0]) return NULL; ncm_http_req_t req; memset(&req, 0, sizeof req); req.url = url; req.method = "GET"; req.extra_hdrs = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"; req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.timeout_ms = 10000; ncm_http_resp_t resp; int rc = ncm_http_request(&req, (char *)g_buf, sizeof g_buf, &resp); if (rc != NCM_OK || resp.status != 200 || resp.body_len == 0) return NULL; if (out_len) *out_len = resp.body_len; return meow_img_load_mem((const void *)resp.body, (int)resp.body_len); } /* 封面缓存键:URL 的 djb2 哈希(稳定性由 URL 决定——同一专辑封面 URL 名即可命中)。 * 走 ncm_cache(LRU 口径沿用 ncm_cache,见 REPORT §14.2):键进 ncm_cache_path => 文件 * `song_cov.jpg`,与 mp3(song_.mp3)同池,lookup/commit 刷新 mtime(LRU)。 */ static void cover_cache_id(const char *url, char *out, size_t out_sz) { unsigned long h = 5381; for (const char *p = url; *p; p++) h = ((h << 5) + h) + (unsigned char)*p; snprintf(out, out_sz, "cov%lx", h); } /* 读整文件到 buf(< cap),成功返回 1,写入 *len。 */ static int read_file_into_buf(const char *path, unsigned char *buf, size_t cap, size_t *len) { FILE *f = fopen(path, "rb"); if (!f) return 0; size_t n = fread(buf, 1, cap, f); fclose(f); if (n == 0) return 0; if (len) *len = n; return 1; } /* 写 cover_current.jpg 供 launcher 媒体卡片读(兼容保留)。调用方须已持有 g_cm * (早前版本在此内再 lock g_cm → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。 */ static void save_cover_raw(const char *url, size_t len) { if (!url || len == 0) return; if (g_st.cache_dir[0]) { char path[NCM_CACHE_DIR_MAX + 32]; /* >512:宿主 glibc fortify 对 %s 展开宽证明可放 */ snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir); ncm_store_write_file(path, g_buf, len); ncm_strlcpy(g_cover_path, path, sizeof g_cover_path); } } static void *cover_worker(void *arg) { (void)arg; while (!g_quit) { pthread_mutex_lock(&g_cm); if (!g_tgt_url[0]) { pthread_mutex_unlock(&g_cm); usleep(250 * 1000); continue; } char url[512]; ncm_strlcpy(url, g_tgt_url, sizeof url); pthread_mutex_unlock(&g_cm); char key[32]; cover_cache_id(url, key, sizeof key); size_t blen = 0; meow_img *img = NULL; /* ① 缓存命中直接解码(免下载、离线可看;LRU:lookup 刷新 mtime) */ char cpath[512]; if (g_st.ctx.cache.dir[0] && ncm_cache_lookup(&g_st.ctx.cache, key, ".jpg", cpath, sizeof cpath) == NCM_OK && read_file_into_buf(cpath, g_buf, sizeof g_buf, &blen)) { img = meow_img_load_mem(g_buf, (int)blen); } /* ② 未命中 → 下载解码;成功后落进 ncm_cache(LRU 池,超限由 ncm_cache_evict 淘汰) */ if (!img) { blen = 0; img = decode_cover(url, &blen); if (!img) { usleep(500 * 1000); continue; } if (g_st.ctx.cache.dir[0]) { ncm_cache_writer_t *w = NULL; if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) { ncm_cache_write(w, g_buf, blen); ncm_cache_commit(w); } } } pthread_mutex_lock(&g_cm); /* 目标仍是 url 才换入(期间未提交更早) */ if (g_tgt_url[0] && strcmp(g_tgt_url, url) == 0) { if (g_cover_img) meow_img_free(g_cover_img); g_cover_img = img; ncm_strlcpy(g_cover_url, url, sizeof g_cover_url); save_cover_raw(url, blen); g_gen++; } else { meow_img_free(img); } pthread_mutex_unlock(&g_cm); } return NULL; } /* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。 */ void cover_request(const char *url) { if (!url || !url[0]) return; pthread_mutex_lock(&g_cm); if (g_cover_url[0] && strcmp(g_cover_url, url) == 0) { pthread_mutex_unlock(&g_cm); return; } ncm_strlcpy(g_tgt_url, url, sizeof g_tgt_url); pthread_mutex_unlock(&g_cm); if (!g_thr_on) { g_thr_on = 1; if (pthread_create(&g_thr, NULL, cover_worker, NULL) == 0) pthread_detach(g_thr); else g_thr_on = 0; } } /* UI 线程:读取当前封面(不拥有)。 */ meow_img *cover_get(void) { meow_img *im; pthread_mutex_lock(&g_cm); im = g_cover_img; pthread_mutex_unlock(&g_cm); return im; } /* 是否有新封面待绘制(UI tick 判变)。 */ int cover_changed(void) { return g_gen != g_acked; } /* UI 消费一次:标记已见当前版本。 */ void cover_ack(void) { g_acked = g_gen; } /* 封面原始文件路径(供 launcher 媒体卡片读);NULL=无。 */ const char *cover_current_path(void) { const char *p; pthread_mutex_lock(&g_cm); p = g_cover_path[0] ? g_cover_path : NULL; pthread_mutex_unlock(&g_cm); return p; } void cover_cache_reset(void) { pthread_mutex_lock(&g_cm); if (g_cover_img) { meow_img_free(g_cover_img); g_cover_img = NULL; } g_cover_url[0] = 0; g_tgt_url[0] = 0; g_cover_path[0] = 0; pthread_mutex_unlock(&g_cm); g_gen++; g_acked = g_gen; }