diff --git a/py/docs/superpowers/specs/2026-04-23-butailing-spider-design.md b/py/docs/superpowers/specs/2026-04-23-butailing-spider-design.md new file mode 100644 index 0000000..52c3c99 --- /dev/null +++ b/py/docs/superpowers/specs/2026-04-23-butailing-spider-design.md @@ -0,0 +1,283 @@ +# 不太灵 Python 爬虫设计 + +## 目标 + +在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的不太灵站点爬虫,直接对接 `https://web5.mukaku.com/prod/api/v1/`,覆盖以下能力: + +- 首页分类 +- 首页推荐 +- 分类列表 +- 搜索 +- 详情解析 +- 网盘分享链接透传播放 + +实现以用户提供的 JS 版本为行为参考,但最终产物遵循当前仓库的单文件 Spider 约定,不引入 JS 端的网盘驱动依赖。 + +## 范围 + +本次实现包含: + +- 新增独立脚本,文件名为 `不太灵.py` +- 使用单一 API 根地址 `https://web5.mukaku.com/prod/api/v1/` +- 支持 `home/category/detail/search/player` 主链路 +- 首页返回固定五个分类 +- 电影和电视剧支持 API 筛选参数映射 +- 详情页提取影片元数据和网盘分享资源 +- 网盘资源按分享链接独立成线路 +- `playerContent` 对分享链接做原样透传 +- 为新增行为补齐 `unittest` + +本次实现不包含: + +- 参考 JS 中的磁力画质线路输出 +- 网盘驱动匹配、`getVod` 预解析或 `play` 二次解析 +- 提取码自动补链 +- 浏览器执行、反爬绕过或多域名探活 +- 修改 `base/` 公共层 + +## 方案选择 + +采用仓库现有的“单站点单文件 + 单测”方案: + +- 对外保持 `Spider` 接口兼容 +- 对内拆分为 API 请求、参数归一、列表项归一、分页处理、网盘类型识别、详情播放源提取几个 helper +- 保留参考 JS 的核心接口映射和过滤参数含义,但输出改为仓库可直接消费的 `vod_play_from` / `vod_play_url` + +不复刻参考 JS 的驱动解析逻辑,原因是: + +- 当前 Python 仓库没有对应的运行时网盘驱动注入机制 +- 用户已明确要求“直接返回分享链接” +- 单测应保持纯离线,不依赖外部驱动行为 + +## 模块边界 + +新增脚本只在站点文件内部维护逻辑,不修改 `base/`。 + +脚本内部职责拆分如下: + +- `init` + - 初始化 API 地址、鉴权参数、请求头、分类和缓存 +- `homeContent` + - 返回固定 `class`,并附带电影/电视剧筛选项 +- `homeVideoContent` + - 调用热门接口,返回首页推荐 +- `categoryContent` + - 根据分类和扩展参数请求对应 API,并归一化结果 +- `searchContent` + - 请求搜索接口,做名称过滤、本地去重和分页 +- `detailContent` + - 请求详情接口,提取影片元数据与网盘分享线路 +- `playerContent` + - 对分享链接直接透传 +- 私有辅助函数 + - 构造带鉴权的 API URL + - 发送请求并解析 JSON + - 归一化列表与详情对象 + - 解析扩展筛选参数 + - 本地去重与分页 + - 识别网盘类型并构造播放线路 + +## API 与请求策略 + +固定配置如下: + +- API 根地址:`https://web5.mukaku.com/prod/api/v1/` +- `app_id`:`83768d9ad4` +- `identity`:`23734adac0301bccdcb107c4aa21f96c` + +接口映射如下: + +- `homeContent` + - `getVideoTypeList` +- `homeVideoContent` + - `getVideoList`,参数 `sc=3` +- `categoryContent` + - 电影、电视剧:`getVideoMovieList` + - 热门分类:`getVideoList` +- `searchContent` + - `getVideoList` +- `detailContent` + - `getVideoDetail` + +请求原则: + +- 统一通过 `self.fetch` 发起 GET 请求 +- 超时固定为 10 秒 +- 请求头包含桌面浏览器 `User-Agent` +- JSON 解析兼容普通 JSON 字符串和带 `callback(...)` 包装的响应 +- 请求失败时返回空列表或空对象,不抛出未处理异常 +- 不额外引入重试和持久缓存 + +## 分类与筛选设计 + +首页固定分类为: + +- `1 -> 电影` +- `2 -> 电视剧` +- `3 -> 近日热门` +- `4 -> 本周热门` +- `5 -> 本月热门` + +`homeContent` 返回: + +- `class` +- `filters` + +筛选项只对电影和电视剧提供,来源于 `getVideoTypeList`,字段沿用参考实现: + +- `sc` 影视类型 +- `sd` 制片地区 +- `se` 上映年份 +- `sf` 资源画质 +- `sh` 影视标签 +- `sg` 排序方式 +- `iswp` 仅网盘资源 +- `status` 剧集状态,仅电视剧可用 + +筛选值归一原则: + +- `不限`、`0`、空字符串统一视为未设置 +- 排序默认值使用 `1` +- `iswp` 只接受 `0/1` +- `ext` 兼容 JSON、URL 编码 JSON 和 Base64 JSON + +## 列表、搜索与分页设计 + +列表项统一映射为: + +- `vod_id` + - 使用上游 `doub_id` +- `vod_name` + - 使用 `title` +- `vod_pic` + - 使用 `image` 或 `epic` +- `vod_remarks` + - 优先 `ejs`,回退 `zqxd` +- `vod_year` + - 使用 `years` +- `vod_content` + - 使用 `abstract` +- `vod_actor` + - 使用 `performer` +- `vod_director` + - 使用 `director` +- `vod_area` + - 使用 `production_area` + +分类处理分两类: + +- 电影、电视剧 + - 直接使用后端分页接口 `getVideoMovieList` + - 单页结果按 `doub_id` 去重 + - 返回 `page/limit/total/list` +- 热门分类 + - 因上游分页不稳定,固定抓取较大批量结果后本地去重和分页 + - 返回 `page/limit/total/list` + +搜索处理规则: + +- 调用 `getVideoList` +- 为避免非标题命中造成噪声,额外按标题包含关键字做一次过滤 +- 结果按 `doub_id` 去重 +- 再做本地分页 + +为了符合仓库当前约定,分类和搜索结果不返回 `pagecount`。 + +## 详情页设计 + +详情页输出单个视频对象,字段至少包含: + +- `vod_id` +- `vod_name` +- `vod_pic` +- `vod_remarks` +- `vod_year` +- `vod_content` +- `vod_actor` +- `vod_director` +- `vod_area` +- `vod_play_from` +- `vod_play_url` + +详情数据源直接使用 `getVideoDetail` 返回对象,不依赖列表缓存。 + +播放源策略只处理 `movies_online_seed`: + +- 忽略参考 JS 中的 `ecca` 和 `all_seeds` 磁力资源 +- 遍历 `movies_online_seed` 的每个分享项 +- 每个有效分享链接独立生成一条线路 +- 线路名格式为 `驱动类型#序号`,例如 `quark#1`、`baidu#2` +- 如果无法识别类型,则归类为 `other#序号` +- 同一链接去重,避免详情页重复输出 + +`vod_play_from` 示例: + +- `baidu#1$$$quark#1$$$aliyun#1` + +`vod_play_url` 示例: + +- `baidu#1$https://pan.baidu.com/s/xxx$$$quark#1$https://pan.quark.cn/s/yyy` + +## 网盘类型识别设计 + +按分享链接域名识别常见网盘类型: + +- `pan.quark.cn` -> `quark` +- `pan.baidu.com` -> `baidu` +- `pan.xunlei.com` -> `xunlei` +- `www.alipan.com` 或 `aliyundrive.com` -> `aliyun` +- `123865.com`、`123684.com`、`123pan.com` -> `a123` +- 其余 -> `other` + +如果 API 返回的分组名本身包含可用类型,但链接域名识别不到,则回退使用分组名归一后的结果。 + +## 播放设计 + +`playerContent` 不做任何解析: + +- 如果传入的是普通分享链接,直接返回 `{"parse": 0, "url": link}` +- 如果未来上层传入了 `push://` 前缀,也兼容去前缀后返回原始分享链接 + +本次不支持: + +- 直链视频嗅探 +- 网盘二次跳转 +- 播放头注入 +- 网盘分享码自动处理 + +## 错误处理设计 + +错误处理以“返回空结果,不中断链路”为原则: + +- API 请求失败 + - 列表接口返回空列表 + - 详情接口返回空对象,`detailContent` 最终返回空列表 +- JSON 解析失败 + - 记录日志后按空结果处理 +- 详情中无网盘资源 + - 返回正常元信息,`vod_play_from` 和 `vod_play_url` 置空 +- 搜索关键字为空 + - 直接返回空列表 + +## 测试设计 + +按 TDD 补充 `py/tests/test_灵机搜盘.py`,全部使用 `unittest.mock` 隔离网络请求。 + +首批测试覆盖: + +1. `homeContent` 返回固定分类,并只给电影、电视剧生成筛选项。 +2. `homeVideoContent` 使用 `sc=3` 请求热门列表并归一化字段。 +3. `categoryContent` 在电影分类下正确构造筛选参数,并且不返回 `pagecount`。 +4. `categoryContent` 对热门分类执行本地去重分页。 +5. `searchContent` 对标题关键字做二次过滤并去重。 +6. `detailContent` 能把多个网盘链接拆成独立线路并生成 `quark#1` 这类线路名。 +7. `detailContent` 忽略磁力资源,只保留网盘分享链接。 +8. `playerContent` 直接透传分享链接或去掉 `push://` 前缀。 +9. 网盘类型识别覆盖百度、夸克、迅雷、阿里、123 盘和兜底类型。 + +## 实施约束 + +- 文件命名使用中文站点名:`不太灵.py` +- 保持 helper 粒度适中,避免把 API 参数、详情播放源和分页逻辑堆在单个大函数里 +- 不修改现有 Spider 的行为 +- 先写测试并验证失败,再写实现代码