feat(adapters): TMDB 列表与筛选、豆瓣榜单 / 标签 / 筛选 / 搜索、bgm 浏览与趋势,豆瓣 / bgm 请求缓存端口

- TMDB:流行趋势(日 / 周)、电影 / 剧集榜单、discover 参数映射、分页搜索;列表类请求 TTL 6 小时
- 豆瓣 / bgm:列表与搜索请求经 MetaRequestCache 与限流,失败(403 / 风控页)不缓存
- contracts:发现页条目、分页、查询校验与筛选字典;storage.cacheTtl 追加 meta* 字段
- testkit:mock TMDB 加列表接口

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-06 18:04:43 +08:00
1 parent 27514f4985
commit e46eb0e3c3
13 files changed
+1344 -91

No files matched your search

+14 -3
View File
@@ -101,8 +101,10 @@ export interface TmdbConfig {
/**
* 默认 max 1024、ttl 24h;空结果(搜索 0 条)用 emptyTtlMs,默认 30min(对照 MP EMPTY_RESULT_CACHE_TTL);
* searchTtlMs:/search/、/find/ 请求的 TTL(缺省同 ttlMs),其余(详情 / 季)用 ttlMs。
* listTtlMs:流行趋势 / 热门 / 正在上映等列表与 /discover/ 的 TTL(缺省 6 小时且不超过 ttlMs;列表会变,不能按详情缓存 30 天),
* 这类请求的空结果 TTL 也不超过它。
*/
cache?: { max?: number; ttlMs?: number; searchTtlMs?: number; emptyTtlMs?: number };
cache?: { max?: number; ttlMs?: number; searchTtlMs?: number; emptyTtlMs?: number; listTtlMs?: number };
/**
* 跨进程共享的请求缓存(服务端传 PG 缓存);给了就不再用进程内 LRU。
* 语义对照 MP @cached(skip_none、业务失败不缓存、results 为空按 emptyTtlMs)。
@@ -134,6 +136,11 @@ export function tmdbRequestKey(path: string, params: Record<string, string | und
return `${path}?${requestQuery(params)}`;
}
/** 列表类请求(发现页):流行趋势、按条件筛选、电影 / 剧集榜单 */
export const isTmdbListPath = (path: string) =>
/^\/(trending|discover)\//.test(path) ||
/^\/(movie|tv)\/(popular|now_playing|upcoming|top_rated|on_the_air|airing_today)$/.test(path);
/** 空结果(搜索 0 条)用短 TTL(对照 MP EMPTY_RESULT_CACHE_TTL) */
const isEmptyResult = (v: unknown) =>
Array.isArray((v as Json | null)?.results) && ((v as Json).results as unknown[]).length === 0;
@@ -176,9 +183,13 @@ export class TmdbClient {
private async get<T>(path: string, params: Record<string, string | undefined>): Promise<T> {
const qs = requestQuery(params);
const key = `${path}?${qs}`;
const emptyTtl = this.cfg.cache?.emptyTtlMs ?? 30 * 60_000;
let emptyTtl = this.cfg.cache?.emptyTtlMs ?? 30 * 60_000;
const baseTtl = this.cfg.cache?.ttlMs ?? 24 * 3600_000;
const ttl = /^\/(search|find)\//.test(path) ? (this.cfg.cache?.searchTtlMs ?? baseTtl) : baseTtl;
let ttl = /^\/(search|find)\//.test(path) ? (this.cfg.cache?.searchTtlMs ?? baseTtl) : baseTtl;
if (isTmdbListPath(path)) {
ttl = this.cfg.cache?.listTtlMs ?? Math.min(baseTtl, 6 * 3600_000);
emptyTtl = Math.min(emptyTtl, ttl);
}
if (this.cfg.sharedCache) {
const shared = this.cfg.sharedCache;
const hit = await shared.get(key);