@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 CHANGED
@@ -1,32 +1,51 @@
1
1
  # @mr-quin/dango-manifests
2
2
 
3
- Built-in dango manifests for the danmaku sources the extension ships with. Each manifest is a JSON file declaring the search/episodes/danmaku pipelines a [@mr-quin/dango](../dango/README.md) `ManifestRunner` interprets at runtime — no per-source TypeScript fetching code.
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` — consumers import each manifest JSON directly via the `./manifests/*` subpath export.
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.danmaku.weeblify.app/ddp/v1` (proxy) | Proxy holds DDP credentials; client carries none. |
12
- | `builtin:ddp-compat` | User-supplied `baseUrl` (configSchema) | Template for user-added `DanDanPlayCompatible` provider configs; auth headers pass through as inputs. |
13
- | `builtin:bilibili` | `api.bilibili.com` | Parallel media_bangumi + media_ft search; xml + protobuf danmaku variants. |
14
- | `builtin:tencent` | `pbaccess.video.qq.com` + `dm.video.qq.com` | POST bodies, page-cursor episode pagination via forEach `breakOn`, two-phase danmaku (segment index → per-segment fetch). |
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-dandanplay.json
22
- builtin-ddp-compat.json
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 backend
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`) goes through the backend proxy at `api.danmaku.weeblify.app/ddp/v1`. The proxy holds `X-AppId` / signing credentials via a Cloudflare service binding to the DDP microservice — clients carry no secret. The upstream path + query is embedded as the value of the `path` query param (e.g. `?path=/v2/search/anime?keyword=frieren`); the microservice splits that into the real upstream request.
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 the extension's `FetchLike` applies via `chrome.declarativeNetRequest`. The smoke script merges those headers in directly since Node fetch lets you set them.
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 backend. Not in CI; useful when adding a manifest or verifying after an upstream change.
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` | works |
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-compat` has no live smoke because there's no canonical self-hosted server to point at.
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 — don't share fixtures across manifests.
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 with the extension. They are not user-installable. User-installed manifests are a separate, future feature 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.
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 backend (manual, **not CI**) |
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.1.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
- "@mr-quin/dango": "0.1.0",
29
- "protobufjs": "^7.5.4"
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&region=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": "1.0.0",
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
- "protoSchemas": {
30
- "bili": "syntax = \"proto3\"; package dm.v1; message DanmakuElem { int64 id = 1; int32 progress = 2; int32 mode = 3; int32 fontsize = 4; uint32 color = 5; string midHash = 6; string content = 7; int64 ctime = 8; int32 weight = 9; string action = 10; int32 pool = 11; string idStr = 12; } message DmSegMobileReply { repeated DanmakuElem elems = 1; }"
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(show_title, /^\\d+$/) ? $number(show_title) : (show_title and show_title != '' ? show_title : undefined), 'imageUrl': cover, 'alternativeTitle': share_copy and share_copy != '' ? [share_copy] : undefined }]"
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.show_title, /^\\d+$/) ? $number(picked.ep.show_title) : (picked.ep.show_title and picked.ep.show_title != '' ? picked.ep.show_title : undefined), 'imageUrl': picked.ep.cover, 'externalLink': picked.ep.link } : undefined }"
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": "1.0.0",
6
- "hosts": ["api.danmaku.weeblify.app"],
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": "'https://api.danmaku.weeblify.app/ddp/v1'",
28
- "query": "{ 'path': '/v2/search/anime?' & $sortedQueryString({ 'keyword': q }) }"
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": "'https://api.danmaku.weeblify.app/ddp/v1'",
43
- "query": "{ 'path': '/v2/bangumi/' & $string(bangumiId) }"
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": "'https://api.danmaku.weeblify.app/ddp/v1'",
58
- "query": "{ 'path': '/v2/comment/' & $string(episodeId) & '?' & $sortedQueryString({ 'withRelated': 'true', 'chConvert': chConvert ? $string(chConvert) : undefined }) }"
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": "1.0.0",
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': $string(serialNo) }])"
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
  }