browser-web-search 0.1.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 +132 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +691 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
- package/sites/36kr/newsflash.js +76 -0
- package/sites/baidu/search.js +62 -0
- package/sites/bilibili/comments.js +74 -0
- package/sites/bilibili/feed.js +79 -0
- package/sites/bilibili/history.js +43 -0
- package/sites/bilibili/me.js +45 -0
- package/sites/bilibili/popular.js +44 -0
- package/sites/bilibili/ranking.js +42 -0
- package/sites/bilibili/search.js +46 -0
- package/sites/bilibili/trending.js +32 -0
- package/sites/bilibili/video.js +73 -0
- package/sites/bing/search.js +40 -0
- package/sites/boss/detail.js +38 -0
- package/sites/boss/search.js +44 -0
- package/sites/cnblogs/search.js +68 -0
- package/sites/csdn/search.js +51 -0
- package/sites/douban/comments.js +58 -0
- package/sites/douban/movie-hot.js +64 -0
- package/sites/douban/movie-top.js +65 -0
- package/sites/douban/movie.js +117 -0
- package/sites/douban/search.js +90 -0
- package/sites/douban/top250.js +73 -0
- package/sites/github/fork.js +38 -0
- package/sites/github/issue-create.js +42 -0
- package/sites/github/issues.js +32 -0
- package/sites/github/me.js +22 -0
- package/sites/github/pr-create.js +55 -0
- package/sites/github/repo.js +27 -0
- package/sites/google/search.js +54 -0
- package/sites/toutiao/hot.js +107 -0
- package/sites/toutiao/search.js +146 -0
- package/sites/xiaohongshu/comments.js +56 -0
- package/sites/xiaohongshu/feed.js +50 -0
- package/sites/xiaohongshu/me.js +49 -0
- package/sites/xiaohongshu/note.js +63 -0
- package/sites/xiaohongshu/search.js +56 -0
- package/sites/xiaohongshu/user_posts.js +53 -0
- package/sites/zhihu/hot.js +36 -0
- package/sites/zhihu/me.js +43 -0
- package/sites/zhihu/question.js +62 -0
- package/sites/zhihu/search.js +58 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "bilibili/search",
|
|
4
|
+
"description": "Search Bilibili videos by keyword",
|
|
5
|
+
"domain": "www.bilibili.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"keyword": {"required": true, "description": "Search keyword"},
|
|
8
|
+
"page": {"required": false, "description": "Page number (default: 1)"},
|
|
9
|
+
"count": {"required": false, "description": "Results per page (default: 20, max: 50)"},
|
|
10
|
+
"order": {"required": false, "description": "Sort order: totalrank (default), click (views), pubdate (newest), dm (danmaku), stow (favorites)"}
|
|
11
|
+
},
|
|
12
|
+
"capabilities": ["network"],
|
|
13
|
+
"readOnly": true,
|
|
14
|
+
"example": "bb-browser site bilibili/search 编程"
|
|
15
|
+
}
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
async function(args) {
|
|
19
|
+
if (!args.keyword) return {error: 'Missing argument: keyword'};
|
|
20
|
+
const page = parseInt(args.page) || 1;
|
|
21
|
+
const ps = Math.min(parseInt(args.count) || 20, 50);
|
|
22
|
+
const order = args.order || 'totalrank';
|
|
23
|
+
const params = new URLSearchParams({search_type: 'video', keyword: args.keyword, page: String(page), page_size: String(ps), order});
|
|
24
|
+
const resp = await fetch('https://api.bilibili.com/x/web-interface/wbi/search/type?' + params, {credentials: 'include'});
|
|
25
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
26
|
+
const d = await resp.json();
|
|
27
|
+
if (d.code !== 0) return {error: d.message || 'API error ' + d.code, hint: 'Not logged in?'};
|
|
28
|
+
const stripHtml = s => (s || '').replace(/<[^>]*>/g, '');
|
|
29
|
+
const videos = (d.data?.result || []).map(r => ({
|
|
30
|
+
bvid: r.bvid,
|
|
31
|
+
title: stripHtml(r.title),
|
|
32
|
+
author: r.author,
|
|
33
|
+
author_mid: r.mid,
|
|
34
|
+
description: stripHtml(r.description).substring(0, 200),
|
|
35
|
+
duration: r.duration,
|
|
36
|
+
play: r.play,
|
|
37
|
+
danmaku: r.danmaku,
|
|
38
|
+
like: r.like,
|
|
39
|
+
favorites: r.favorites,
|
|
40
|
+
cover: r.pic?.startsWith('//') ? 'https:' + r.pic : r.pic,
|
|
41
|
+
pub_date: r.pubdate ? new Date(r.pubdate * 1000).toISOString() : null,
|
|
42
|
+
tags: r.tag || '',
|
|
43
|
+
url: 'https://www.bilibili.com/video/' + r.bvid
|
|
44
|
+
}));
|
|
45
|
+
return {keyword: args.keyword, page, total: d.data?.numResults || 0, count: videos.length, videos};
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "bilibili/trending",
|
|
4
|
+
"description": "Get Bilibili trending search keywords (hot searches)",
|
|
5
|
+
"domain": "www.bilibili.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"count": {"required": false, "description": "Number of trending items (default: 20, max: 50)"}
|
|
8
|
+
},
|
|
9
|
+
"capabilities": ["network"],
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bb-browser site bilibili/trending"
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
const count = Math.min(parseInt(args.count) || 20, 50);
|
|
17
|
+
const resp = await fetch('https://api.bilibili.com/x/web-interface/wbi/search/square?limit=' + count, {credentials: 'include'});
|
|
18
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
19
|
+
const d = await resp.json();
|
|
20
|
+
if (d.code !== 0) return {error: d.message || 'API error ' + d.code, hint: 'Not logged in?'};
|
|
21
|
+
|
|
22
|
+
const items = (d.data?.trending?.list || []).slice(0, count).map((item, i) => ({
|
|
23
|
+
rank: i + 1,
|
|
24
|
+
keyword: item.keyword,
|
|
25
|
+
show_name: item.show_name,
|
|
26
|
+
is_hot: !!item.icon,
|
|
27
|
+
icon: item.icon || null,
|
|
28
|
+
search_url: 'https://search.bilibili.com/all?keyword=' + encodeURIComponent(item.keyword)
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
return {count: items.length, items};
|
|
32
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "bilibili/video",
|
|
4
|
+
"description": "Get Bilibili video details by bvid",
|
|
5
|
+
"domain": "www.bilibili.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"bvid": {"required": true, "description": "Video BV ID (e.g. BV1xx411c7mD)"}
|
|
8
|
+
},
|
|
9
|
+
"capabilities": ["network"],
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bb-browser site bilibili/video BV1LGwHzrE4A"
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
const bvid = args.bvid || args._positional?.[0];
|
|
17
|
+
if (!bvid) return {error: 'Missing argument: bvid'};
|
|
18
|
+
const resp = await fetch('https://api.bilibili.com/x/web-interface/view?bvid=' + encodeURIComponent(bvid), {credentials: 'include'});
|
|
19
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
20
|
+
const d = await resp.json();
|
|
21
|
+
if (d.code !== 0) return {error: d.message || 'API error ' + d.code, hint: d.code === -404 ? 'Video not found' : 'Not logged in?'};
|
|
22
|
+
const v = d.data;
|
|
23
|
+
const result = {
|
|
24
|
+
bvid: v.bvid,
|
|
25
|
+
aid: v.aid,
|
|
26
|
+
title: v.title,
|
|
27
|
+
description: v.desc,
|
|
28
|
+
cover: v.pic,
|
|
29
|
+
duration: v.duration,
|
|
30
|
+
duration_text: Math.floor(v.duration / 60) + ':' + String(v.duration % 60).padStart(2, '0'),
|
|
31
|
+
author: v.owner?.name,
|
|
32
|
+
author_mid: v.owner?.mid,
|
|
33
|
+
author_face: v.owner?.face,
|
|
34
|
+
category: v.tname,
|
|
35
|
+
tags: v.tag || null,
|
|
36
|
+
pub_date: v.pubdate ? new Date(v.pubdate * 1000).toISOString() : null,
|
|
37
|
+
stat: {
|
|
38
|
+
view: v.stat?.view,
|
|
39
|
+
like: v.stat?.like,
|
|
40
|
+
dislike: v.stat?.dislike,
|
|
41
|
+
coin: v.stat?.coin,
|
|
42
|
+
favorite: v.stat?.favorite,
|
|
43
|
+
share: v.stat?.share,
|
|
44
|
+
reply: v.stat?.reply,
|
|
45
|
+
danmaku: v.stat?.danmaku
|
|
46
|
+
},
|
|
47
|
+
pages: (v.pages || []).map(p => ({
|
|
48
|
+
page: p.page,
|
|
49
|
+
cid: p.cid,
|
|
50
|
+
title: p.part,
|
|
51
|
+
duration: p.duration
|
|
52
|
+
})),
|
|
53
|
+
url: 'https://www.bilibili.com/video/' + v.bvid
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Also fetch related videos
|
|
57
|
+
try {
|
|
58
|
+
const resp2 = await fetch('https://api.bilibili.com/x/web-interface/archive/related?bvid=' + encodeURIComponent(bvid), {credentials: 'include'});
|
|
59
|
+
const d2 = await resp2.json();
|
|
60
|
+
if (d2.code === 0 && d2.data) {
|
|
61
|
+
result.related = d2.data.slice(0, 5).map(r => ({
|
|
62
|
+
bvid: r.bvid,
|
|
63
|
+
title: r.title,
|
|
64
|
+
author: r.owner?.name,
|
|
65
|
+
view: r.stat?.view,
|
|
66
|
+
duration: r.duration,
|
|
67
|
+
url: 'https://www.bilibili.com/video/' + r.bvid
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
} catch(e) {}
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "bing/search",
|
|
4
|
+
"description": "Bing 搜索",
|
|
5
|
+
"domain": "www.bing.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"query": {"required": true, "description": "Search query"},
|
|
8
|
+
"count": {"required": false, "description": "Number of results (default 10)"}
|
|
9
|
+
},
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bb-browser site bing/search \"Claude Code\""
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
const query = args.query;
|
|
17
|
+
if (!query) return {error: 'query is required'};
|
|
18
|
+
const count = args.count || 10;
|
|
19
|
+
|
|
20
|
+
const url = 'https://www.bing.com/search?q=' + encodeURIComponent(query) + '&count=' + count;
|
|
21
|
+
const resp = await fetch(url, {credentials: 'include'});
|
|
22
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status};
|
|
23
|
+
|
|
24
|
+
const html = await resp.text();
|
|
25
|
+
const parser = new DOMParser();
|
|
26
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
27
|
+
|
|
28
|
+
const items = doc.querySelectorAll('li.b_algo');
|
|
29
|
+
const results = [];
|
|
30
|
+
items.forEach(li => {
|
|
31
|
+
const anchor = li.querySelector('h2 > a');
|
|
32
|
+
if (!anchor) return;
|
|
33
|
+
const title = anchor.textContent.trim();
|
|
34
|
+
const href = anchor.getAttribute('href') || '';
|
|
35
|
+
const snippet = (li.querySelector('p') || {}).textContent || '';
|
|
36
|
+
results.push({title, url: href, snippet: snippet.trim()});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {query, count: results.length, results};
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "boss/detail",
|
|
4
|
+
"description": "获取 BOSS直聘职位详情(JD、公司信息)",
|
|
5
|
+
"domain": "www.zhipin.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"securityId": {"required": true, "description": "Job securityId (from boss/search results)"}
|
|
8
|
+
},
|
|
9
|
+
"readOnly": true,
|
|
10
|
+
"example": "bb-browser site boss/detail <securityId>"
|
|
11
|
+
}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
async function(args) {
|
|
15
|
+
if (!args.securityId) return {error: 'Missing argument: securityId', hint: 'Run boss/search first to get securityId'};
|
|
16
|
+
const resp = await fetch('/wapi/zpgeek/job/detail.json?securityId=' + encodeURIComponent(args.securityId), {credentials: 'include'});
|
|
17
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status};
|
|
18
|
+
const d = await resp.json();
|
|
19
|
+
if (d.code !== 0) return {error: d.message || 'API error', code: d.code};
|
|
20
|
+
const job = d.zpData?.jobInfo || {};
|
|
21
|
+
const brand = d.zpData?.brandComInfo || {};
|
|
22
|
+
const boss = d.zpData?.bossInfo || {};
|
|
23
|
+
return {
|
|
24
|
+
job: {
|
|
25
|
+
name: job.jobName, salary: job.salaryDesc, experience: job.experienceName,
|
|
26
|
+
degree: job.degreeName, location: job.locationName, address: job.address,
|
|
27
|
+
skills: job.showSkills, description: job.postDescription, status: job.jobStatusDesc,
|
|
28
|
+
url: job.encryptId ? `https://www.zhipin.com/job_detail/${job.encryptId}.html` : undefined
|
|
29
|
+
},
|
|
30
|
+
company: {
|
|
31
|
+
name: brand.brandName, stage: brand.stageName, scale: brand.scaleName,
|
|
32
|
+
industry: brand.industryName, intro: brand.introduce
|
|
33
|
+
},
|
|
34
|
+
boss: {
|
|
35
|
+
name: boss.name, title: boss.title
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "boss/search",
|
|
4
|
+
"description": "BOSS直聘搜索职位",
|
|
5
|
+
"domain": "www.zhipin.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"query": {"required": true, "description": "Search keyword (e.g. AI agent, 前端)"},
|
|
8
|
+
"city": {"required": false, "description": "City code (default 101010100=北京, 101020100=上海, 101280100=广州, 101210100=杭州, 101280600=深圳)"},
|
|
9
|
+
"page": {"required": false, "description": "Page number (default 1)"},
|
|
10
|
+
"experience": {"required": false, "description": "Experience filter (e.g. 101=在校, 102=应届, 103=1年以内, 104=1-3年, 105=3-5年, 106=5-10年, 107=10年以上)"},
|
|
11
|
+
"degree": {"required": false, "description": "Degree filter (e.g. 209=高中, 208=大专, 206=本科, 203=硕士, 201=博士)"}
|
|
12
|
+
},
|
|
13
|
+
"readOnly": true,
|
|
14
|
+
"example": "bb-browser site boss/search \"AI agent\""
|
|
15
|
+
}
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
async function(args) {
|
|
19
|
+
if (!args.query) return {error: 'Missing argument: query', hint: 'Provide a job search keyword'};
|
|
20
|
+
const city = args.city || '101010100';
|
|
21
|
+
const page = parseInt(args.page) || 1;
|
|
22
|
+
const params = new URLSearchParams({
|
|
23
|
+
scene: '1', query: args.query, city, page: String(page), pageSize: '15',
|
|
24
|
+
experience: args.experience || '', degree: args.degree || '',
|
|
25
|
+
payType: '', partTime: '', industry: '', scale: '', stage: '',
|
|
26
|
+
position: '', jobType: '', salary: '', multiBusinessDistrict: '', multiSubway: ''
|
|
27
|
+
});
|
|
28
|
+
const resp = await fetch('/wapi/zpgeek/search/joblist.json?' + params.toString(), {credentials: 'include'});
|
|
29
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status};
|
|
30
|
+
const d = await resp.json();
|
|
31
|
+
if (d.code !== 0) return {error: d.message || 'API error', code: d.code};
|
|
32
|
+
const zpData = d.zpData || {};
|
|
33
|
+
const jobs = (zpData.jobList || []).map(j => ({
|
|
34
|
+
name: j.jobName, salary: j.salaryDesc, company: j.brandName,
|
|
35
|
+
city: j.cityName, area: j.areaDistrict, district: j.businessDistrict,
|
|
36
|
+
experience: j.jobExperience, degree: j.jobDegree,
|
|
37
|
+
skills: j.skills, welfare: j.welfareList,
|
|
38
|
+
boss: j.bossName, bossTitle: j.bossTitle, bossOnline: j.bossOnline,
|
|
39
|
+
industry: j.brandIndustry, scale: j.brandScaleName, stage: j.brandStageName,
|
|
40
|
+
jobId: j.encryptJobId, securityId: j.securityId,
|
|
41
|
+
url: j.encryptJobId ? `https://www.zhipin.com/job_detail/${j.encryptJobId}.html` : undefined
|
|
42
|
+
}));
|
|
43
|
+
return {query: args.query, city, page, total: zpData.totalCount, count: jobs.length, jobs};
|
|
44
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "cnblogs/search",
|
|
4
|
+
"description": "博客园技术文章搜索",
|
|
5
|
+
"domain": "zzk.cnblogs.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"query": {"required": true, "description": "Search query"},
|
|
8
|
+
"page": {"required": false, "description": "Page number (default 1)"}
|
|
9
|
+
},
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bb-browser site cnblogs/search \"Python\""
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
const query = args.query;
|
|
17
|
+
if (!query) return {error: 'query is required'};
|
|
18
|
+
const page = args.page || 1;
|
|
19
|
+
|
|
20
|
+
const url = 'https://zzk.cnblogs.com/s?w=' + encodeURIComponent(query) + '&p=' + page;
|
|
21
|
+
const resp = await fetch(url, {credentials: 'include'});
|
|
22
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status};
|
|
23
|
+
|
|
24
|
+
const html = await resp.text();
|
|
25
|
+
const parser = new DOMParser();
|
|
26
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
27
|
+
|
|
28
|
+
const items = doc.querySelectorAll('.searchItem');
|
|
29
|
+
const results = [];
|
|
30
|
+
|
|
31
|
+
items.forEach(item => {
|
|
32
|
+
const titleEl = item.querySelector('.searchItemTitle a');
|
|
33
|
+
if (!titleEl) return;
|
|
34
|
+
|
|
35
|
+
const title = (titleEl.textContent || '').trim();
|
|
36
|
+
if (!title) return;
|
|
37
|
+
|
|
38
|
+
const href = titleEl.getAttribute('href') || '';
|
|
39
|
+
|
|
40
|
+
const authorEl = item.querySelector('.searchItemInfo-userName a');
|
|
41
|
+
const author = authorEl ? (authorEl.textContent || '').trim() : '';
|
|
42
|
+
|
|
43
|
+
const snippetEl = item.querySelector('.searchCon');
|
|
44
|
+
const snippet = snippetEl ? (snippetEl.textContent || '').trim() : '';
|
|
45
|
+
|
|
46
|
+
const dateEl = item.querySelector('.searchItemInfo-publishDate');
|
|
47
|
+
const date = dateEl ? (dateEl.textContent || '').trim() : '';
|
|
48
|
+
|
|
49
|
+
const viewEl = item.querySelector('.searchItemInfo-views');
|
|
50
|
+
const views = viewEl ? (viewEl.textContent || '').trim() : '';
|
|
51
|
+
|
|
52
|
+
results.push({
|
|
53
|
+
title: title,
|
|
54
|
+
url: href,
|
|
55
|
+
author: author,
|
|
56
|
+
snippet: snippet.substring(0, 300),
|
|
57
|
+
date: date,
|
|
58
|
+
views: views
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
query: query,
|
|
64
|
+
page: page,
|
|
65
|
+
count: results.length,
|
|
66
|
+
results: results
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "csdn/search",
|
|
4
|
+
"description": "CSDN 技术文章搜索",
|
|
5
|
+
"domain": "so.csdn.net",
|
|
6
|
+
"args": {
|
|
7
|
+
"query": {"required": true, "description": "Search query"},
|
|
8
|
+
"page": {"required": false, "description": "Page number (default 1)"}
|
|
9
|
+
},
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bb-browser site csdn/search \"Python\""
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
if (!args.query) return {error: 'Missing argument: query'};
|
|
17
|
+
const page = parseInt(args.page) || 1;
|
|
18
|
+
|
|
19
|
+
const url = 'https://so.csdn.net/api/v3/search?q=' + encodeURIComponent(args.query)
|
|
20
|
+
+ '&t=all&p=' + page
|
|
21
|
+
+ '&s=0&tm=0&lv=-1&ft=0&l=&u=&ct=-1&pnt=-1&ry=-1&ss=-1&dct=-1&vco=-1&cc=-1&sc=-1&ald=-1&ep=&wp=0';
|
|
22
|
+
|
|
23
|
+
const resp = await fetch(url, {credentials: 'include'});
|
|
24
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Make sure so.csdn.net is accessible'};
|
|
25
|
+
|
|
26
|
+
const d = await resp.json();
|
|
27
|
+
|
|
28
|
+
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ')
|
|
29
|
+
.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
30
|
+
|
|
31
|
+
const results = (d.result_vos || []).map((item, i) => ({
|
|
32
|
+
rank: (page - 1) * 20 + i + 1,
|
|
33
|
+
type: item.type,
|
|
34
|
+
title: strip(item.title || ''),
|
|
35
|
+
url: item.url || '',
|
|
36
|
+
description: strip(item.description || item.body || '').substring(0, 300),
|
|
37
|
+
author: item.nickname || item.author || '',
|
|
38
|
+
views: parseInt(item.view) || 0,
|
|
39
|
+
likes: parseInt(item.digg) || 0,
|
|
40
|
+
collections: parseInt(item.collections) || 0,
|
|
41
|
+
created: item.create_time ? new Date(parseInt(item.create_time)).toISOString() : null
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
query: args.query,
|
|
46
|
+
page: page,
|
|
47
|
+
total: d.total || 0,
|
|
48
|
+
count: results.length,
|
|
49
|
+
results: results
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "douban/comments",
|
|
4
|
+
"description": "Get short reviews/comments for a Douban movie or TV show",
|
|
5
|
+
"domain": "movie.douban.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"id": {"required": true, "description": "Douban subject ID (e.g. 1292052)"},
|
|
8
|
+
"sort": {"required": false, "description": "Sort order: new_score (default, hot), time (newest first)"},
|
|
9
|
+
"count": {"required": false, "description": "Number of comments (default: 20, max: 50)"}
|
|
10
|
+
},
|
|
11
|
+
"capabilities": ["network"],
|
|
12
|
+
"readOnly": true,
|
|
13
|
+
"example": "bb-browser site douban/comments 1292052"
|
|
14
|
+
}
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
async function(args) {
|
|
18
|
+
if (!args.id) return {error: 'Missing argument: id'};
|
|
19
|
+
const id = String(args.id).trim();
|
|
20
|
+
const sort = args.sort || 'new_score';
|
|
21
|
+
const count = Math.min(parseInt(args.count) || 20, 50);
|
|
22
|
+
|
|
23
|
+
if (sort !== 'new_score' && sort !== 'time') {
|
|
24
|
+
return {error: 'Invalid sort. Use "new_score" (hot) or "time" (newest)'};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const url = 'https://movie.douban.com/j/subject/' + id + '/comments?start=0&limit=' + count + '&status=P&sort=' + sort;
|
|
28
|
+
|
|
29
|
+
const resp = await fetch(url, {credentials: 'include'});
|
|
30
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
31
|
+
const d = await resp.json();
|
|
32
|
+
|
|
33
|
+
if (d.retcode !== 1 || !d.result) return {error: 'Failed to fetch comments', hint: 'Invalid ID or not logged in?'};
|
|
34
|
+
|
|
35
|
+
const ratingMap = {'1': '很差', '2': '较差', '3': '还行', '4': '推荐', '5': '力荐'};
|
|
36
|
+
|
|
37
|
+
const comments = (d.result.normal || []).map(function(c) {
|
|
38
|
+
var userId = c.user?.path?.match(/people\/([^/]+)/)?.[1];
|
|
39
|
+
return {
|
|
40
|
+
id: c.id,
|
|
41
|
+
author: c.user?.name || '',
|
|
42
|
+
author_id: userId || '',
|
|
43
|
+
rating: c.rating ? parseInt(c.rating) : null,
|
|
44
|
+
rating_label: c.rating_word || ratingMap[c.rating] || '',
|
|
45
|
+
content: c.content || '',
|
|
46
|
+
votes: c.votes || 0,
|
|
47
|
+
date: c.time || ''
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
subject_id: id,
|
|
53
|
+
sort: sort,
|
|
54
|
+
total: d.result.total_num || 0,
|
|
55
|
+
count: comments.length,
|
|
56
|
+
comments: comments
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "douban/movie-hot",
|
|
4
|
+
"description": "Get hot/trending movies or TV shows on Douban by tag",
|
|
5
|
+
"domain": "movie.douban.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"type": {"required": false, "description": "Type: movie (default) or tv"},
|
|
8
|
+
"tag": {"required": false, "description": "Tag filter (default: 热门). Movies: 热门/最新/豆瓣高分/冷门佳片/华语/欧美/韩国/日本. TV: 热门/国产剧/综艺/美剧/日剧/韩剧/日本动画/纪录片"},
|
|
9
|
+
"count": {"required": false, "description": "Number of results (default: 20, max: 50)"}
|
|
10
|
+
},
|
|
11
|
+
"capabilities": ["network"],
|
|
12
|
+
"readOnly": true,
|
|
13
|
+
"example": "bb-browser site douban/movie-hot movie 豆瓣高分"
|
|
14
|
+
}
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
async function(args) {
|
|
18
|
+
const type = (args.type || 'movie').toLowerCase();
|
|
19
|
+
if (type !== 'movie' && type !== 'tv') return {error: 'Invalid type. Use "movie" or "tv"'};
|
|
20
|
+
|
|
21
|
+
const tag = args.tag || '热门';
|
|
22
|
+
const count = Math.min(parseInt(args.count) || 20, 50);
|
|
23
|
+
|
|
24
|
+
const url = 'https://movie.douban.com/j/search_subjects?type=' + type
|
|
25
|
+
+ '&tag=' + encodeURIComponent(tag)
|
|
26
|
+
+ '&page_limit=' + count
|
|
27
|
+
+ '&page_start=0';
|
|
28
|
+
|
|
29
|
+
const resp = await fetch(url, {credentials: 'include'});
|
|
30
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
31
|
+
const d = await resp.json();
|
|
32
|
+
|
|
33
|
+
if (!d.subjects) return {error: 'No data returned', hint: 'Invalid tag or not logged in?'};
|
|
34
|
+
|
|
35
|
+
const items = d.subjects.map(function(s, i) {
|
|
36
|
+
return {
|
|
37
|
+
rank: i + 1,
|
|
38
|
+
id: s.id,
|
|
39
|
+
title: s.title,
|
|
40
|
+
rating: s.rate ? parseFloat(s.rate) : null,
|
|
41
|
+
cover: s.cover,
|
|
42
|
+
url: s.url,
|
|
43
|
+
playable: s.playable,
|
|
44
|
+
is_new: s.is_new,
|
|
45
|
+
episodes_info: s.episodes_info || null
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Also fetch available tags for reference
|
|
50
|
+
var tagsResp = await fetch('https://movie.douban.com/j/search_tags?type=' + type + '&source=index', {credentials: 'include'});
|
|
51
|
+
var availableTags = [];
|
|
52
|
+
if (tagsResp.ok) {
|
|
53
|
+
var tagsData = await tagsResp.json();
|
|
54
|
+
availableTags = tagsData.tags || [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
type: type,
|
|
59
|
+
tag: tag,
|
|
60
|
+
count: items.length,
|
|
61
|
+
available_tags: availableTags,
|
|
62
|
+
items: items
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "douban/movie-top",
|
|
4
|
+
"description": "Get top rated movies by genre from Douban charts",
|
|
5
|
+
"domain": "movie.douban.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"genre": {"required": false, "description": "Genre (default: 剧情). Options: 剧情/喜剧/动作/爱情/科幻/动画/悬疑/惊悚/恐怖/纪录片/短片/情色/音乐/歌舞/家庭/儿童/传记/历史/战争/犯罪/西部/奇幻/冒险/灾难/武侠/古装/运动/黑色电影"},
|
|
8
|
+
"count": {"required": false, "description": "Number of results (default: 20, max: 50)"}
|
|
9
|
+
},
|
|
10
|
+
"capabilities": ["network"],
|
|
11
|
+
"readOnly": true,
|
|
12
|
+
"example": "bb-browser site douban/movie-top 科幻 10"
|
|
13
|
+
}
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
async function(args) {
|
|
17
|
+
// Genre name to type ID mapping
|
|
18
|
+
const genreMap = {
|
|
19
|
+
'剧情': 11, '喜剧': 24, '动作': 5, '爱情': 13, '科幻': 17,
|
|
20
|
+
'动画': 25, '悬疑': 10, '惊悚': 19, '恐怖': 20, '纪录片': 1,
|
|
21
|
+
'短片': 23, '情色': 6, '音乐': 14, '歌舞': 7, '家庭': 28,
|
|
22
|
+
'儿童': 8, '传记': 2, '历史': 4, '战争': 22, '犯罪': 3,
|
|
23
|
+
'西部': 27, '奇幻': 26, '冒险': 15, '灾难': 12, '武侠': 29,
|
|
24
|
+
'古装': 30, '运动': 18, '黑色电影': 31
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const genre = args.genre || '剧情';
|
|
28
|
+
const typeId = genreMap[genre];
|
|
29
|
+
if (!typeId) return {error: 'Unknown genre: ' + genre, hint: 'Available: ' + Object.keys(genreMap).join(', ')};
|
|
30
|
+
|
|
31
|
+
const count = Math.min(parseInt(args.count) || 20, 50);
|
|
32
|
+
|
|
33
|
+
const url = 'https://movie.douban.com/j/chart/top_list?type=' + typeId
|
|
34
|
+
+ '&interval_id=100%3A90&action=&start=0&limit=' + count;
|
|
35
|
+
|
|
36
|
+
const resp = await fetch(url, {credentials: 'include'});
|
|
37
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
38
|
+
const data = await resp.json();
|
|
39
|
+
|
|
40
|
+
if (!Array.isArray(data)) return {error: 'Unexpected response format'};
|
|
41
|
+
|
|
42
|
+
const items = data.map(function(s) {
|
|
43
|
+
return {
|
|
44
|
+
rank: s.rank,
|
|
45
|
+
id: s.id,
|
|
46
|
+
title: s.title,
|
|
47
|
+
rating: parseFloat(s.score) || null,
|
|
48
|
+
votes: s.vote_count,
|
|
49
|
+
types: s.types,
|
|
50
|
+
regions: s.regions,
|
|
51
|
+
actors: (s.actors || []).slice(0, 5),
|
|
52
|
+
release_date: s.release_date,
|
|
53
|
+
cover: s.cover_url,
|
|
54
|
+
url: s.url,
|
|
55
|
+
playable: s.is_playable
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
genre: genre,
|
|
61
|
+
count: items.length,
|
|
62
|
+
available_genres: Object.keys(genreMap),
|
|
63
|
+
items: items
|
|
64
|
+
};
|
|
65
|
+
}
|