Last active
November 5, 2026 22:42
-
-
Save zhaocai/3d0db6a39abde677fb6eaf829ef27b06 to your computer and use it in GitHub Desktop.
在影巢影视详情页缺少豆瓣信息时,通过 IMDb ID 或片名自动反查并无缝补全豆瓣链接与评分
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name 影巢影视详情页自动补全豆瓣信息 | |
| // @namespace zhaowu | |
| // @version 1.2.3 | |
| // @description 在影巢影视详情页缺少豆瓣或IMDb信息时自动补全链接与评分,并在 IMDb 显示 0.0 异常时自动反查修正真实评分 | |
| // @author zhaowu | |
| // @icon data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="%2300B51D" d="M1003.52 872.448c0 72.39-58.68 131.072-131.072 131.072H151.552c-72.39 0-131.072-58.68-131.072-131.072V151.552c0-72.39 58.68-131.072 131.072-131.072h720.896c72.39 0 131.072 58.68 131.072 131.072v720.896z"/><path fill="%23FFF" d="M822.6 184.32H201.4a11.5 11.5 0 0 0-11.5 11.5v46a11.5 11.5 0 0 0 11.5 11.5h621.2a11.5 11.5 0 0 0 11.5-11.5v-46a11.5 11.5 0 0 0-11.5-11.5zm0 586.33H663.84l47.46-150.1h58.89a11.5 11.5 0 0 0 11.5-11.5V330.41a11.5 11.5 0 0 0-11.5-11.5H253.82a11.5 11.5 0 0 0-11.5 11.5v278.64a11.5 11.5 0 0 0 11.5 11.5h364.18l-47.45 150.1H439.32l-34.8-110.05a11.5 11.5 0 0 0-15.14-11.5H319.06c-6.36 0-9.88 5.14-7.86 11.5l34.8 110.05H201.4a11.5 11.5 0 0 0-11.5 11.5v46a11.5 11.5 0 0 0 11.5 11.5h621.2a11.5 11.5 0 0 0 11.5-11.5v-46a11.5 11.5 0 0 0-11.5-11.5zM351.6 536.83V402.62a11.5 11.5 0 0 1 11.5-11.5h297.81a11.5 11.5 0 0 1 11.5 11.5v134.21a11.5 11.5 0 0 1-11.5 11.5H363.09a11.5 11.5 0 0 1-11.5-11.5z"/></svg> | |
| // @downloadURL https://gist.githubusercontent.com/zhaocai/3d0db6a39abde677fb6eaf829ef27b06/raw/%E5%BD%B1%E5%B7%A2%E5%BD%B1%E8%A7%86%E8%AF%A6%E6%83%85%E9%A1%B5%E8%87%AA%E5%8A%A8%E8%A1%A5%E5%85%A8%E8%B1%86%E7%93%A3%E4%BF%A1%E6%81%AF.user.js | |
| // @updateURL https://gist.githubusercontent.com/zhaocai/3d0db6a39abde677fb6eaf829ef27b06/raw/%E5%BD%B1%E5%B7%A2%E5%BD%B1%E8%A7%86%E8%AF%A6%E6%83%85%E9%A1%B5%E8%87%AA%E5%8A%A8%E8%A1%A5%E5%85%A8%E8%B1%86%E7%93%A3%E4%BF%A1%E6%81%AF.user.js | |
| // @match https://re0.me/* | |
| // @match https://hdhive.com/* | |
| // @grant GM_xmlhttpRequest | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @connect douban.com | |
| // @connect imdb.com | |
| // @connect www.imdb.com | |
| // @connect media-imdb.com | |
| // @connect v3.sg.media-imdb.com | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const CACHE_KEY_PREFIX = 'hdhive_douban_'; | |
| const IMDB_CACHE_PREFIX = 'hdhive_imdb_fix_'; | |
| const IMDB_SEARCH_CACHE_PREFIX = 'hdhive_imdb_search_'; | |
| const DEFAULT_TTL = 7 * 24 * 60 * 60 * 1000; | |
| const NEGATIVE_TTL = 2 * 24 * 60 * 60 * 1000; // 负缓存 2 天 | |
| const fetchingIds = new Set(); | |
| const fetchingImdbIds = new Set(); | |
| const fetchingImdbSearches = new Set(); | |
| function log(...args) { | |
| console.log('[影巢评分增强]', ...args); | |
| } | |
| // 智能分级缓存计算:老片(上映年份 < 当年)缓存 90 天,新片 7 天 | |
| function calculateTTL(year) { | |
| const currentYear = new Date().getFullYear(); | |
| const y = parseInt(year, 10); | |
| if (!isNaN(y) && y < currentYear) { | |
| return 90 * 24 * 60 * 60 * 1000; // 老片缓存 90 天 | |
| } | |
| return DEFAULT_TTL; // 新片或当年电影缓存 7 天 | |
| } | |
| // 跨脚本共享缓存工具(GM_getValue + localStorage 双写双读) | |
| function getCache(key) { | |
| let raw = null; | |
| try { raw = GM_getValue(key, null); } catch (e) {} | |
| if (!raw) { | |
| try { raw = localStorage.getItem(key); } catch (e) {} | |
| } | |
| if (!raw) return null; | |
| try { | |
| const cached = JSON.parse(raw); | |
| const ttl = cached.ttl || DEFAULT_TTL; | |
| if (Date.now() - cached.timestamp < ttl) { | |
| return cached.data; | |
| } | |
| } catch (e) {} | |
| return null; | |
| } | |
| function setCache(key, data, ttl) { | |
| const val = JSON.stringify({ timestamp: Date.now(), data, ttl: ttl || DEFAULT_TTL }); | |
| try { GM_setValue(key, val); } catch (e) {} | |
| try { localStorage.setItem(key, val); } catch (e) {} | |
| } | |
| // 格式化投票人数,如 53186 -> 53.2k | |
| function formatCount(rawCount) { | |
| const clean = String(rawCount).replace(/,/g, '').trim(); | |
| const num = parseInt(clean, 10); | |
| if (isNaN(num)) return rawCount; | |
| if (num >= 1000) { | |
| return (num / 1000).toFixed(1) + 'k'; | |
| } | |
| return num.toString(); | |
| } | |
| // 提取原始标题 / 外文原名 | |
| function extractOriginalTitle() { | |
| const container = document.querySelector('[class*="MediaInfo"], [class*="detail"], [class*="intro"], header') || document.body; | |
| const allElements = container.querySelectorAll('*'); | |
| for (const el of allElements) { | |
| if (el.children.length <= 1) { | |
| const text = el.textContent.trim(); | |
| const m = text.match(/^(?:原始标题|原名|Original\s*Title)[::]\s*(.+)$/i); | |
| if (m && m[1]) return m[1].trim(); | |
| } | |
| } | |
| for (const el of allElements) { | |
| if (el.textContent.includes('原始标题') || el.textContent.includes('原名')) { | |
| const m = el.textContent.match(/(?:原始标题|原名)[::]\s*([^\n\r\t]+)/); | |
| if (m && m[1]) return m[1].trim(); | |
| } | |
| } | |
| return ''; | |
| } | |
| // 提取当前详情页关键容器与数据 | |
| function getPageInfo() { | |
| // 查找评分栏容器 | |
| const container = document.querySelector( | |
| '.MediaInfo_ratings__Ym3j4, [class*="MediaInfo_ratings"], [class*="ratings__"]' | |
| ); | |
| if (!container) return null; | |
| // 是否已有豆瓣链接或已补全 | |
| const hasDouban = !!container.querySelector( | |
| 'a[href*="douban.com"], [data-autofill-douban="true"]' | |
| ); | |
| // 提取 IMDb 链接元素与 ID | |
| const imdbLinkEl = container.querySelector('a[href*="imdb.com"]'); | |
| const autofillImdbEl = container.querySelector('[data-autofill-imdb="true"]'); | |
| const hasImdb = !!(imdbLinkEl || autofillImdbEl); | |
| const imdbLink = imdbLinkEl?.href || autofillImdbEl?.href; | |
| const imdbMatch = imdbLink?.match(/tt\d+/); | |
| const imdbId = imdbMatch ? imdbMatch[0] : null; | |
| // 提取标题与年份 | |
| const titleEl = document.querySelector('h1'); | |
| const rawTitle = titleEl ? titleEl.textContent.trim() : document.title; | |
| const cleanTitle = rawTitle.replace(/\s*\(?\d{4}\)?.*$/, '').trim(); | |
| const yearMatch = rawTitle.match(/\b(19\d\d|20\d\d)\b/) || | |
| document.querySelector('[class*="meta"], [class*="year"]')?.textContent.match(/\b(19\d\d|20\d\d)\b/); | |
| const year = yearMatch ? yearMatch[1] : ''; | |
| // 提取原始标题与影视类型 (是否为电视剧) | |
| const originalTitle = extractOriginalTitle(); | |
| const isTv = location.pathname.includes('/tv/') || location.href.includes('/tv/'); | |
| return { container, imdbId, imdbLinkEl, cleanTitle, originalTitle, year, isTv, hasDouban, hasImdb }; | |
| } | |
| // 从豆瓣搜索结果解析评分条目 | |
| function parseDoubanSearch(html) { | |
| if (!html) return null; | |
| const doc = new DOMParser().parseFromString(html, 'text/html'); | |
| // 查找影视类别下的第一个结果条目 | |
| const linkEl = doc.querySelector( | |
| '.result .title a[href*="movie.douban.com%2Fsubject"], .result .title a[href*="subject"]' | |
| ); | |
| if (!linkEl) return null; | |
| let subjectUrl = linkEl.getAttribute('href') || ''; | |
| // 解码 link2 重定向地址 | |
| if (subjectUrl.includes('url=')) { | |
| try { | |
| const rawUrl = new URL(subjectUrl, 'https://www.douban.com').searchParams.get('url'); | |
| if (rawUrl) subjectUrl = rawUrl; | |
| } catch (e) { | |
| const m = subjectUrl.match(/url=([^&]+)/); | |
| if (m) subjectUrl = decodeURIComponent(m[1]); | |
| } | |
| } | |
| // 提取 subject id 并规范化 URL | |
| const sidMatch = subjectUrl.match(/subject\/(\d+)/); | |
| if (!sidMatch) return null; | |
| const finalUrl = `https://movie.douban.com/subject/${sidMatch[1]}/`; | |
| // 提取评分 | |
| const resultItem = linkEl.closest('.result'); | |
| const ratingEl = resultItem?.querySelector('.rating_nums'); | |
| let score = ratingEl ? ratingEl.textContent.trim() : ''; | |
| // 如果没有数字评分,从条目文本中提取豆瓣原状态(如:评价人数不足、尚未上映、暂无评分) | |
| if (!score && resultItem) { | |
| const rawText = resultItem.textContent; | |
| const statusMatch = rawText.match(/\((评价人数不足|尚未上映|暂无评分|暂无)\)/); | |
| if (statusMatch) { | |
| score = statusMatch[1] === '暂无' ? '暂无评分' : statusMatch[1]; | |
| } else { | |
| score = '暂无评分'; | |
| } | |
| } | |
| if (!score) score = '暂无评分'; | |
| // 提取评价人数 | |
| let countText = ''; | |
| if (resultItem) { | |
| const countMatch = resultItem.textContent.match(/\(([\d,]+)\s*人评价\)/); | |
| if (countMatch) { | |
| countText = formatCount(countMatch[1]); | |
| } | |
| } | |
| return { | |
| url: finalUrl, | |
| score, | |
| count: countText | |
| }; | |
| } | |
| // 豆瓣官方搜索请求 | |
| function queryDouban(keyword) { | |
| return new Promise((resolve, reject) => { | |
| const searchUrl = `https://www.douban.com/search?cat=1002&q=${encodeURIComponent(keyword)}`; | |
| GM_xmlhttpRequest({ | |
| method: 'GET', | |
| url: searchUrl, | |
| headers: { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' | |
| }, | |
| onload: (res) => { | |
| if (res.status >= 200 && res.status < 300) { | |
| const data = parseDoubanSearch(res.responseText); | |
| resolve(data); | |
| } else { | |
| reject(new Error(`HTTP ${res.status}`)); | |
| } | |
| }, | |
| onerror: reject, | |
| ontimeout: () => reject(new Error('请求超时')) | |
| }); | |
| }); | |
| } | |
| // 创建原生同款视觉的豆瓣评分元素 | |
| function createDoubanElement(data, queryKey) { | |
| const a = document.createElement('a'); | |
| a.target = '_blank'; | |
| a.rel = 'noopener noreferrer'; | |
| a.style.textDecoration = 'none'; | |
| let targetUrl = (data && data.url && typeof data.url === 'string' && data.url.startsWith('http')) ? data.url : ''; | |
| if (!targetUrl && queryKey) { | |
| targetUrl = `https://www.douban.com/search?cat=1002&q=${encodeURIComponent(queryKey)}`; | |
| } | |
| a.href = targetUrl || 'https://movie.douban.com/'; | |
| a.setAttribute('data-autofill-douban', 'true'); | |
| a.title = '豆瓣评分 (脚本自动匹配)'; | |
| const itemDiv = document.createElement('div'); | |
| itemDiv.setAttribute('data-rating-item', ''); | |
| itemDiv.className = 'hdhive-un7kiq'; | |
| itemDiv.style.cssText = 'display:inline-flex;align-items:center;gap:6px;'; | |
| let displayScore = data.score; | |
| if (!displayScore || displayScore === '暂无') { | |
| displayScore = '暂无评分'; | |
| } | |
| const isNumeric = /^\d+(\.\d+)?$/.test(displayScore); | |
| const scoreText = isNumeric ? `${displayScore}/10` : displayScore; | |
| const scoreStyle = isNumeric | |
| ? 'font-weight:700;color:#00b81e;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;' | |
| : 'font-weight:normal;color:#00b81e;font-size:13px;letter-spacing:0.2px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;'; | |
| if (isNumeric) { | |
| a.title = `豆瓣评分: ${displayScore}/10${data.count ? ' (' + data.count + '人评价)' : ''} · 点击前往豆瓣详情页`; | |
| } else { | |
| a.title = `豆瓣状态: ${displayScore} · 点击前往豆瓣搜索`; | |
| } | |
| // 豆瓣官方矢量图标 SVG (拟物弧光径向微光) | |
| itemDiv.innerHTML = ` | |
| <svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20" data-rating-logo="douban" style="flex-shrink:0;"> | |
| <defs> | |
| <radialGradient id="hdhive-db-glow" cx="50%" cy="20%" r="75%"> | |
| <stop offset="0%" stop-color="#009919"></stop> | |
| <stop offset="65%" stop-color="#007013"></stop> | |
| <stop offset="100%" stop-color="#004a0c"></stop> | |
| </radialGradient> | |
| </defs> | |
| <path d="M1003.52 872.448c0 72.38997333-58.68202667 131.072-131.072 131.072H151.552c-72.38997333 0-131.072-58.68202667-131.072-131.072V151.552c0-72.38997333 58.68202667-131.072 131.072-131.072h720.896c72.38997333 0 131.072 58.68202667 131.072 131.072v720.896z" fill="url(#hdhive-db-glow)"></path> | |
| <path d="M822.59694933 184.32H201.40305067a11.501568 11.501568 0 0 0-11.501568 11.501568v46.01173333c0 6.35153067 5.15003733 11.501568 11.501568 11.501568h621.19389866a11.501568 11.501568 0 0 0 11.501568-11.501568v-46.01173333A11.501568 11.501568 0 0 0 822.59694933 184.32zM822.59694933 770.654208h-158.75549866l47.45352533-150.09928533h58.88955733a11.501568 11.501568 0 0 0 11.501568-11.501568V330.41066667a11.49610667 11.49610667 0 0 0-11.501568-11.501568h-516.36906666a11.501568 11.501568 0 0 0-11.501568 11.501568v278.642688c0 6.35153067 5.144576 11.501568 11.501568 11.501568h364.183552l-47.45898667 150.09928533h-131.23584l-34.799616-110.051328c-2.00430933-6.356992-8.781824-11.501568-15.13335467-11.501568H319.05655467c-6.35153067 0-9.87409067 5.144576-7.85885867 11.501568l34.79415467 110.051328H201.40305067a11.501568 11.501568 0 0 0-11.501568 11.501568v46.01719467c0 6.35153067 5.15003733 11.501568 11.501568 11.501568h621.19389866a11.501568 11.501568 0 0 0 11.501568-11.501568v-46.01719467a11.501568 11.501568 0 0 0-11.501568-11.501568z m-471.007232-233.82152533V402.620416c0-6.35153067 5.144576-11.501568 11.501568-11.501568h297.811968c6.35153067 0 11.501568 5.15549867 11.501568 11.501568v134.21226667a11.501568 11.501568 0 0 1-11.501568 11.501568H363.09128533a11.501568 11.501568 0 0 1-11.501568-11.501568z" fill="#FFFFFF"></path> | |
| </svg> | |
| <span style="${scoreStyle}">${scoreText}</span> | |
| ${data.count ? `<span data-rating-count="" style="color:rgba(255,255,255,0.45);font-size:12px;margin-left:2px;">(${data.count})</span>` : ''} | |
| `; | |
| a.appendChild(itemDiv); | |
| return a; | |
| } | |
| // IMDb 官方结构化数据抓取与解析 | |
| function queryIMDb(imdbId) { | |
| return new Promise((resolve, reject) => { | |
| const url = `https://www.imdb.com/title/${imdbId}/`; | |
| GM_xmlhttpRequest({ | |
| method: 'GET', | |
| url: url, | |
| headers: { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', | |
| 'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' | |
| }, | |
| onload: (res) => { | |
| if (res.status >= 200 && res.status < 300) { | |
| try { | |
| const html = res.responseText; | |
| // 1. 优先从 ld+json 解析 | |
| const ldMatch = html.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/i); | |
| if (ldMatch && ldMatch[1]) { | |
| try { | |
| const json = JSON.parse(ldMatch[1]); | |
| const agg = json.aggregateRating; | |
| if (agg && agg.ratingValue) { | |
| resolve({ | |
| score: parseFloat(agg.ratingValue).toFixed(1), | |
| count: agg.ratingCount ? formatCount(agg.ratingCount) : '' | |
| }); | |
| return; | |
| } | |
| } catch (e) { | |
| // json 解析失败,尝试正则兜底 | |
| } | |
| } | |
| // 2. 正则兜底 | |
| const scoreMatch = html.match(/"ratingValue":\s*"?([\d\.]+)"?/); | |
| const countMatch = html.match(/"ratingCount":\s*(\d+)/); | |
| if (scoreMatch && parseFloat(scoreMatch[1]) > 0) { | |
| resolve({ | |
| score: parseFloat(scoreMatch[1]).toFixed(1), | |
| count: countMatch ? formatCount(countMatch[1]) : '' | |
| }); | |
| return; | |
| } | |
| resolve(null); | |
| } catch (err) { | |
| reject(err); | |
| } | |
| } else { | |
| reject(new Error(`IMDb HTTP ${res.status}`)); | |
| } | |
| }, | |
| onerror: reject, | |
| ontimeout: () => reject(new Error('IMDb 请求超时')) | |
| }); | |
| }); | |
| } | |
| // IMDb 官方搜索建议 API | |
| function searchImdbSuggestion(query) { | |
| return new Promise((resolve) => { | |
| const url = `https://v3.sg.media-imdb.com/suggestion/x/${encodeURIComponent(query)}.json`; | |
| GM_xmlhttpRequest({ | |
| method: 'GET', | |
| url: url, | |
| headers: { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' | |
| }, | |
| onload: (res) => { | |
| if (res.status >= 200 && res.status < 300) { | |
| try { | |
| const json = JSON.parse(res.responseText); | |
| resolve(json.d || []); | |
| } catch (e) { | |
| resolve([]); | |
| } | |
| } else { | |
| resolve([]); | |
| } | |
| }, | |
| onerror: () => resolve([]), | |
| ontimeout: () => resolve([]) | |
| }); | |
| }); | |
| } | |
| // 从候选条目中择优匹配最贴近的影视 | |
| function findBestImdbMatch(items, year, isTv) { | |
| if (!items || items.length === 0) return null; | |
| const y = parseInt(year, 10); | |
| let candidates = items.filter(it => it.id && it.id.startsWith('tt') && (!it.qid || it.qid !== 'person')); | |
| if (candidates.length === 0) return null; | |
| // 若指定了类型,优先考虑类型匹配的候选者 | |
| if (typeof isTv === 'boolean') { | |
| const typeMatched = candidates.filter(it => { | |
| const isSeries = it.qid === 'tvSeries' || it.qid === 'tvMiniSeries' || (it.q && it.q.toLowerCase().includes('tv')); | |
| return isTv ? isSeries : !isSeries; | |
| }); | |
| if (typeMatched.length > 0) { | |
| candidates = typeMatched; | |
| } | |
| } | |
| if (!isNaN(y)) { | |
| // 精确年份匹配 | |
| const exact = candidates.find(it => it.y === y); | |
| if (exact) return exact; | |
| // 差异 <= 1 年 | |
| const close1 = candidates.find(it => Math.abs(it.y - y) <= 1); | |
| if (close1) return close1; | |
| // 差异 <= 2 年 | |
| const close2 = candidates.find(it => Math.abs(it.y - y) <= 2); | |
| if (close2) return close2; | |
| } | |
| return candidates[0]; | |
| } | |
| // 搜索 IMDb ID (优先原始标题,回退中文标题) | |
| async function searchImdbId(cleanTitle, originalTitle, year, isTv) { | |
| let match = null; | |
| if (originalTitle && originalTitle !== cleanTitle) { | |
| const items = await searchImdbSuggestion(originalTitle); | |
| match = findBestImdbMatch(items, year, isTv); | |
| } | |
| if (!match && cleanTitle) { | |
| const items = await searchImdbSuggestion(cleanTitle); | |
| match = findBestImdbMatch(items, year, isTv); | |
| } | |
| return match ? match.id : null; | |
| } | |
| // 创建原生同款视觉的 IMDb 评分元素 | |
| function createImdbElement(imdbId, data) { | |
| const a = document.createElement('a'); | |
| a.target = '_blank'; | |
| a.rel = 'noopener noreferrer'; | |
| a.style.textDecoration = 'none'; | |
| a.href = `https://www.imdb.com/title/${imdbId}/`; | |
| a.setAttribute('data-autofill-imdb', 'true'); | |
| const itemDiv = document.createElement('div'); | |
| itemDiv.setAttribute('data-rating-item', ''); | |
| itemDiv.className = 'hdhive-un7kiq'; | |
| itemDiv.style.cssText = 'display:inline-flex;align-items:center;gap:6px;'; | |
| const displayScore = data.score || '暂无评分'; | |
| const isNumeric = /^\d+(\.\d+)?$/.test(displayScore) && parseFloat(displayScore) > 0; | |
| const scoreText = isNumeric ? `${displayScore}/10` : displayScore; | |
| const scoreStyle = isNumeric | |
| ? 'font-weight:700;color:#e6a800;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;' | |
| : 'font-weight:normal;color:#e6a800;font-size:13px;letter-spacing:0.2px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;'; | |
| if (isNumeric) { | |
| a.title = `IMDb 评分: ${displayScore}/10${data.count ? ' (' + data.count + '人评价)' : ''} · 点击前往 IMDb 详情页`; | |
| } else { | |
| a.title = `IMDb 状态: ${displayScore} · 点击前往 IMDb 详情页`; | |
| } | |
| // 原生同款 IMDb 官方矢量图标 (拟物弧光径向微光) | |
| const imdbSvg = `<svg id="home_img" class="ipc-logo" xmlns="http://www.w3.org/2000/svg" width="40" height="20" viewBox="0 0 64 32" data-rating-logo="imdb" style="flex-shrink:0"><defs><radialGradient id="hdhive-imdb-glow" cx="50%" cy="20%" r="75%"><stop offset="0%" stop-color="#e6a800"></stop><stop offset="65%" stop-color="#ba8300"></stop><stop offset="100%" stop-color="#7a5400"></stop></radialGradient></defs><g fill="url(#hdhive-imdb-glow)"><rect x="0" y="0" width="100%" height="100%" rx="4"></rect></g><g transform="translate(8.000000, 7.000000)" fill="#000000" fill-rule="nonzero"><polygon points="0 18 5 18 5 0 0 0"></polygon><path d="M15.6725178,0 L14.5534833,8.40846934 L13.8582008,3.83502426 C13.65661,2.37009263 13.4632474,1.09175121 13.278113,0 L7,0 L7,18 L11.2416347,18 L11.2580911,6.11380679 L13.0436094,18 L16.0633571,18 L17.7583653,5.8517865 L17.7707076,18 L22,18 L22,0 L15.6725178,0 Z"></path><path d="M24,18 L24,0 L31.8045586,0 C33.5693522,0 35,1.41994415 35,3.17660424 L35,14.8233958 C35,16.5777858 33.5716617,18 31.8045586,18 L24,18 Z M29.8322479,3.2395236 C29.6339219,3.13233348 29.2545158,3.08072342 28.7026524,3.08072342 L28.7026524,14.8914865 C29.4312846,14.8914865 29.8796736,14.7604764 30.0478195,14.4865461 C30.2159654,14.2165858 30.3021941,13.486105 30.3021941,12.2871637 L30.3021941,5.3078959 C30.3021941,4.49404499 30.272014,3.97397442 30.2159654,3.74371416 C30.1599168,3.5134539 30.0348852,3.34671372 29.8322479,3.2395236 Z"></path><path d="M44.4299079,4.50685823 L44.749518,4.50685823 C46.5447098,4.50685823 48,5.91267586 48,7.64486762 L48,14.8619906 C48,16.5950653 46.5451816,18 44.749518,18 L44.4299079,18 C43.3314617,18 42.3602746,17.4736618 41.7718697,16.6682739 L41.4838962,17.7687785 L37,17.7687785 L37,0 L41.7843263,0 L41.7843263,5.78053556 C42.4024982,5.01015739 43.3551514,4.50685823 44.4299079,4.50685823 Z M43.4055679,13.2842155 L43.4055679,9.01907814 C43.4055679,8.31433946 43.3603268,7.85185468 43.2660746,7.63896485 C43.1718224,7.42607505 42.7955881,7.2893916 42.5316822,7.2893916 C42.267776,7.2893916 41.8607934,7.40047379 41.7816216,7.58767002 L41.7816216,9.01907814 L41.7816216,13.4207851 L41.7816216,14.8074788 C41.8721037,15.0130276 42.2602358,15.1274059 42.5316822,15.1274059 C42.8031285,15.1274059 43.1982131,15.0166981 43.281155,14.8074788 C43.3640968,14.5982595 43.4055679,14.0880581 43.4055679,13.2842155 Z"></path></g></svg>`; | |
| itemDiv.innerHTML = ` | |
| ${imdbSvg} | |
| <span style="${scoreStyle}">${scoreText}</span> | |
| ${data.count ? `<span data-rating-count="" style="color:rgba(255,255,255,0.45);font-size:12px;margin-left:2px;">(${data.count})</span>` : ''} | |
| `; | |
| a.appendChild(itemDiv); | |
| return a; | |
| } | |
| // 规范化插入 IMDb 评分元素(保持 TMDB -> IMDb -> 豆瓣 的原生排列顺序) | |
| function insertImdbElement(container, imdbEl) { | |
| if (!container || !imdbEl) return; | |
| if (container.querySelector('a[href*="imdb.com"], [data-autofill-imdb="true"]')) return; | |
| // 1. 若已有豆瓣元素,插入到豆瓣之前 | |
| const doubanEl = container.querySelector('a[href*="douban.com"], [data-autofill-douban="true"]'); | |
| if (doubanEl) { | |
| doubanEl.insertAdjacentElement('beforebegin', imdbEl); | |
| return; | |
| } | |
| // 2. 若有 TMDB 元素,插入到 TMDB 之后 | |
| const tmdbEl = container.querySelector('a[href*="themoviedb.org"], [data-rating-logo="tmdb"]')?.closest('a'); | |
| if (tmdbEl && tmdbEl.nextElementSibling) { | |
| tmdbEl.insertAdjacentElement('afterend', imdbEl); | |
| return; | |
| } | |
| // 3. 否则默认追加到末尾 | |
| container.appendChild(imdbEl); | |
| } | |
| // 渲染纠错后的 IMDb 评分与人数 | |
| function applyImdbFix(imdbLinkEl, data) { | |
| if (!data || !data.score || parseFloat(data.score) === 0) return; | |
| imdbLinkEl.setAttribute('data-imdb-fixed', 'true'); | |
| imdbLinkEl.title = `IMDb 官方真实评分: ${data.score}/10${data.count ? ' (' + data.count + '人评价)' : ''} (已自动纠错)`; | |
| // 寻找分数 span(包含 /10 或纯数字) | |
| const spans = Array.from(imdbLinkEl.querySelectorAll('span')); | |
| const scoreSpan = spans.find(s => /\/10/.test(s.textContent) || /^\d+(\.\d+)?$/.test(s.textContent.trim())); | |
| if (scoreSpan) { | |
| scoreSpan.innerHTML = `${data.score}<!-- -->/10`; | |
| } | |
| // 寻找人数 span(带 data-rating-count 或者包含括号) | |
| let countSpan = imdbLinkEl.querySelector('[data-rating-count]') || spans.find(s => /\(.*\)/.test(s.textContent)); | |
| if (data.count) { | |
| if (countSpan) { | |
| countSpan.innerHTML = `(<!-- -->${data.count}<!-- -->)`; | |
| countSpan.style.display = ''; | |
| } else if (scoreSpan) { | |
| const newCountSpan = document.createElement('span'); | |
| newCountSpan.setAttribute('data-rating-count', ''); | |
| newCountSpan.style.cssText = 'color:rgba(255,255,255,0.45);font-size:12px;margin-left:2px;'; | |
| newCountSpan.innerHTML = `(<!-- -->${data.count}<!-- -->)`; | |
| scoreSpan.insertAdjacentElement('afterend', newCountSpan); | |
| } | |
| } | |
| } | |
| // 自动检测并修正原生 IMDb 0.0 异常 | |
| async function fixImdbScore(imdbLinkEl, imdbId, year) { | |
| if (!imdbLinkEl || !imdbId) return; | |
| if (imdbLinkEl.getAttribute('data-imdb-fixed') === 'true') return; | |
| // 检查是否显示 0.0 | |
| const text = imdbLinkEl.textContent || ''; | |
| const hasZeroScore = /\b0(\.0)?\s*\/\s*10\b/.test(text) || /\b0\.0\b/.test(text); | |
| if (!hasZeroScore) return; | |
| // 检查本地缓存 | |
| const cachedData = getCache(IMDB_CACHE_PREFIX + imdbId); | |
| if (cachedData) { | |
| log('命中 IMDb 本地缓存,自动纠正评分:', imdbId, cachedData); | |
| applyImdbFix(imdbLinkEl, cachedData); | |
| return; | |
| } | |
| if (fetchingImdbIds.has(imdbId)) return; | |
| fetchingImdbIds.add(imdbId); | |
| log('检测到 IMDb 0.0 异常,开始反查官方真实评分:', imdbId); | |
| try { | |
| const data = await queryIMDb(imdbId); | |
| if (data && parseFloat(data.score) > 0) { | |
| log('IMDb 真实评分反查成功:', imdbId, data); | |
| setCache(IMDB_CACHE_PREFIX + imdbId, data, calculateTTL(year)); | |
| applyImdbFix(imdbLinkEl, data); | |
| } else { | |
| log('IMDb 未查到有效评分或评分确为0:', imdbId); | |
| setCache(IMDB_CACHE_PREFIX + imdbId, null, NEGATIVE_TTL); | |
| } | |
| } catch (err) { | |
| log('反查 IMDb 评分出错:', err.message); | |
| } finally { | |
| fetchingImdbIds.delete(imdbId); | |
| } | |
| } | |
| // 处理 IMDb 缺失时的自动反查与补全 | |
| async function processImdbFill(container, cleanTitle, originalTitle, year, isTv) { | |
| const searchKey = `${cleanTitle}_${originalTitle || ''}_${year}_${isTv ? 'tv' : 'movie'}`; | |
| if (fetchingImdbSearches.has(searchKey)) return; | |
| // 检查搜索缓存 | |
| const cachedSearch = getCache(IMDB_SEARCH_CACHE_PREFIX + searchKey); | |
| if (cachedSearch !== null) { | |
| if (cachedSearch && cachedSearch.imdbId) { | |
| if (!container.querySelector('a[href*="imdb.com"], [data-autofill-imdb="true"]')) { | |
| log('命中 IMDb 补全缓存,直接渲染:', cachedSearch); | |
| insertImdbElement(container, createImdbElement(cachedSearch.imdbId, cachedSearch)); | |
| } | |
| } | |
| return; | |
| } | |
| fetchingImdbSearches.add(searchKey); | |
| log('页面缺少 IMDb,开始反查 IMDb ID:', { cleanTitle, originalTitle, year, isTv }); | |
| try { | |
| const imdbId = await searchImdbId(cleanTitle, originalTitle, year, isTv); | |
| if (imdbId) { | |
| log('IMDb 反查匹配成功,获取到 ID:', imdbId); | |
| // 查询详情评分 (先查单项缓存) | |
| let ratingData = getCache(IMDB_CACHE_PREFIX + imdbId); | |
| if (!ratingData) { | |
| try { | |
| ratingData = await queryIMDb(imdbId); | |
| } catch (e) { | |
| log('抓取 IMDb 详情评分失败:', e.message); | |
| } | |
| } | |
| if (!ratingData || !ratingData.score) { | |
| ratingData = { score: '暂无评分', count: '' }; | |
| } | |
| // 写入缓存 | |
| const fullData = { imdbId, ...ratingData }; | |
| setCache(IMDB_CACHE_PREFIX + imdbId, ratingData, calculateTTL(year)); | |
| setCache(IMDB_SEARCH_CACHE_PREFIX + searchKey, fullData, calculateTTL(year)); | |
| insertImdbElement(container, createImdbElement(imdbId, ratingData)); | |
| } else { | |
| log('未在 IMDb 检索到匹配条目:', cleanTitle); | |
| setCache(IMDB_SEARCH_CACHE_PREFIX + searchKey, null, NEGATIVE_TTL); | |
| } | |
| } catch (err) { | |
| log('IMDb 自动反查补全出错:', err.message); | |
| } finally { | |
| fetchingImdbSearches.delete(searchKey); | |
| } | |
| } | |
| // 处理豆瓣自动补全 | |
| async function processDoubanFill(container, imdbId, cleanTitle, year) { | |
| const queryKey = imdbId || cleanTitle; | |
| if (!queryKey) return; | |
| if (fetchingIds.has(queryKey)) return; | |
| // 检查缓存 | |
| const cachedData = getCache(CACHE_KEY_PREFIX + queryKey); | |
| if (cachedData !== null) { | |
| if (cachedData) { | |
| if (!container.querySelector('[data-autofill-douban="true"]')) { | |
| log('命中豆瓣本地缓存,直接渲染评分:', queryKey, cachedData); | |
| container.appendChild(createDoubanElement(cachedData, queryKey)); | |
| } | |
| // 若旧缓存缺少完整 url 或缺少 count,后台静默重新拉取完整数据并更新 DOM 与缓存 | |
| if (!cachedData.url || !cachedData.url.startsWith('http') || !cachedData.count) { | |
| log('检测到旧缓存缺少豆瓣详情页 URL 或人数,后台异步拉取补齐:', queryKey); | |
| queryDouban(queryKey).then(freshData => { | |
| if (freshData && freshData.score) { | |
| setCache(CACHE_KEY_PREFIX + queryKey, freshData, calculateTTL(year)); | |
| const curA = container.querySelector('[data-autofill-douban="true"]'); | |
| if (curA) { | |
| if (freshData.url) curA.href = freshData.url; | |
| if (freshData.count) { | |
| let countSpan = curA.querySelector('[data-rating-count]'); | |
| if (!countSpan) { | |
| countSpan = document.createElement('span'); | |
| countSpan.setAttribute('data-rating-count', ''); | |
| countSpan.style.cssText = 'color:rgba(255,255,255,0.45);font-size:12px;margin-left:2px;'; | |
| curA.querySelector('.hdhive-un7kiq')?.appendChild(countSpan); | |
| } | |
| countSpan.textContent = `(${freshData.count})`; | |
| } | |
| const isNumeric = /^\d+(\.\d+)?$/.test(freshData.score); | |
| curA.title = isNumeric | |
| ? `豆瓣评分: ${freshData.score}/10${freshData.count ? ' (' + freshData.count + '人评价)' : ''} · 点击前往豆瓣详情页` | |
| : `豆瓣状态: ${freshData.score} · 点击前往豆瓣搜索`; | |
| } | |
| } | |
| }).catch(() => {}); | |
| } | |
| } | |
| return; | |
| } | |
| fetchingIds.add(queryKey); | |
| log('开始查询豆瓣评分,关键词:', queryKey); | |
| try { | |
| const data = await queryDouban(queryKey); | |
| if (data) { | |
| log('豆瓣查询成功,补全评分:', data); | |
| setCache(CACHE_KEY_PREFIX + queryKey, data, calculateTTL(year)); | |
| if (!container.querySelector('[data-autofill-douban="true"]')) { | |
| container.appendChild(createDoubanElement(data, queryKey)); | |
| } | |
| } else { | |
| log('未在豆瓣检索到对应条目:', queryKey); | |
| // 负缓存 2 天,避免每次刷新重试 | |
| setCache(CACHE_KEY_PREFIX + queryKey, null, NEGATIVE_TTL); | |
| } | |
| } catch (err) { | |
| log('查询豆瓣出错:', err.message); | |
| } finally { | |
| fetchingIds.delete(queryKey); | |
| } | |
| } | |
| // 主补全与纠错入口 | |
| async function processPage() { | |
| const info = getPageInfo(); | |
| if (!info) return; | |
| const { container, imdbId, imdbLinkEl, cleanTitle, originalTitle, year, isTv, hasDouban, hasImdb } = info; | |
| // 1. 若页面自带的 IMDb 为 0.0 异常,独立触发反查纠正 | |
| if (imdbLinkEl && imdbId) { | |
| fixImdbScore(imdbLinkEl, imdbId, year); | |
| } else if (!hasImdb) { | |
| // 2. 若页面没有 IMDb,触发 IMDb 补全 | |
| processImdbFill(container, cleanTitle, originalTitle, year, isTv); | |
| } | |
| // 3. 若缺少豆瓣评分,执行豆瓣补全 | |
| if (!hasDouban) { | |
| processDoubanFill(container, imdbId, cleanTitle, year); | |
| } | |
| } | |
| // SPA 路由与 DOM 变化监控 | |
| let lastUrl = location.href; | |
| const observer = new MutationObserver(() => { | |
| if (location.href !== lastUrl) { | |
| lastUrl = location.href; | |
| setTimeout(processPage, 500); | |
| } else { | |
| processPage(); | |
| } | |
| }); | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| // 页面初次加载执行 | |
| setTimeout(processPage, 600); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment