import { ArgumentError } from '@jackwener/opencli/errors'; /** * Normalize an IMDb title or person input to a bare ID. * Accepts bare IDs, desktop URLs, mobile URLs, and URLs with language prefixes or query params. */ export function normalizeImdbId(input, prefix) { const trimmed = input.trim(); const barePattern = new RegExp(`^${prefix}\\d{7,8}$`); if (barePattern.test(trimmed)) { return trimmed; } const pathPattern = new RegExp(`/(?:[a-z]{2}/)?(?:title|name)/(${prefix}\\d{7,8})(?:[/?#]|$)`, 'i'); const pathMatch = trimmed.match(pathPattern); if (pathMatch) { return pathMatch[1]; } throw new ArgumentError(`Invalid IMDb ID: "${input}"`, `Expected ${prefix === 'tt' ? 'title' : 'name'} ID like ${prefix === 'tt' ? 'tt1375666' : 'nm0634240'} or an IMDb URL`); } /** * Convert an ISO 8601 duration string to a short human-readable format for table display. * Example: PT2H28M -> 2h 28m. */ export function formatDuration(iso) { if (!iso) { return ''; } const match = iso.match(/^PT(?:(\d+)H)?(?:(\d+)M)?$/); if (!match) { return ''; } const parts = []; if (match[1]) { parts.push(`${match[1]}h`); } if (match[2]) { parts.push(`${match[2]}m`); } return parts.join(' '); } /** * Force an IMDb page URL to use the English language parameter, * reducing structural differences across localized pages. */ export function forceEnglishUrl(url) { const parsed = new URL(url); parsed.searchParams.set('language', 'en-US'); return parsed.toString(); } /** * Normalize IMDb title-type payloads that may be represented as an object, * a raw string, or an empty text field with only an internal id. */ export function normalizeImdbTitleType(input) { const raw = (() => { if (typeof input === 'string') return input; if (!input || typeof input !== 'object') return ''; const value = input; return typeof value.text === 'string' && value.text.trim() ? value.text : typeof value.id === 'string' ? value.id : ''; })().trim(); if (!raw) return ''; const known = { movie: 'Movie', short: 'Short', video: 'Video', tvEpisode: 'TV Episode', tvMiniSeries: 'TV Mini Series', tvMovie: 'TV Movie', tvSeries: 'TV Series', tvShort: 'TV Short', tvSpecial: 'TV Special', videoGame: 'Video Game', }; return known[raw] ?? raw; } /** * Extract structured JSON-LD data from the page. * Accepts a single type string or an array of types to match against @type. */ export async function extractJsonLd(page, type) { const filterTypes = type ? (Array.isArray(type) ? type : [type]) : []; return page.evaluate(` (function() { var scripts = document.querySelectorAll('script[type="application/ld+json"]'); var wantedTypes = ${JSON.stringify(filterTypes)}; function matchesType(data) { if (wantedTypes.length === 0) { return true; } if (!data || typeof data !== 'object') { return false; } if (wantedTypes.indexOf(data['@type']) !== -1) { return true; } if (Array.isArray(data['@type'])) { for (var t = 0; t < data['@type'].length; t++) { if (wantedTypes.indexOf(data['@type'][t]) !== -1) return true; } } return false; } function findMatch(data) { if (Array.isArray(data)) { for (var i = 0; i < data.length; i++) { var itemMatch = findMatch(data[i]); if (itemMatch) { return itemMatch; } } return null; } if (!data || typeof data !== 'object') { return null; } if (matchesType(data)) { return data; } if (Array.isArray(data['@graph'])) { return findMatch(data['@graph']); } return null; } for (var i = 0; i < scripts.length; i++) { try { var parsed = JSON.parse(scripts[i].textContent || 'null'); var match = findMatch(parsed); if (match) { return match; } } catch (error) { void error; } } return null; })() `); } /** * Poll until the current IMDb page path matches the expected entity/search path. */ export async function waitForImdbPath(page, pathPattern, timeoutMs = 15000) { const result = await page.evaluate(` (async function() { var deadline = Date.now() + ${timeoutMs}; var pattern = new RegExp(${JSON.stringify(pathPattern)}, 'i'); while (Date.now() < deadline) { if (pattern.test(window.location.pathname)) { return true; } await new Promise(function(resolve) { setTimeout(resolve, 250); }); } return pattern.test(window.location.pathname); })() `); return Boolean(result); } /** * Wait until IMDb search results (or the search UI state) has rendered. */ export async function waitForImdbSearchReady(page, timeoutMs = 15000) { const result = await page.evaluate(` (async function() { var deadline = Date.now() + ${timeoutMs}; function hasSearchResults() { var nextDataEl = document.getElementById('__NEXT_DATA__'); if (nextDataEl) { try { var nextData = JSON.parse(nextDataEl.textContent || 'null'); var pageProps = nextData && nextData.props && nextData.props.pageProps; var titleResults = (pageProps && pageProps.titleResults && pageProps.titleResults.results) || []; var nameResults = (pageProps && pageProps.nameResults && pageProps.nameResults.results) || []; if (titleResults.length > 0 || nameResults.length > 0) { return true; } } catch (error) { void error; } } if (document.querySelector('a[href*="/title/"], a[href*="/name/"]')) { return true; } var body = document.body ? (document.body.textContent || '') : ''; return body.includes('No results found for') || body.includes('No exact matches'); } while (Date.now() < deadline) { if (hasSearchResults()) { return true; } await new Promise(function(resolve) { setTimeout(resolve, 250); }); } return hasSearchResults(); })() `); return Boolean(result); } /** * Wait until IMDb review cards (or the page review summary) has rendered. */ export async function waitForImdbReviewsReady(page, timeoutMs = 15000) { const result = await page.evaluate(` (async function() { var deadline = Date.now() + ${timeoutMs}; function hasReviewContent() { if (document.querySelector('article.user-review-item, [data-testid="review-card-parent"], [data-testid="tturv-total-reviews"]')) { return true; } var body = document.body ? (document.body.textContent || '') : ''; return body.includes('No user reviews') || body.includes('Review this title'); } while (Date.now() < deadline) { if (hasReviewContent()) { return true; } await new Promise(function(resolve) { setTimeout(resolve, 250); }); } return hasReviewContent(); })() `); return Boolean(result); } /** * Read the current IMDb entity id from the page URL/canonical metadata. */ export async function getCurrentImdbId(page, prefix) { const result = await page.evaluate(` (function() { var pattern = new RegExp('(${prefix}\\\\d{7,8})', 'i'); var candidates = [ window.location.pathname || '', document.querySelector('link[rel="canonical"]')?.getAttribute('href') || '', document.querySelector('meta[property="og:url"]')?.getAttribute('content') || '' ]; for (var i = 0; i < candidates.length; i++) { var match = candidates[i].match(pattern); if (match) { return match[1]; } } return ''; })() `); return typeof result === 'string' ? result : ''; } /** * Detect whether the current page is an IMDb bot-challenge or verification page. */ export async function isChallengePage(page) { const result = await page.evaluate(` (function() { var title = document.title || ''; var body = document.body ? (document.body.textContent || '') : ''; return title.includes('Robot Check') || title.includes('Are you a robot') || title.includes('JavaScript is disabled') || body.includes('captcha') || body.includes('verify that you are human') || body.includes('not a robot'); })() `); return Boolean(result); }