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
163 lines
7.1 KiB
JavaScript
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 || '',
|
|
}));
|
|
},
|
|
});
|