@jackwener/opencli 1.4.1 → 1.5.1
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/.github/workflows/build-extension.yml +2 -6
- package/.github/workflows/ci.yml +21 -1
- package/README.md +35 -6
- package/README.zh-CN.md +12 -5
- package/SKILL.md +2 -0
- package/dist/browser/cdp.d.ts +2 -1
- package/dist/browser/cdp.js +5 -0
- package/dist/browser/discover.d.ts +4 -1
- package/dist/browser/discover.js +6 -2
- package/dist/browser/errors.d.ts +2 -2
- package/dist/browser/errors.js +4 -12
- package/dist/browser/mcp.d.ts +2 -1
- package/dist/browser/page.d.ts +3 -0
- package/dist/browser/page.js +24 -1
- package/dist/build-manifest.d.ts +2 -0
- package/dist/build-manifest.js +39 -14
- package/dist/build-manifest.test.js +21 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +2 -1
- package/dist/cli-manifest.json +1567 -108
- package/dist/cli.js +68 -6
- package/dist/clis/36kr/article.d.ts +1 -0
- package/dist/clis/36kr/article.js +62 -0
- package/dist/clis/36kr/hot.d.ts +3 -0
- package/dist/clis/36kr/hot.js +80 -0
- package/dist/clis/36kr/hot.test.d.ts +1 -0
- package/dist/clis/36kr/hot.test.js +15 -0
- package/dist/clis/36kr/news.d.ts +1 -0
- package/dist/clis/36kr/news.js +51 -0
- package/dist/clis/36kr/news.test.d.ts +1 -0
- package/dist/clis/36kr/news.test.js +85 -0
- package/dist/clis/36kr/search.d.ts +1 -0
- package/dist/clis/36kr/search.js +72 -0
- package/dist/clis/bilibili/comments.d.ts +5 -0
- package/dist/clis/bilibili/comments.js +40 -0
- package/dist/clis/bilibili/comments.test.d.ts +1 -0
- package/dist/clis/bilibili/comments.test.js +82 -0
- package/dist/clis/bluesky/feeds.yaml +29 -0
- package/dist/clis/bluesky/followers.yaml +33 -0
- package/dist/clis/bluesky/following.yaml +33 -0
- package/dist/clis/bluesky/profile.yaml +27 -0
- package/dist/clis/bluesky/search.yaml +34 -0
- package/dist/clis/bluesky/starter-packs.yaml +34 -0
- package/dist/clis/bluesky/thread.yaml +32 -0
- package/dist/clis/bluesky/trending.yaml +27 -0
- package/dist/clis/bluesky/user.yaml +34 -0
- package/dist/clis/chatgpt/ask.js +29 -14
- package/dist/clis/chatgpt/ax.d.ts +6 -0
- package/dist/clis/chatgpt/ax.js +172 -1
- package/dist/clis/chatgpt/model.d.ts +1 -0
- package/dist/clis/chatgpt/model.js +24 -0
- package/dist/clis/chatgpt/send.js +12 -3
- package/dist/clis/douban/download.d.ts +1 -0
- package/dist/clis/douban/download.js +67 -0
- package/dist/clis/douban/download.test.d.ts +1 -0
- package/dist/clis/douban/download.test.js +170 -0
- package/dist/clis/douban/photos.d.ts +1 -0
- package/dist/clis/douban/photos.js +34 -0
- package/dist/clis/douban/utils.d.ts +25 -0
- package/dist/clis/douban/utils.js +190 -1
- package/dist/clis/douban/utils.test.d.ts +1 -0
- package/dist/clis/douban/utils.test.js +64 -0
- package/dist/clis/imdb/person.d.ts +1 -0
- package/dist/clis/imdb/person.js +203 -0
- package/dist/clis/imdb/reviews.d.ts +1 -0
- package/dist/clis/imdb/reviews.js +88 -0
- package/dist/clis/imdb/search.d.ts +1 -0
- package/dist/clis/imdb/search.js +161 -0
- package/dist/clis/imdb/title.d.ts +1 -0
- package/dist/clis/imdb/title.js +93 -0
- package/dist/clis/imdb/top.d.ts +1 -0
- package/dist/clis/imdb/top.js +53 -0
- package/dist/clis/imdb/trending.d.ts +1 -0
- package/dist/clis/imdb/trending.js +52 -0
- package/dist/clis/imdb/utils.d.ts +46 -0
- package/dist/clis/imdb/utils.js +285 -0
- package/dist/clis/imdb/utils.test.d.ts +1 -0
- package/dist/clis/imdb/utils.test.js +88 -0
- package/dist/clis/jd/item.d.ts +4 -0
- package/dist/clis/jd/item.js +16 -15
- package/dist/clis/jd/item.test.js +16 -1
- package/dist/clis/linux-do/categories.yaml +38 -9
- package/dist/clis/linux-do/category.d.ts +1 -0
- package/dist/clis/linux-do/category.js +36 -0
- package/dist/clis/linux-do/feed.d.ts +45 -0
- package/dist/clis/linux-do/feed.js +397 -0
- package/dist/clis/linux-do/feed.test.d.ts +1 -0
- package/dist/clis/linux-do/feed.test.js +118 -0
- package/dist/clis/linux-do/hot.d.ts +1 -0
- package/dist/clis/linux-do/hot.js +25 -0
- package/dist/clis/linux-do/latest.d.ts +1 -0
- package/dist/clis/linux-do/latest.js +18 -0
- package/dist/clis/linux-do/tags.yaml +41 -0
- package/dist/clis/linux-do/topic.yaml +41 -3
- package/dist/clis/linux-do/user-posts.yaml +67 -0
- package/dist/clis/linux-do/user-topics.yaml +54 -0
- package/dist/clis/paperreview/commands.test.d.ts +3 -0
- package/dist/clis/paperreview/commands.test.js +243 -0
- package/dist/clis/paperreview/feedback.d.ts +1 -0
- package/dist/clis/paperreview/feedback.js +52 -0
- package/dist/clis/paperreview/review.d.ts +1 -0
- package/dist/clis/paperreview/review.js +37 -0
- package/dist/clis/paperreview/submit.d.ts +1 -0
- package/dist/clis/paperreview/submit.js +85 -0
- package/dist/clis/paperreview/utils.d.ts +46 -0
- package/dist/clis/paperreview/utils.js +197 -0
- package/dist/clis/paperreview/utils.test.d.ts +1 -0
- package/dist/clis/paperreview/utils.test.js +49 -0
- package/dist/clis/producthunt/browse.d.ts +1 -0
- package/dist/clis/producthunt/browse.js +99 -0
- package/dist/clis/producthunt/hot.d.ts +1 -0
- package/dist/clis/producthunt/hot.js +110 -0
- package/dist/clis/producthunt/posts.d.ts +1 -0
- package/dist/clis/producthunt/posts.js +28 -0
- package/dist/clis/producthunt/today.d.ts +1 -0
- package/dist/clis/producthunt/today.js +35 -0
- package/dist/clis/producthunt/utils.d.ts +29 -0
- package/dist/clis/producthunt/utils.js +99 -0
- package/dist/clis/producthunt/utils.test.d.ts +1 -0
- package/dist/clis/producthunt/utils.test.js +64 -0
- package/dist/clis/twitter/article.js +4 -28
- package/dist/clis/twitter/likes.d.ts +24 -0
- package/dist/clis/twitter/likes.js +217 -0
- package/dist/clis/twitter/likes.test.d.ts +1 -0
- package/dist/clis/twitter/likes.test.js +85 -0
- package/dist/clis/twitter/profile.js +4 -28
- package/dist/clis/twitter/search.js +2 -1
- package/dist/clis/twitter/search.test.js +2 -0
- package/dist/clis/twitter/shared.d.ts +6 -0
- package/dist/clis/twitter/shared.js +35 -0
- package/dist/clis/twitter/timeline.js +2 -13
- package/dist/clis/twitter/trending.js +29 -61
- package/dist/clis/v2ex/hot.yaml +17 -3
- package/dist/clis/weixin/download.d.ts +17 -0
- package/dist/clis/weixin/download.js +88 -20
- package/dist/clis/weread/book.js +2 -2
- package/dist/clis/weread/commands.test.d.ts +3 -0
- package/dist/clis/weread/commands.test.js +43 -0
- package/dist/clis/weread/highlights.js +2 -2
- package/dist/clis/weread/notebooks.js +2 -2
- package/dist/clis/weread/notes.js +3 -3
- package/dist/clis/weread/shelf.js +2 -2
- package/dist/clis/weread/utils.d.ts +4 -4
- package/dist/clis/weread/utils.js +32 -14
- package/dist/clis/weread/utils.test.js +1 -28
- package/dist/clis/xiaohongshu/comments.d.ts +5 -0
- package/dist/clis/xiaohongshu/comments.js +74 -0
- package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/comments.test.js +79 -0
- package/dist/clis/xiaohongshu/publish.js +179 -47
- package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/publish.test.js +131 -0
- package/dist/clis/xiaohongshu/search.d.ts +8 -1
- package/dist/clis/xiaohongshu/search.js +20 -1
- package/dist/clis/xiaohongshu/search.test.d.ts +1 -1
- package/dist/clis/xiaohongshu/search.test.js +32 -1
- package/dist/commanderAdapter.d.ts +1 -0
- package/dist/commanderAdapter.js +176 -29
- package/dist/commanderAdapter.test.d.ts +1 -0
- package/dist/commanderAdapter.test.js +62 -0
- package/dist/daemon.js +17 -1
- package/dist/discovery.js +48 -42
- package/dist/doctor.d.ts +2 -2
- package/dist/doctor.js +11 -4
- package/dist/download/index.js +63 -51
- package/dist/download/index.test.js +17 -4
- package/dist/engine.test.js +42 -0
- package/dist/errors.d.ts +4 -2
- package/dist/errors.js +17 -34
- package/dist/execution.d.ts +1 -3
- package/dist/execution.js +66 -8
- package/dist/execution.test.d.ts +1 -0
- package/dist/execution.test.js +40 -0
- package/dist/external.js +6 -1
- package/dist/hooks.js +2 -0
- package/dist/main.js +6 -0
- package/dist/output.js +5 -1
- package/dist/pipeline/executor.js +3 -4
- package/dist/plugin-manifest.d.ts +70 -0
- package/dist/plugin-manifest.js +160 -0
- package/dist/plugin-manifest.test.d.ts +4 -0
- package/dist/plugin-manifest.test.js +179 -0
- package/dist/plugin-scaffold.d.ts +28 -0
- package/dist/plugin-scaffold.js +142 -0
- package/dist/plugin-scaffold.test.d.ts +4 -0
- package/dist/plugin-scaffold.test.js +83 -0
- package/dist/plugin.d.ts +82 -11
- package/dist/plugin.js +870 -84
- package/dist/plugin.test.js +1032 -17
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +2 -0
- package/dist/runtime-detect.d.ts +21 -0
- package/dist/runtime-detect.js +32 -0
- package/dist/runtime-detect.test.d.ts +1 -0
- package/dist/runtime-detect.test.js +27 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +2 -2
- package/dist/serialization.d.ts +2 -0
- package/dist/serialization.js +6 -0
- package/dist/types.d.ts +3 -0
- package/dist/update-check.d.ts +22 -0
- package/dist/update-check.js +112 -0
- package/dist/weixin-download.test.d.ts +1 -0
- package/dist/weixin-download.test.js +30 -0
- package/dist/weread-private-api-regression.test.d.ts +1 -0
- package/dist/weread-private-api-regression.test.js +122 -0
- package/dist/yaml-schema.d.ts +3 -0
- package/dist/yaml-schema.js +18 -1
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/36kr.md +47 -0
- package/docs/adapters/browser/bluesky.md +53 -0
- package/docs/adapters/browser/douban.md +14 -0
- package/docs/adapters/browser/imdb.md +47 -0
- package/docs/adapters/browser/jd.md +2 -2
- package/docs/adapters/browser/linux-do.md +181 -20
- package/docs/adapters/browser/paperreview.md +43 -0
- package/docs/adapters/browser/producthunt.md +49 -0
- package/docs/adapters/desktop/chatgpt.md +5 -0
- package/docs/adapters/index.md +6 -2
- package/docs/advanced/download.md +4 -0
- package/docs/advanced/rate-limiter-plugin.md +99 -0
- package/docs/guide/electron-app-cli.md +200 -0
- package/docs/guide/getting-started.md +1 -0
- package/docs/guide/plugins.md +97 -0
- package/docs/zh/guide/electron-app-cli.md +188 -0
- package/docs/zh/guide/getting-started.md +1 -0
- package/docs/zh/guide/plugins.md +65 -0
- package/extension/package.json +1 -0
- package/extension/scripts/package-release.mjs +179 -0
- package/extension/src/background.ts +2 -0
- package/package.json +4 -1
- package/scripts/postinstall.js +10 -0
- package/src/browser/cdp.ts +8 -1
- package/src/browser/discover.ts +8 -3
- package/src/browser/errors.ts +13 -14
- package/src/browser/mcp.ts +2 -1
- package/src/browser/page.ts +24 -1
- package/src/build-manifest.test.ts +23 -0
- package/src/build-manifest.ts +40 -15
- package/src/capabilityRouting.ts +2 -1
- package/src/cli.ts +69 -6
- package/src/clis/36kr/article.ts +69 -0
- package/src/clis/36kr/hot.test.ts +19 -0
- package/src/clis/36kr/hot.ts +100 -0
- package/src/clis/36kr/news.test.ts +90 -0
- package/src/clis/36kr/news.ts +54 -0
- package/src/clis/36kr/search.ts +78 -0
- package/src/clis/bilibili/comments.test.ts +102 -0
- package/src/clis/bilibili/comments.ts +44 -0
- package/src/clis/bluesky/feeds.yaml +29 -0
- package/src/clis/bluesky/followers.yaml +33 -0
- package/src/clis/bluesky/following.yaml +33 -0
- package/src/clis/bluesky/profile.yaml +27 -0
- package/src/clis/bluesky/search.yaml +34 -0
- package/src/clis/bluesky/starter-packs.yaml +34 -0
- package/src/clis/bluesky/thread.yaml +32 -0
- package/src/clis/bluesky/trending.yaml +27 -0
- package/src/clis/bluesky/user.yaml +34 -0
- package/src/clis/chatgpt/ask.ts +28 -14
- package/src/clis/chatgpt/ax.ts +180 -1
- package/src/clis/chatgpt/model.ts +27 -0
- package/src/clis/chatgpt/send.ts +16 -6
- package/src/clis/douban/download.test.ts +196 -0
- package/src/clis/douban/download.ts +78 -0
- package/src/clis/douban/photos.ts +36 -0
- package/src/clis/douban/utils.test.ts +97 -0
- package/src/clis/douban/utils.ts +232 -1
- package/src/clis/imdb/person.ts +232 -0
- package/src/clis/imdb/reviews.ts +111 -0
- package/src/clis/imdb/search.ts +179 -0
- package/src/clis/imdb/title.ts +121 -0
- package/src/clis/imdb/top.ts +67 -0
- package/src/clis/imdb/trending.ts +66 -0
- package/src/clis/imdb/utils.test.ts +117 -0
- package/src/clis/imdb/utils.ts +305 -0
- package/src/clis/jd/item.test.ts +18 -1
- package/src/clis/jd/item.ts +18 -15
- package/src/clis/linux-do/categories.yaml +38 -9
- package/src/clis/linux-do/category.ts +37 -0
- package/src/clis/linux-do/feed.test.ts +132 -0
- package/src/clis/linux-do/feed.ts +501 -0
- package/src/clis/linux-do/hot.ts +26 -0
- package/src/clis/linux-do/latest.ts +19 -0
- package/src/clis/linux-do/tags.yaml +41 -0
- package/src/clis/linux-do/topic.yaml +41 -3
- package/src/clis/linux-do/user-posts.yaml +67 -0
- package/src/clis/linux-do/user-topics.yaml +54 -0
- package/src/clis/paperreview/commands.test.ts +283 -0
- package/src/clis/paperreview/feedback.ts +64 -0
- package/src/clis/paperreview/review.ts +47 -0
- package/src/clis/paperreview/submit.ts +119 -0
- package/src/clis/paperreview/utils.test.ts +68 -0
- package/src/clis/paperreview/utils.ts +276 -0
- package/src/clis/producthunt/browse.ts +109 -0
- package/src/clis/producthunt/hot.ts +127 -0
- package/src/clis/producthunt/posts.ts +29 -0
- package/src/clis/producthunt/today.ts +37 -0
- package/src/clis/producthunt/utils.test.ts +72 -0
- package/src/clis/producthunt/utils.ts +122 -0
- package/src/clis/twitter/article.ts +5 -28
- package/src/clis/twitter/likes.test.ts +91 -0
- package/src/clis/twitter/likes.ts +256 -0
- package/src/clis/twitter/profile.ts +5 -28
- package/src/clis/twitter/search.test.ts +2 -0
- package/src/clis/twitter/search.ts +3 -1
- package/src/clis/twitter/shared.ts +45 -0
- package/src/clis/twitter/timeline.ts +2 -13
- package/src/clis/twitter/trending.ts +29 -77
- package/src/clis/v2ex/hot.yaml +17 -3
- package/src/clis/weixin/download.ts +114 -20
- package/src/clis/weread/book.ts +2 -2
- package/src/clis/weread/commands.test.ts +57 -0
- package/src/clis/weread/highlights.ts +2 -2
- package/src/clis/weread/notebooks.ts +2 -2
- package/src/clis/weread/notes.ts +3 -3
- package/src/clis/weread/shelf.ts +2 -2
- package/src/clis/weread/utils.test.ts +1 -32
- package/src/clis/weread/utils.ts +41 -16
- package/src/clis/xiaohongshu/comments.test.ts +96 -0
- package/src/clis/xiaohongshu/comments.ts +81 -0
- package/src/clis/xiaohongshu/publish.test.ts +151 -0
- package/src/clis/xiaohongshu/publish.ts +206 -54
- package/src/clis/xiaohongshu/search.test.ts +39 -1
- package/src/clis/xiaohongshu/search.ts +19 -1
- package/src/commanderAdapter.test.ts +78 -0
- package/src/commanderAdapter.ts +188 -24
- package/src/daemon.ts +19 -1
- package/src/discovery.ts +49 -48
- package/src/doctor.ts +15 -5
- package/src/download/index.test.ts +14 -4
- package/src/download/index.ts +67 -55
- package/src/engine.test.ts +38 -0
- package/src/errors.ts +26 -63
- package/src/execution.test.ts +47 -0
- package/src/execution.ts +67 -9
- package/src/external.ts +6 -1
- package/src/hooks.ts +1 -0
- package/src/main.ts +7 -0
- package/src/output.ts +3 -1
- package/src/pipeline/executor.ts +4 -6
- package/src/plugin-manifest.test.ts +223 -0
- package/src/plugin-manifest.ts +206 -0
- package/src/plugin-scaffold.test.ts +98 -0
- package/src/plugin-scaffold.ts +170 -0
- package/src/plugin.test.ts +1104 -17
- package/src/plugin.ts +1101 -86
- package/src/registry.ts +6 -1
- package/src/runtime-detect.test.ts +30 -0
- package/src/runtime-detect.ts +36 -0
- package/src/runtime.ts +3 -3
- package/src/serialization.ts +4 -0
- package/src/types.ts +3 -0
- package/src/update-check.ts +114 -0
- package/src/weixin-download.test.ts +64 -0
- package/src/weread-private-api-regression.test.ts +150 -0
- package/src/yaml-schema.ts +20 -0
- package/tests/e2e/browser-auth.test.ts +13 -9
- package/tests/e2e/browser-public-extended.test.ts +1 -1
- package/tests/e2e/browser-public.test.ts +62 -4
- package/tests/e2e/helpers.ts +2 -1
- package/tests/e2e/public-commands.test.ts +37 -3
- package/tests/smoke/api-health.test.ts +1 -1
- package/vitest.config.ts +10 -0
- package/dist/clis/linux-do/category.yaml +0 -51
- package/dist/clis/linux-do/hot.yaml +0 -50
- package/dist/clis/linux-do/latest.yaml +0 -40
- package/src/clis/linux-do/category.yaml +0 -51
- package/src/clis/linux-do/hot.yaml +0 -50
- package/src/clis/linux-do/latest.yaml +0 -40
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import {
|
|
4
|
+
forceEnglishUrl,
|
|
5
|
+
getCurrentImdbId,
|
|
6
|
+
isChallengePage,
|
|
7
|
+
normalizeImdbId,
|
|
8
|
+
waitForImdbPath,
|
|
9
|
+
} from './utils.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read IMDb person details from public profile pages.
|
|
13
|
+
*/
|
|
14
|
+
cli({
|
|
15
|
+
site: 'imdb',
|
|
16
|
+
name: 'person',
|
|
17
|
+
description: 'Get actor or director info',
|
|
18
|
+
domain: 'www.imdb.com',
|
|
19
|
+
strategy: Strategy.PUBLIC,
|
|
20
|
+
browser: true,
|
|
21
|
+
args: [
|
|
22
|
+
{ name: 'id', positional: true, required: true, help: 'IMDb person ID (nm0634240) or URL' },
|
|
23
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Max filmography entries' },
|
|
24
|
+
],
|
|
25
|
+
columns: ['field', 'value'],
|
|
26
|
+
func: async (page, args) => {
|
|
27
|
+
const id = normalizeImdbId(String(args.id), 'nm');
|
|
28
|
+
// Clamp to 30 to match the internal evaluate cap
|
|
29
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 10, 30));
|
|
30
|
+
const url = forceEnglishUrl(`https://www.imdb.com/name/${id}/`);
|
|
31
|
+
|
|
32
|
+
await page.goto(url);
|
|
33
|
+
const onPersonPage = await waitForImdbPath(page, `^/name/${id}/`);
|
|
34
|
+
|
|
35
|
+
if (await isChallengePage(page)) {
|
|
36
|
+
throw new CommandExecutionError(
|
|
37
|
+
'IMDb blocked this request',
|
|
38
|
+
'Try again with a normal browser session or extension mode',
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (!onPersonPage) {
|
|
42
|
+
throw new CommandExecutionError(
|
|
43
|
+
`Person page did not finish loading: ${id}`,
|
|
44
|
+
'Retry the command; if it persists, IMDb may have changed their navigation flow',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const currentId = await getCurrentImdbId(page, 'nm');
|
|
49
|
+
if (currentId && currentId !== id) {
|
|
50
|
+
throw new CommandExecutionError(
|
|
51
|
+
`IMDb redirected to a different person: ${currentId}`,
|
|
52
|
+
'Retry the command; if it persists, the person page may have changed',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = await page.evaluate(`
|
|
57
|
+
(function() {
|
|
58
|
+
var result = {
|
|
59
|
+
nameId: '',
|
|
60
|
+
name: '',
|
|
61
|
+
description: '',
|
|
62
|
+
birthDate: '',
|
|
63
|
+
filmography: []
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
var scripts = document.querySelectorAll('script[type="application/ld+json"]');
|
|
67
|
+
for (var i = 0; i < scripts.length; i++) {
|
|
68
|
+
try {
|
|
69
|
+
var ld = JSON.parse(scripts[i].textContent || 'null');
|
|
70
|
+
if (ld && ld['@type'] === 'Person') {
|
|
71
|
+
if (typeof ld.url === 'string') {
|
|
72
|
+
var ldMatch = ld.url.match(/(nm\\d{7,8})/);
|
|
73
|
+
if (ldMatch) {
|
|
74
|
+
result.nameId = ldMatch[1];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
result.name = result.name || ld.name || '';
|
|
78
|
+
result.description = result.description || ld.description || '';
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
void error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
var nextDataEl = document.getElementById('__NEXT_DATA__');
|
|
87
|
+
if (!nextDataEl) {
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
var nextData = JSON.parse(nextDataEl.textContent || 'null');
|
|
93
|
+
var pageProps = nextData && nextData.props && nextData.props.pageProps;
|
|
94
|
+
var above = pageProps && (pageProps.aboveTheFold || pageProps.aboveTheFoldData);
|
|
95
|
+
var main = pageProps && (pageProps.mainColumnData || pageProps.belowTheFold);
|
|
96
|
+
|
|
97
|
+
if (above) {
|
|
98
|
+
if (!result.nameId && above.id) {
|
|
99
|
+
result.nameId = String(above.id);
|
|
100
|
+
}
|
|
101
|
+
if (!result.name && above.nameText && above.nameText.text) {
|
|
102
|
+
result.name = above.nameText.text;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (above.birthDate) {
|
|
106
|
+
if (above.birthDate.displayableProperty && above.birthDate.displayableProperty.value) {
|
|
107
|
+
result.birthDate = above.birthDate.displayableProperty.value.plainText || '';
|
|
108
|
+
}
|
|
109
|
+
if (!result.birthDate && above.birthDate.dateComponents) {
|
|
110
|
+
var dc = above.birthDate.dateComponents;
|
|
111
|
+
result.birthDate = [dc.year, dc.month, dc.day].filter(Boolean).join('-');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (above.bio && above.bio.text && above.bio.text.plainText) {
|
|
116
|
+
result.description = above.bio.text.plainText.substring(0, 300);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
var pushFilmography = function(title, year, role) {
|
|
121
|
+
if (!title) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
result.filmography.push({
|
|
125
|
+
title: title,
|
|
126
|
+
year: year || '',
|
|
127
|
+
role: role || ''
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
var knownFor = main && main.knownForFeatureV2;
|
|
132
|
+
if (knownFor && Array.isArray(knownFor.credits)) {
|
|
133
|
+
for (var j = 0; j < knownFor.credits.length; j++) {
|
|
134
|
+
var knownNode = knownFor.credits[j];
|
|
135
|
+
if (!knownNode || !knownNode.title) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
var knownRole = '';
|
|
139
|
+
var knownRoleEdge = knownNode.creditedRoles && Array.isArray(knownNode.creditedRoles.edges)
|
|
140
|
+
? knownNode.creditedRoles.edges[0]
|
|
141
|
+
: null;
|
|
142
|
+
if (knownRoleEdge && knownRoleEdge.node) {
|
|
143
|
+
knownRole = knownRoleEdge.node.text
|
|
144
|
+
|| (knownRoleEdge.node.category ? knownRoleEdge.node.category.text || '' : '');
|
|
145
|
+
}
|
|
146
|
+
pushFilmography(
|
|
147
|
+
knownNode.title.titleText ? knownNode.title.titleText.text : '',
|
|
148
|
+
knownNode.title.releaseYear ? String(knownNode.title.releaseYear.year || '') : '',
|
|
149
|
+
knownRole
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (result.filmography.length === 0) {
|
|
155
|
+
var creditSources = [];
|
|
156
|
+
if (main && main.released && Array.isArray(main.released.edges)) {
|
|
157
|
+
creditSources.push(main.released.edges);
|
|
158
|
+
}
|
|
159
|
+
if (main && main.groupings && Array.isArray(main.groupings.edges)) {
|
|
160
|
+
creditSources.push(main.groupings.edges);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (var k = 0; k < creditSources.length && result.filmography.length < 30; k++) {
|
|
164
|
+
var groups = creditSources[k];
|
|
165
|
+
for (var m = 0; m < groups.length && result.filmography.length < 30; m++) {
|
|
166
|
+
var groupNode = groups[m] && groups[m].node;
|
|
167
|
+
if (!groupNode) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
var roleName = groupNode.grouping ? groupNode.grouping.text || '' : '';
|
|
172
|
+
var credits = groupNode.credits && Array.isArray(groupNode.credits.edges)
|
|
173
|
+
? groupNode.credits.edges
|
|
174
|
+
: [];
|
|
175
|
+
for (var n = 0; n < credits.length && result.filmography.length < 30; n++) {
|
|
176
|
+
var creditNode = credits[n] && credits[n].node;
|
|
177
|
+
if (!creditNode || !creditNode.title) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
pushFilmography(
|
|
181
|
+
creditNode.title.titleText ? creditNode.title.titleText.text : (creditNode.title.originalTitleText ? creditNode.title.originalTitleText.text : ''),
|
|
182
|
+
creditNode.title.releaseYear ? String(creditNode.title.releaseYear.year || '') : '',
|
|
183
|
+
roleName
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
void error;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
})()
|
|
195
|
+
`);
|
|
196
|
+
|
|
197
|
+
if (!data || typeof data !== 'object' || !('name' in data) || !(data as Record<string, unknown>).name) {
|
|
198
|
+
throw new CommandExecutionError(`Person not found: ${id}`, 'Check the person ID and try again');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const result = data as Record<string, any>;
|
|
202
|
+
if (result.nameId && result.nameId !== id) {
|
|
203
|
+
throw new CommandExecutionError(
|
|
204
|
+
`IMDb returned a different person payload: ${result.nameId}`,
|
|
205
|
+
'Retry the command; if it persists, the person parser may need updating',
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const filmography = Array.isArray(result.filmography) ? result.filmography : [];
|
|
209
|
+
|
|
210
|
+
// Override url with a clean canonical URL (no query params like ?language=en-US)
|
|
211
|
+
result.url = `https://www.imdb.com/name/${id}/`;
|
|
212
|
+
|
|
213
|
+
const rows = Object.entries(result)
|
|
214
|
+
.filter(([field, value]) => field !== 'filmography' && field !== 'nameId' && value !== '' && value != null)
|
|
215
|
+
.map(([field, value]) => ({ field, value: String(value) }));
|
|
216
|
+
|
|
217
|
+
if (filmography.length > 0) {
|
|
218
|
+
rows.push({ field: 'filmography', value: '' });
|
|
219
|
+
for (const entry of filmography.slice(0, limit)) {
|
|
220
|
+
const suffix = [entry.year ? `(${entry.year})` : '', entry.role ? `[${entry.role}]` : '']
|
|
221
|
+
.filter(Boolean)
|
|
222
|
+
.join(' ');
|
|
223
|
+
rows.push({
|
|
224
|
+
field: String(entry.title || ''),
|
|
225
|
+
value: suffix,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return rows;
|
|
231
|
+
},
|
|
232
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import {
|
|
4
|
+
forceEnglishUrl,
|
|
5
|
+
getCurrentImdbId,
|
|
6
|
+
isChallengePage,
|
|
7
|
+
normalizeImdbId,
|
|
8
|
+
waitForImdbPath,
|
|
9
|
+
waitForImdbReviewsReady,
|
|
10
|
+
} from './utils.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read IMDb user reviews from the first review page.
|
|
14
|
+
*/
|
|
15
|
+
cli({
|
|
16
|
+
site: 'imdb',
|
|
17
|
+
name: 'reviews',
|
|
18
|
+
description: 'Get user reviews for a movie or TV show',
|
|
19
|
+
domain: 'www.imdb.com',
|
|
20
|
+
strategy: Strategy.PUBLIC,
|
|
21
|
+
browser: true,
|
|
22
|
+
args: [
|
|
23
|
+
{ name: 'id', positional: true, required: true, help: 'IMDb title ID (tt1375666) or URL' },
|
|
24
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of reviews' },
|
|
25
|
+
],
|
|
26
|
+
columns: ['rank', 'title', 'rating', 'author', 'date', 'text'],
|
|
27
|
+
func: async (page, args) => {
|
|
28
|
+
const id = normalizeImdbId(String(args.id), 'tt');
|
|
29
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 10, 25));
|
|
30
|
+
const url = forceEnglishUrl(`https://www.imdb.com/title/${id}/reviews/`);
|
|
31
|
+
|
|
32
|
+
await page.goto(url);
|
|
33
|
+
const onReviewsPage = await waitForImdbPath(page, `^/title/${id}/reviews/?$`);
|
|
34
|
+
const reviewsReady = await waitForImdbReviewsReady(page, 15000);
|
|
35
|
+
|
|
36
|
+
if (await isChallengePage(page)) {
|
|
37
|
+
throw new CommandExecutionError(
|
|
38
|
+
'IMDb blocked this request',
|
|
39
|
+
'Try again with a normal browser session or extension mode',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (!onReviewsPage || !reviewsReady) {
|
|
43
|
+
throw new CommandExecutionError(
|
|
44
|
+
'IMDb reviews did not finish loading',
|
|
45
|
+
'Retry the command; if it persists, the review page structure may have changed',
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const currentId = await getCurrentImdbId(page, 'tt');
|
|
50
|
+
if (currentId && currentId !== id) {
|
|
51
|
+
throw new CommandExecutionError(
|
|
52
|
+
`IMDb redirected to a different title: ${currentId}`,
|
|
53
|
+
'Retry the command; if it persists, the review page may have changed',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const reviews = await page.evaluate(`
|
|
58
|
+
(function() {
|
|
59
|
+
var limit = ${limit};
|
|
60
|
+
var items = [];
|
|
61
|
+
var containers = document.querySelectorAll('article.user-review-item, [data-testid="review-card-parent"], .imdb-user-review, [data-testid="review-card"], .review-container');
|
|
62
|
+
|
|
63
|
+
for (var i = 0; i < containers.length && items.length < limit; i++) {
|
|
64
|
+
var el = containers[i];
|
|
65
|
+
var titleEl = el.querySelector('.title, [data-testid="review-summary"], a.title');
|
|
66
|
+
var ratingEl = el.querySelector('.review-rating .ipc-rating-star--rating, .rating-other-user-rating span:first-child, [data-testid="review-rating"]');
|
|
67
|
+
var authorEl = el.querySelector('.display-name-link a, [data-testid="author-link"], .author-text, a[href*="/user/"]');
|
|
68
|
+
var dateEl = el.querySelector('.review-date, [data-testid="review-date"]');
|
|
69
|
+
var textEl = el.querySelector('.content .text, .content .show-more__control, [data-testid="review-overflow"]');
|
|
70
|
+
|
|
71
|
+
var title = titleEl ? (titleEl.textContent || '').trim() : '';
|
|
72
|
+
var text = textEl ? (textEl.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 200) : '';
|
|
73
|
+
|
|
74
|
+
if (!title && !text) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Deduplicate: IMDb renders both preview and expanded versions of each review
|
|
79
|
+
var isDupe = false;
|
|
80
|
+
for (var d = 0; d < items.length; d++) {
|
|
81
|
+
if (items[d].title === title) { isDupe = true; break; }
|
|
82
|
+
}
|
|
83
|
+
if (isDupe) { continue; }
|
|
84
|
+
|
|
85
|
+
items.push({
|
|
86
|
+
title: title,
|
|
87
|
+
rating: ratingEl ? (ratingEl.textContent || '').trim() : '',
|
|
88
|
+
author: authorEl ? (authorEl.textContent || '').trim() : '',
|
|
89
|
+
date: dateEl ? (dateEl.textContent || '').trim() : '',
|
|
90
|
+
text: text
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return items;
|
|
95
|
+
})()
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
if (!Array.isArray(reviews)) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return reviews.map((item: any, index: number) => ({
|
|
103
|
+
rank: index + 1,
|
|
104
|
+
title: item.title || '',
|
|
105
|
+
rating: item.rating || '',
|
|
106
|
+
author: item.author || '',
|
|
107
|
+
date: item.date || '',
|
|
108
|
+
text: item.text || '',
|
|
109
|
+
}));
|
|
110
|
+
},
|
|
111
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { ArgumentError, CommandExecutionError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import {
|
|
4
|
+
forceEnglishUrl,
|
|
5
|
+
isChallengePage,
|
|
6
|
+
normalizeImdbTitleType,
|
|
7
|
+
waitForImdbPath,
|
|
8
|
+
waitForImdbSearchReady,
|
|
9
|
+
} from './utils.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Search IMDb via the public search page and parse Next.js payload first.
|
|
13
|
+
*/
|
|
14
|
+
cli({
|
|
15
|
+
site: 'imdb',
|
|
16
|
+
name: 'search',
|
|
17
|
+
description: 'Search IMDb for movies, TV shows, and people',
|
|
18
|
+
domain: 'www.imdb.com',
|
|
19
|
+
strategy: Strategy.PUBLIC,
|
|
20
|
+
browser: true,
|
|
21
|
+
args: [
|
|
22
|
+
{ name: 'query', positional: true, required: true, help: 'Search query' },
|
|
23
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
24
|
+
],
|
|
25
|
+
columns: ['rank', 'id', 'title', 'year', 'type', 'url'],
|
|
26
|
+
func: async (page, args) => {
|
|
27
|
+
const query = String(args.query || '').trim();
|
|
28
|
+
// Reject empty or whitespace-only queries early
|
|
29
|
+
if (!query) {
|
|
30
|
+
throw new ArgumentError('Search query cannot be empty');
|
|
31
|
+
}
|
|
32
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 20, 50));
|
|
33
|
+
const url = forceEnglishUrl(`https://www.imdb.com/find/?q=${encodeURIComponent(query)}&ref_=nv_sr_sm`);
|
|
34
|
+
|
|
35
|
+
await page.goto(url);
|
|
36
|
+
const onSearchPage = await waitForImdbPath(page, '^/find/?$');
|
|
37
|
+
const searchReady = await waitForImdbSearchReady(page, 15000);
|
|
38
|
+
|
|
39
|
+
if (await isChallengePage(page)) {
|
|
40
|
+
throw new CommandExecutionError(
|
|
41
|
+
'IMDb blocked this request',
|
|
42
|
+
'Try again with a normal browser session or extension mode',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (!onSearchPage || !searchReady) {
|
|
46
|
+
throw new CommandExecutionError(
|
|
47
|
+
'IMDb search results did not finish loading',
|
|
48
|
+
'Retry the command; if it persists, the search page structure may have changed',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const results = await page.evaluate(`
|
|
53
|
+
(function() {
|
|
54
|
+
var results = [];
|
|
55
|
+
|
|
56
|
+
function pushResult(item) {
|
|
57
|
+
if (!item || !item.id || !item.title) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
results.push(item);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
var nextDataEl = document.getElementById('__NEXT_DATA__');
|
|
64
|
+
if (nextDataEl) {
|
|
65
|
+
try {
|
|
66
|
+
var nextData = JSON.parse(nextDataEl.textContent || 'null');
|
|
67
|
+
var pageProps = nextData && nextData.props && nextData.props.pageProps;
|
|
68
|
+
if (pageProps) {
|
|
69
|
+
// IMDb wraps results as {index: "tt...", listItem: {...}}
|
|
70
|
+
var titleResults = (pageProps.titleResults && pageProps.titleResults.results) || [];
|
|
71
|
+
for (var i = 0; i < titleResults.length; i++) {
|
|
72
|
+
var tr = titleResults[i] || {};
|
|
73
|
+
var tItem = tr.listItem || {};
|
|
74
|
+
var tId = tr.index || '';
|
|
75
|
+
var tTitle = typeof tItem.originalTitleText === 'string'
|
|
76
|
+
? tItem.originalTitleText
|
|
77
|
+
: (tItem.originalTitleText && tItem.originalTitleText.text) || '';
|
|
78
|
+
if (!tTitle) {
|
|
79
|
+
tTitle = typeof tItem.titleText === 'string'
|
|
80
|
+
? tItem.titleText
|
|
81
|
+
: (tItem.titleText && tItem.titleText.text) || '';
|
|
82
|
+
}
|
|
83
|
+
var tYear = '';
|
|
84
|
+
if (typeof tItem.releaseYear === 'number' || typeof tItem.releaseYear === 'string') {
|
|
85
|
+
tYear = String(tItem.releaseYear);
|
|
86
|
+
} else if (tItem.releaseYear && typeof tItem.releaseYear === 'object') {
|
|
87
|
+
tYear = String(tItem.releaseYear.year || '');
|
|
88
|
+
}
|
|
89
|
+
pushResult({
|
|
90
|
+
id: tId,
|
|
91
|
+
title: tTitle,
|
|
92
|
+
year: tYear,
|
|
93
|
+
type: tItem.titleType || (tItem.endYear != null ? 'tvSeries' : ''),
|
|
94
|
+
url: tId ? 'https://www.imdb.com/title/' + tId + '/' : ''
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var nameResults = (pageProps.nameResults && pageProps.nameResults.results) || [];
|
|
99
|
+
for (var j = 0; j < nameResults.length; j++) {
|
|
100
|
+
var nr = nameResults[j] || {};
|
|
101
|
+
var nItem = nr.listItem || {};
|
|
102
|
+
var nId = nr.index || '';
|
|
103
|
+
var nTitle = typeof nItem.nameText === 'string'
|
|
104
|
+
? nItem.nameText
|
|
105
|
+
: (nItem.nameText && nItem.nameText.text) || '';
|
|
106
|
+
if (!nTitle) {
|
|
107
|
+
nTitle = typeof nItem.originalNameText === 'string'
|
|
108
|
+
? nItem.originalNameText
|
|
109
|
+
: (nItem.originalNameText && nItem.originalNameText.text) || '';
|
|
110
|
+
}
|
|
111
|
+
var nType = '';
|
|
112
|
+
if (typeof nItem.primaryProfession === 'string') {
|
|
113
|
+
nType = nItem.primaryProfession;
|
|
114
|
+
} else if (Array.isArray(nItem.primaryProfessions) && nItem.primaryProfessions.length > 0) {
|
|
115
|
+
nType = String(nItem.primaryProfessions[0] || '');
|
|
116
|
+
} else if (Array.isArray(nItem.professions) && nItem.professions.length > 0) {
|
|
117
|
+
nType = String(nItem.professions[0] || '');
|
|
118
|
+
}
|
|
119
|
+
pushResult({
|
|
120
|
+
id: nId,
|
|
121
|
+
title: nTitle,
|
|
122
|
+
year: nItem.knownFor && nItem.knownFor.yearRange ? String(nItem.knownFor.yearRange.year || '') : (nItem.knownForTitleYear ? String(nItem.knownForTitleYear) : ''),
|
|
123
|
+
type: nType || 'Person',
|
|
124
|
+
url: nId ? 'https://www.imdb.com/name/' + nId + '/' : ''
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
void error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (results.length === 0) {
|
|
134
|
+
var items = document.querySelectorAll('[class*="find-title-result"], [class*="find-name-result"], .ipc-metadata-list-summary-item');
|
|
135
|
+
for (var k = 0; k < items.length; k++) {
|
|
136
|
+
var el = items[k];
|
|
137
|
+
var linkEl = el.querySelector('a[href*="/title/"], a[href*="/name/"]');
|
|
138
|
+
if (!linkEl) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
var href = linkEl.getAttribute('href') || '';
|
|
143
|
+
var idMatch = href.match(/(tt|nm)\\d{7,8}/);
|
|
144
|
+
if (!idMatch) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
var titleEl = el.querySelector('.ipc-metadata-list-summary-item__t, h3, a');
|
|
149
|
+
var metaEls = el.querySelectorAll('.ipc-metadata-list-summary-item__li, span');
|
|
150
|
+
var absoluteUrl = href.startsWith('http') ? href : 'https://www.imdb.com' + href.split('?')[0];
|
|
151
|
+
|
|
152
|
+
pushResult({
|
|
153
|
+
id: idMatch[0],
|
|
154
|
+
title: titleEl ? (titleEl.textContent || '').trim() : '',
|
|
155
|
+
year: metaEls.length > 0 ? (metaEls[0].textContent || '').trim() : '',
|
|
156
|
+
type: metaEls.length > 1 ? (metaEls[1].textContent || '').trim() : '',
|
|
157
|
+
url: absoluteUrl
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return results;
|
|
163
|
+
})()
|
|
164
|
+
`);
|
|
165
|
+
|
|
166
|
+
if (!Array.isArray(results)) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return results.slice(0, limit).map((item: any, index: number) => ({
|
|
171
|
+
rank: index + 1,
|
|
172
|
+
id: item.id || '',
|
|
173
|
+
title: item.title || '',
|
|
174
|
+
year: item.year || '',
|
|
175
|
+
type: normalizeImdbTitleType(item.type),
|
|
176
|
+
url: item.url || '',
|
|
177
|
+
}));
|
|
178
|
+
},
|
|
179
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import {
|
|
4
|
+
extractJsonLd,
|
|
5
|
+
forceEnglishUrl,
|
|
6
|
+
formatDuration,
|
|
7
|
+
getCurrentImdbId,
|
|
8
|
+
isChallengePage,
|
|
9
|
+
normalizeImdbId,
|
|
10
|
+
waitForImdbPath,
|
|
11
|
+
} from './utils.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read IMDb title details from JSON-LD on the public page.
|
|
15
|
+
*/
|
|
16
|
+
cli({
|
|
17
|
+
site: 'imdb',
|
|
18
|
+
name: 'title',
|
|
19
|
+
description: 'Get movie or TV show details',
|
|
20
|
+
domain: 'www.imdb.com',
|
|
21
|
+
strategy: Strategy.PUBLIC,
|
|
22
|
+
browser: true,
|
|
23
|
+
args: [
|
|
24
|
+
{ name: 'id', positional: true, required: true, help: 'IMDb title ID (tt1375666) or URL' },
|
|
25
|
+
],
|
|
26
|
+
columns: ['field', 'value'],
|
|
27
|
+
func: async (page, args) => {
|
|
28
|
+
const id = normalizeImdbId(String(args.id), 'tt');
|
|
29
|
+
const url = forceEnglishUrl(`https://www.imdb.com/title/${id}/`);
|
|
30
|
+
|
|
31
|
+
await page.goto(url);
|
|
32
|
+
const onTitlePage = await waitForImdbPath(page, `^/title/${id}/`);
|
|
33
|
+
|
|
34
|
+
if (await isChallengePage(page)) {
|
|
35
|
+
throw new CommandExecutionError(
|
|
36
|
+
'IMDb blocked this request',
|
|
37
|
+
'Try again with a normal browser session or extension mode',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (!onTitlePage) {
|
|
41
|
+
throw new CommandExecutionError(
|
|
42
|
+
`Title page did not finish loading: ${id}`,
|
|
43
|
+
'Retry the command; if it persists, IMDb may have changed their navigation flow',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const currentId = await getCurrentImdbId(page, 'tt');
|
|
48
|
+
if (currentId && currentId !== id) {
|
|
49
|
+
throw new CommandExecutionError(
|
|
50
|
+
`IMDb redirected to a different title: ${currentId}`,
|
|
51
|
+
'Retry the command; if it persists, the title page may have changed',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Single browser roundtrip: fetch title JSON-LD by type whitelist
|
|
56
|
+
const titleTypes = ['Movie', 'TVSeries', 'TVEpisode', 'TVMiniseries', 'TVMovie', 'TVSpecial', 'VideoGame', 'ShortFilm'];
|
|
57
|
+
const ld = await extractJsonLd(page, titleTypes);
|
|
58
|
+
if (!ld) {
|
|
59
|
+
throw new CommandExecutionError(`Title not found: ${id}`, 'Check the title ID and try again');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = ld as Record<string, any>;
|
|
63
|
+
const type = String(data['@type'] || '');
|
|
64
|
+
const isTvSeries = type === 'TVSeries' || type === 'TVMiniseries';
|
|
65
|
+
|
|
66
|
+
// Handle both array and single-object JSON-LD person fields
|
|
67
|
+
const toPeople = (arr: any): string => {
|
|
68
|
+
if (!arr) return '';
|
|
69
|
+
const list = Array.isArray(arr) ? arr : [arr];
|
|
70
|
+
return list
|
|
71
|
+
.slice(0, 5)
|
|
72
|
+
.map((p: any) => p.name || '')
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join(', ');
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const year = (() => {
|
|
78
|
+
if (isTvSeries && typeof data.startDate === 'string') {
|
|
79
|
+
const startYear = data.startDate.split('-')[0] || '';
|
|
80
|
+
const endYear = typeof data.endDate === 'string' ? data.endDate.split('-')[0] || '' : '';
|
|
81
|
+
// Show "2024-" for ongoing series (no endDate) or "2010-2015" for ended ones
|
|
82
|
+
return endYear ? `${startYear}-${endYear}` : `${startYear}-`;
|
|
83
|
+
}
|
|
84
|
+
if (typeof data.datePublished === 'string') {
|
|
85
|
+
return data.datePublished.split('-')[0] || '';
|
|
86
|
+
}
|
|
87
|
+
return '';
|
|
88
|
+
})();
|
|
89
|
+
|
|
90
|
+
const directorField = isTvSeries ? 'creator' : 'director';
|
|
91
|
+
const directorValue = isTvSeries ? toPeople(data.creator) : toPeople(data.director);
|
|
92
|
+
|
|
93
|
+
const fields: Record<string, string> = {
|
|
94
|
+
title: String(data.name || ''),
|
|
95
|
+
type,
|
|
96
|
+
year,
|
|
97
|
+
rating: data.aggregateRating?.ratingValue != null ? String(data.aggregateRating.ratingValue) : '',
|
|
98
|
+
votes: data.aggregateRating?.ratingCount != null ? String(data.aggregateRating.ratingCount) : '',
|
|
99
|
+
genre: Array.isArray(data.genre) ? data.genre.join(', ') : String(data.genre || ''),
|
|
100
|
+
[directorField]: directorValue,
|
|
101
|
+
cast: toPeople(data.actor),
|
|
102
|
+
duration: formatDuration(String(data.duration || '')),
|
|
103
|
+
contentRating: String(data.contentRating || ''),
|
|
104
|
+
plot: String(data.description || ''),
|
|
105
|
+
url: `https://www.imdb.com/title/${id}/`,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (isTvSeries) {
|
|
109
|
+
if (data.numberOfSeasons != null) {
|
|
110
|
+
fields.seasons = String(data.numberOfSeasons);
|
|
111
|
+
}
|
|
112
|
+
if (data.numberOfEpisodes != null) {
|
|
113
|
+
fields.episodes = String(data.numberOfEpisodes);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return Object.entries(fields)
|
|
118
|
+
.filter(([, value]) => value !== '')
|
|
119
|
+
.map(([field, value]) => ({ field, value }));
|
|
120
|
+
},
|
|
121
|
+
});
|