From 00bf1361d36aad925fe5df13dd1726dde17fd8d3 Mon Sep 17 00:00:00 2001 From: Nixevol Date: Fri, 25 Sep 2026 23:49:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=92=AD=E6=94=BE=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E4=BC=98=E5=85=88=E4=BD=BF=E7=94=A8=20playlist=20=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=8E=A5=E5=8F=A3=E6=B8=85=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/接口清单.md | 470 +++++++++++++++++++++++++++++++++++++++++++++++ rapt_scraper.py | 42 ++++- 2 files changed, 505 insertions(+), 7 deletions(-) create mode 100644 docs/接口清单.md diff --git a/docs/接口清单.md b/docs/接口清单.md new file mode 100644 index 0000000..4ba3c26 --- /dev/null +++ b/docs/接口清单.md @@ -0,0 +1,470 @@ +# RaptDrama 接口清单 + +基址使用 `https://apis.raptdrama.com`。`https://app.raptdrama.com` 上部分路径也能通,脚本以 `apis` 为准。 + +本文只记录已经实际调用过的返回结构。安装包里还有一批路径,返回格式没有核对,放在最后一节,不要把它们当成已确认的协议。 + +## 通用约定 + +业务响应外面都有同一层包装。HTTP 状态经常仍是 200,成功与否看 `code`。 + +```json +{ + "code": 200, + "msg": "ok", + "data": {} +} +``` + +已见到的业务码: + +| code | 含义 | +| --- | --- | +| 200 | 成功。`msg` 可能是 `ok`、`成功` 或 `Login success` | +| 299 | 未登录,`msg` 为 `Please login first`。用 `Authorization: Bearer` 调播放接口时会出现 | +| 500 | 参数错误,例如 `Invalid email format`、`Password cannot be empty`、`Error Type` | + +请求头: + +| 头 | 值 | +| --- | --- | +| Content-Type | `application/json`。播放和登录都用 JSON,不要用表单 | +| Accept | `application/json` | +| User-Agent | `RaptDrama/1.1.81 (Android 12; 23113RKC6C; Redmi)` | +| apasstk | 登录后拿到的 `token`。游客 `autologin` 本身不带这个头 | +| x-app-build | `139` | +| x-app-version | `1.1.81` | +| x-device-type | `phone` | +| x-device-brand | `Redmi` | +| x-device-model | `23113RKC6C` | +| x-device-id | 设备号,例如 `rd_` 开头的字符串 | +| x-device-fingerprint | 设备指纹 | +| x-device-physical | `true` | +| x-os-name | `Android` | +| x-os-version | `12` | +| accept-language | `en-US` | +| accept-language-custom | `en-US` | + +登录凭证只放在 `apasstk`。不要再使用 `Authorization: Bearer`。 + +## 登录 + +### POST /app/open/autologin + +每次调用都新建一个游客,不会沿用上一次的账号。不需要 `apasstk`。 + +请求体:`{}` + +`data`: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | string | 新游客 id | +| nickname | string | 随机昵称 | +| token | string | 后续请求的 `apasstk` | +| score | int | | +| vipday | int | | +| badge | string | 例如 `BRONZE` | +| watched | int | | +| rewards | int | | +| avatarNum | int | | +| avatarNumUrl | string | 头像地址 | + +### POST /app/open/emailLogin + +同一邮箱重复登录返回同一个用户。不需要先带 `apasstk`。 + +请求体: + +```json +{"email": "邮箱", "password": "密码"} +``` + +`email` 或 `password` 用错字段名时,`code` 为 500。 + +`data`: + +| 字段 | 类型 | +| --- | --- | +| id | int | +| nickname | string | +| email | string | +| email_bind | int | +| score | int | +| tgid | int | +| isguanzhu | int | +| sxid | int | +| os | string | +| channel | string | +| token | string | +| google_bind | int | + +`/auth/login` 在这个基址上没有返回上述 JSON 包装,邮箱登录以 `/app/open/emailLogin` 为准。 + +### POST /app/user/getUserInfo + +需要 `apasstk`。游客 token 也可以调用。 + +请求体:`{}` + +`data`: + +| 字段 | 类型 | +| --- | --- | +| id | int | +| nickname | string | +| email_bind | int | +| mobile_bind | int | +| score | int | +| tgid | int | +| isguanzhu | int | +| sxid | int | +| os | string | +| channel | string | +| google_bind | int | +| apple_bind | int | +| login_day | int | +| vipday | int | +| badge | string | +| watched | int | +| rewards | int | +| avatarNum | int | +| avatarNumUrl | string | + +这里的 `id` 是数字。`autologin` 返回的 `id` 是字符串。 + +## 分类和剧列表 + +剧对象在多个接口里重复出现,下面称它为剧目: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | int | 剧 id,播放接口的 `vid` 用这个值 | +| title | string | | +| update_time | int | Unix 时间 | +| view | int | | +| image | string | 封面 | +| tstype | string | 分类 id,Passion 为 `"20"` | +| desc | string | | +| forstaus | string | | +| forstausen | string | 连载状态文案,例如完结 | +| author | string | | +| classify | string[] | 题材标签 | +| score | string | 评分,是字符串 | + +### POST /app/open/classifyv2 + +请求体:`{}` + +`data` 是分类数组: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| id | int | 传给分类视频接口的 `type` | +| label | string | 英文名 | +| label_origin | string | | +| image | string 或 null | | + +已见到的分类:`18` Comedy,`20` Passion,`21` Newest,`22` Western,`23` Comic drama。 + +### POST /app/open/classify_video + +按分类分页。`type` 才是分类 id。传 `tstype`、`classify` 或 `id` 会返回 `code` 500、`Error Type`。 + +请求体: + +```json +{"page": 1, "limit": 10, "type": 20} +``` + +`page` 从 1 开始。每页 10 条,空页表示结束。`limit` 增大也不会一次返回超过 10 条。 + +`data` 是剧目数组。 + +### POST /app/open/recommend + +请求体:`{}` + +`data` 是剧目数组。空对象也只返回约 10 条,不是全站列表。 + +### POST /app/open/foryou + +请求体:`{}` + +`data` 是剧目数组,并多一个 `firstchapter`: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| firstchapter.id | int | 集 id | +| firstchapter.title | string | | +| firstchapter.idx | int | 集序号 | +| firstchapter.playurl | string | 这一集的播放地址 | + +### POST /app/open/extra_broadcast + +请求体:`{}` + +`data` 是剧目数组。 + +### GET /app/open/orgin + +无请求体。`data` 是剧目数组。 + +### POST /app/open/search + +请求体: + +```json +{"keyword": "love", "page": 1, "limit": 10} +``` + +`data` 是剧目数组,并多一个 `totalvideo`(int,集数)。 + +### POST /app/open/bank + +请求体:`{}` + +`data` 不是剧目数组,而是观看或片库记录: + +| 字段 | 类型 | +| --- | --- | +| id | int | +| vid | int | +| cid | int | +| create_time | int | +| video.id | int | +| video.title | string | +| video.image | string | +| video.desc | string | +| video.forstaus | string | +| video.author | string | +| video.forstausen | string | + +## 剧集和播放地址 + +### GET /app/video/getchapters + +查询参数: + +| 参数 | 说明 | +| --- | --- | +| vid | 剧 id | +| start | 起始偏移,从 0 开始 | +| limit | 每页条数,可用 200 | + +示例:`/app/video/getchapters?vid=1942&start=0&limit=200` + +`data`: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| count | int | 总集数 | +| list[].id | int | 集 id,对应播放列表里的 `cid` | +| list[].title | string | | +| list[].idx | int | 集序号 | +| list[].isvip | string | `"0"` 免费,`"1"` 锁定。这是字符串,不是布尔值 | + +### POST /app/video/playlist + +安装包里没有这个路径,但服务端仍接受,GET 和 POST 都可以。请求体与 `playlistv2` 相同。 + +`data` 的字段和 `playlistv2` 一样。区别是它一次返回该剧全部集,不再按每页 10 条分页。剧 1942 一次返回 `count` 30、`list` 30 条,其中免费的 3 集有 `src`,其余为空。第一集的 `src` 与 `playlistv2` 第一页相同。脚本优先调用这个接口,结果为空、条数少于 `count` 或请求失败时,再按页调用 `playlistv2`。 + +`playlistv1` 和 `playlistv3` 返回 nginx 404。 + +### POST /app/video/playlistv2 + +这是播放地址接口。`vid` 是剧 id,不是集 id。 + +请求体: + +```json +{"vid": 1942, "page": 1, "before": 1} +``` + +第一页 `before` 为 `1`,后面的页为 `0`。每页 10 集。`data.count` 到齐后停止。 + +`data`: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| count | int | 总集数 | +| list[].cid | int | 集 id | +| list[].idx | int | 集序号 | +| list[].msg | string | 例如 `EP.1` | +| list[].src | string | 主 m3u8。未解锁时是空字符串 | +| list[].state | string | 例如 `pause` | +| list[].like | int | | +| list[].like_n | int | | +| list[].playNumber | int | | +| list[].fufei | int | | +| list[].playIng | bool | | +| list[].isShowimage | bool | | +| list[].needScore | string | | +| list[].isplay | bool | | + +游客和未购买账号只能拿到免费集的 `src`。锁定集的 `src` 为空,接口不会因为换了登录方式就返回地址。 + +`src` 是 BunnyCDN 签名地址,形如: + +```text +https://vz-<编号>.b-cdn.net/bcdn_token=<签名>&token_path=%2F<视频uuid>%2F&expires=/<视频uuid>/playlist.m3u8 +``` + +签名写在路径里,不是查询字符串。主列表里有一行相对路径 `1080p/video.m3u8`。子列表是点播,分片名为 `video0.ts`、`video1.ts`。同一段签名前缀可以访问该视频目录下的子列表和 ts。签名会过期,需要时重新请求 `playlistv2`。 + +### POST /app/video/videoinfo + +请求体: + +```json +{"vid": 1942} +``` + +`data`: + +| 字段 | 类型 | +| --- | --- | +| moshi | int | +| video.id | int | +| video.title | string | +| video.view | int | +| video.desc | string | +| video.ishot | string | +| video.like | int | +| video.is_on | int | +| video.poster | string | +| video.forstaus | string | +| video.author | string | +| video.forstausen | string | + +带已登录账号时,`data` 里还出现过 `history`。 + +### POST /app/video/addreadlog + +App 播放时发出的请求体如下,返回格式没有单独保存: + +```json +{"vid": 2286, "del": 0, "cid": 26125} +``` + +`vid` 是剧 id,`cid` 是集 id。 + +## 其他已核对接口 + +### POST /app/open/contact + +请求体:`{}` + +`data`: + +| 字段 | 类型 | +| --- | --- | +| techniacl_email | string | +| business_email | string | + +字段名在服务端就是 `techniacl_email`。 + +### POST /app/appopen/getAuthConfig + +请求体:`{}` + +`data`: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| platform | string | | +| google.enabled | bool | | +| google.android_client_id | string | | +| google.ios_client_id | string | | +| google.web_client_id | string | | +| google.client_id | string | | +| google.server_client_id | string | | +| apple.enabled | bool | | +| apple.service_id | string | | +| apple.team_id | string | | +| apple.key_id | string | | +| supported_platforms | string[] | | + +## 安装包中的路径 + +这些路径从 `libapp.so` 提取。除上面已写明的接口外,访问方式和返回格式都还没有核对。 + +认证: + +- POST /app/user/appleLogin +- POST /app/user/googleLogin +- POST /app/user/bindEmail +- POST /auth/register +- POST /auth/refresh +- POST /auth/bind-email + +用户: + +- /app/user/accountBindingStatus +- /app/user/editProfile +- /app/user/changePassword +- /app/user/deleteAccount +- /app/user/activate +- /app/user/activationSubOverview +- /app/user/getPlatformBinds +- /app/user/getSensorsProfile +- /app/user/recordAttribution +- /app/user/readhistory +- /app/user/delhistory + +视频: + +- /app/video/overview +- /app/video/addToWishlist +- /app/video/markvideo +- /app/video/exitRecommendm +- /app/video/screening +- /app/User/checkViewTime + +支付和订阅: + +- /app/pay/google_iap_create_order +- /app/pay/google_iap_verify +- /app/pay/google_iap_status +- /app/pay/google_iap_recovery +- /app/pay/apple_iap_create_order +- /app/pay/apple_iap_verify +- /app/pay/apple_iap_recovery +- /app/pay/paylmpay_create_order +- /app/pay/paypal_return +- /app/pay/paypal_cancel +- /app/pay/paystatus +- /app/pay/paysuccess +- /app/pay/initiateCheckout +- /app/pay/report_payment_panel_context +- /app/pay_center/create_order +- /app/pay_center/paylist +- /app/pay_center/third_party_qualification +- /app/user/paylist +- /app/user/paylistv2 +- /app/user/paylistv3 +- /app/subscription/list +- /app/subscription/cancel + +签到、反馈、通知: + +- /app/checkin/config +- /app/checkin/status +- /app/checkin/perform +- /app/checkin/history +- /app/checkin/statistics +- /app/checkin/tasks +- /app/feedback/submit +- /app/feedback/list +- /app/feedback/detail +- /app/feedback/read +- /app/feedback/categories +- /app/feedback/unreadCount +- /app/feedback/upload +- /app/notification/getList +- /app/notification/getDetail +- /app/notification/device +- /app/notification/settings +- /app/notification/action +- /app/notification/clear diff --git a/rapt_scraper.py b/rapt_scraper.py index 858fb2e..8ea90b8 100644 --- a/rapt_scraper.py +++ b/rapt_scraper.py @@ -268,7 +268,23 @@ def list_chapters(token, vid): return chapters -def list_playlist(token, vid): +def _playlist_items(data): + if isinstance(data, list): + return data, len(data) + items = (data or {}).get("list") or [] + total = (data or {}).get("count") + return items, total + + +def _playlist_complete(items, total): + if not items: + return False + if total is None: + return True + return len(items) >= int(total) + + +def list_playlist_v2(token, vid): episodes = [] page = 1 total = None @@ -278,12 +294,9 @@ def list_playlist(token, vid): "page": page, "before": 1 if page == 1 else 0, }) - if isinstance(data, list): - items = data - total = len(items) - else: - items = (data or {}).get("list") or [] - total = (data or {}).get("count", total) + items, page_total = _playlist_items(data) + if page_total is not None: + total = page_total if not items: break episodes.extend(items) @@ -294,6 +307,21 @@ def list_playlist(token, vid): return episodes +def list_playlist(token, vid): + try: + data = api("/app/video/playlist", token, "POST", { + "vid": vid, "page": 1, "before": 1, + }) + items, total = _playlist_items(data) + if _playlist_complete(items, total): + return items + except Exception as exc: + print(f" playlist 失败,改用 playlistv2:{exc}", flush=True) + return list_playlist_v2(token, vid) + print(f" playlist 结果不完整,改用 playlistv2", flush=True) + return list_playlist_v2(token, vid) + + def expand_hls(master_url): """把主 m3u8 展开成子播放列表和 ts 绝对地址。失败时只保留主地址。""" if not master_url: