// ==UserScript==
// @name 豆b番——豆瓣电影显示IMDb和烂番茄评分
// @namespace https://github.com/fisheepx
// @version 1.0.0
// @description 在豆瓣电影页面显示IMDb和烂番茄评分
// @author fish
// @license MIT
// @match https://movie.douban.com/subject/*
// @grant GM_xmlhttpRequest
// @homepage https://github.com/fisheepx/douban-imdb-rt
// @supportURL https://github.com/fisheepx/douban-imdb-rt/issues
// @icon https://raw.githubusercontent.com/fisheepx/douban-imdb-rt/main/assets/icon/icon.png
// @compatible edge 最新版 运行正常
// @compatible chrome 最新版 运行正常
// @compatible firefox 最新版 运行正常
// @compatible safari 最新版 运行正常
// @downloadURL https://update.greasyfork.icu/scripts/527986/%E8%B1%86b%E7%95%AA%E2%80%94%E2%80%94%E8%B1%86%E7%93%A3%E7%94%B5%E5%BD%B1%E6%98%BE%E7%A4%BAIMDb%E5%92%8C%E7%83%82%E7%95%AA%E8%8C%84%E8%AF%84%E5%88%86.user.js
// @updateURL https://update.greasyfork.icu/scripts/527986/%E8%B1%86b%E7%95%AA%E2%80%94%E2%80%94%E8%B1%86%E7%93%A3%E7%94%B5%E5%BD%B1%E6%98%BE%E7%A4%BAIMDb%E5%92%8C%E7%83%82%E7%95%AA%E8%8C%84%E8%AF%84%E5%88%86.meta.js
// ==/UserScript==
(function() {
'use strict';
// SVG图标定义
const TOMATO_ICONS = {
fresh: `
`,
rotten: `
`,
empty: `
`
};
const POPCORN_ICONS = {
full: `
`,
tipped: `
`,
empty: `
`
};
// 创建评分区域的HTML模板
const imdbSectionHTML = `
`;
const rtSectionHTML = `
`;
// 主要功能类
class MovieRatings {
constructor() {
this.initialize();
}
initialize() {
const imdbId = this.getImdbId();
if (!imdbId) return;
this.createRatingsSections();
this.fetchImdbRatings(imdbId);
}
getImdbId() {
const imdbSpan = Array.from(document.querySelectorAll('#info span.pl'))
.find(span => span.textContent.includes('IMDb:'));
return imdbSpan ? imdbSpan.nextSibling.textContent.trim() : null;
}
createRatingsSections() {
const ratingsOnWeight = document.querySelector('.ratings-on-weight');
if (!ratingsOnWeight?.parentNode) return;
// 创建IMDb评分区域
this.imdbSection = this.createSection('imdb', imdbSectionHTML);
// 创建烂番茄评分区域
this.rtSection = this.createSection('rt', rtSectionHTML);
// 插入到页面
ratingsOnWeight.parentNode.insertBefore(this.imdbSection, ratingsOnWeight.nextSibling);
ratingsOnWeight.parentNode.insertBefore(this.rtSection, this.imdbSection.nextSibling);
}
createSection(type, template) {
const section = document.createElement('div');
section.className = 'rating_wrap clearbox';
section.style.cssText = 'margin-top: 12px; padding-top: 12px;';
section.innerHTML = template;
return section;
}
async fetchImdbRatings(imdbId) {
try {
const response = await this.makeRequest(`https://www.imdb.com/title/${imdbId}/`);
const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
const ratingData = this.extractImdbRating(doc);
this.updateImdbUI(ratingData, imdbId);
const englishTitle = this.getEnglishTitle(doc);
if (englishTitle) {
await this.fetchRottenTomatoesRatings(englishTitle);
}
} catch (error) {
this.handleError('imdb');
}
}
extractImdbRating(doc) {
const ratingContainer = doc.querySelector('[data-testid="hero-rating-bar__aggregate-rating__score"]');
if (!ratingContainer) return { score: 'N/A', count: '0' };
const score = ratingContainer.querySelector('span:first-child')?.textContent.trim();
const count = ratingContainer.parentElement?.querySelector('div[class*="dwhNqC"]')?.textContent.trim();
return { score, count };
}
getEnglishTitle(doc) {
return doc.querySelector('h1[data-testid="hero__pageTitle"]')?.textContent.trim();
}
async fetchRottenTomatoesRatings(title) {
try {
const searchUrl = `https://www.rottentomatoes.com/search?search=${encodeURIComponent(title)}`;
const searchResponse = await this.makeRequest(searchUrl);
const searchDoc = new DOMParser().parseFromString(searchResponse.responseText, 'text/html');
const movieLink = searchDoc.querySelector('search-page-media-row a[data-qa="info-name"]');
if (!movieLink) throw new Error('未找到匹配的烂番茄页面');
const movieUrl = movieLink.getAttribute('href');
const movieResponse = await this.makeRequest(movieUrl);
this.processRottenTomatoesPage(movieResponse, movieUrl);
} catch (error) {
this.handleError('rt');
}
}
processRottenTomatoesPage(response, movieUrl) {
const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
const scoreData = this.extractRottenTomatoesScores(doc);
this.updateRottenTomatoesUI(scoreData, movieUrl);
}
extractRottenTomatoesScores(doc) {
const scripts = doc.querySelectorAll('script[type="application/json"]');
for (const script of scripts) {
if (script.textContent.includes('"audienceScore"') && script.textContent.includes('"criticsScore"')) {
try {
const data = JSON.parse(script.textContent);
if (data.audienceScore && data.criticsScore) {
return data;
}
} catch (e) {}
}
}
return null;
}
updateImdbUI(ratingData, imdbId) {
const ratingNum = this.imdbSection.querySelector('.rating_num');
const ratingSum = this.imdbSection.querySelector('span');
ratingNum.textContent = `${ratingData.score}/10`;
ratingSum.textContent = `${ratingData.count}人评价`;
ratingNum.onclick = () => window.open(`https://www.imdb.com/title/${imdbId}/`, '_blank');
}
updateRottenTomatoesUI(scoreData, movieUrl) {
if (!scoreData) {
this.handleError('rt');
return;
}
const [criticsRating, audienceRating] = this.rtSection.querySelectorAll('.rating_num');
const [criticsCount, audienceCount] = this.rtSection.querySelectorAll('span');
// 更新影评人评分
this.updateCriticsScore(scoreData.criticsScore, criticsRating, criticsCount);
// 更新观众评分
this.updateAudienceScore(scoreData.audienceScore, audienceRating, audienceCount);
// 添加点击事件
[criticsRating, audienceRating].forEach(rating => {
rating.style.cursor = 'pointer';
rating.onclick = () => window.open(movieUrl, '_blank');
});
}
updateCriticsScore(criticsScore, ratingElement, countElement) {
const tomatoContainer = this.rtSection.querySelector('.tomato-container');
if (!criticsScore?.score) {
ratingElement.textContent = 'N/A';
countElement.textContent = '影评人 • 暂无数据';
tomatoContainer.innerHTML = TOMATO_ICONS.empty;
} else {
ratingElement.textContent = criticsScore.score + '%';
countElement.textContent = '影评人';
tomatoContainer.innerHTML = criticsScore.sentiment === 'POSITIVE'
? TOMATO_ICONS.fresh
: TOMATO_ICONS.rotten;
}
this.setIconStyle(tomatoContainer);
}
updateAudienceScore(audienceScore, ratingElement, countElement) {
const popcornContainer = this.rtSection.querySelector('.popcorn-container');
if (!audienceScore?.score) {
ratingElement.textContent = 'N/A';
countElement.textContent = '观众 • 暂无数据';
popcornContainer.innerHTML = POPCORN_ICONS.empty;
} else {
const score = parseInt(audienceScore.score);
ratingElement.textContent = score + '%';
countElement.textContent = '观众';
popcornContainer.innerHTML = score >= 60
? POPCORN_ICONS.full
: POPCORN_ICONS.tipped;
}
this.setIconStyle(popcornContainer);
}
setIconStyle(container) {
const svg = container.querySelector('svg');
if (svg) {
svg.style.width = '24px';
svg.style.height = '24px';
}
}
handleError(type) {
if (type === 'imdb') {
const ratingNum = this.imdbSection.querySelector('.rating_num');
const ratingSum = this.imdbSection.querySelector('span');
ratingNum.textContent = '获取失败';
ratingSum.textContent = '获取失败';
ratingNum.style.color = '#999';
} else if (type === 'rt') {
const [criticsRating, audienceRating] = this.rtSection.querySelectorAll('.rating_num');
const [criticsCount, audienceCount] = this.rtSection.querySelectorAll('span');
criticsRating.textContent = 'N/A';
audienceRating.textContent = 'N/A';
criticsCount.textContent = '影评人 • 暂无数据';
audienceCount.textContent = '观众 • 暂无数据';
}
}
makeRequest(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: resolve,
onerror: reject
});
});
}
}
// 初始化
new MovieRatings();
})();