Skip to content

Commit aa47de7

Browse files
harveyyuejackwener
andauthored
feat(bilibili): add feed-detail and enhance feed command (#974)
* feat(bilibili): add feed-detail and enhance feed command * docs: add binance adapter documentation * docs: add feed-detail command to bilibili docs * docs: sync bilibili adapter contract for feed-detail * docs: add ke adapter page for doc coverage --------- Co-authored-by: jackwener <jakevingoo@gmail.com>
1 parent 232b682 commit aa47de7

4 files changed

Lines changed: 262 additions & 50 deletions

File tree

clis/bilibili/feed.js

Lines changed: 202 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,218 @@
11
import { cli, Strategy } from '@jackwener/opencli/registry';
2-
import { apiGet, payloadData, stripHtml } from './utils.js';
2+
import { apiGet, payloadData, resolveUid, stripHtml } from './utils.js';
3+
4+
/** Map bilibili dynamic type to readable short name */
5+
const TYPE_MAP = {
6+
DYNAMIC_TYPE_AV: 'video',
7+
DYNAMIC_TYPE_DRAW: 'draw',
8+
DYNAMIC_TYPE_ARTICLE: 'article',
9+
DYNAMIC_TYPE_FORWARD: 'forward',
10+
DYNAMIC_TYPE_WORD: 'text',
11+
DYNAMIC_TYPE_LIVE_RCMD: 'live',
12+
DYNAMIC_TYPE_PGC: 'bangumi',
13+
};
14+
15+
function parseItem(item) {
16+
const modules = item.modules ?? {};
17+
const authorModule = modules.module_author ?? {};
18+
const dynamicModule = modules.module_dynamic ?? {};
19+
const major = dynamicModule.major ?? {};
20+
const stat = modules.module_stat ?? {};
21+
22+
let title = '';
23+
let url = item.id_str ? `https://t.bilibili.com/${item.id_str}` : '';
24+
const itemType = TYPE_MAP[item.type] ?? item.type ?? '';
25+
26+
// video
27+
if (major.archive) {
28+
title = major.archive.title ?? '';
29+
url = major.archive.jump_url ? `https:${major.archive.jump_url}` : url;
30+
}
31+
// article
32+
if (!title && major.article) {
33+
title = major.article.title ?? '';
34+
url = major.article.jump_url ? `https:${major.article.jump_url}` : url;
35+
}
36+
// text content in desc
37+
if (!title && dynamicModule.desc?.text) {
38+
title = stripHtml(dynamicModule.desc.text).slice(0, 60);
39+
}
40+
// draw (图文) — use opus or draw items count as hint
41+
if (!title && major.draw) {
42+
const imgCount = major.draw.items?.length ?? 0;
43+
title = imgCount > 0 ? `[图片x${imgCount}]` : '[图文动态]';
44+
}
45+
// VIP only content
46+
if (!title && item.basic?.is_only_fans) {
47+
title = '[充电专属]';
48+
}
49+
// forward
50+
if (!title && item.type === 'DYNAMIC_TYPE_FORWARD') {
51+
title = '[转发动态]';
52+
}
53+
// final fallback
54+
if (!title) {
55+
title = `[${itemType || '动态'}]`;
56+
}
57+
58+
const time = authorModule.pub_time ?? '';
59+
const likes = stat.like?.count ?? 0;
60+
const comments = stat.comment?.count ?? 0;
61+
62+
return { title, url, itemType, author: authorModule.name ?? '', time, likes, comments };
63+
}
64+
365
cli({
466
site: 'bilibili',
567
name: 'feed',
6-
description: '关注的人的动态时间线',
68+
description: '动态时间线(不传 uid 查关注时间线,传 uid 查指定用户动态)',
769
domain: 'www.bilibili.com',
870
strategy: Strategy.COOKIE,
971
args: [
10-
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
11-
{ name: 'type', default: 'all', help: 'Filter: all, video, article' },
72+
{ name: 'uid', positional: true, required: false, help: '用户 UID 或用户名(不传则显示关注时间线)' },
73+
{ name: 'limit', type: 'int', default: 20, help: 'Max results to return' },
74+
{ name: 'type', default: 'all', help: 'Filter: all, video, article, draw, text' },
75+
{ name: 'pages', type: 'int', default: 1, help: 'Number of pages to fetch (each ~20 items)' },
1276
],
13-
columns: ['rank', 'author', 'title', 'type', 'url'],
77+
columns: ['rank', 'time', 'author', 'title', 'type', 'likes', 'url'],
1478
func: async (page, kwargs) => {
15-
const { limit = 20, type = 'all' } = kwargs;
16-
const typeMap = { all: 'all', video: 'video', article: 'article' };
17-
const updateBaseline = '';
18-
const payload = await apiGet(page, '/x/polymer/web-dynamic/v1/feed/all', {
19-
params: {
20-
timezone_offset: -480,
21-
type: typeMap[type] ?? 'all',
22-
page: 1,
23-
...(updateBaseline ? { update_baseline: updateBaseline } : {}),
24-
},
25-
});
26-
const items = payloadData(payload)?.items ?? [];
79+
const maxResults = Number(kwargs.limit) || 20;
80+
const maxPages = Number(kwargs.pages) || 1;
81+
const filterType = kwargs.type === 'all' ? '' : (kwargs.type ?? '');
82+
83+
const isUserFeed = !!kwargs.uid;
84+
const uid = isUserFeed ? await resolveUid(page, String(kwargs.uid)) : null;
85+
2786
const rows = [];
28-
for (let i = 0; i < Math.min(items.length, Number(limit)); i++) {
29-
const item = items[i];
30-
const modules = item.modules ?? {};
31-
const authorModule = modules.module_author ?? {};
32-
const dynamicModule = modules.module_dynamic ?? {};
33-
const major = dynamicModule.major ?? {};
34-
let title = '';
35-
let url = '';
36-
let itemType = item.type ?? '';
37-
if (major.archive) {
38-
title = major.archive.title ?? '';
39-
url = major.archive.jump_url ? `https:${major.archive.jump_url}` : '';
40-
itemType = 'video';
41-
}
42-
else if (major.article) {
43-
title = major.article.title ?? '';
44-
url = major.article.jump_url ? `https:${major.article.jump_url}` : '';
45-
itemType = 'article';
87+
let offset = '';
88+
89+
for (let p = 0; p < maxPages; p++) {
90+
if (rows.length >= maxResults) break;
91+
92+
let payload;
93+
if (isUserFeed) {
94+
const params = { host_mid: uid, timezone_offset: -480 };
95+
if (offset) params.offset = offset;
96+
payload = await apiGet(page, '/x/polymer/web-dynamic/v1/feed/space', { params });
97+
} else {
98+
const params = {
99+
timezone_offset: -480,
100+
type: filterType || 'all',
101+
page: p + 1,
102+
};
103+
if (offset) params.offset = offset;
104+
payload = await apiGet(page, '/x/polymer/web-dynamic/v1/feed/all', { params });
46105
}
47-
else if (dynamicModule.desc) {
48-
title = stripHtml(dynamicModule.desc.text ?? '').slice(0, 60);
49-
url = item.id_str ? `https://t.bilibili.com/${item.id_str}` : '';
50-
itemType = 'dynamic';
106+
107+
const data = payloadData(payload) ?? {};
108+
const items = data.items ?? [];
109+
if (items.length === 0) break;
110+
111+
for (const item of items) {
112+
if (rows.length >= maxResults) break;
113+
const parsed = parseItem(item);
114+
if (filterType && parsed.itemType !== filterType) continue;
115+
rows.push({
116+
rank: rows.length + 1,
117+
time: parsed.time,
118+
author: parsed.author,
119+
title: parsed.title,
120+
type: parsed.itemType,
121+
likes: parsed.likes,
122+
url: parsed.url,
123+
});
51124
}
52-
if (!title)
53-
continue;
54-
rows.push({
55-
rank: rows.length + 1,
56-
author: authorModule.name ?? '',
57-
title,
58-
type: itemType,
59-
url,
60-
});
125+
126+
offset = data.offset ?? items[items.length - 1]?.id_str ?? '';
127+
if (!offset || !data.has_more) break;
128+
}
129+
130+
return rows;
131+
},
132+
});
133+
134+
cli({
135+
site: 'bilibili',
136+
name: 'feed-detail',
137+
description: '查看 Bilibili 动态详情(支持充电专属内容)',
138+
domain: 'www.bilibili.com',
139+
strategy: Strategy.COOKIE,
140+
args: [
141+
{ name: 'id', positional: true, required: true, help: '动态 ID(从 feed 命令的 url 中获取)' },
142+
],
143+
columns: ['field', 'value'],
144+
func: async (page, kwargs) => {
145+
const id = String(kwargs.id);
146+
const payload = await apiGet(page, '/x/polymer/web-dynamic/v1/detail', {
147+
params: { id, timezone_offset: -480 },
148+
});
149+
150+
const rows = [];
151+
const data = payloadData(payload);
152+
const item = data?.item;
153+
if (!item) {
154+
rows.push({ field: 'error', value: '动态不存在或无权查看'});
155+
return rows;
156+
}
157+
158+
const modules = item.modules ?? {};
159+
const author = modules.module_author ?? {};
160+
const dynamicModule = modules.module_dynamic ?? {};
161+
const major = dynamicModule.major ?? {};
162+
const stat = modules.module_stat ?? {};
163+
164+
rows.push({ field: 'id', value: item.id_str ?? id });
165+
rows.push({ field: 'author', value: author.name ?? '' });
166+
rows.push({ field: 'time', value: author.pub_time ?? '' });
167+
rows.push({ field: 'type', value: TYPE_MAP[item.type] ?? item.type ?? '' });
168+
169+
// text content
170+
if (dynamicModule.desc?.text) {
171+
rows.push({ field: 'text', value: stripHtml(dynamicModule.desc.text) });
172+
}
173+
174+
// video
175+
if (major.archive) {
176+
rows.push({ field: 'video_title', value: major.archive.title ?? '' });
177+
rows.push({ field: 'video_desc', value: major.archive.desc ?? '' });
178+
rows.push({ field: 'video_url', value: major.archive.jump_url ? `https:${major.archive.jump_url}` : '' });
179+
rows.push({ field: 'play', value: String(major.archive.stat?.play ?? '') });
180+
rows.push({ field: 'danmaku', value: String(major.archive.stat?.danmaku ?? '') });
181+
}
182+
183+
// article
184+
if (major.article) {
185+
rows.push({ field: 'article_title', value: major.article.title ?? '' });
186+
rows.push({ field: 'article_url', value: major.article.jump_url ? `https:${major.article.jump_url}` : '' });
187+
}
188+
189+
// draw (images)
190+
if (major.draw?.items?.length) {
191+
rows.push({ field: 'images', value: major.draw.items.map((img) => img.src).join('\n') });
192+
}
193+
194+
// opus (rich text, some dynamics use this)
195+
if (major.opus?.summary?.text) {
196+
rows.push({ field: 'opus_text', value: stripHtml(major.opus.summary.text) });
197+
}
198+
if (major.opus?.title) {
199+
rows.push({ field: 'opus_title', value: major.opus.title });
200+
}
201+
202+
// forward - show original dynamic info
203+
if (item.orig) {
204+
const origAuthor = item.orig.modules?.module_author?.name ?? '';
205+
const origDesc = item.orig.modules?.module_dynamic?.desc?.text ?? '';
206+
rows.push({ field: 'forward_from', value: origAuthor });
207+
if (origDesc) rows.push({ field: 'forward_text', value: stripHtml(origDesc).slice(0, 200) });
61208
}
209+
210+
// stats
211+
rows.push({ field: 'likes', value: String(stat.like?.count ?? 0) });
212+
rows.push({ field: 'comments', value: String(stat.comment?.count ?? 0) });
213+
rows.push({ field: 'forwards', value: String(stat.forward?.count ?? 0) });
214+
rows.push({ field: 'url', value: `https://t.bilibili.com/${item.id_str ?? id}` });
215+
62216
return rows;
63217
},
64218
});

docs/adapters/browser/bilibili.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
| `opencli bilibili me` | |
1212
| `opencli bilibili favorite` | |
1313
| `opencli bilibili history` | |
14-
| `opencli bilibili feed` | |
14+
| `opencli bilibili feed` | Read the following feed, or a specific user's dynamics by uid/name |
15+
| `opencli bilibili feed-detail` | Read one dynamic in detail, including exclusive content |
1516
| `opencli bilibili subtitle` | |
1617
| `opencli bilibili dynamic` | |
1718
| `opencli bilibili ranking` | |
@@ -31,6 +32,18 @@ opencli bilibili search 黑神话 --limit 10
3132
# Read one creator's videos
3233
opencli bilibili user-videos 2 --limit 10
3334

35+
# Read following feed
36+
opencli bilibili feed --limit 10
37+
38+
# Read one user's dynamics by UID
39+
opencli bilibili feed 2 --limit 10
40+
41+
# Read one user's dynamics by username and paginate
42+
opencli bilibili feed 老番茄 --pages 2 --type video
43+
44+
# Read one dynamic in detail
45+
opencli bilibili feed-detail 1234567890123456789
46+
3447
# Fetch subtitles
3548
opencli bilibili subtitle BV1xx411c7mD --lang zh-CN
3649

@@ -45,3 +58,9 @@ opencli bilibili hot -v
4558

4659
- Chrome running and **logged into** bilibili.com
4760
- [Browser Bridge extension](/guide/browser-bridge) installed
61+
62+
## Notes
63+
64+
- `opencli bilibili feed` without `uid` reads your following feed
65+
- `opencli bilibili feed <uid-or-name>` reads a specific user's dynamics
66+
- `feed-detail` expects the dynamic ID from a `https://t.bilibili.com/<id>` URL

docs/adapters/browser/ke.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Ke
2+
3+
**Mode**: 🔐 Browser · **Domain**: `ke.com`
4+
5+
## Commands
6+
7+
| Command | Description |
8+
|---------|-------------|
9+
| `opencli ke ershoufang` | Browse second-hand housing listings |
10+
| `opencli ke zufang` | Browse rental listings |
11+
| `opencli ke xiaoqu` | Browse neighborhood / community listings |
12+
| `opencli ke chengjiao` | Browse recent transaction records |
13+
14+
## Usage Examples
15+
16+
```bash
17+
# Beijing second-hand housing
18+
opencli ke ershoufang --city bj --district chaoyang --limit 10
19+
20+
# Rentals in Shanghai
21+
opencli ke zufang --city sh --district pudong --max-price 8000 --limit 10
22+
23+
# Communities in Guangzhou
24+
opencli ke xiaoqu --city gz --district tianhe --limit 10
25+
26+
# Recent transactions in Beijing Haidian
27+
opencli ke chengjiao --city bj --district haidian --limit 10
28+
```
29+
30+
## Prerequisites
31+
32+
- Chrome running and logged into `ke.com`
33+
- [Browser Bridge extension](/guide/browser-bridge) installed
34+
35+
## Notes
36+
37+
- `city` uses short city codes such as `bj`, `sh`, `gz`, `sz`
38+
- `district` expects the district slug used in Beike URLs, for example `chaoyang` or `haidian`

docs/adapters/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Run `opencli list` for the live registry.
1010
| **[reddit](./browser/reddit.md)** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | 🔐 Browser |
1111
| **[tieba](./browser/tieba.md)** | `hot` `posts` `search` `read` | 🔐 Browser |
1212
| **[hupu](./browser/hupu.md)** | `hot` `search` `detail` `mentions` `reply` `like` `unlike` | 🌐 / 🔐 |
13-
| **[bilibili](./browser/bilibili.md)** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` `download` | 🔐 Browser |
13+
| **[bilibili](./browser/bilibili.md)** | `hot` `search` `me` `favorite` `history` `feed` `feed-detail` `subtitle` `dynamic` `ranking` `following` `user-videos` `download` | 🔐 Browser |
1414
| **[zhihu](./browser/zhihu.md)** | `hot` `search` `question` `download` `follow` `like` `favorite` `comment` `answer` | 🔐 Browser |
1515
| **[xiaohongshu](./browser/xiaohongshu.md)** | `search` `notifications` `feed` `user` `note` `comments` `download` `publish` `creator-notes` `creator-note-detail` `creator-notes-summary` `creator-profile` `creator-stats` | 🔐 Browser |
1616
| **[xiaoe](./browser/xiaoe.md)** | `courses` `detail` `catalog` `play-url` `content` | 🔐 Browser |
@@ -26,6 +26,7 @@ Run `opencli list` for the live registry.
2626
| **[reuters](./browser/reuters.md)** | `search` | 🔐 Browser |
2727
| **[smzdm](./browser/smzdm.md)** | `search` | 🔐 Browser |
2828
| **[jike](./browser/jike.md)** | `feed` `search` `post` `topic` `user` `create` `comment` `like` `repost` `notifications` | 🔐 Browser |
29+
| **[ke](./browser/ke.md)** | `ershoufang` `zufang` `xiaoqu` `chengjiao` | 🔐 Browser |
2930
| **[jimeng](./browser/jimeng.md)** | `generate` `history` | 🔐 Browser |
3031
| **[yollomi](./browser/yollomi.md)** | `generate` `video` `edit` `upload` `models` `remove-bg` `upscale` `face-swap` `restore` `try-on` `background` `object-remover` | 🔐 Browser |
3132
| **[linux-do](./browser/linux-do.md)** | `hot` `latest` `feed` `search` `categories` `category` `tags` `topic` `topic-content` `user-posts` `user-topics` | 🔐 Browser |

0 commit comments

Comments
 (0)