From 4bfa54d2a1ebd47f23fd6e23bd5cdbb3f7f8e7a9 Mon Sep 17 00:00:00 2001 From: Harold <8866033@gmail.com> Date: Thu, 23 Apr 2026 19:05:46 +0800 Subject: [PATCH] docs: add kuwo tingshu spider design --- .../2026-04-23-kuwo-tingshu-spider-design.md | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 py/docs/superpowers/specs/2026-04-23-kuwo-tingshu-spider-design.md diff --git a/py/docs/superpowers/specs/2026-04-23-kuwo-tingshu-spider-design.md b/py/docs/superpowers/specs/2026-04-23-kuwo-tingshu-spider-design.md new file mode 100644 index 0000000..dcb6de5 --- /dev/null +++ b/py/docs/superpowers/specs/2026-04-23-kuwo-tingshu-spider-design.md @@ -0,0 +1,389 @@ +# 酷我听书 Python 爬虫设计 + +## 目标 + +在当前 Python 仓库中新增一个符合 `base.spider.Spider` 接口的酷我听书爬虫,行为参考用户提供的 JS 版本,但交付物遵循现有仓库的单文件 Spider 结构和测试约定。 + +本次设计覆盖以下能力: + +- 首页分类与筛选 +- 首页推荐 +- 分类浏览 +- 搜索 +- 专辑详情 +- 章节播放 +- VIP 章节外部解析 + +## 范围 + +本次实现包含: + +- 新增独立脚本 `py/酷我听书.py` +- 新增独立测试 `py/tests/test_酷我听书.py` +- 固定站点分类、筛选项和默认排序 +- 首页按四个主分类聚合推荐 +- 分类接口支持子类型、权限、排序筛选 +- 详情页输出专辑元数据和章节列表 +- 免费章节走酷我官方播放接口 +- VIP 章节保留外部解析接口支持 +- 针对非标准搜索 JSON、空页翻页和付费判断补齐离线测试 + +本次实现不包含: + +- 修改 `base/` 公共层 +- 引入新的第三方依赖 +- 新增缓存层、持久化状态或代理层 +- 对公益解析接口做探活、限流或多地址切换 +- 真实联网集成测试 + +## 方案选择 + +采用“Python 原生适配,保留核心能力”的方案,而不是直接翻译参考 JS。 + +原因如下: + +- 当前仓库的交付物是 Python `Spider` 单文件,不是 Express 风格插件 +- 仓库约定中分类和搜索结果通常返回 `page/limit/total/list`,且多数模块不返回 `pagecount` +- 参考实现中的全局状态、路由入口和长播放 URL 更适合在 Python 中收敛成内部 helper +- 参考实现的核心能力仍可完整保留:固定分类、筛选、强制翻页、详情、免费播放、VIP 解析 + +## 模块边界 + +新增模块 `py/酷我听书.py` 仅负责该站点逻辑,不修改 `base/`。 + +模块对外实现以下接口: + +- `init` +- `getName` +- `homeContent` +- `homeVideoContent` +- `categoryContent` +- `detailContent` +- `searchContent` +- `playerContent` + +模块内部使用 helper 收敛行为: + +- `_api_get` + - 访问主站 API `http://tingshu.kuwo.cn` +- `_search_get` + - 访问搜索接口 `http://search.kuwo.cn` +- `_normalize_vod` + - 统一列表项字段 +- `_format_play_count` + - 格式化播放量显示 +- `_is_paid_track` + - 判断章节是否为 VIP +- `_parse_search_payload` + - 兼容搜索接口返回的非标准 JSON +- `_fetch_category_page` + - 获取分类页原始数据 +- `_force_page_fetch` + - 处理空页补齐和本地 VIP 过滤 +- `_build_filters` + - 组装首页筛选结构 +- `_build_play_id` + - 构造短播放 ID +- `_resolve_free_play` + - 解析免费章节直链 +- `_resolve_vip_play` + - 解析 VIP 章节外部接口 + +## 站点配置 + +固定配置来源于参考实现,直接内置在模块常量中: + +- 站点名:`酷我听书` +- 主站:`http://tingshu.kuwo.cn` +- 搜索站:`http://search.kuwo.cn` +- 播放站:`http://mobi.kuwo.cn` +- VIP 解析接口:`https://music-api.gdstudio.xyz/api.php` +- 默认请求头: + - `User-Agent: kwplayer_ar_9.1.8.1_tvivo.apk` + +固定分类如下: + +- `2 -> 有声小说` +- `37 -> 音乐金曲` +- `5 -> 相声评书` +- `62 -> 影视原声` + +固定筛选项沿用参考实现: + +- 每个主分类带一个 `class` 子类型筛选 +- 所有分类追加统一的 `vip` 权限筛选 +- 所有分类追加统一的 `sort` 排序筛选 + +其中: + +- `vip` + - `全部权限` + - `免费权限` + - `会员权限` +- `sort` + - `综合排序` + - `最新上架` + - `按总播放` + +## 首页设计 + +`homeContent` 只返回静态分类和筛选定义,并附带首页推荐列表。 + +返回结构包含: + +- `class` +- `filters` +- `list` + +`homeVideoContent` 会按四个主分类分别请求默认子分类第一页,每类提取列表后聚合返回。默认子分类取该分类第一个筛选项的 `init` 值。 + +列表项统一映射为: + +- `vod_id` + - `albumId` +- `vod_name` + - `albumName` +- `vod_pic` + - `coverImg` +- `vod_remarks` + - `会员|免费 + 播放量 + 分类名或作者` + +播放量格式化规则: + +- `>= 1e8` 显示为 `x.x亿` +- `>= 1e4` 显示为 `x.x万` +- 其他按整数原样显示 + +## 分类与分页设计 + +`categoryContent` 支持主分类、页码和扩展筛选参数。 + +筛选参数映射如下: + +- `class` + - 子类型 ID,默认取当前分类的 `init` +- `vip` + - `""`、`0`、`1` +- `sort` + - `tsScore`、`pubDate`、`playCnt` + +返回遵循仓库现有习惯: + +- `page` +- `limit` +- `total` +- `list` + +默认不返回 `pagecount`。 + +上游分类接口可能出现中间页为空的情况,因此保留参考实现中的“强制翻页补齐”策略: + +- 用户请求第 `pg` 页时,从该页开始请求 +- 若当前页无数据,则继续尝试后续页 +- 最多尝试 `maxPage=10` 次 +- 一旦拿到有效数据,则将该页作为当前返回数据源 + +VIP 权限筛选在本地二次处理: + +- `vip=""` + - 不过滤 +- `vip="0"` + - 仅保留免费专辑 +- `vip="1"` + - 仅保留会员专辑 + +`total` 和 `limit` 处理原则: + +- `limit` 固定返回 `21` +- 若还有后续可翻页空间,`total` 返回一个足够大的估算值,以维持翻页体验 +- 若已无后续数据,则 `total` 退化为当前页结果长度 + +## 搜索设计 + +`searchContent` 访问 `search.kuwo.cn` 的专辑搜索接口,关键词参数对应参考实现的 `all`。 + +搜索接口的响应可能是单引号 JSON 或普通 JSON 字符串,因此需要容错解析: + +- 优先按字符串读取 +- 若存在单引号包裹的对象结构,先做最小修正再解析 +- 解析失败则返回空结果 + +搜索结果映射字段: + +- `vod_id` + - `DC_TARGETID` +- `vod_name` + - `name` +- `vod_pic` + - `img` +- `vod_remarks` + - `会员|免费 + artist` + +搜索返回结构: + +- `page` +- `limit` + - 固定 `21` +- `total` +- `list` + +搜索空关键词时直接返回空结果: + +- `{"page": 1, "limit": 0, "total": 0, "list": []}` + +## 详情设计 + +`detailContent` 根据专辑 ID 请求专辑详情接口,返回单个专辑对象。 + +详情字段映射: + +- `vod_id` + - 专辑 ID +- `vod_name` + - 根级 `name` +- `vod_pic` + - 根级 `img`,若为相对路径则补全为完整封面地址 +- `vod_remarks` + - `会员|免费 + 完结状态 + 集数 + 总播放量` +- `vod_content` + - `info`,回退 `title` +- `vod_actor` + - `artist` +- `vod_director` + - `company` +- `vod_year` + - 从 `pub` 提取年份,格式为 `2026年` +- `vod_area` + - `lang` +- `vod_lang` + - `lang` +- `vod_time` + - `pub` +- `vod_tag` + - `完结` 或 `连载` + +章节列表使用单线路输出: + +- `vod_play_from = "酷我听书"` +- `vod_play_url = 章节1$播放ID#章节2$播放ID...` + +章节名称规则: + +- 使用章节名 `track.name` +- 缺失时回退为 `第N集` +- VIP 章节前面加 `💎` + +### 播放 ID 设计 + +不直接把长播放 URL 写进详情,而是构造成短载荷: + +- 免费章节:`free|` +- VIP 章节:`vip|` + +这样可以把真实播放解析逻辑集中在 `playerContent`,降低 `vod_play_url` 的耦合度,也方便单测断言。 + +## 播放设计 + +`playerContent` 根据播放 ID 类型分两条链路处理。 + +### 免费章节 + +当播放 ID 形如 `free|musicrid` 时: + +- 请求 `http://mobi.kuwo.cn/mobi.s` +- 参数: + - `f=web` + - `type=convert_url_with_sign` + - `rid=` + - `br=320kmp3` +- 若接口返回 JSON 中存在最终音频 URL,则返回直链播放 + +输出结构: + +- `parse: 0` +- `url: ` +- `header` + - `User-Agent` + - `Referer` + +### VIP 章节 + +当播放 ID 形如 `vip|musicrid` 时: + +- 请求外部解析接口 `https://music-api.gdstudio.xyz/api.php` +- 保留参考实现中的固定查询参数 +- 若返回 `url` 字段,则作为最终音频地址 + +输出结构: + +- `parse: 0` +- `url: ` +- `header` + - `User-Agent: LX-Music-Mobile` + - `Referer: https://music-api.gdstudio.xyz` + +### 回退策略 + +若播放解析失败: + +- 免费章节返回一个基于 `musicrid` 拼装的官方播放请求地址作为兜底 `url` +- VIP 章节返回外部解析接口请求地址作为兜底 `url` +- 不抛出未处理异常 + +## 错误处理 + +整体遵循当前仓库的“失败返回空结果或兜底结果”策略。 + +- 首页、分类、搜索接口失败 + - 返回空列表结果 +- 详情接口失败或章节为空 + - 返回 `{"list": []}` +- 搜索 JSON 解析失败 + - 返回空结果,不中断流程 +- 播放解析失败 + - 返回兜底 `url`,保持 `playerContent` 可消费 + +所有网络请求都通过 `self.fetch` 实现,便于在单测中统一 mock。 + +## 测试设计 + +测试采用 `unittest + SourceFileLoader + unittest.mock`,全部离线执行,不依赖真实网络。 + +覆盖范围: + +- `homeContent` + - 固定分类、筛选结构、`vip/sort` 追加逻辑 +- `homeVideoContent` + - 四类首页推荐聚合、分类名写入 `vod_remarks` +- `categoryContent` + - 分类参数映射、空页后移、VIP 本地过滤、不返回 `pagecount` +- `searchContent` + - 非标准 JSON 解析、字段映射、空关键词返回空结果 +- `detailContent` + - 专辑元数据映射、封面补全、章节名和播放 ID 构造 +- `playerContent` + - 免费章节直链解析 + - VIP 章节外部解析 + - 失败回退路径 +- helper + - 播放量格式化 + - VIP 判断 + - 播放 ID 编解码 + +## 风险与约束 + +- 搜索接口返回结构不是严格标准 JSON,解析策略需要尽量保守,避免误替换内容 +- 公益解析接口是外部依赖,稳定性不可控,因此必须保留失败回退 +- 详情章节数可能很大,播放列表生成应保持简单,不在本次实现中引入额外排序和缓存 +- 站点是音频内容,不涉及仓库里常见的视频网盘线路,因此 `playerContent` 返回头信息时应按音频接口的需求最小化设置 + +## 交付物 + +- `py/酷我听书.py` +- `py/tests/test_酷我听书.py` + +本次设计已经明确: + +- 站点模块名使用 `酷我听书` +- VIP 外部解析接口默认保留并启用 +- 结果结构遵循当前 Python 仓库习惯,而不是直接复刻 JS 插件路由层行为