@mr-quin/dango-manifests 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -31
- package/package.json +3 -3
- package/src/manifests/builtin-aiyifan.json +152 -0
- package/src/manifests/builtin-bahamut.json +61 -0
- package/src/manifests/builtin-bilibili.json +8 -7
- package/src/manifests/builtin-dandanplay.json +72 -10
- package/src/manifests/builtin-hanjutv.json +3 -3
- package/src/manifests/builtin-iqiyi.json +130 -0
- package/src/manifests/builtin-maiduidui.json +96 -0
- package/src/manifests/builtin-mango.json +114 -0
- package/src/manifests/builtin-migu.json +3 -3
- package/src/manifests/builtin-renren.json +122 -0
- package/src/manifests/builtin-sohu.json +70 -0
- package/src/manifests/builtin-tencent.json +5 -4
- package/src/manifests/builtin-youku.json +114 -0
- package/src/manifests/builtin-ddp-compat.json +0 -96
package/README.md
CHANGED
|
@@ -1,32 +1,51 @@
|
|
|
1
1
|
# @mr-quin/dango-manifests
|
|
2
2
|
|
|
3
|
-
Built-in dango manifests for
|
|
3
|
+
Built-in dango manifests for a curated set of danmaku sources. Each manifest is a JSON file declaring the search/episodes/danmaku pipelines a [@mr-quin/dango](../dango/README.md) `ManifestRunner` interprets at runtime, with no per-source TypeScript fetching code.
|
|
4
4
|
|
|
5
|
-
This package is data, not code. There is no `index.ts
|
|
5
|
+
This package is data, not code. There is no `index.ts`: consumers import each manifest JSON directly via the `./manifests/*` subpath export.
|
|
6
6
|
|
|
7
7
|
## Shipped manifests
|
|
8
8
|
|
|
9
|
-
| Id | Endpoint | Notes
|
|
10
|
-
| -------------------- | ------------------------------------------- |
|
|
11
|
-
| `builtin:dandanplay` | `api.
|
|
12
|
-
| `builtin:
|
|
13
|
-
| `builtin:
|
|
14
|
-
| `builtin:
|
|
9
|
+
| Id | Endpoint | Notes |
|
|
10
|
+
| -------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `builtin:dandanplay` | `baseUrl` (default `api.dandanplay.net`) | Unified DDP source. Default official; optional `appId`/`appSecret` signing, `auth` headers, or none (proxy). `baseUrl` overrides the endpoint (proxy / self-hosted). |
|
|
12
|
+
| `builtin:bilibili` | `api.bilibili.com` | Parallel media_bangumi + media_ft search; xml + protobuf danmaku variants. |
|
|
13
|
+
| `builtin:tencent` | `pbaccess.video.qq.com` + `dm.video.qq.com` | POST bodies, page-cursor episode pagination via forEach `breakOn`, two-phase danmaku (segment index then per-segment fetch). |
|
|
14
|
+
| `builtin:hanjutv` | `hxqapi.hiyun.tv` + `hxqapi.zmdcq.com` | Korean-drama source; search, detail-driven episodes, per-segment danmaku. |
|
|
15
|
+
| `builtin:mango` | `*.mgtv.com` + `*.hitv.com` | Mango TV; source=imgo search, two-stage showlist episodes, per-minute CDN danmaku shards. |
|
|
16
|
+
| `builtin:migu` | `*.migu.cn` + `*.miguvideo.com` | Migu Video; content-info lookup, episodes list, segmented danmaku. |
|
|
17
|
+
| `builtin:youku` | `*.youku.com` + `log.mmstat.com` | Youku; HTML-stripped search, openapi episodes, signed per-minute danmaku POSTs (cna guid + \_m_h5_tk token). |
|
|
18
|
+
| `builtin:iqiyi` | `*.iqiyi.com` + `mesh.if.iqiyi.com` | iQIYI; /v\_ watch-page search, signed base_info episodes, zlib-deflated XML bullet shards. |
|
|
19
|
+
| `builtin:aiyifan` | `yfsp.tv` + `rankv21.tripdata.app` | Aiyifan; homepage-scraped pub/priv keys sign each JSON request; getBarrage danmaku. |
|
|
20
|
+
| `builtin:bahamut` | `api.gamer.com.tw` | Bahamut Anime (Traditional Chinese); single danmu.php fetch, time in tenths of a second. |
|
|
21
|
+
| `builtin:maiduidui` | `mob.mddcloud.com.cn` | Maiduidui; nested search, listVodSactions episodes, per-minute vodBarrage shards. |
|
|
22
|
+
| `builtin:renren` | `api.gorafie.com` + `static-dm.qwdjapp.com` | Renren (foreign drama); array-rooted danmaku with CSV `p` strings. |
|
|
23
|
+
| `builtin:sohu` | `*.sohu.com` | Sohu TV; site=1 search, album episodes, paginated dmListAll danmaku (decimal/hex color handling). |
|
|
15
24
|
|
|
16
25
|
## Layout
|
|
17
26
|
|
|
18
27
|
```
|
|
19
28
|
src/
|
|
20
29
|
manifests/
|
|
21
|
-
builtin-
|
|
22
|
-
builtin-
|
|
30
|
+
builtin-aiyifan.json
|
|
31
|
+
builtin-bahamut.json
|
|
23
32
|
builtin-bilibili.json
|
|
33
|
+
builtin-dandanplay.json
|
|
34
|
+
builtin-hanjutv.json
|
|
35
|
+
builtin-iqiyi.json
|
|
36
|
+
builtin-maiduidui.json
|
|
37
|
+
builtin-mango.json
|
|
38
|
+
builtin-migu.json
|
|
39
|
+
builtin-renren.json
|
|
40
|
+
builtin-sohu.json
|
|
24
41
|
builtin-tencent.json
|
|
42
|
+
builtin-youku.json
|
|
25
43
|
__tests__/
|
|
26
44
|
builtin-*.test.ts # per-manifest pipeline tests
|
|
27
45
|
fixtures/ # captured representative responses
|
|
46
|
+
mockFetcher.ts # shared test fetcher (string + Uint8Array bodies)
|
|
28
47
|
scripts/
|
|
29
|
-
smoke-test.ts # manual end-to-end test against the real
|
|
48
|
+
smoke-test.ts # manual end-to-end test against the real API
|
|
30
49
|
```
|
|
31
50
|
|
|
32
51
|
## Usage
|
|
@@ -49,23 +68,21 @@ const danmaku = await runner.runDanmaku({
|
|
|
49
68
|
|
|
50
69
|
## Routing notes
|
|
51
70
|
|
|
52
|
-
**DanDanPlay** (`builtin:dandanplay`)
|
|
53
|
-
|
|
54
|
-
**DanDanPlay-compatible self-hosted** (`builtin:ddp-compat`) takes `baseUrl` via `configSchema` and `authHeaders` as a per-call inputs array of `{key, value}` pairs. One manifest serves any number of user-added `DanDanPlayCompatible` provider configs; the extension passes the user's config blob into each pipeline run.
|
|
71
|
+
**DanDanPlay** (`builtin:dandanplay`) is one manifest for every DDP endpoint over the `/api/v2` paths. `baseUrl` (config) defaults to the official `api.dandanplay.net`; point it at a proxy or self-hosted server to override. Auth is picked by config: `appId` + `appSecret` sign each request (`X-Signature: Base64(SHA256(appId + timestamp + path + appSecret))`, secret never sent); else `auth.enabled` + `auth.headers` attach custom headers; else no auth (for a proxy that signs server-side). The official API is just the case with no `baseUrl` and credentials set.
|
|
55
72
|
|
|
56
|
-
**Bilibili** and **Tencent** call their public APIs directly. Both rely on `rewriteHeaders` for `Origin` / `Referer`, which
|
|
73
|
+
**Bilibili** and **Tencent** call their public APIs directly. Both rely on `rewriteHeaders` for `Origin` / `Referer`, which a browser host's `FetchLike` applies via a request-header rewrite mechanism such as `chrome.declarativeNetRequest`. The smoke script merges those headers in directly since Node fetch lets you set them.
|
|
57
74
|
|
|
58
75
|
## Smoke testing
|
|
59
76
|
|
|
60
|
-
`bun run smoke <source> [keyword]` walks search → first result → episodes → first episode → danmaku against the real
|
|
77
|
+
`bun run smoke <source> [keyword]` walks search → first result → episodes → first episode → danmaku against the real API. Not in CI; useful when adding a manifest or verifying after an upstream change.
|
|
61
78
|
|
|
62
|
-
| Source | Command | Status
|
|
63
|
-
| -------- | ------------------------------- |
|
|
64
|
-
| ddp | `bun run smoke ddp Frieren` |
|
|
65
|
-
| bilibili | `bun run smoke bilibili Naruto` | works (after a cookie warm-up via `www.bilibili.com`)
|
|
66
|
-
| tencent | `bun run smoke tencent 庆余年` | works
|
|
79
|
+
| Source | Command | Status |
|
|
80
|
+
| -------- | ------------------------------- | -------------------------------------------------------- |
|
|
81
|
+
| ddp | `bun run smoke ddp Frieren` | needs `DDP_APP_ID` / `DDP_APP_SECRET` env (official API) |
|
|
82
|
+
| bilibili | `bun run smoke bilibili Naruto` | works (after a cookie warm-up via `www.bilibili.com`) |
|
|
83
|
+
| tencent | `bun run smoke tencent 庆余年` | works |
|
|
67
84
|
|
|
68
|
-
The smoke fetcher keeps a per-host cookie jar so bilibili's anti-bot wall lets us through, and it merges `rewriteHeaders` into the outgoing request (Node has no `Origin`/`Referer` restriction). `ddp
|
|
85
|
+
The smoke fetcher keeps a per-host cookie jar so bilibili's anti-bot wall lets us through, and it merges `rewriteHeaders` into the outgoing request (Node has no `Origin`/`Referer` restriction). The default `ddp` smoke hits the official API and needs `DDP_APP_ID` / `DDP_APP_SECRET`.
|
|
69
86
|
|
|
70
87
|
## Adding a new manifest
|
|
71
88
|
|
|
@@ -77,18 +94,18 @@ The smoke fetcher keeps a per-host cookie jar so bilibili's anti-bot wall lets u
|
|
|
77
94
|
- Asserts the canonical shape the engine emits
|
|
78
95
|
4. (Optional) Add a section to `scripts/smoke-test.ts` to exercise the new manifest against a live endpoint.
|
|
79
96
|
|
|
80
|
-
Each manifest's tests own their own fixtures
|
|
97
|
+
Each manifest's tests own their own fixtures, don't share fixtures across manifests.
|
|
81
98
|
|
|
82
99
|
## Trust model
|
|
83
100
|
|
|
84
|
-
Manifests in this package are vetted at PR review time and shipped
|
|
101
|
+
Manifests in this package are vetted at PR review time and shipped as built-ins. They are not user-installable. User-installed manifests are a separate concern a host may add with explicit consent UX. See dango's [trust model](../dango/README.md#trust-model) for the engine-level invariants that apply regardless of where a manifest comes from.
|
|
85
102
|
|
|
86
103
|
## Scripts
|
|
87
104
|
|
|
88
|
-
| Command | Description
|
|
89
|
-
| ------------------------ |
|
|
90
|
-
| `bun test` | bun test, fixture-backed pipeline tests
|
|
91
|
-
| `bun run smoke <source>` | Live end-to-end test against the real
|
|
92
|
-
| `bun run type-check` | tsc --noEmit
|
|
93
|
-
| `bun run lint` | oxlint
|
|
94
|
-
| `bun run build` | Compile with tsc
|
|
105
|
+
| Command | Description |
|
|
106
|
+
| ------------------------ | -------------------------------------------------------------- |
|
|
107
|
+
| `bun test` | bun test, fixture-backed pipeline tests |
|
|
108
|
+
| `bun run smoke <source>` | Live end-to-end test against the real API (manual, **not CI**) |
|
|
109
|
+
| `bun run type-check` | tsc --noEmit |
|
|
110
|
+
| `bun run lint` | oxlint |
|
|
111
|
+
| `bun run build` | Compile with tsc |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mr-quin/dango-manifests",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Built-in dango manifests for danmaku sources",
|
|
5
5
|
"author": "Mr-Quin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"smoke": "bun run scripts/smoke-test.ts"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"
|
|
28
|
+
"@bufbuild/protobuf": "^2",
|
|
29
|
+
"@mr-quin/dango": "0.1.0"
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiVersion": 1,
|
|
3
|
+
"id": "builtin:aiyifan",
|
|
4
|
+
"name": "Aiyifan",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"hosts": ["www.yfsp.tv", "rankv21.tripdata.app", "m10.yfsp.tv"],
|
|
7
|
+
"search": {
|
|
8
|
+
"inputs": ["q"],
|
|
9
|
+
"steps": [
|
|
10
|
+
{
|
|
11
|
+
"type": "http",
|
|
12
|
+
"id": "config",
|
|
13
|
+
"request": {
|
|
14
|
+
"method": "GET",
|
|
15
|
+
"url": "'https://www.yfsp.tv/'",
|
|
16
|
+
"format": "text",
|
|
17
|
+
"rewriteHeaders": {
|
|
18
|
+
"User-Agent": "'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0 Safari/537.36'"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"extract": {
|
|
22
|
+
"pub": "$regexExtract($, '\"pConfig\"\\\\s*:\\\\s*\\\\{\\\\s*\"publicKey\"\\\\s*:\\\\s*\"([^\"]+)\"')",
|
|
23
|
+
"priv": "$regexExtract($, '\"privateKey\"\\\\s*:\\\\s*\\\\[\\\\s*\"([^\"]+)\"')"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "assign",
|
|
28
|
+
"id": "qs",
|
|
29
|
+
"values": {
|
|
30
|
+
"canon": "'tags=' & q & '&orderby=4&page=1&size=10&desc=1&isserial=-1'"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"type": "http",
|
|
35
|
+
"id": "search",
|
|
36
|
+
"request": {
|
|
37
|
+
"method": "GET",
|
|
38
|
+
"url": "'https://rankv21.tripdata.app/v3/list/briefsearch'",
|
|
39
|
+
"query": "{ 'tags': q, 'orderby': '4', 'page': '1', 'size': '10', 'desc': '1', 'isserial': '-1', 'vv': $md5(config.pub & '&' & $lowercase(qs.canon) & '&' & config.priv), 'pub': config.pub }",
|
|
40
|
+
"rewriteHeaders": {
|
|
41
|
+
"User-Agent": "'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0 Safari/537.36'"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"output": "[search.data.info.result[contxt and title].{ 'providerIds': { 'vid': contxt }, 'indexedId': contxt, 'title': title, 'type': atypeName ? atypeName : 'Aiyifan', 'year': postTime ? $number($substring(postTime, 0, 4)) : undefined, 'imageUrl': imgPath }]"
|
|
47
|
+
},
|
|
48
|
+
"episodes": {
|
|
49
|
+
"inputs": ["vid"],
|
|
50
|
+
"steps": [
|
|
51
|
+
{
|
|
52
|
+
"type": "http",
|
|
53
|
+
"id": "config",
|
|
54
|
+
"request": {
|
|
55
|
+
"method": "GET",
|
|
56
|
+
"url": "'https://www.yfsp.tv/'",
|
|
57
|
+
"format": "text",
|
|
58
|
+
"rewriteHeaders": {
|
|
59
|
+
"User-Agent": "'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0 Safari/537.36'"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"extract": {
|
|
63
|
+
"pub": "$regexExtract($, '\"pConfig\"\\\\s*:\\\\s*\\\\{\\\\s*\"publicKey\"\\\\s*:\\\\s*\"([^\"]+)\"')",
|
|
64
|
+
"priv": "$regexExtract($, '\"privateKey\"\\\\s*:\\\\s*\\\\[\\\\s*\"([^\"]+)\"')"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"type": "assign",
|
|
69
|
+
"id": "qs",
|
|
70
|
+
"values": {
|
|
71
|
+
"canon": "'cinema=1&vid=' & vid & '&lsk=1&taxis=0&cid=0,1,4,152'"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"type": "http",
|
|
76
|
+
"id": "playlist",
|
|
77
|
+
"request": {
|
|
78
|
+
"method": "GET",
|
|
79
|
+
"url": "'https://m10.yfsp.tv/v3/video/languagesplaylist'",
|
|
80
|
+
"query": "{ 'cinema': '1', 'vid': vid, 'lsk': '1', 'taxis': '0', 'cid': '0,1,4,152', 'vv': $md5(config.pub & '&' & $lowercase(qs.canon) & '&' & config.priv), 'pub': config.pub }",
|
|
81
|
+
"rewriteHeaders": {
|
|
82
|
+
"User-Agent": "'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0 Safari/537.36'"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"output": "[playlist.data.info.playList[key].{ 'providerIds': { 'epKey': key }, 'indexedId': key, 'title': name and $length(name) > 0 ? name : ('第' & $string($$.i + 1) & '集'), 'episodeNumber': $exists(id) ? $number(id) : undefined }]"
|
|
88
|
+
},
|
|
89
|
+
"danmaku": {
|
|
90
|
+
"inputs": ["epKey"],
|
|
91
|
+
"steps": [
|
|
92
|
+
{
|
|
93
|
+
"type": "http",
|
|
94
|
+
"id": "config",
|
|
95
|
+
"request": {
|
|
96
|
+
"method": "GET",
|
|
97
|
+
"url": "'https://www.yfsp.tv/'",
|
|
98
|
+
"format": "text",
|
|
99
|
+
"rewriteHeaders": {
|
|
100
|
+
"User-Agent": "'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0 Safari/537.36'"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"extract": {
|
|
104
|
+
"pub": "$regexExtract($, '\"pConfig\"\\\\s*:\\\\s*\\\\{\\\\s*\"publicKey\"\\\\s*:\\\\s*\"([^\"]+)\"')",
|
|
105
|
+
"priv": "$regexExtract($, '\"privateKey\"\\\\s*:\\\\s*\\\\[\\\\s*\"([^\"]+)\"')"
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"type": "assign",
|
|
110
|
+
"id": "qs1",
|
|
111
|
+
"values": {
|
|
112
|
+
"canon": "'cinema=1&id=' & epKey & '&a=0&lang=none&usersign=1®ion=GL.&device=0&isMasterSupport=1'"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"type": "http",
|
|
117
|
+
"id": "video",
|
|
118
|
+
"request": {
|
|
119
|
+
"method": "GET",
|
|
120
|
+
"url": "'https://m10.yfsp.tv/v3/video/play'",
|
|
121
|
+
"query": "{ 'cinema': '1', 'id': epKey, 'a': '0', 'lang': 'none', 'usersign': '1', 'region': 'GL.', 'device': '0', 'isMasterSupport': '1', 'vv': $md5(config.pub & '&' & $lowercase(qs1.canon) & '&' & config.priv), 'pub': config.pub }",
|
|
122
|
+
"rewriteHeaders": {
|
|
123
|
+
"User-Agent": "'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0 Safari/537.36'"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"extract": {
|
|
127
|
+
"uniqueKey": "data.info[0].uniqueKey"
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"type": "assign",
|
|
132
|
+
"id": "qs2",
|
|
133
|
+
"values": {
|
|
134
|
+
"canon": "'cinema=1&page=1&size=30000&uniqueKey=' & video.uniqueKey"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"type": "http",
|
|
139
|
+
"id": "bar",
|
|
140
|
+
"request": {
|
|
141
|
+
"method": "GET",
|
|
142
|
+
"url": "'https://m10.yfsp.tv/api/video/getBarrage'",
|
|
143
|
+
"query": "{ 'cinema': '1', 'page': '1', 'size': '30000', 'uniqueKey': video.uniqueKey, 'vv': $md5(config.pub & '&' & $lowercase(qs2.canon) & '&' & config.priv), 'pub': config.pub }",
|
|
144
|
+
"rewriteHeaders": {
|
|
145
|
+
"User-Agent": "'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0 Safari/537.36'"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
"output": "[bar.data.info[contxt or content].{ 'p': $string(second ? second : 0) & ',' & (position = 1 ? '5' : '1') & ',' & $string($hexToInt($replace(color ? color : '#FFFFFF', /^#/, ''))) & ',', 'm': contxt ? contxt : content }]"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiVersion": 1,
|
|
3
|
+
"id": "builtin:bahamut",
|
|
4
|
+
"name": "Bahamut",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"hosts": ["api.gamer.com.tw"],
|
|
7
|
+
"search": {
|
|
8
|
+
"inputs": ["q"],
|
|
9
|
+
"steps": [
|
|
10
|
+
{
|
|
11
|
+
"type": "http",
|
|
12
|
+
"id": "search",
|
|
13
|
+
"request": {
|
|
14
|
+
"method": "GET",
|
|
15
|
+
"url": "'https://api.gamer.com.tw/mobile_app/anime/v1/search.php'",
|
|
16
|
+
"query": "{ 'kw': q }",
|
|
17
|
+
"rewriteHeaders": {
|
|
18
|
+
"User-Agent": "'Anime/2.29.2 (7N5749MM3F.tw.com.gamer.anime; build:972; iOS 26.0.0) Alamofire/5.6.4'"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"output": "[search.anime[video_sn and title].{ 'providerIds': { 'videoSn': $string(video_sn) }, 'indexedId': $string(video_sn), 'title': title, 'type': '动漫', 'year': $exists(info) ? $number($regexExtract(info, '([12][890][0-9][0-9])')) : undefined, 'imageUrl': cover, 'episodeCount': $exists(info) ? $number($regexExtract(info, '共\\\\s*(\\\\d+)\\\\s*集')) : undefined }]"
|
|
24
|
+
},
|
|
25
|
+
"episodes": {
|
|
26
|
+
"inputs": ["videoSn"],
|
|
27
|
+
"steps": [
|
|
28
|
+
{
|
|
29
|
+
"type": "http",
|
|
30
|
+
"id": "video",
|
|
31
|
+
"request": {
|
|
32
|
+
"method": "GET",
|
|
33
|
+
"url": "'https://api.gamer.com.tw/anime/v1/video.php'",
|
|
34
|
+
"query": "{ 'videoSn': videoSn }",
|
|
35
|
+
"rewriteHeaders": {
|
|
36
|
+
"User-Agent": "'Anime/2.29.2 (7N5749MM3F.tw.com.gamer.anime; build:972; iOS 26.0.0) Alamofire/5.6.4'"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"output": "[video.data.anime.episodes.*[$exists(videoSn)].{ 'providerIds': { 'videoSn': $string(videoSn) }, 'indexedId': $string(videoSn), 'title': '第' & $string(episode) & '集', 'episodeNumber': $exists(episode) ? $number(episode) : undefined }]"
|
|
42
|
+
},
|
|
43
|
+
"danmaku": {
|
|
44
|
+
"inputs": ["videoSn"],
|
|
45
|
+
"steps": [
|
|
46
|
+
{
|
|
47
|
+
"type": "http",
|
|
48
|
+
"id": "fetch",
|
|
49
|
+
"request": {
|
|
50
|
+
"method": "GET",
|
|
51
|
+
"url": "'https://api.gamer.com.tw/anime/v1/danmu.php'",
|
|
52
|
+
"query": "{ 'geo': 'TW,HK', 'videoSn': videoSn }",
|
|
53
|
+
"rewriteHeaders": {
|
|
54
|
+
"User-Agent": "'Anime/2.29.2 (7N5749MM3F.tw.com.gamer.anime; build:972; iOS 26.0.0) Alamofire/5.6.4'"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"output": "[fetch.data.danmu[text].{ 'p': $string(time / 10) & ',' & (position = 1 ? '5' : (position = 2 ? '4' : '1')) & ',' & $string($hexToInt($replace(color ? color : '#FFFFFF', /^#/, ''))) & ',' & (userid ? userid : ''), 'm': text }]"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"apiVersion": 1,
|
|
3
3
|
"id": "builtin:bilibili",
|
|
4
4
|
"name": "Bilibili",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"hosts": ["api.bilibili.com"],
|
|
7
7
|
"urlMatch": [
|
|
8
8
|
{
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
|
-
"
|
|
30
|
-
"bili": "
|
|
29
|
+
"protoDescriptors": {
|
|
30
|
+
"bili": "Cp4CCgtkbV92MS5wcm90bxIFZG0udjEiyAEKC0Rhbm1ha3VFbGVtEgoKAmlkGAEgASgDEhAKCHByb2dyZXNzGAIgASgFEgwKBG1vZGUYAyABKAUSEAoIZm9udHNpemUYBCABKAUSDQoFY29sb3IYBSABKA0SDwoHbWlkSGFzaBgGIAEoCRIPCgdjb250ZW50GAcgASgJEg0KBWN0aW1lGAggASgDEg4KBndlaWdodBgJIAEoBRIOCgZhY3Rpb24YCiABKAkSDAoEcG9vbBgLIAEoBRINCgVpZFN0chgMIAEoCSI1ChBEbVNlZ01vYmlsZVJlcGx5EiEKBWVsZW1zGAEgAygLMhIuZG0udjEuRGFubWFrdUVsZW1iBnByb3RvMw=="
|
|
31
31
|
},
|
|
32
32
|
"search": {
|
|
33
33
|
"inputs": ["q"],
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"extract": {
|
|
45
45
|
"imgKey": "$regexExtract(data.wbi_img.img_url, '/([a-f0-9]+)\\\\.png')",
|
|
46
46
|
"subKey": "$regexExtract(data.wbi_img.sub_url, '/([a-f0-9]+)\\\\.png')"
|
|
47
|
-
}
|
|
47
|
+
},
|
|
48
|
+
"cache": { "key": "bilibili:wbi-nav", "ttlSeconds": 43200 }
|
|
48
49
|
},
|
|
49
50
|
{
|
|
50
51
|
"type": "assign",
|
|
@@ -102,7 +103,7 @@
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
],
|
|
105
|
-
"output": "[season.result.episodes[$not($contains(badge ? badge : '', '预告'))].{ 'providerIds': { 'cid': cid, 'aid': aid, 'bvid': bvid, 'epid': id }, 'indexedId': $string(cid), 'title': long_title and long_title != '' ? long_title : show_title, 'episodeNumber': $match(
|
|
106
|
+
"output": "[season.result.episodes[$not($contains(badge ? badge : '', '预告'))].{ 'providerIds': { 'cid': cid, 'aid': aid, 'bvid': bvid, 'epid': id }, 'indexedId': $string(cid), 'title': long_title and long_title != '' ? long_title : show_title, 'episodeNumber': $match(title, /^\\d+$/) ? $number(title) : (title and title != '' ? title : undefined), 'imageUrl': cover, 'alternativeTitle': share_copy and share_copy != '' ? [share_copy] : undefined }]"
|
|
106
107
|
},
|
|
107
108
|
"season": {
|
|
108
109
|
"inputs": ["seasonId"],
|
|
@@ -119,7 +120,7 @@
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
],
|
|
122
|
-
"output": "season.result ? { 'providerIds': { 'seasonId': season.result.season_id, 'mediaId': season.result.media_id }, 'indexedId': $string(season.result.season_id), 'title': $replace(season.result.title, /<[^>]+>/, ''), 'type': $string(season.result.type), 'imageUrl': season.result.cover, 'episodeCount': $count(season.result.episodes) } : undefined"
|
|
123
|
+
"output": "season.result ? { 'providerIds': { 'seasonId': season.result.season_id, 'mediaId': season.result.media_id }, 'indexedId': $string(season.result.season_id), 'title': $replace(season.result.title, /<[^>]+>/, ''), 'type': season.result.season_type_name ? season.result.season_type_name : $string(season.result.type), 'imageUrl': season.result.cover, 'episodeCount': $count(season.result.episodes[$not($contains(badge ? badge : '', '预告'))]) } : undefined"
|
|
123
124
|
},
|
|
124
125
|
"danmaku": [
|
|
125
126
|
{
|
|
@@ -219,6 +220,6 @@
|
|
|
219
220
|
}
|
|
220
221
|
}
|
|
221
222
|
],
|
|
222
|
-
"output": "{ 'seasonInsert': { 'providerIds': { 'seasonId': season.result.season_id }, 'indexedId': $string(season.result.season_id), 'title': $replace(season.result.title, /<[^>]+>/, ''), 'type': $string(season.result.type), 'imageUrl': season.result.cover, 'episodeCount': $count(season.result.episodes) }, 'episodeMeta': $exists(picked.ep) ? { 'providerIds': { 'cid': picked.ep.cid, 'aid': picked.ep.aid, 'bvid': picked.ep.bvid, 'epid': picked.ep.id }, 'indexedId': $string(picked.ep.cid), 'title': picked.ep.long_title and picked.ep.long_title != '' ? picked.ep.long_title : picked.ep.show_title, 'episodeNumber': $match(picked.ep.
|
|
223
|
+
"output": "{ 'seasonInsert': { 'providerIds': { 'seasonId': season.result.season_id }, 'indexedId': $string(season.result.season_id), 'title': $replace(season.result.title, /<[^>]+>/, ''), 'type': $string(season.result.type), 'imageUrl': season.result.cover, 'episodeCount': $count(season.result.episodes) }, 'episodeMeta': $exists(picked.ep) ? { 'providerIds': { 'cid': picked.ep.cid, 'aid': picked.ep.aid, 'bvid': picked.ep.bvid, 'epid': picked.ep.id }, 'indexedId': $string(picked.ep.cid), 'title': picked.ep.long_title and picked.ep.long_title != '' ? picked.ep.long_title : picked.ep.show_title, 'episodeNumber': $match(picked.ep.title, /^\\d+$/) ? $number(picked.ep.title) : (picked.ep.title and picked.ep.title != '' ? picked.ep.title : undefined), 'imageUrl': picked.ep.cover, 'externalLink': picked.ep.link } : undefined }"
|
|
223
224
|
}
|
|
224
225
|
}
|
|
@@ -2,11 +2,50 @@
|
|
|
2
2
|
"apiVersion": 1,
|
|
3
3
|
"id": "builtin:dandanplay",
|
|
4
4
|
"name": "DanDanPlay",
|
|
5
|
-
"version": "
|
|
6
|
-
"hosts": ["
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"hosts": ["*"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"properties": {
|
|
10
|
+
"baseUrl": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"title": "Base URL",
|
|
13
|
+
"description": "Root of the DanDanPlay (or compatible) API. Defaults to the official https://api.dandanplay.net. Point this at a proxy or self-hosted server to override."
|
|
14
|
+
},
|
|
15
|
+
"appId": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"title": "App ID",
|
|
18
|
+
"description": "DanDanPlay open-platform App ID. Required for the official API; omit when using a proxy that signs server-side."
|
|
19
|
+
},
|
|
20
|
+
"appSecret": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"title": "App Secret",
|
|
23
|
+
"description": "DanDanPlay open-platform App Secret, used to sign requests. Only feeds the local signature, never sent."
|
|
24
|
+
},
|
|
25
|
+
"auth": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"title": "Authentication",
|
|
28
|
+
"description": "Custom request headers (e.g. a token) sent on every request. Used instead of signing when appId/appSecret are absent, e.g. a self-hosted compatible server.",
|
|
29
|
+
"properties": {
|
|
30
|
+
"enabled": {
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"default": false,
|
|
33
|
+
"title": "Enable auth headers"
|
|
34
|
+
},
|
|
35
|
+
"headers": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"title": "Headers",
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"required": ["key", "value"],
|
|
41
|
+
"properties": {
|
|
42
|
+
"key": { "type": "string", "title": "Header name" },
|
|
43
|
+
"value": { "type": "string", "title": "Value" }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
10
49
|
"chConvert": {
|
|
11
50
|
"type": "integer",
|
|
12
51
|
"enum": [0, 1, 2],
|
|
@@ -19,43 +58,66 @@
|
|
|
19
58
|
"search": {
|
|
20
59
|
"inputs": ["q"],
|
|
21
60
|
"steps": [
|
|
61
|
+
{
|
|
62
|
+
"type": "assign",
|
|
63
|
+
"id": "prep",
|
|
64
|
+
"values": {
|
|
65
|
+
"req": "($base := baseUrl ? $replace(baseUrl, /\\/$/, '') : 'https://api.dandanplay.net'; $p := '/api/v2/search/anime'; $ts := $string($now()); $headers := (appId and appSecret) ? { 'X-AppId': appId, 'X-Timestamp': $ts, 'X-Signature': $sha256Base64(appId & $ts & $p & appSecret) } : ((auth.enabled and auth.headers) ? $merge(auth.headers.{ key: value }) : {}); { 'url': $base & $p, 'headers': $headers })"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
22
68
|
{
|
|
23
69
|
"type": "http",
|
|
24
70
|
"id": "search",
|
|
25
71
|
"request": {
|
|
26
72
|
"method": "GET",
|
|
27
|
-
"url": "
|
|
28
|
-
"query": "{ '
|
|
73
|
+
"url": "prep.req.url",
|
|
74
|
+
"query": "{ 'keyword': q }",
|
|
75
|
+
"headers": "prep.req.headers"
|
|
29
76
|
}
|
|
30
77
|
}
|
|
31
78
|
],
|
|
32
|
-
"output": "[search.animes.{ 'providerIds': { 'animeId': animeId, 'bangumiId': bangumiId }, 'indexedId': $string(animeId), 'title': animeTitle, 'type': type, 'typeDescription': typeDescription, 'imageUrl': imageUrl, 'episodeCount': episodeCount, 'year': $number($substringBefore(startDate, '-')) }]"
|
|
79
|
+
"output": "[search.animes.{ 'providerIds': { 'animeId': animeId, 'bangumiId': bangumiId }, 'indexedId': $string(animeId), 'title': animeTitle, 'type': type, 'typeDescription': typeDescription, 'imageUrl': imageUrl, 'episodeCount': episodeCount, 'year': startDate and $contains(startDate, '-') ? $number($substringBefore(startDate, '-')) : undefined }]"
|
|
33
80
|
},
|
|
34
81
|
"episodes": {
|
|
35
82
|
"inputs": ["bangumiId"],
|
|
36
83
|
"steps": [
|
|
84
|
+
{
|
|
85
|
+
"type": "assign",
|
|
86
|
+
"id": "prep",
|
|
87
|
+
"values": {
|
|
88
|
+
"req": "($base := baseUrl ? $replace(baseUrl, /\\/$/, '') : 'https://api.dandanplay.net'; $p := '/api/v2/bangumi/' & $string(bangumiId); $ts := $string($now()); $headers := (appId and appSecret) ? { 'X-AppId': appId, 'X-Timestamp': $ts, 'X-Signature': $sha256Base64(appId & $ts & $p & appSecret) } : ((auth.enabled and auth.headers) ? $merge(auth.headers.{ key: value }) : {}); { 'url': $base & $p, 'headers': $headers })"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
37
91
|
{
|
|
38
92
|
"type": "http",
|
|
39
93
|
"id": "bangumi",
|
|
40
94
|
"request": {
|
|
41
95
|
"method": "GET",
|
|
42
|
-
"url": "
|
|
43
|
-
"
|
|
96
|
+
"url": "prep.req.url",
|
|
97
|
+
"headers": "prep.req.headers"
|
|
44
98
|
}
|
|
45
99
|
}
|
|
46
100
|
],
|
|
47
|
-
"output": "[bangumi.bangumi.episodes.{ 'providerIds': { 'episodeId': episodeId, 'animeId': %.animeId, 'bangumiId': %.bangumiId }, 'indexedId': $string(episodeId), 'title': episodeTitle, 'episodeNumber': episodeNumber }]"
|
|
101
|
+
"output": "[bangumi.bangumi.episodes.{ 'providerIds': { 'episodeId': episodeId, 'animeId': %.animeId, 'bangumiId': %.bangumiId }, 'indexedId': $string(episodeId), 'title': episodeTitle, 'episodeNumber': $match(episodeNumber, /^\\d+$/) ? $number(episodeNumber) : episodeNumber }]"
|
|
48
102
|
},
|
|
49
103
|
"danmaku": {
|
|
50
104
|
"inputs": ["episodeId"],
|
|
51
105
|
"steps": [
|
|
106
|
+
{
|
|
107
|
+
"type": "assign",
|
|
108
|
+
"id": "prep",
|
|
109
|
+
"values": {
|
|
110
|
+
"req": "($base := baseUrl ? $replace(baseUrl, /\\/$/, '') : 'https://api.dandanplay.net'; $p := '/api/v2/comment/' & $string(episodeId); $ts := $string($now()); $headers := (appId and appSecret) ? { 'X-AppId': appId, 'X-Timestamp': $ts, 'X-Signature': $sha256Base64(appId & $ts & $p & appSecret) } : ((auth.enabled and auth.headers) ? $merge(auth.headers.{ key: value }) : {}); { 'url': $base & $p, 'headers': $headers })"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
52
113
|
{
|
|
53
114
|
"type": "http",
|
|
54
115
|
"id": "comments",
|
|
55
116
|
"request": {
|
|
56
117
|
"method": "GET",
|
|
57
|
-
"url": "
|
|
58
|
-
"query": "{ '
|
|
118
|
+
"url": "prep.req.url",
|
|
119
|
+
"query": "{ 'withRelated': 'true', 'chConvert': chConvert ? $string(chConvert) : undefined }",
|
|
120
|
+
"headers": "prep.req.headers"
|
|
59
121
|
}
|
|
60
122
|
}
|
|
61
123
|
],
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"apiVersion": 1,
|
|
3
3
|
"id": "builtin:hanjutv",
|
|
4
4
|
"name": "Hanjutv",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"hosts": ["hxqapi.hiyun.tv", "hxqapi.zmdcq.com"],
|
|
7
7
|
"search": {
|
|
8
8
|
"inputs": ["q"],
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
],
|
|
49
|
-
"output": "($sidStr := $string(sid); [detail.playItems[$exists(pid)].{ 'providerIds': { 'sid': $sidStr, 'pid': $string(pid) }, 'indexedId': $string(pid), 'title': $exists(title) and title != '' ? title : ('第' & $string(serialNo) & '集'), 'episodeNumber':
|
|
49
|
+
"output": "($sidStr := $string(sid); [detail.playItems[$exists(pid)].{ 'providerIds': { 'sid': $sidStr, 'pid': $string(pid) }, 'indexedId': $string(pid), 'title': $exists(title) and title != '' ? title : ('第' & $string(serialNo) & '集'), 'episodeNumber': serialNo }])"
|
|
50
50
|
},
|
|
51
51
|
"danmaku": {
|
|
52
52
|
"inputs": ["pid"],
|
|
@@ -72,6 +72,6 @@
|
|
|
72
72
|
"breakOn": "$count($) = 0"
|
|
73
73
|
}
|
|
74
74
|
],
|
|
75
|
-
"output": "[segments.{ 'cid': $number(did), 'p': $string(t / 1000) & ',' & $string(tp = 2 ? 5 : tp) & ',' & $string($number(sc)), 'm': con }]"
|
|
75
|
+
"output": "[segments.{ 'cid': $number(did), 'p': $string(t / 1000) & ',' & $string(tp = 2 ? 5 : (tp in [1,4,5,6] ? tp : 1)) & ',' & $string($number(sc)), 'm': con }]"
|
|
76
76
|
}
|
|
77
77
|
}
|