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 || '', })); }, });