Files
UHD_Media_Tool/docs/porting/moviepilot-discover.md
T
rootandClaude Opus 5.5 850034b87a docs: 统一元数据缓存运行手册与 MP 差异
runbook/cache.md 重写为统一缓存说明(两级、策略表、过期先用旧值、同请求合并、pinned 与预热、跨进程失效、命中统计、清空);
storage.md 补新 TTL 字段;moviepilot-tmdb-cache.md / moviepilot-discover.md 记与 MP 的差异。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 19:37:34 +08:00

55 lines
7.5 KiB
Markdown
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.
# MoviePilot 推荐 / 探索对照(发现页)
上游:前端 `MoviePilot-Frontend/src/pages/{recommend,discover,browse}.vue`、`views/discover/*`、`utils/recommendSources.ts`;
后端 `app/chain/{recommend,tmdb,douban,bangumi}.py`、`app/api/endpoints/{recommend,discover}.py`、`app/modules/douban/apiv2.py`、`app/modules/bangumi/bangumi.py`、`app/modules/themoviedb/tmdbapi.py`(v3.0.10-1)。
## MP 有什么
| 页面 | 结构 | 数据源 / 分类 |
| --- | --- | --- |
| 推荐 `/recommend` | 按分区横向滑动(`MediaCardSlideView`),每区「查看全部」进 `/browse/<api>`;顶部按「全部 / 电影 / 剧集 / 动漫 / 音乐 / 榜单」筛分区;分区可开关(存用户配置) | TMDB 流行趋势(`trending/all/week`)、TMDB 热门电影 / 剧集(`discover`,剧集限 zh/en/ja/ko)、豆瓣正在热映、豆瓣热门电影 / 剧集 / 动画、豆瓣最新电影 / 剧集(`movie|tv/recommend sort=R`)、豆瓣 Top250、豆瓣华语 / 全球口碑剧集榜、Bangumi 每日放送、AniList、音乐 |
| 探索 `/discover` | 按来源分标签(TMDB / 豆瓣 / Bangumi / …),每个来源一组筛选芯片,下面是无限滚动网格(`MediaCardListView`,`page++`) | TMDB:类型(电影 / 剧集)、排序、风格、原始语言、评分(+最少评分人数);豆瓣:类型、排序(综合 / 首映 / 近期热度 / 高分)、风格、地区、年代(拼成 `tags`);Bangumi:分类、排序(排名 / 日期)、年份 |
| 卡片 | 海报、评分、标题、年份;右上角「已入库 / 已订阅」标记;点开进媒体详情(豆瓣 / bgm 条目在详情页再识别 TMDB);卡片上可订阅 | — |
缓存:推荐链每个列表 `@cached(ttl=24h, skip_empty=True)`;TMDB / 豆瓣 / bgm 底层请求另有 `@cached(ttl=CONF.meta)`,失败(None)不缓存。
## 我们原来有什么
只有 bgm 每日放送、豆瓣实时热门电影 / 剧集三个标签,没有 TMDB 列表、榜单与筛选;卡片没有本地状态。
## 这次做了什么
页面选「综合 + 可拆分」:**推荐**(综合,按分区横排,每区「查看全部」带着同样的参数跳到探索)与**探索**(选来源后出筛选器,无限滚动),两者在同一个路由 `/console/discover?view=recommend|explore`,状态全部放在 URL 里,可分享、可回退。理由:MP 两页的分工(推荐看热点、探索做筛选)好用,但两页之间来回跳要重选条件;放一页、共享 URL 参数后「查看全部」就是带参数切到探索。
| 来源 | 列表 | 接口 |
| --- | --- | --- |
| TMDB | 流行趋势(日 / 周 × 全部 / 电影 / 剧集)、热门、正在上映 / 正在播出、即将上映 / 今日播出、高分、按条件筛选(类型、风格、原始语言、出品地区、年份或年代、最低评分、排序)、关键词搜索 | `GET /discover/tmdb` |
| 豆瓣 | 榜单(实时热门、正在热映、热门、即将上映、Top250、国产 / 欧美 / 日 / 韩剧、动画、综艺、华语 / 全球口碑剧集榜)、标签(热门 / 最新 / 豆瓣高分 / 冷门佳片 / 华语 / 美剧…)、筛选(排序 + 风格 + 地区 + 年代,同 MP)、关键词搜索 | `GET /discover/douban` |
| Bangumi | 每日放送、热门趋势、排行(可选年份)、最新、关键词搜索 | `GET /discover/bangumi-calendar`、`GET /discover/bangumi` |
卡片:海报(懒加载)、评分、标题、年份 · 类型 · 风格;本地状态只显示非默认值(已订阅 / 媒体库已有 / 下载中)。点卡片:有本地作品进媒体库详情;TMDB 条目进 `media/tmdb/:type/:id`;豆瓣 / bgm 条目先按名称 + 年份匹配 TMDB(原有匹配逻辑),确定就进作品页,不确定列候选。卡片上「订阅」「搜索资源」同样先拿到 TMDB 作品再开订阅表单 / 选站点。
豆瓣 / bgm 条目确定匹配后记在 `kv_cache discover.match`(30 天),下次列表据此标本地状态,再点也不用重新匹配;不确定 / 失败不记。
### 取舍与偏离
- **HTTP 直连,不登录,不用浏览器**:豆瓣(rexxar 带 `Referer: https://m.douban.com/`;桌面版 `j/search_subjects`、`j/subject_suggest` 带浏览器 UA)与 bgm(官方 API,只要 UA)2026-10-06 实测都能直接拿到。被风控(403、返回验证页而不是 JSON)时按失败处理、不缓存、界面报错可重试;以后真的频繁被拦,再考虑经内置浏览器代取(适配器的 `fetch` 是注入的,换成浏览器代理的 fetch 即可,本线不实现)。
- **bgm 热门趋势**没有 API,抓 `bgm.tv/anime/browser?sort=trends` 的服务端渲染 HTML;HTML 是外部不可信字节,按 ADR-005 在沙箱任务 `bangumi.trends` 里解析(`parseBangumiBrowser`)。
- **豆瓣搜索**用 `j/subject_suggest`(联想,不分页,约 10 条);MP 用要签名的 frodo 接口,我们不用。
- **豆瓣筛选**用 rexxar `movie|tv/recommend`(MP 用 frodo 同名接口,参数相同);标签列表用桌面版 `j/search_subjects`。
- **TMDB 列表缓存**:仍走 TMDB 请求缓存(`tmdb.request`,失败不缓存),但列表类路径(`/trending/`、`/discover/`、电影 / 剧集榜单)TTL 单独取 6 小时、空结果不超过它(`TmdbConfig.cache.listTtlMs`);我们详情缓存 30 天,列表不能跟着它。MP 为 24 小时(`CONF.meta` / `recommend_ttl`),这里取短一些,日榜不至于整天不变。
- **MP 的音乐、AniList、插件扩展来源**不做;推荐分区的开关配置不做(分区不多,按「全部 / 电影 / 剧集 / 动画 / 榜单」筛即可)。
- TMDB 风格、语言、地区字典写在 `@umt/contracts`(`TMDB_MOVIE_GENRES` 等),界面与服务端校验共用,不请求 `/genre/list`(同 MP)。
## 豆瓣 / bgm 请求缓存(对照 `DoubanApi.__invoke / __post`、`BangumiApi` 的 `@cached`)
| 上游 | 我们 |
| --- | --- |
| 每个请求 `@cached(maxsize=CONF.douban, ttl=CONF.meta, skip_none=True)`,键 = 函数 + 参数 | 列表 / 搜索请求经 `MetaRequestCache` 端口 → 服务端 `cachedCall`(region `douban.request` / `bangumi.request`,上限 5000 条);键 = `方法 + 参数排序后的地址 + 请求体`(`metaRequestKey`) |
| TTL 只有一个 `CONF.meta` | 按类别取 `storage.cacheTtl`(统一元数据缓存策略表,见 runbook/cache.md):搜索 `metaSearchDays`(缺省 7 天,空结果 `metaMissDays` 1 天)、榜单 / 热门 / 每日放送 / 趋势页 `metaListHours`(缺省 6 小时,空结果 `metaEmptyHours` 1 小时),空结果 TTL 不超过正常 TTL;过期先用旧值后台刷新 |
| `skip_none`:请求失败返回 None 不缓存 | 网络失败、非 2xx(含 403)、返回的不是 JSON(风控 / 验证页)、趋势页不是浏览页,适配器都抛错,`cachedCall` 不写 |
| 条目详情同样走请求缓存 | 条目详情**不走**请求缓存:已有条目级缓存 `douban.subject` / `bangumi.subject`(30 天,存归一化后的标题 / 别名 / 年份,还收站点详情页与网页搜索得到的条目),再缓存一遍原始响应就是同一份数据存两遍;`meta.subject_miss`(6 小时)保留,是识别升级时防止反复打外站的负缓存,不属于请求缓存 |
| 限流:无(靠缓存) | 每源进程内限流(豆瓣 2 并发、起始间隔 300 ms;bgm 3 并发、100 ms),只包真正发出的网络请求,缓存命中不占额度 |
设置字段只在 `contracts/settings.ts` 追加(`metaSearchDays`、`metaListHours`、`metaEmptyHours`),设置页界面由设置线决定是否展示。