Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/logic/cover.c
T
AskaEth 9d93f1b1ef fix(ncm): 封面矩阵实锤修正 —— 200解码失败=死对象返JSON错误体(非webp)/UA改桌面浏览器(CNMPlayer UA被拦403)/非图k不定性不重试
真机(设备恢复)CDN 行为矩阵结论并落地:
- 裸请求(无Referer)→403: 防盗链坐实, 封面请求头加 Referer https://music.163.com/。
- +Referer 仍不返真图; 全部变体只见 403/404/200+JSON错误体(43B {"code, application/json)。
  ③「200解码失败」真因=源站死对象返 200+JSON错误体, 非webp; stbi解不了→旧逻辑盲指数退避8次。
  → download_cover 回填 *out_kind(0可解码/1 webp/2非图), worker 对 kind=1/2 与确定性 4xx 一律
  立即清目标走兜底(保留歌单封面)不重试; kind=2 打 'not an image (origin error body/200)'。
- UA 分表: CNMPlayer口径 UA(Mozilla/5.0 CNMPlayer/0.1)同Referer也返403(CDN拦bot串);
  桌面 Chrome UA(旧cover.c)返200。→ NCM_COVER_UA 改回桌面浏览器 UA。
- ?param=300y300 在死对象上→404(对死链无效); 存活URL未获样本, 保留 USE_PARAM=1(标准缩略)。
- cover_chain_sim 增 T10(200+JSON→非图不重试) + stub 内容加 JPEG 魔数(过 body_is_decodable_img);
  T1-T10 ALL PASS。mipsel 0 warning(text 1020306/bss 1362500 持平)。

新 ncm md5 06896b54f31ddf55980cbf921a00d67a (未部署, 留主会话上卡)
2026-09-02 10:50:07 +08:00

455 lines
22 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* logic/cover.c — 封面下载(HTTPS)+ 限幅 + 解码缓存(单张,URL 变更即换;后台线程)。
*
* 内存纪律:下载限幅 NCM_COVER_MAX(字节上限);解码后的 RGBA 保持单张(换封面先 free 旧再解码)。
* 线程模型:一个持久的 cover 工作线程负责「目标 URL → 下载 → 解码 → 换入」;UI 线程只经
* cover_request(url)(提交目标)与 cover_get()/cover_changed()(读取/判变)+ cover_ack()(消费)。
* 工作线程不碰 meow_ui(绘制在 UI 线程 tick 判 cover_changed 后触发 invalidate)。
*
* §18 修复(切歌封面不更新/CPU 忙等根因):
* - worker 换入成功后**清目标**(g_tgt_url[0]=0)转怠速——旧版成功后不清目标,每圈
* 缓存读+JPEG 解码+换入+g_gen++ 无 sleep 死循环:CPU 占满、cover_changed() 恒真导致
* 播放页每帧整盘重绘,慢网络下新封面请求被饿到表现如「切歌封面不更新」。
* - 下载/解码失败**指数退避**重试(0.5s→4s),连续 8 次放弃(UI 重进页会重新提交目标);
* 失败写 WARN 日志(排查资产),不再无声死等。
* - 绘制安全:cover_get() 持锁返回、cover_put() 解锁——旧版 UI 绘制期 worker 可 free
* 同一 meow_img(use-after-free);调用方须在 cover_get 后配对 cover_put。
*
* §30 修复(真机封面全加载失败):旧版 `static g_buf[NCM_COVER_MAX(288KB)]` 作为下载缓冲,
* 网易云封面全尺寸实测 ~1.34MB(>288KB)→ ncm_http 回 NCM_ERR_FULL → 下载失败×8 放弃。
* 且设备 RAM 仅 56MB,不能再涨静态 bss。改为**流式写文件**:ncm_http_download 的写回调直接
* 把 body 写进 ncm_cache 写者临时文件(write(2) 直写,无 stdio 缓冲),meow_img_load_file 从
* 临时文件解码,commit 落盘。全程零封面大缓冲、无 bss 上涨;NCM_COVER_MAX 仅作下载字节上限。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/stat.h>
/* 封面 CDN 请求头(REPORT §NEW 任务2,对齐 CNMPlayer):网易云图片 CDN 靠 Referer 防盗链,
* 缺失被拦返回 403(p3.music.126.net 150B text/html,REPORT §NEW 三分型②);UA 分表口径与
* CNMPlayer 客户端一致(Mozilla/5.0 CNMPlayer/0.1 + Referer https://music.163.com/)。两者
* 均为常量:真机矩阵实测需换 UA 时只改 NCM_COVER_UA 一处,不涉逻辑(见 REPORT §NEW 矩阵结论)。 */
#define NCM_COVER_REFERER "https://music.163.com/"
/* UA 分表(真机矩阵实测结论):本批在设备端逐 UA 试,`Mozilla/5.0 CNMPlayer/0.1`(CNMPlayer 口径)
* 对封面 CDN 同 Referer 也返 403(CDN 拦 bot 串);桌面浏览器 UA(旧 cover.c 一直用、设备实测返 200)
* 被 CDN 接受。故封面走**桌面浏览器 UA**,Referer 对齐 CNMPlayer(防盗链,防裸请求 403)。 */
#define NCM_COVER_UA "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"
/* ?param=WxH 缩略后缀宏 NCM_COVER_PARAM / NCM_COVER_USE_PARAM 见 ncm_app_internal.h(与
* bench/cover_chain_sim.c 同源)。 */
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) */
/* 封面缓存键:URL 的 djb2 哈希(稳定性由 URL 决定——同一专辑封面 URL 名即可命中)。
* 走 ncm_cache(LRU 口径沿用 ncm_cache,见 REPORT §14.2):键进 ncm_cache_path => 文件
* `song_cov<hex>.jpg`,与 mp3(song_<id>.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);
}
/* 文件存在且非空(封面缓存命中用;streaming 后无需整读进内存)。 */
static int file_nonempty(const char *path)
{
struct stat st;
return (path && stat(path, &st) == 0 && st.st_size > 0) ? 1 : 0;
}
/* 流式写回调:body 字节直接写进 ncm_cache 写者临时文件(write(2) 直写)。ctx=cover_sink_t*。 */
typedef struct { ncm_cache_writer_t *w; size_t total; } cover_sink_t;
static int cover_sink_write(void *ctx, const unsigned char *data, size_t len)
{
cover_sink_t *s = (cover_sink_t *)ctx;
if (ncm_cache_write(s->w, data, len) != 0) return -1;
s->total += len;
return 0;
}
/* 封面 URL 规范化:追加 NCM_COVER_PARAM 尺寸后缀(REPORT §NEW 任务2)。已含 param= 的
* URL 原样保留(尊重调用方/宿主食之已有尺寸);否则追加(已有其它查询用 &,无则用 ?)。
* 溢出(512 缓冲放不下)则放弃追加——宁可用全尺寸 URL 也不截断出坏 URL。 */
static void cover_norm_url(const char *src, char *out, size_t out_sz)
{
ncm_strlcpy(out, src, out_sz);
#if NCM_COVER_USE_PARAM
if (out[0] && strstr(out, "param=")) return; /* 已指定尺寸,不叠 */
size_t l = strlen(out);
size_t add = strlen("?param=") + strlen(NCM_COVER_PARAM);
if (!out[0] || l + add + 1 >= out_sz) return; /* 放不下:保原 URL */
strcat(out, strchr(out, '?') ? "&param=" : "?param=");
strcat(out, NCM_COVER_PARAM);
#endif
}
/* 大小写不敏感的 posix 子串检测(内容 webp 判定用;不依赖 strcasestr/_GNU_SOURCE 桩)。 */
static int str_ci_contains(const char *hay, const char *needle)
{
if (!hay || !needle) return 0;
size_t n = strlen(needle);
if (!n) return 1;
for (; *hay; hay++) {
if (tolower((unsigned char)hay[0]) == tolower((unsigned char)needle[0])) {
const char *a = hay, *b = needle;
while (*b && tolower((unsigned char)*a) == tolower((unsigned char)*b)) { a++; b++; }
if (!*b) return 1;
}
}
return 0;
}
/* 文件是否 WebP:魔数 RIFF....WEBP(stb_image 不支持 webp,解码必 NULL)。读文件前 12 字节。 */
static int file_is_webp(const char *path)
{
if (!path) return 0;
unsigned char b[12];
FILE *f = fopen(path, "rb");
if (!f) return 0;
size_t n = fread(b, 1, sizeof b, f);
fclose(f);
if (n < 12) return 0;
return b[0]=='R'&&b[1]=='I'&&b[2]=='F'&&b[3]=='F' &&
b[8]=='W'&&b[9]=='E'&&b[10]=='B'&&b[11]=='P';
}
/* body 是否为 stb 可解码的静态图:JPEG(FFD8FF)/PNG(89504E47)/BMP(424D)/GIF(474946)。
* 真机矩阵(REPORT §34.1)实锤:源站死对象对封面 URL 返 **200 + JSON 错误体**(43B `{"code..`,
* content-type application/json),旧逻辑 stbi 解码 NULL → 当成「fetch failed」盲指数重试 8 次(真机
* 日志 `rc=0 status=200` + `retry`)。故 200 且 body 非**已知可解码图**(含 JSON 错误体/未知格式)
* 均判确定性失败不重试(重试救不了死对象)。content-type 明确非 image/ 前缀(如 application/json)
* 也直接判非图。 */
static int body_is_decodable_img(const char *path, const ncm_http_resp_t *resp)
{
if (resp && resp->has_content_type && resp->content_type[0]) {
if (strncmp(resp->content_type, "image/", 6) != 0) return 0; /* 如 application/json */
}
if (!path) return 0;
unsigned char b[8];
FILE *f = fopen(path, "rb");
if (!f) return 0;
size_t n = fread(b, 1, sizeof b, f);
fclose(f);
if (n < 4) return 0;
if (b[0]==0xFF && b[1]==0xD8 && b[2]==0xFF) return 1; /* JPEG */
if (b[0]==0x89 && b[1]=='P' && b[2]=='N' && b[3]=='G') return 1; /* PNG */
if (b[0]=='B' && b[1]=='M') return 1; /* BMP */
if (b[0]=='G' && b[1]=='I' && b[2]=='F' && b[3]=='8') return 1; /* GIF */
return 0;
}
/* 下载 url 到 cache 写者 w(流式,写进其临时文件),成功后从临时文件解码并返回 img。
* 失败返回 NULL(不 commit,调用方 abort)。*out_len=body 字节数;*out_rc 与 *out_status
* 回填下载 rc 与 HTTP status(封面 4xx 加固判定:确定性 4xx 不重试,REPORT §NEW 任务3);
* *out_kind 回填 200 体类型:0=可解码图、1=WebP(stb 不支持)、2=非图(源站死对象 200+JSON 错误体/
* 未知格式)——1/2 均不解码,确定性不重试(REPORT §34.1 真机矩阵:200 解码失败实为死对象回 JSON 体)。 */
static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *out_len,
int *out_rc, int *out_status, int *out_kind)
{
ncm_http_req_t req;
memset(&req, 0, sizeof req);
/* 头:Referer 防盗链(CNMPlayer 口径)+ 桌面浏览器 UA(真机矩阵:CNMPlayer UA 被拦 403)。 */
char hdrs[256];
snprintf(hdrs, sizeof hdrs, "Referer: %s\r\nUser-Agent: %s", NCM_COVER_REFERER, NCM_COVER_UA);
req.url = url;
req.method = "GET";
req.extra_hdrs = hdrs;
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
req.timeout_ms = 15000;
ncm_http_resp_t resp;
cover_sink_t sink = { w, 0 };
int rc = ncm_http_download(&req, cover_sink_write, &sink, NCM_COVER_MAX, &resp);
if (out_rc) *out_rc = rc;
if (out_status) *out_status = resp.status;
if (rc != NCM_OK || resp.status != 200 || sink.total == 0) return NULL;
if (out_len) *out_len = sink.total;
/* 从写者临时文件解码(未 commit,文件仍在 .tmp;解码到内存 RGBA,与落盘无关) */
const char *tmp = ncm_cache_writer_tmp_path(w);
if (!tmp) return NULL;
/* WebP 先判:content-type 含 webp 或文件魔数 RIFF....WEBP → 不解码(stb 不支持),
* 回 webp=1 走兜底;避免 stbi_load 空转返回 NULL 后当成「fetch failed」盲指数退重试。 */
if (out_kind) *out_kind = 0;
if ((resp.has_content_type && str_ci_contains(resp.content_type, "webp")) || file_is_webp(tmp)) {
if (out_kind) *out_kind = 1; /* webp(stb 不支持) */
return NULL;
}
if (!body_is_decodable_img(tmp, &resp)) {
if (out_kind) *out_kind = 2; /* 非图(源站死对象 200+JSON 错误体/未知格式) */
return NULL;
}
return meow_img_load_file(tmp);
}
/* 封面原始文件路径写入(供 launcher 媒体卡片读;兼容保留)。调用方须已持有 g_cm。
* 从 cache 写者临时文件把已下载字节复制到 cover_current.jpg(早前版本在此内再 lock g_cm
* → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。 */
static void save_cover_raw(const char *path, size_t len)
{
if (!path || len == 0) return;
if (g_st.cache_dir[0]) {
char dst[NCM_CACHE_DIR_MAX + 32];
snprintf(dst, sizeof dst, "%s/cover_current.jpg", g_st.cache_dir);
/* 文件复制(源=临时文件,目标=cover_current.jpg)。 */
FILE *in = fopen(path, "rb");
FILE *out = NULL;
if (in) {
out = fopen(dst, "wb");
if (out) {
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof buf, in)) > 0)
if (fwrite(buf, 1, n, out) != n) { fclose(out); out = NULL; break; }
if (out) fclose(out);
}
fclose(in);
}
ncm_strlcpy(g_cover_path, dst, sizeof g_cover_path);
}
}
#define COVER_RETRY_MAX 8 /* 连续失败放弃次数(UI 重进页/切歌会重新提交目标) */
/* 内存瘦身:封面解码后按显示上限降采样驻留。网易云全尺寸封面(如 1448×1448)解码成
* RGBA = 8MB,而应用显示只需 ≤480px(播放页黑胶方图+歌词页模糊铺底均 ≤ 屏幕 480 宽)——
* 缩到 480×480 = 0.9MB,省 ~7MB anon(设备 RAM 仅 56MB,OOM 真机被杀,最高优先级)。
* 若已 ≤上限(小缩略图)则原样保留。返回本函数处理后的图(可能替换/释放旧图)。 */
#define NCM_COVER_DECODE_MAX 480
static meow_img *cover_cap(meow_img *im)
{
if (!im) return NULL;
meow_img *small = meow_img_downscale(im, NCM_COVER_DECODE_MAX);
if (small) { meow_img_free(im); return small; } /* 已缩放:释放原始大图,换小图驻留 */
return im; /* 本就够小:保留原图 */
}
static void *cover_worker(void *arg)
{
(void)arg;
int backoff_ms = 0, fails = 0;
while (!g_quit) {
pthread_mutex_lock(&g_cm);
if (!g_tgt_url[0]) {
pthread_mutex_unlock(&g_cm);
backoff_ms = 0; fails = 0;
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;
int drc = NCM_ERR_NET, dstatus = 0, dkind = 0; /* 下载 rc/HTTP status/200 体类型(分诊用) */
/* ① 缓存命中直接解码(免下载、离线可看;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 &&
file_nonempty(cpath)) {
img = meow_img_load_file(cpath);
}
/* ② 未命中 → 流式下载解码;成功后落进 ncm_cache(LRU 池,超限由 ncm_cache_evict 淘汰) */
if (!img) {
if (g_st.ctx.cache.dir[0]) {
ncm_cache_evict(&g_st.ctx.cache);
ncm_cache_writer_t *w = NULL;
if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) {
blen = 0;
img = download_cover(url, w, &blen, &drc, &dstatus, &dkind);
if (img) {
pthread_mutex_lock(&g_cm);
save_cover_raw(ncm_cache_writer_tmp_path(w), blen);
pthread_mutex_unlock(&g_cm);
if (ncm_cache_commit(w) != 0) { ncm_cache_abort(w); }
} else {
ncm_cache_abort(w);
}
ncm_cache_evict(&g_st.ctx.cache);
} else {
/* 打开写者失败(罕见):退避重试,不空转 */
img = NULL;
}
}
}
/* 内存瘦身:全尺寸封面解码后降采样到 ≤480px 驻留(省 ~7MB anon,OOM 修复)。 */
if (img) img = cover_cap(img);
if (!img) {
/* 失败分诊(REPORT §NEW 任务3):
* - webp:200 但 stb 不支持解码(三分型③),**非瞬时、非重试可愈** → 立即清目标
* 走兜底(播放页保留歌单封面 g_st.cover_url,不盲目指数重试空转);
* - 确定性 4xx(404 死链 / **403 防盗链**)**不重试**,立即清目标跳下一张;
* - 仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。
* 日志一律打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么);
* 403 单独日志(防盗链特征,与 404 死链区分——三者修法不同)。 */
if (dkind == 1) { /* webp:200 但 stb 不支持解码,非瞬时非重试可愈 → 立即清目标走兜底 */
meow_log(MEOW_LOG_WARN, "cover: webp unsupported url=%.64s rc=%d status=%d (no decode)",
url, drc, dstatus);
pthread_mutex_lock(&g_cm);
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张(走兜底) */
pthread_mutex_unlock(&g_cm);
backoff_ms = 0; fails = 0;
continue;
}
if (dkind == 2) { /* 200 但 body 非可解码图(源站死对象回 JSON 错误体/未知格式):
* 真机矩阵(REPORT §34.1)确认为「200 解码失败」主因,老逻辑空转 8 次;
* 确定性失败(重试救不了死对象)→ 立即清目标走兜底 */
meow_log(MEOW_LOG_WARN, "cover: not an image (origin error body/200) url=%.64s rc=%d status=%d (no retry)",
url, drc, dstatus);
pthread_mutex_lock(&g_cm);
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0;
pthread_mutex_unlock(&g_cm);
backoff_ms = 0; fails = 0;
continue;
}
int deterministic = (dstatus >= 400 && dstatus < 500 && dstatus != 408 && dstatus != 429);
if (dstatus == 403) {
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=403 (anti-hotlink/forbidden, no retry)",
url, drc);
} else if (deterministic) {
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=%d (4xx, no retry)",
url, drc, dstatus);
} else {
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=%d",
url, drc, dstatus);
}
if (deterministic) {
pthread_mutex_lock(&g_cm);
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张 */
pthread_mutex_unlock(&g_cm);
backoff_ms = 0; fails = 0;
continue;
}
/* 瞬时错误:指数退避重试,连续 COVER_RETRY_MAX 次放弃(不无声死等;
* 目标保留期间 UI 每帧重请求由 cover_request 去重挡住)。 */
fails++;
if (fails >= COVER_RETRY_MAX) {
meow_log(MEOW_LOG_WARN, "cover: give up after %d fails url=%.64s", fails, url);
pthread_mutex_lock(&g_cm);
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0;
pthread_mutex_unlock(&g_cm);
backoff_ms = 0; fails = 0;
continue;
}
meow_log(MEOW_LOG_WARN, "cover: fetch failed (%d) url=%.64s retry in %dms",
fails, url, backoff_ms ? backoff_ms : 500);
if (g_app_quit) return NULL;
usleep(backoff_ms ? backoff_ms * 1000 : 500 * 1000);
backoff_ms = backoff_ms ? (backoff_ms * 2 > 4000 ? 4000 : backoff_ms * 2) : 500;
continue;
}
backoff_ms = 0; fails = 0;
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);
g_gen++;
g_tgt_url[0] = 0; /* 目标达成即清:worker 转怠速(修忙等死循环,§18) */
} else {
meow_img_free(img);
}
pthread_mutex_unlock(&g_cm);
}
return NULL;
}
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。
* 先 cover_norm_url 规范化(追加 ?param=300y300 缩略后缀,REPORT §NEW 任务2),使
* g_tgt_url/g_cover_url/缓存键/下载 URL 走同一参数版本(小图省内存、命中高)。 */
void cover_request(const char *url)
{
if (!url || !url[0]) return;
if (g_host_mock) return; /* 宿主 mock = 0 网络纪律(mock URL 不得发真实 HTTPS) */
char norm[512];
cover_norm_url(url, norm, sizeof norm);
const char *u = norm[0] ? norm : url; /* 规范化溢出保护:退回原 URL */
pthread_mutex_lock(&g_cm);
/* 去重:已在展示 或 已在拉取途中的同一 URL 不重复提交。 */
if ((g_cover_url[0] && strcmp(g_cover_url, u) == 0) ||
(g_tgt_url[0] && strcmp(g_tgt_url, u) == 0)) { pthread_mutex_unlock(&g_cm); return; }
ncm_strlcpy(g_tgt_url, u, sizeof g_tgt_url);
pthread_mutex_unlock(&g_cm);
if (!g_thr_on) {
g_thr_on = 1;
if (ncm_thread_create(&g_thr, cover_worker, NULL) == 0)
pthread_detach(g_thr);
else
g_thr_on = 0;
}
}
/* UI 线程:读取当前封面——**持锁返回**,绘制完必须配对 cover_put()(§18 修 UAF:
* 旧版 get 后无锁绘制,worker 换入时 free 同一张图 → use-after-free)。 */
meow_img *cover_get(void)
{
pthread_mutex_lock(&g_cm);
return g_cover_img;
}
void cover_put(void)
{
pthread_mutex_unlock(&g_cm);
}
/* 是否有新封面待绘制(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;
}
#ifdef MEOW_UI_HOST_TEST
/* 宿主测试桩(仅宿主构建;产品构建无此符号):注入一张本地封面图(不经网络),
* 验证封面页方形圆角大图与歌词页模糊置底路径(§22)。0 真实请求纪律不变。 */
void cover_host_inject(const char *path)
{
if (!path || !path[0]) return;
meow_img *img = meow_img_load_file(path);
if (!img) { meow_log(MEOW_LOG_WARN, "cover host inject: load failed %s", path); return; }
pthread_mutex_lock(&g_cm);
if (g_cover_img) meow_img_free(g_cover_img);
g_cover_img = img;
ncm_strlcpy(g_cover_url, path, sizeof g_cover_url);
g_cover_path[0] = 0;
g_gen++;
pthread_mutex_unlock(&g_cm);
}
#endif