Files
UHD_Media_Tool/docs/runbook/cache.md
T
rootandClaude Opus 5.5 7862573944 feat(cache): TMDB image proxy, posters, cache admin API and settings cards
GET /img/tmdb/{size}/{file} ported from MP ImageHelper / fetch_image
(sanitized cache path under DATA_DIR/cache/images, GLOBAL_IMAGE_CACHE
read switch, md5 ETag / 304, day-based cleanup job). Cache module adds
cache.sweep / cache.images_clean jobs and /cache/tmdb, /cache/images
list and clear endpoints. Library list/detail and the search palette
show posters with the neutral placeholder as fallback; metadata settings
get TMDB cache and image cache cards. Runbook docs/runbook/cache.md and
porting notes docs/porting/moviepilot-tmdb-cache.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 22:26:29 +08:00

4.6 KiB

缓存与限流运行手册(C2-A)

不引入 Redis / Deno KV。跨进程共享的状态都放 PG:站点限流令牌桶、TMDB 缓存。缓存语义照搬 MoviePilot, 对照与偏离见 docs/porting/moviepilot-tmdb-cache.md。

跨进程站点限流

  • 表 rate_limit_bucket(迁移 0041_rate_limit):key site:<站点 id>,tokens / capacity / refill_per_sec / blocked_until。
  • @umt/db 的 PgTokenBucket.acquire(key, cost, {capacity, refillPerSec, waitMs}):事务里 select … for update, 按 DB 时钟补令牌后扣减;拿不到按需要等待的时间加抖动轮询,超过 waitMs 返回 ok:false(站点限流器会重新排队)。
  • 站点适配器用 createSharedSiteRateLimiter:进程内先串行排队,再到 PG 扣令牌;429 的 Retry-After 写 blocked_until。 站点限流间隔为 0(不限流)时只走本地;PG 出错时本次退回进程内令牌桶并记 warn。
  • 容量 = 站点并发(burst),速率 = 1000 / 最小间隔毫秒。改站点限流设置即时生效(每次扣令牌带上最新参数)。

排查:select * from rate_limit_bucket where key = 'site:<id>';blocked_until 不为空说明站点要求暂停。

PG 缓存(kv_cache)

  • 表 kv_cache(迁移 0042_cache,UNLOGGED:不写 WAL,崩溃后清空):(region, key) 主键,value jsonb, expires_at 每条独立,accessed_at 为最近读写时间。
  • @umt/db 的 PgCache(对照 MP TTLCache):set 带 ttl(秒,≤ 0 删除)与 maxsize(按 region 计条数,超出先删过期再删最久未用); get 命中刷新最近使用;cachedCall 对照 MP @cached(skip_none / skip_if / empty_ttl / fresh)。
  • 定时任务 cache.sweep(维护组,每 10 分钟)物理删除过期行。
region 内容 键 TTL 条数上限
tmdb.request TMDB 请求响应(MP TMDb.request) path?query(不含凭证) cache.tmdbTtlHours;results 为空按 cache.emptyResultTtlSec cache.tmdbMaxSize
tmdb.recognize TMDB 识别结果(MP TmdbCache) [类型][语言]{tmdbid 或名称}-{年份}-{季} 同上;未识别 {id:0} 按空结果 TTL cache.tmdbMaxSize

设置(设置 → 元数据 → TMDB 缓存):cache.tmdbTtlHours(缺省 24,MP META_CACHE_EXPIRE)、cache.tmdbMaxSize(缺省 256,MP CONF.tmdb)、 cache.emptyResultTtlSec(缺省 1800,MP EMPTY_RESULT_CACHE_TTL)。

API:GET /api/v1/cache/tmdb?kind=recognize|request&limit= 列出条目(键 / 过期 / 大小,识别条目带 tmdbId / 标题 / 海报); POST /api/v1/cache/tmdb/clear 清空两类(仅 admin)。

识别缓存何时读写(sources-identity/service/tmdb-recognize-cache.ts):

  • 识别资源时先读:命中且有标题 → 按 id 取 TMDB 详情作为唯一远端候选;命中 {id:0} → 不再搜索;强制复查(force)不读。
  • 写:自动识别到 TMDB 条目写正常条目;确认无匹配写 {id:0};TMDB 出错或有歧义不写。
  • 人工认定资源身份后回填(MP update_recognize_cache),覆盖名称负缓存。
  • "元数据是剧集、缓存是电影"视为类型冲突:读时丢弃,写时拒绝。

图片代理与海报

  • GET /api/v1/img/tmdb/{size}/{file}(会话或 API Key read):只代理 image.tmdb.org,size 走白名单(w92…original), file 只允许 [A-Za-z0-9_-]+.(jpg|jpeg|png|webp)。
  • 缓存路径对照 MP sanitize_url_path:DATA_DIR/cache/images/t/p/<size>/<file>。
  • cache.globalImageCache(缺省关闭,MP GLOBAL_IMAGE_CACHE)开启才读磁盘缓存;拉到的合法图片总会写盘。
  • 按文件头识别图片类型,非图片 / SVG 拒绝;ETag = md5(内容),Cache-Control: public, max-age=604800,If-None-Match 相同回 304。
  • 上游超时(20s)/ 非 200 / 非图片 → 502,前端回退无彩占位。
  • 清理:定时任务 cache.images_clean(每天)删除 mtime 早于 cache.globalImageCacheDays(缺省 7)天的文件。
  • 用量 / 清空:GET /api/v1/cache/images、POST /api/v1/cache/images/clear(仅 admin);设置 → 元数据 → 图片代理与缓存。

前端:作品库列表、作品详情、搜索面板的 TMDB 候选在有 posterPath(metadata.posterPath / TMDB 候选)时显示海报, 加载失败回退原来的无彩占位。TMDB 详情刷新会把 posterPath / backdropPath 写进 metadata_field。

常见问题

  • TMDB 缓存不生效:确认 select region, count(*) from kv_cache group by 1 有 tmdb.request;PG 读写失败时按未命中处理并记 TMDB 缓存读写失败,按未命中处理。
  • 改了 TMDB 缓存设置:新写入的条目按新 TTL / 上限;已有条目保持原过期时间,必要时清空。
  • 数据库重启后缓存为空:UNLOGGED 表的正常行为。