Files
wehub-resource-sync 9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

286 lines
8.7 KiB
JavaScript

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