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.
Files changed (46) hide show
  1. package/README.md +132 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +691 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +45 -0
  6. package/sites/36kr/newsflash.js +76 -0
  7. package/sites/baidu/search.js +62 -0
  8. package/sites/bilibili/comments.js +74 -0
  9. package/sites/bilibili/feed.js +79 -0
  10. package/sites/bilibili/history.js +43 -0
  11. package/sites/bilibili/me.js +45 -0
  12. package/sites/bilibili/popular.js +44 -0
  13. package/sites/bilibili/ranking.js +42 -0
  14. package/sites/bilibili/search.js +46 -0
  15. package/sites/bilibili/trending.js +32 -0
  16. package/sites/bilibili/video.js +73 -0
  17. package/sites/bing/search.js +40 -0
  18. package/sites/boss/detail.js +38 -0
  19. package/sites/boss/search.js +44 -0
  20. package/sites/cnblogs/search.js +68 -0
  21. package/sites/csdn/search.js +51 -0
  22. package/sites/douban/comments.js +58 -0
  23. package/sites/douban/movie-hot.js +64 -0
  24. package/sites/douban/movie-top.js +65 -0
  25. package/sites/douban/movie.js +117 -0
  26. package/sites/douban/search.js +90 -0
  27. package/sites/douban/top250.js +73 -0
  28. package/sites/github/fork.js +38 -0
  29. package/sites/github/issue-create.js +42 -0
  30. package/sites/github/issues.js +32 -0
  31. package/sites/github/me.js +22 -0
  32. package/sites/github/pr-create.js +55 -0
  33. package/sites/github/repo.js +27 -0
  34. package/sites/google/search.js +54 -0
  35. package/sites/toutiao/hot.js +107 -0
  36. package/sites/toutiao/search.js +146 -0
  37. package/sites/xiaohongshu/comments.js +56 -0
  38. package/sites/xiaohongshu/feed.js +50 -0
  39. package/sites/xiaohongshu/me.js +49 -0
  40. package/sites/xiaohongshu/note.js +63 -0
  41. package/sites/xiaohongshu/search.js +56 -0
  42. package/sites/xiaohongshu/user_posts.js +53 -0
  43. package/sites/zhihu/hot.js +36 -0
  44. package/sites/zhihu/me.js +43 -0
  45. package/sites/zhihu/question.js +62 -0
  46. 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(/&nbsp;/g, ' ')
29
+ .replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/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
+ }