diff --git a/py/docs/superpowers/specs/2026-04-24-四万影视-design.md b/py/docs/superpowers/specs/2026-04-24-四万影视-design.md new file mode 100644 index 0000000..f8a55f9 --- /dev/null +++ b/py/docs/superpowers/specs/2026-04-24-四万影视-design.md @@ -0,0 +1,293 @@ +# 四万影视 Spider 设计 + +## 目标 + +在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的 `四万影视` spider,参考用户提供的 JS 采集脚本,接入 `https://40000.me/api/maccms`,实现分类、搜索、详情和播放能力。 + +## 范围 + +本次实现包含: + +- 新增 `py/四万影视.py` +- 新增 `py/tests/test_四万影视.py` +- 固定主分类与静态筛选 +- 分类接口请求与列表字段标准化 +- 搜索接口请求与列表字段标准化 +- 详情接口请求与多线路播放字段组装 +- 播放直链透传与非直链 `parse=1` 回退 + +本次实现不包含: + +- 首页推荐抓取 +- 动态分类发现 +- 站点 HTML 页面解析 +- 真实联网测试 +- 第三方解析器实现 + +## 目标接口形态 + +Spider 将实现以下标准方法: + +- `homeContent(filter)` +- `homeVideoContent()` +- `categoryContent(tid, pg, filter, extend)` +- `searchContent(key, quick, pg="1")` +- `detailContent(ids)` +- `playerContent(flag, id, vipFlags)` + +遵循当前仓库约定: + +- 单文件 spider +- 使用 `fetch` 发起请求,不改共享基类 +- 列表与搜索结果不返回 `pagecount` +- 详情结果使用 `vod_play_from` / `vod_play_url` + +## 分类与筛选 + +沿用参考脚本的固定分类 ID 与名称: + +- `20` 电影 +- `30` 电视剧 +- `39` 动漫 +- `45` 综艺 +- `32` 欧美 + +筛选为静态定义,不从远端动态读取。 + +每个主分类都返回三类筛选: + +- `subType` + - 使用参考脚本中的子分类映射 + - 默认值为当前主分类 `type_id` +- `year` + - `全部` 加 `2026` 到 `2000` +- `sort` + - `time` + - `hits` + - `score` + - `up` + +Python 版本筛选值沿用仓库现有格式: + +- `{"n": "显示名", "v": "实际值"}` + +## 数据模型 + +### 列表项 + +分类和搜索返回的每个视频项统一标准化为: + +- `vod_id` +- `vod_name` +- `vod_pic` +- `vod_remarks` +- `vod_year` +- `type_name` + +字段来源以 maccms JSON 为准。 + +额外规范: + +- `vod_pic` 为空时回退到 `https://40000.me/public/favicon.png` +- `type_name` 优先使用接口值,缺失时按固定主分类映射兜底 +- `vod_actor` 中的 `'` 还原为 `'` +- 演员列表中的逗号空白归一化为英文逗号 + +### 详情项 + +详情页输出保持仓库当前约定,不保留 JS 版本里的 `vod_play_sources` 数组,而是转换为: + +- `vod_play_from` +- `vod_play_url` + +组装规则: + +- 上游 `vod_play_from` 按 `$$$` 拆成线路名 +- 上游 `vod_play_url` 按 `$$$` 拆成线路内容 +- 每条线路内部按 `#` 拆分剧集 +- 每个剧集按 `剧集名$playId` 编码 +- 若某条线路名缺失,回退为 `线路N` +- 若某个剧集标题缺失,回退为 `第N集` + +最终: + +- `vod_play_from = 线路A$$$线路B` +- `vod_play_url = 第1集$playId#第2集$playId$$$正片$playId` + +### 播放 ID + +播放 ID 不再额外编码,直接复用接口里的剧集值: + +- 若值本身是 `http/https` 链接,则播放器直接返回 +- 若值不是完整 URL,则播放器返回 `parse=1` 交给外部解析 + +## 请求映射 + +Spider 只请求一个接口端点: + +- `https://40000.me/api/maccms` + +### homeContent + +不请求远端,直接返回固定分类和固定筛选。 + +### homeVideoContent + +按用户确认,固定返回: + +- `{"list": []}` + +### categoryContent + +请求参数: + +- `ac=detail` +- `t=` +- `pg=` +- `h=`,仅在年份非空时传入 +- `by=` + +其中: + +- `subType` 来自 `extend["subType"]`,缺失时回退 `tid` +- `sort` 来自 `extend["sort"]`,默认 `time` +- `year` 来自 `extend["year"]` + +返回: + +- `page` +- `limit` +- `total` +- `list` + +`limit` 使用当前返回列表长度,`total` 优先使用接口 `total`,缺失时回退为列表长度。 + +### searchContent + +请求参数: + +- `ac=detail` +- `wd=` +- `pg=` + +空关键字直接返回空列表。 + +返回: + +- `page` +- `limit` +- `total` +- `list` + +### detailContent + +请求参数: + +- `ac=detail` +- `ids=` + +流程: + +1. 读取第一条详情记录 +2. 标准化基础字段 +3. 解析 `vod_play_from` / `vod_play_url` +4. 输出单个详情对象 + +重点字段: + +- `vod_id` +- `vod_name` +- `vod_pic` +- `type_name` +- `vod_year` +- `vod_area` +- `vod_director` +- `vod_actor` +- `vod_content` +- `vod_remarks` +- `vod_play_from` +- `vod_play_url` + +### playerContent + +处理规则保持与参考脚本一致: + +1. 空播放 ID 返回: + - `parse = 1` + - `jx = 1` + - `url = ""` +2. 若 `id` 以 `http://` 或 `https://` 开头: + - `parse = 0` + - `jx = 0` + - `url = id` +3. 否则: + - `parse = 1` + - `jx = 1` + - `url = id` + +播放器返回头固定包含: + +- `User-Agent` +- `Referer: https://40000.me/` + +## 架构与辅助函数 + +`py/四万影视.py` 保持单类实现,但拆成小 helper,避免方法过长。 + +建议 helper: + +- `_build_filters()` + - 生成固定筛选配置 +- `_type_name_by_id(type_id)` + - 固定主分类名称映射 +- `_normalize_vod(item)` + - 统一标准化列表/详情基础字段 +- `_api_get(params)` + - 调用 maccms JSON 接口 + - 非 200 或非对象响应时抛出 `ValueError` + - 上层入口方法负责捕获并返回空结果 +- `_parse_play_groups(item)` + - 把上游播放源字段转换成仓库需要的 `vod_play_from` / `vod_play_url` +- `_page_result(items, page, total)` + - 构造列表返回体 + +## 错误处理 + +- 请求失败时不向外抛未处理异常 +- 分类失败返回: + - `{"page": 当前页, "limit": 0, "total": 0, "list": []}` +- 搜索失败返回: + - `{"page": 当前页, "limit": 0, "total": 0, "list": []}` +- 详情失败返回: + - `{"list": []}` +- 播放失败返回: + - `{"parse": 1, "jx": 1, "url": "", "header": {...}}` + +这样与仓库现有 spider 的防御性风格保持一致。 + +## 测试策略 + +使用 `unittest` 与 `unittest.mock`,不走真网。 + +至少覆盖以下行为: + +1. `homeContent` 返回预期分类 ID 与筛选 key +2. `_build_filters` 生成 `subType/year/sort` 三组筛选 +3. `_api_get` 正确请求 `/api/maccms`,并处理非 200 / 非对象响应 +4. `categoryContent` 正确映射 `subType/year/sort/page` +5. `categoryContent` 不返回 `pagecount` +6. `searchContent` 正确映射 `wd` 和分页 +7. `searchContent` 在空关键字时直接返回空列表 +8. `detailContent` 正确提取详情基础字段并组装 `vod_play_from` / `vod_play_url` +9. `_parse_play_groups` 能处理多线路、多剧集、缺失线路名与缺失剧集名 +10. `playerContent` 对直链返回 `parse=0` +11. `playerContent` 对非直链返回 `parse=1` + +## 验收标准 + +- 新增 `py/四万影视.py` 和 `py/tests/test_四万影视.py` +- 可独立运行 `python -m unittest tests.test_四万影视 -v` +- 分类、搜索、详情的离线夹具输出稳定 +- `homeVideoContent()` 固定返回空列表 +- 播放器对直链和非直链的行为与参考脚本一致 +- 不修改共享基类即可完成接入