Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

163 lines
7.1 KiB
JavaScript

import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
import { cli, Strategy } from '@jackwener/opencli/registry';
import { forceEnglishUrl, isChallengePage, normalizeImdbTitleType, waitForImdbPath, waitForImdbSearchReady, } from './utils.js';
/**
* Search IMDb via the public search page and parse Next.js payload first.
*/
cli({
site: 'imdb',
name: 'search',
access: 'read',
description: 'Search IMDb for movies, TV shows, and people',
domain: 'www.imdb.com',
strategy: Strategy.PUBLIC,
browser: true,
args: [
{ name: 'query', positional: true, required: true, help: 'Search query' },
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
],
columns: ['rank', 'id', 'title', 'year', 'type', 'url'],
func: async (page, args) => {
const query = String(args.query || '').trim();
// Reject empty or whitespace-only queries early
if (!query) {
throw new ArgumentError('Search query cannot be empty');
}
const limit = Math.max(1, Math.min(Number(args.limit) || 20, 50));
const url = forceEnglishUrl(`https://www.imdb.com/find/?q=${encodeURIComponent(query)}&ref_=nv_sr_sm`);
await page.goto(url);
const onSearchPage = await waitForImdbPath(page, '^/find/?$');
const searchReady = await waitForImdbSearchReady(page, 15000);
if (await isChallengePage(page)) {
throw new CommandExecutionError('IMDb blocked this request', 'Try again with a normal browser session or extension mode');
}
if (!onSearchPage || !searchReady) {
throw new CommandExecutionError('IMDb search results did not finish loading', 'Retry the command; if it persists, the search page structure may have changed');
}
const results = await page.evaluate(`
(function() {
var results = [];
function pushResult(item) {
if (!item || !item.id || !item.title) {
return;
}
results.push(item);
}
var nextDataEl = document.getElementById('__NEXT_DATA__');
if (nextDataEl) {
try {
var nextData = JSON.parse(nextDataEl.textContent || 'null');
var pageProps = nextData && nextData.props && nextData.props.pageProps;
if (pageProps) {
// IMDb wraps results as {index: "tt...", listItem: {...}}
var titleResults = (pageProps.titleResults && pageProps.titleResults.results) || [];
for (var i = 0; i < titleResults.length; i++) {
var tr = titleResults[i] || {};
var tItem = tr.listItem || {};
var tId = tr.index || '';
var tTitle = typeof tItem.originalTitleText === 'string'
? tItem.originalTitleText
: (tItem.originalTitleText && tItem.originalTitleText.text) || '';
if (!tTitle) {
tTitle = typeof tItem.titleText === 'string'
? tItem.titleText
: (tItem.titleText && tItem.titleText.text) || '';
}
var tYear = '';
if (typeof tItem.releaseYear === 'number' || typeof tItem.releaseYear === 'string') {
tYear = String(tItem.releaseYear);
} else if (tItem.releaseYear && typeof tItem.releaseYear === 'object') {
tYear = String(tItem.releaseYear.year || '');
}
pushResult({
id: tId,
title: tTitle,
year: tYear,
type: tItem.titleType || (tItem.endYear != null ? 'tvSeries' : ''),
url: tId ? 'https://www.imdb.com/title/' + tId + '/' : ''
});
}
var nameResults = (pageProps.nameResults && pageProps.nameResults.results) || [];
for (var j = 0; j < nameResults.length; j++) {
var nr = nameResults[j] || {};
var nItem = nr.listItem || {};
var nId = nr.index || '';
var nTitle = typeof nItem.nameText === 'string'
? nItem.nameText
: (nItem.nameText && nItem.nameText.text) || '';
if (!nTitle) {
nTitle = typeof nItem.originalNameText === 'string'
? nItem.originalNameText
: (nItem.originalNameText && nItem.originalNameText.text) || '';
}
var nType = '';
if (typeof nItem.primaryProfession === 'string') {
nType = nItem.primaryProfession;
} else if (Array.isArray(nItem.primaryProfessions) && nItem.primaryProfessions.length > 0) {
nType = String(nItem.primaryProfessions[0] || '');
} else if (Array.isArray(nItem.professions) && nItem.professions.length > 0) {
nType = String(nItem.professions[0] || '');
}
pushResult({
id: nId,
title: nTitle,
year: nItem.knownFor && nItem.knownFor.yearRange ? String(nItem.knownFor.yearRange.year || '') : (nItem.knownForTitleYear ? String(nItem.knownForTitleYear) : ''),
type: nType || 'Person',
url: nId ? 'https://www.imdb.com/name/' + nId + '/' : ''
});
}
}
} catch (error) {
void error;
}
}
if (results.length === 0) {
var items = document.querySelectorAll('[class*="find-title-result"], [class*="find-name-result"], .ipc-metadata-list-summary-item');
for (var k = 0; k < items.length; k++) {
var el = items[k];
var linkEl = el.querySelector('a[href*="/title/"], a[href*="/name/"]');
if (!linkEl) {
continue;
}
var href = linkEl.getAttribute('href') || '';
var idMatch = href.match(/(tt|nm)\\d{7,8}/);
if (!idMatch) {
continue;
}
var titleEl = el.querySelector('.ipc-metadata-list-summary-item__t, h3, a');
var metaEls = el.querySelectorAll('.ipc-metadata-list-summary-item__li, span');
var absoluteUrl = href.startsWith('http') ? href : 'https://www.imdb.com' + href.split('?')[0];
pushResult({
id: idMatch[0],
title: titleEl ? (titleEl.textContent || '').trim() : '',
year: metaEls.length > 0 ? (metaEls[0].textContent || '').trim() : '',
type: metaEls.length > 1 ? (metaEls[1].textContent || '').trim() : '',
url: absoluteUrl
});
}
}
return results;
})()
`);
if (!Array.isArray(results)) {
return [];
}
return results.slice(0, limit).map((item, index) => ({
rank: index + 1,
id: item.id || '',
title: item.title || '',
year: item.year || '',
type: normalizeImdbTitleType(item.type),
url: item.url || '',
}));
},
});