您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Display Rotten Tomatoes and IMDb ratings next to movie titles on amctheatres site
// ==UserScript== // @name AMC Rating Helper // @namespace http://tampermonkey.net/ // @version 2.0 // @description Display Rotten Tomatoes and IMDb ratings next to movie titles on amctheatres site // @author wu5bocheng // @match *://www.amctheatres.com/movie-theatres/* // @match *://www.amctheatres.com/movies/* // @match *://www.amctheatres.com/movies // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_openInTab // @license MIT // ==/UserScript== /* SETUP INSTRUCTIONS: 1. Get a free OMDb API key from https://www.omdbapi.com/apikey.aspx 2. Replace 'YOUR_OMDB_API_KEY_HERE' with your actual API key 3. Install the script in Tampermonkey/Greasemonkey 4. Visit AMC Theatres website to see ratings displayed next to movie titles FEATURES: - Shows IMDb ratings (via OMDb API) - Shows Rotten Tomatoes ratings (via OMDb API) - Shows box office information when available - Hover tooltips with detailed movie information - Intelligent caching (24-hour cache duration) - Instant display for previously loaded movies - Rate limiting to avoid overwhelming APIs - Loading indicators while fetching data - Error handling for failed requests - Clickable ratings that open the respective service */ (function () { 'use strict'; // Storage keys const STORAGE_KEYS = { omdbApiKey: 'amc_omdb_api_key' }; // Helpers to get/set API key function getStoredApiKey() { try { return (GM_getValue(STORAGE_KEYS.omdbApiKey, '') || '').trim(); } catch { return ''; } } function setStoredApiKey(key) { try { GM_setValue(STORAGE_KEYS.omdbApiKey, (key || '').trim()); } catch {} } // Setup tab rendering (hash or query param triggers it) function isSetupMode() { try { const hash = (location.hash || '').toLowerCase(); if (hash.includes('#amc-omdb-setup')) return true; const qs = new URLSearchParams(location.search); return qs.get('amc-omdb-setup') === '1'; } catch { return false; } } function renderSetupPage() { const existing = document.getElementById('amc-omdb-setup-root'); if (existing) return; const root = document.createElement('div'); root.id = 'amc-omdb-setup-root'; root.setAttribute('style', ` position: fixed; inset: 0; background: #0b1020; color: #fff; z-index: 2147483647; display: flex; align-items: center; justify-content: center; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; `); const panel = document.createElement('div'); panel.setAttribute('style', ` width: min(560px, 92vw); background: #121a34; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,.5); padding: 24px; border: 1px solid rgba(255,255,255,.08); `); panel.innerHTML = ` <div style="display:flex; align-items:center; gap:12px; margin-bottom:16px;"> <div style="width:36px; height:36px; border-radius:8px; background:#2a5298; display:flex; align-items:center; justify-content:center; font-weight:800;">AMC</div> <div style="font-size:18px; font-weight:700;">AMC Rating Helper – Setup</div> </div> <div style="font-size:14px; opacity:.9; margin-bottom:16px;"> Enter your free OMDb API key to enable IMDb and Rotten Tomatoes ratings. Get one at <a href="https://www.omdbapi.com/apikey.aspx" target="_blank" style="color:#4ea1ff; text-decoration:underline;">omdbapi.com</a>. </div> <label style="display:block; font-size:12px; opacity:.8; margin-bottom:6px;">OMDb API Key</label> <input id="amc-omdb-key-input" type="text" placeholder="e.g. abcd1234" style="width:100%; box-sizing:border-box; padding:10px 12px; border-radius:10px; border:1px solid rgba(255,255,255,.14); background:#0e152b; color:#fff; outline:none;" /> <div style="display:flex; gap:8px; justify-content:flex-end; margin-top:16px;"> <button id="amc-omdb-cancel" style="padding:8px 12px; border-radius:10px; background:#263154; color:#fff; border:1px solid rgba(255,255,255,.1); cursor:pointer;">Cancel</button> <button id="amc-omdb-save" style="padding:8px 12px; border-radius:10px; background:#2a7ade; color:#fff; border:1px solid #2a7ade; cursor:pointer; font-weight:700;">Save & Apply</button> </div> `; root.appendChild(panel); document.documentElement.appendChild(root); const input = panel.querySelector('#amc-omdb-key-input'); input.value = getStoredApiKey(); const close = () => { if (root && root.parentNode) root.parentNode.removeChild(root); // Clean URL hash if used if (location.hash && location.hash.includes('amc-omdb-setup')) { try { history.replaceState(null, '', location.pathname + location.search); } catch {} } }; panel.querySelector('#amc-omdb-cancel').addEventListener('click', close); panel.querySelector('#amc-omdb-save').addEventListener('click', () => { const val = (input.value || '').trim(); if (!val) { input.focus(); input.style.borderColor = '#d9534f'; return; } setStoredApiKey(val); close(); // Optionally refresh to apply immediately location.reload(); }); } // Ensure a menu command to open setup try { GM_registerMenuCommand('AMC Ratings: Set OMDb API Key…', () => { // Use current tab: set hash and render try { location.hash = '#amc-omdb-setup'; } catch {} if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', renderSetupPage, { once: true }); } else { renderSetupPage(); } }); } catch {} // First-run onboarding: open setup in current tab if key missing try { const hasKey = !!getStoredApiKey(); if (!hasKey) { try { location.hash = '#amc-omdb-setup'; } catch {} if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', renderSetupPage, { once: true }); } else { renderSetupPage(); } } } catch {} // If in setup mode, render setup UI if (isSetupMode()) { // Defer to ensure DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', renderSetupPage); } else { renderSetupPage(); } } // Use stored key for requests function getApiKey() { const key = getStoredApiKey(); return key && key.length > 0 ? key : null; } // Rate limiting to avoid overwhelming APIs const requestQueue = []; const maxConcurrentRequests = 10; // Increased for faster processing let activeRequests = 0; // Cache for storing ratings to avoid repeated API calls const ratingsCache = new Map(); const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds // Cache management functions function getCacheKey(title) { return cleanTitle(title).toLowerCase().trim(); } function getCachedRatings(title) { const key = getCacheKey(title); const cached = ratingsCache.get(key); if (cached && (Date.now() - cached.timestamp) < CACHE_DURATION) { return cached.data; } return null; } function setCachedRatings(title, ratings) { const key = getCacheKey(title); ratingsCache.set(key, { data: ratings, timestamp: Date.now() }); } function clearExpiredCache() { const now = Date.now(); for (const [key, value] of ratingsCache.entries()) { if (now - value.timestamp >= CACHE_DURATION) { ratingsCache.delete(key); } } } // Rate limiting function function rateLimitedRequest(requestFn) { return new Promise((resolve, reject) => { requestQueue.push({ requestFn, resolve, reject }); processQueue(); }); } function processQueue() { if (activeRequests >= maxConcurrentRequests || requestQueue.length === 0) { return; } activeRequests++; const { requestFn, resolve, reject } = requestQueue.shift(); requestFn() .then(resolve) .catch(reject) .finally(() => { activeRequests--; processQueue(); }); } function cleanTitle(title) { let clean = title.trim(); // General removals clean = clean.replace(/\bQ&A.*$/i, ""); clean = clean.split("/")[0]; clean = clean.replace(/[-–]\s*studio ghibli fest\s*\d{4}/gi, ""); clean = clean.replace(/\bstudio ghibli fest\s*\d{4}/gi, ""); clean = clean.replace(/\bstudio ghibli fest\b/gi, ""); clean = clean.replace(/[-–]\s*\d+(st|nd|rd|th)?\s+anniversary\b/gi, ""); clean = clean.replace(/\b\d+(st|nd|rd|th)?\s+anniversary\b/gi, ""); clean = clean.replace(/\bunrated\b/gi, ""); clean = clean.replace(/\b4k\b/gi, ""); clean = clean.replace(/\b3d\b/gi, ""); clean = clean.replace(/\bfathom\s*\d{4}\b/gi, ""); clean = clean.replace(/\bfathom\b/gi, ""); clean = clean.replace(/\bopening night fan event\b/gi, ""); clean = clean.replace(/\bopening night\b/gi, ""); clean = clean.replace(/\bfan event\b/gi, ""); clean = clean.replace(/\bspecial screening\b/gi, ""); clean = clean.replace(/\bdouble feature\b/gi, ""); clean = clean.replace(/\([^)]*\)/g, ""); // Targeted cleanups const cutPhrases = [ " - Opening Weekend Event", "Private Theatre", "Early Access", "Sneak Peak", "IMAX", "Sensory Friendly Screening" ]; for (let phrase of cutPhrases) { const regex = new RegExp(`[:\\-]?\\s*${phrase}.*$`, "i"); clean = clean.replace(regex, ""); } return clean.trim(); } // Helper function to parse OMDb data consistently function parseOMDbData(data) { const imdb = data.imdbRating || null; const imdbId = data.imdbID || null; const rottenTomatoes = data.Ratings?.find(r => r.Source === 'Rotten Tomatoes')?.Value || null; const boxOffice = data.BoxOffice || null; // Extract additional movie details for tooltip const movieDetails = { title: data.Title, year: data.Year, rated: data.Rated, released: data.Released, runtime: data.Runtime, genre: data.Genre, director: data.Director, writer: data.Writer, actors: data.Actors, plot: data.Plot, language: data.Language, country: data.Country, awards: data.Awards, poster: data.Poster, metascore: data.Metascore, imdbVotes: data.imdbVotes, type: data.Type, dvd: data.DVD, production: data.Production, website: data.Website }; return { imdb, imdbId, rottenTomatoes, boxOffice, movieDetails }; } // Create search-friendly title variants function createSearchVariants(title) { const cleaned = cleanTitle(title); const variants = [cleaned]; // Remove subtitles const noSubtitle = cleaned.split(':')[0].trim(); if (noSubtitle !== cleaned) variants.push(noSubtitle); // Remove "The" from beginning if (cleaned.startsWith('The ')) variants.push(cleaned.substring(4)); // Remove descriptive words const simplified = cleaned.replace(/\b(movie|film|the movie|infinity castle)\b/gi, '').trim(); if (simplified !== cleaned && simplified.length > 3) variants.push(simplified); return [...new Set(variants)]; } // Search for movie using OMDb search API function searchOMDbMovie(title) { const variants = createSearchVariants(title); async function tryVariant(index) { if (index >= variants.length) return null; const API_KEY = getApiKey(); if (!API_KEY) return null; const searchTitle = variants[index]; return new Promise((resolve) => { const searchUrl = `https://www.omdbapi.com/?s=${encodeURIComponent(searchTitle)}&apikey=${API_KEY}&type=movie`; GM_xmlhttpRequest({ method: 'GET', url: searchUrl, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.Response === 'True' && data.Search && data.Search.length > 0) { resolve(data.Search[0].imdbID); } else { tryVariant(index + 1).then(resolve); } } catch (error) { tryVariant(index + 1).then(resolve); } }, onerror: function() { tryVariant(index + 1).then(resolve); } }); }); } return rateLimitedRequest(() => tryVariant(0)); } // Fetch detailed movie info by IMDb ID function fetchOMDbByID(imdbId) { return rateLimitedRequest(() => { return new Promise((resolve) => { const API_KEY = getApiKey(); if (!API_KEY) { resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null }); return; } const url = `https://www.omdbapi.com/?i=${imdbId}&apikey=${API_KEY}`; GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.Response === 'True') { resolve(parseOMDbData(data)); } else { resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null }); } } catch (error) { resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null }); } }, onerror: function() { resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null }); } }); }); }); } // Fetch ratings from OMDb API with search fallback function fetchOMDbRatings(title) { const cached = getCachedRatings(title); if (cached && cached.omdb) { return Promise.resolve(cached.omdb); } return rateLimitedRequest(() => { return new Promise((resolve) => { const API_KEY = getApiKey(); if (!API_KEY) { console.warn('OMDb API key not set. Open the menu "AMC Ratings: Set OMDb API Key…" to configure.'); resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null }); return; } const directUrl = `https://www.omdbapi.com/?t=${encodeURIComponent(title)}&apikey=${API_KEY}`; GM_xmlhttpRequest({ method: 'GET', url: directUrl, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.Response === 'True') { const ratings = parseOMDbData(data); const existingCache = getCachedRatings(title) || {}; setCachedRatings(title, { ...existingCache, omdb: ratings }); resolve(ratings); } else { searchOMDbMovie(title).then(imdbId => { if (imdbId) { fetchOMDbByID(imdbId).then(ratings => { const existingCache = getCachedRatings(title) || {}; setCachedRatings(title, { ...existingCache, omdb: ratings }); resolve(ratings); }).catch(() => { resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null }); }); } else { resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null }); } }).catch(() => { resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null }); }); } } catch (error) { resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null }); } }, onerror: function() { resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null }); } }); }); }); } // Rotten Tomatoes slug function makeRtSlug(title) { let clean = cleanTitle(title).toLowerCase(); clean = clean.replace(/&/g, " and "); clean = clean.replace(/[':;!?,.\-–]/g, ""); clean = clean.replace(/\s+/g, "_"); clean = clean.replace(/^_+|_+$/g, ""); return clean.trim(); } // Create rating display element function createRatingElement(service, rating, url, movieDetails = null) { const container = document.createElement("div"); container.className = `${service}-rating`; container.style.cssText = ` display: inline-flex; align-items: center; margin-left: 8px; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; text-decoration: none; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.2); position: relative; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; pointer-events: auto; `; if (url) { container.onclick = (e) => { e.stopPropagation(); e.preventDefault(); window.open(url, '_blank'); }; } // Prevent event bubbling to parent elements container.onmousedown = (e) => e.stopPropagation(); container.onmouseup = (e) => e.stopPropagation(); // Simplified hover handling - tooltip won't interfere due to pointer-events: none let showTimeout = null; let hideTimeout = null; let isShowing = false; container.addEventListener('mouseenter', (e) => { e.stopPropagation(); if (isShowing) return; // Clear any pending hide timeout if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; } if (movieDetails) { showTimeout = setTimeout(() => { if (!isShowing) { showTooltip(container, movieDetails); isShowing = true; } }, 150); // Reduced delay for faster response } }); container.addEventListener('mouseleave', (e) => { e.stopPropagation(); // Clear any pending show timeout if (showTimeout) { clearTimeout(showTimeout); showTimeout = null; } if (movieDetails && isShowing) { hideTimeout = setTimeout(() => { hideTooltip(container); isShowing = false; }, 200); } }); // Set colors and content based on service switch (service) { case 'imdb': container.style.backgroundColor = '#f5c518'; container.style.color = '#000'; container.innerHTML = `IMDb: ${rating || 'N/A'}`; break; case 'rotten': container.style.backgroundColor = '#d92323'; container.style.color = '#fff'; container.innerHTML = `🍅 ${rating || 'N/A'}`; break; } return container; } // Create loading indicator function createLoadingElement() { const loading = document.createElement("div"); loading.className = "ratings-loading"; loading.style.cssText = ` display: inline-flex; align-items: center; margin-left: 8px; padding: 4px 8px; border-radius: 12px; font-size: 12px; color: #666; background-color: #f0f0f0; `; loading.textContent = "Loading ratings..."; return loading; } // Helper function to validate box office data function isValidBoxOffice(boxOffice) { if (!boxOffice) { return false; } if (typeof boxOffice !== 'string') { return false; } const trimmed = boxOffice.trim(); if (trimmed === '' || trimmed === 'N/A' || trimmed === 'null' || trimmed === 'undefined') { return false; } // Check if it contains any currency symbols or numbers const isValid = /[\$£€¥₹]|\d/.test(trimmed); return isValid; } // Create box office element function createBoxOfficeElement(boxOffice) { // Clean up box office value let cleanBoxOffice = boxOffice; if (typeof cleanBoxOffice === 'string') { cleanBoxOffice = cleanBoxOffice.trim(); } const container = document.createElement("div"); container.className = "box-office"; container.style.cssText = ` display: inline-flex; align-items: center; margin-left: 8px; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; text-decoration: none; cursor: default; transition: all 0.2s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.2); background-color: #4caf50; color: white; `; container.innerHTML = `💰 ${cleanBoxOffice}`; // Prevent event bubbling container.onmousedown = (e) => e.stopPropagation(); container.onmouseup = (e) => e.stopPropagation(); container.onmouseover = (e) => e.stopPropagation(); container.onmouseout = (e) => e.stopPropagation(); return container; } // Create tooltip element function createTooltip(movieDetails) { if (!movieDetails) return null; const tooltip = document.createElement("div"); tooltip.className = "movie-tooltip"; tooltip.style.cssText = ` position: absolute; background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: white; padding: 16px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); z-index: 9999; max-width: 400px; font-size: 13px; line-height: 1.4; opacity: 1; transform: translateY(0); pointer-events: auto; border: 1px solid rgba(255,255,255,0.1); visibility: visible; display: block; `; // Create tooltip content let content = `<div style="display: flex; gap: 12px; margin-bottom: 12px;">`; // Poster if (movieDetails.poster && movieDetails.poster !== 'N/A') { content += ` <img src="${movieDetails.poster}" style="width: 80px; height: 120px; object-fit: cover; border-radius: 6px; flex-shrink: 0;" onerror="this.style.display='none'"> `; } // Basic info content += ` <div style="flex: 1;"> <div style="font-size: 16px; font-weight: bold; margin-bottom: 8px; color: #ffd700;"> ${movieDetails.title} (${movieDetails.year}) </div> <div style="margin-bottom: 4px;"><strong>Rated:</strong> ${movieDetails.rated || 'N/A'}</div> <div style="margin-bottom: 4px;"><strong>Runtime:</strong> ${movieDetails.runtime || 'N/A'}</div> <div style="margin-bottom: 4px;"><strong>Genre:</strong> ${movieDetails.genre || 'N/A'}</div> <div style="margin-bottom: 4px;"><strong>Released:</strong> ${movieDetails.released || 'N/A'}</div> </div> </div>`; // Plot if (movieDetails.plot && movieDetails.plot !== 'N/A') { content += `<div style="margin-bottom: 12px;"><strong>Plot:</strong><br>${movieDetails.plot}</div>`; } // Crew and cast content += `<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px;">`; if (movieDetails.director && movieDetails.director !== 'N/A') { content += `<div><strong>Director:</strong><br>${movieDetails.director}</div>`; } if (movieDetails.actors && movieDetails.actors !== 'N/A') { const actors = movieDetails.actors.split(',').slice(0, 3).join(', '); content += `<div><strong>Cast:</strong><br>${actors}${movieDetails.actors.split(',').length > 3 ? '...' : ''}</div>`; } content += `</div>`; // Additional info if (movieDetails.awards && movieDetails.awards !== 'N/A') { content += `<div style="margin-bottom: 8px;"><strong>Awards:</strong> ${movieDetails.awards}</div>`; } if (movieDetails.country && movieDetails.country !== 'N/A') { content += `<div style="margin-bottom: 8px;"><strong>Country:</strong> ${movieDetails.country}</div>`; } if (movieDetails.language && movieDetails.language !== 'N/A') { content += `<div style="margin-bottom: 8px;"><strong>Language:</strong> ${movieDetails.language}</div>`; } tooltip.innerHTML = content; // Tooltip has pointer-events: none, so no mouse handlers needed return tooltip; } // Show tooltip function showTooltip(element, movieDetails) { if (!movieDetails) { return; } // Check if tooltip already exists if (element._tooltip) { return; } // Hide any existing tooltip first hideTooltip(element); const tooltip = createTooltip(movieDetails); if (!tooltip) return; // Force a reflow to get accurate dimensions tooltip.offsetHeight; const rect = element.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); // Position tooltip let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); let top = rect.top - tooltipRect.height - 10; // Adjust if tooltip goes off screen if (left < 10) left = 10; if (left + tooltipRect.width > window.innerWidth - 10) { left = window.innerWidth - tooltipRect.width - 10; } if (top < 10) { top = rect.bottom + 10; } tooltip.setAttribute('style', ` position: fixed !important; left: ${left}px !important; top: ${top}px !important; background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%) !important; color: white !important; padding: 16px !important; border-radius: 12px !important; box-shadow: 0 8px 32px rgba(0,0,0,0.3) !important; z-index: 999999 !important; max-width: 400px !important; font-size: 13px !important; line-height: 1.4 !important; opacity: 1 !important; visibility: visible !important; display: block !important; border: 1px solid rgba(255,255,255,0.1) !important; pointer-events: none !important; `); // Append to documentElement for maximum compatibility document.documentElement.appendChild(tooltip); // Store reference for cleanup element._tooltip = tooltip; } // Hide tooltip function hideTooltip(element) { if (element._tooltip) { element._tooltip.style.opacity = '0'; element._tooltip.style.transform = 'translateY(10px)'; setTimeout(() => { if (element._tooltip && element._tooltip.parentNode) { element._tooltip.parentNode.removeChild(element._tooltip); } element._tooltip = null; // Reset hovering state element.isHovering = false; }, 300); } } async function addRatings() { const movieTitleEls = document.querySelectorAll(".md\\:text-2xl.font-bold, h3 > a.headline, h1.headline"); movieTitleEls.forEach(async (movieTitleEl) => { const rawTitle = movieTitleEl.textContent.trim(); if (!rawTitle || /amc/i.test(rawTitle)) return; if (movieTitleEl.querySelector(".ratings-container")) return; const cleanTitleText = cleanTitle(rawTitle); // Check if we have all ratings cached const cached = getCachedRatings(cleanTitleText); const hasAllCached = cached && cached.omdb && (cached.omdb.imdb !== null || cached.omdb.rottenTomatoes !== null); // Create container for all ratings const ratingsContainer = document.createElement("div"); ratingsContainer.className = "ratings-container"; ratingsContainer.style.cssText = ` display: inline-flex; align-items: center; margin-left: 8px; gap: 4px; position: relative; z-index: 10; `; // Prevent event bubbling to parent elements (AMC navigation) ratingsContainer.onmousedown = (e) => e.stopPropagation(); ratingsContainer.onmouseup = (e) => e.stopPropagation(); ratingsContainer.onclick = (e) => e.stopPropagation(); // If we have cached data, show it immediately if (hasAllCached) { // Use IMDb ID if available, otherwise fall back to search const imdbUrl = cached.omdb.imdbId ? `https://www.imdb.com/title/${cached.omdb.imdbId}/` : `https://www.imdb.com/find/?q=${encodeURIComponent(cleanTitleText)}`; const rtSlug = makeRtSlug(rawTitle); const rtUrl = `https://www.rottentomatoes.com/m/${rtSlug}`; // Add IMDb rating const imdbEl = createRatingElement('imdb', cached.omdb.imdb, imdbUrl, cached.omdb.movieDetails); ratingsContainer.appendChild(imdbEl); // Add Rotten Tomatoes rating const rtEl = createRatingElement('rotten', cached.omdb.rottenTomatoes, rtUrl, cached.omdb.movieDetails); ratingsContainer.appendChild(rtEl); // Add box office if available if (isValidBoxOffice(cached.omdb.boxOffice)) { const boxOfficeEl = createBoxOfficeElement(cached.omdb.boxOffice); ratingsContainer.appendChild(boxOfficeEl); } movieTitleEl.appendChild(ratingsContainer); return; } // Add loading indicator for non-cached data const loadingEl = createLoadingElement(); ratingsContainer.appendChild(loadingEl); movieTitleEl.appendChild(ratingsContainer); try { // Fetch ratings const omdbRatings = await fetchOMDbRatings(cleanTitleText); // Remove loading indicator ratingsContainer.removeChild(loadingEl); // Create rating elements // Use IMDb ID if available, otherwise fall back to search const imdbUrl = omdbRatings.imdbId ? `https://www.imdb.com/title/${omdbRatings.imdbId}/` : `https://www.imdb.com/find/?q=${encodeURIComponent(cleanTitleText)}`; const rtSlug = makeRtSlug(rawTitle); const rtUrl = `https://www.rottentomatoes.com/m/${rtSlug}`; // Add IMDb rating const imdbEl = createRatingElement('imdb', omdbRatings.imdb, imdbUrl, omdbRatings.movieDetails); ratingsContainer.appendChild(imdbEl); // Add Rotten Tomatoes rating const rtEl = createRatingElement('rotten', omdbRatings.rottenTomatoes, rtUrl, omdbRatings.movieDetails); ratingsContainer.appendChild(rtEl); // Add box office if available if (isValidBoxOffice(omdbRatings.boxOffice)) { const boxOfficeEl = createBoxOfficeElement(omdbRatings.boxOffice); ratingsContainer.appendChild(boxOfficeEl); } } catch (error) { // Remove loading and show error ratingsContainer.removeChild(loadingEl); const errorEl = document.createElement("div"); errorEl.style.cssText = ` display: inline-flex; align-items: center; margin-left: 8px; padding: 4px 8px; border-radius: 12px; font-size: 12px; color: #d32f2f; background-color: #ffebee; `; errorEl.textContent = "Error loading ratings"; ratingsContainer.appendChild(errorEl); } }); } // Clean up tooltips function cleanupTooltips() { const tooltips = document.querySelectorAll('.movie-tooltip'); tooltips.forEach(tooltip => { if (tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); } }); } // Initialize script clearExpiredCache(); // Clean up expired cache entries on startup addRatings(); const observer = new MutationObserver(() => { cleanupTooltips(); // Clean up tooltips when DOM changes addRatings(); }); observer.observe(document.body, { childList: true, subtree: true }); })();