NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @namespace https://openuserjs.org/users/SB100
// @name PTP Better Ratings Box
// @description Replace the current ratings box on movie pages with one that has more details in it
// @version 2.3.6
// @author SB100
// @copyright 2021, SB100 (https://openuserjs.org/users/SB100)
// @updateURL https://openuserjs.org/meta/SB100/PTP_Better_Ratings_Box.meta.js
// @license MIT
// @grant GM.xmlHttpRequest
// @match https://passthepopcorn.me/torrents.php?id=*
// @connect imdb.com
// @connect metacritic.com
// @connect rottentomatoes.com
// @connect letterboxd.com
// ==/UserScript==
// ==OpenUserJS==
// @author SB100
// ==/OpenUserJS==
/* jshint esversion: 9 */
/**
* =============================
* ADVANCED OPTIONS
* =============================
*/
// show debug logs in the browser console
const SETTING_DEBUG = true;
// Chose which providers you'd like to see, and in what order
// Valid options are: ['IMDb', 'Metacritic', 'Rotten Tomatoes', 'PTP', 'Letterboxd'];
const SETTING_RATING_PROVIDER_ORDER = [
'IMDb',
'Metacritic',
'Rotten Tomatoes',
'Letterboxd',
'PTP',
];
// Should we show an average of all user / critic results? If so, what is the minimum number of providers that must be present before an average is shown? 0 to turn off
const SETTING_CRITIC_AVG_PROVIDER_NUM = 2;
const SETTING_USER_AVG_PROVIDER_NUM = 2;
// Which providers to include in the Average/Combined score. Note that this provider must have a % based rating (e.g. Letterboxd "critics" doesn't have % scores)
// For each provider, the number denotes how many votes must have been cast before it is counted towards the average
const SETTING_CRITIC_AVG_PROVIDER_INCLUDE = {
IMDb: 1,
Metacritic: 1,
'Rotten Tomatoes': 1,
};
const SETTING_USER_AVG_PROVIDER_INCLUDE = {
IMDb: 1,
Metacritic: 1,
'Rotten Tomatoes': 1,
Letterboxd: 1,
PTP: 1,
};
// If you have rotten tomatoes enabled above, this setting will also pull the critic consensus quote and display it below all the ratings
const SETTING_SHOW_RT_CRITIC_CONSENSUS = true;
// Greyscale filter to apply to PTP icon if you haven't rated the movie.
// 0% = disable
// 100% = Fully gray (default)
const SETTING_GRAYSCALE_UNRATED_PTP_FILMS = '100%';
// Allows you to set the color of the rating at different thresholds.
// add a "percent: color" entry to SETTING_THRESHOLDS to set custom colors
const SETTING_ENABLE_THRESHOLD = false;
const SETTING_THRESHOLDS = {
0: '#ff0000',
60: '#009000',
};
/**
* =============================
* END ADVANCED OPTIONS
* DO NOT MODIFY BELOW THIS LINE
* =============================
*/
/**
* Print a debug message, if enabled
*/
function debug(strOrStrArray) {
if (!SETTING_DEBUG) return;
// eslint-disable-next-line no-console
console.log(
`[PTP Better Ratings Box] ${
Array.isArray(strOrStrArray) ? strOrStrArray.join(' - ') : strOrStrArray
}`
);
}
/**
* Turn a HTML string into a HTML element so that we can run querySelector calls against it
*/
function htmlToElement(html) {
const template = document.createElement('template');
template.innerHTML = html.trim();
return template.content;
}
/**
* Query a url for its HTML
*/
function query(url) {
let resolver;
let rejecter;
const p = new Promise((resolveFn, rejectFn) => {
resolver = resolveFn;
rejecter = rejectFn;
});
GM.xmlHttpRequest({
method: 'get',
url,
timeout: 10000,
onloadstart: () => {},
onload: (result) => {
if (result.status !== 200) {
rejecter(
new Error(
`[PTP Better Ratings Box] Error received from remote call: ${url}`
)
);
return;
}
if (typeof result.response === 'undefined') {
rejecter(
new Error(`[PTP Better Ratings Box] No result received from ${url}`)
);
return;
}
resolver(htmlToElement(result.response));
},
onerror: (result) => {
rejecter(result);
},
ontimeout: (result) => {
rejecter(result);
},
});
return p;
}
/**
* Process a PTP page, and turn into a JSON object of information
*/
function processPtp(page, data) {
try {
const userRating = parseInt(
page.getElementById('user_rating').textContent,
10
);
const userCount = parseInt(
page.getElementById('user_total').textContent,
10
);
const ratingUrl = new URL(data.url);
ratingUrl.search += '&action=ratings';
const personalRating =
parseInt(
page
.getElementById('ptp_your_rating')
.textContent.replace(/[^\d]+/g, ''),
10
) || null;
return {
name: 'PTP',
icon: 'https://passthepopcorn.me/i/NmgLuzre5CK.png',
url: data.url.toString(),
user: {
score: userRating,
count: userCount,
url: ratingUrl.toString(),
display: 'Users',
countType: 'vote',
},
critic: {
score: personalRating,
count: null,
url: null,
display: 'Personal',
countType: 'vote',
},
};
}
catch (e) {
debug(['[PTP]', e]);
return null;
}
}
/**
* Process IMDb details from PTP and turn into a JSON object of information
* @deprecated - we're now parsing from IMDb itself
*/
// eslint-disable-next-line no-unused-vars
function processImdbFromPTP(page, data) {
try {
const td =
page.getElementById('imdb-title-link').parentNode.parentNode
.nextElementSibling;
const score = parseInt(
parseFloat(td.querySelector('.rating').textContent) * 10,
10
);
Array.from(td.querySelectorAll('span, br')).map((node) => node.remove());
const count = parseInt(td.textContent.replace(/[^\d]+/g, ''), 10);
const ratingUrl = new URL(data.url);
ratingUrl.pathname += '/ratings';
return {
name: 'IMDb',
icon: 'https://m.media-amazon.com/images/G/01/IMDb/brand/guidelines/imdb/IMDb_Logo_Square_Gold.png',
url: data.url.toString(),
user: {
score,
count,
url: ratingUrl.toString(),
display: 'Users',
countType: 'vote',
},
critic: null,
};
}
catch (e) {
debug(['[IMDb]', e]);
return null;
}
}
/**
* Process an IMDb page and turn into a JSON object of information
*/
function processImdb(page, data) {
try {
const json = JSON.parse(page.getElementById('__NEXT_DATA__').innerHTML);
const userScore =
json.props.pageProps.contentData.entityMetadata.ratingsSummary
.aggregateRating * 10 || null;
const userCount =
json.props.pageProps.contentData.entityMetadata.ratingsSummary
.voteCount || 0;
const userUrl = new URL(data.url);
userUrl.search += '?demo=imdb_users';
return {
name: 'IMDb',
icon: 'https://m.media-amazon.com/images/G/01/IMDb/brand/guidelines/imdb/IMDb_Logo_Square_Gold.png',
url: data.url.toString().replace(/ratings$/, ''),
user: {
score: userScore,
count: userCount,
url: userUrl.toString(),
display: 'Users',
countType: 'vote',
},
critic: {
score: null,
count: null,
url: userUrl.toString(),
display: 'Top 1k',
countType: 'vote',
},
};
}
catch (e) {
debug(['[IMDb]', e]);
return null;
}
}
/**
* Process a Metacritic page, and turn into a JSON object of information
*/
function processMetacritic(page, data) {
try {
const userScore =
parseInt(
parseFloat(
page.querySelector('.c-siteReviewScore_user span').innerText
) * 10,
10
) || null;
const userCount =
parseInt(
page
.querySelectorAll('span.c-productScoreInfo_reviewsTotal span')?.[1]
?.innerText?.replace(/\D/g, ''),
10
) || 0;
const userUrl = new URL(data.url);
userUrl.pathname += '/user-reviews';
const criticScore =
parseInt(
page.querySelector('div.c-siteReviewScore[aria-label^="Metascore"]')
.innerText,
10
) || null;
const criticCount =
parseInt(
page
.querySelector('span.c-productScoreInfo_reviewsTotal span')
?.innerText?.match(/\d+/),
10
) || 0;
const criticUrl = new URL(data.url);
criticUrl.pathname += '/critic-reviews';
return {
name: 'Metacritic',
icon: 'https://www.metacritic.com/images/icons/metacritic-icon.svg',
url: data.url.toString(),
user: {
score: userScore,
count: userCount,
url: userUrl.toString(),
display: 'Users',
countType: 'vote',
},
critic: {
score: criticScore,
count: criticCount,
url: criticUrl.toString(),
display: 'Critics',
countType: 'vote',
},
};
}
catch (e) {
debug(['[Metacritic]', e]);
return null;
}
}
/**
* Process a Letterboxd page and turn into a JSON object of information
*/
async function processLetterboxd(page, data) {
try {
const jsonString = page
.querySelector('script[type="application/ld+json"]')
.innerText.replace(/\/\*.*?\*\//g, '')
.trim();
const json = JSON.parse(jsonString);
const userScore = parseInt(json.aggregateRating.ratingValue * 20, 10); // * 20 to make it a %
const userCount = json.aggregateRating.ratingCount;
const membersPage = await query(`${json['@id']}members`);
const likes =
parseInt(
membersPage
.querySelector('.js-route-likes a')
?.title?.replace(/[^\d]/g, ''),
10
) || 0;
const fans =
parseInt(
membersPage
.querySelector('.js-route-fans a')
?.title?.replace(/[^\d]/g, ''),
10
) || 0;
return {
name: 'Letterboxd',
icon: 'https://a.ltrbxd.com/logos/letterboxd-decal-dots-neg-rgb.svg',
url: data.url.toString(),
user: {
score: userScore,
count: userCount,
url: `${json['@id']}ratings`,
display: 'Users',
countType: 'vote',
},
custom: {
numbers: [likes, fans],
urls: [`${json['@id']}likes`, `${json['@id']}fans`],
displays: ['Likes', 'Fans'],
},
};
}
catch (e) {
debug(['[Letterboxd]', e]);
return null;
}
}
/**
* Process a Rotten Tomatoes page, and turn into a JSON object of information
*/
function processRottenTomatoes(page, data) {
const jsonElem = page.getElementById('media-scorecard-json');
try {
const json = JSON.parse(jsonElem.textContent);
const userUrl = new URL(data.url);
userUrl.pathname += '/reviews';
userUrl.search = 'type=user';
const criticUrl = new URL(data.url);
criticUrl.pathname += '/reviews';
return {
name: 'Rotten Tomatoes',
// eslint-disable-next-line no-use-before-define
icon: getRottenTomatoesLogo(
parseInt(json.criticsScore.score, 10),
json.criticsScore.certified
),
url: data.url.toString(),
user: {
score: parseInt(json.audienceScore.score, 10),
count: json.audienceScore.likedCount,
url: userUrl.toString(),
display: 'Users',
countType: 'vote',
},
critic: {
score: parseInt(json.criticsScore.score, 10),
count: json.criticsScore.ratingCount,
url: criticUrl.toString(),
display: 'Critics',
countType: 'vote',
},
custom: {
criticConsensus: page.querySelector('#critics-consensus p')
?.textContent,
// eslint-disable-next-line no-use-before-define
criticConsensusIcon: getRottenTomatoesLogo(
parseInt(json.criticsScore.score, 10),
json.criticsScore.certified,
false
),
},
};
}
catch (e) {
debug(['[Rotten Tomatoes]', e]);
return null;
}
}
/**
* Get the Rotten Tomatoes fresh logo, depending on the movie status
*/
function getRottenTomatoesLogo(criticScore, isCertified, isLarge = true) {
if (Number.isNaN(criticScore)) {
// empty
return 'https://www.rottentomatoes.com/assets/pizza-pie/images/icons/tomatometer/tomatometer-empty.cd930dab34a.svg';
}
if (criticScore < 60) {
// rotten
return 'https://www.rottentomatoes.com/assets/pizza-pie/images/icons/tomatometer/tomatometer-rotten.f1ef4f02ce3.svg';
}
// fresh
if (isCertified) {
if (isLarge) {
return 'https://www.rottentomatoes.com/assets/pizza-pie/images/icons/tomatometer/certified_fresh.75211285dbb.svg';
}
return 'https://www.rottentomatoes.com/assets/pizza-pie/images/icons/tomatometer/certified_fresh-notext.56a89734a59.svg';
}
return 'https://www.rottentomatoes.com/assets/pizza-pie/images/icons/tomatometer/tomatometer-fresh.149b5e8adc3.svg';
}
/**
* Get the rotten tomatoes critic consensus text and icon
*/
function getRottenTomatoesCriticConsensus(details, numCells) {
if (!SETTING_SHOW_RT_CRITIC_CONSENSUS) {
return '';
}
let text;
let icon;
for (let i = 0, len = SETTING_RATING_PROVIDER_ORDER.length; i < len; i += 1) {
const detail = details.find(
(d) => d.name === SETTING_RATING_PROVIDER_ORDER[i]
);
if (!detail) {
continue;
}
if (detail.name === 'Rotten Tomatoes') {
text = detail.custom.criticConsensus;
icon = detail.custom.criticConsensusIcon;
break;
}
}
if (!text || !icon) {
return '';
}
return `<tr><td colspan='${numCells}' style='padding-top: 20px;'>
<table style='max-width: 85%; margin: 0 auto;'>
<tr>
<td style='padding-right: 10px;'><img src='${icon}' style='height: 20px!important; width: 20px!important; margin:0; padding: 0;' /></td>
<td style='font-size: 0.85em; opacity: 0.75;'><em>${text}</em></td>
</tr>
</table>
</td></tr>`;
}
/**
* Returns the provider depending on the domain found in the link
*/
function getProviderFromLink(link) {
if (link.includes('imdb.com')) return 'imdb';
if (link.includes('metacritic.com')) return 'metacritic';
if (link.includes('rottentomatoes.com')) return 'rottentomatoes';
return 'Unknown';
}
/**
* Links a provider to a processing function
*/
function getProcessor(provider) {
switch (provider) {
case 'ptp':
return processPtp;
case 'imdb':
return processImdb;
case 'metacritic':
return processMetacritic;
case 'rottentomatoes':
return processRottenTomatoes;
case 'letterboxd':
return processLetterboxd;
default:
return () => null;
}
}
/**
* Decides whether we should pass the PTP page to the processing function, or process the site directly
*/
function shouldProcessFromPTPMoviePage(provider) {
switch (provider) {
case 'ptp':
return true;
case 'imdb':
return false;
case 'metacritic':
return false;
case 'rottentomatoes':
return false;
case 'letterboxd':
return false;
default:
return true;
}
}
/**
* Returns an array of obejcts for all the ratings links on a movie page
*/
function getLinks(ratingsTable, imdbId) {
const results = Array.from(
ratingsTable.querySelectorAll('a[target="_blank"]')
)
.map((elem) => {
let {
href
} = elem;
const provider = getProviderFromLink(href);
if (!provider) return null;
// for imdb, we want to parse the ratings page, not the main page
if (provider === 'imdb') {
href += 'ratings';
}
return {
name: provider,
url: new URL(href.replace(/(\?.*$|\/$)/, '')),
processor: getProcessor(provider),
shouldProcessFromPTPMoviePage: shouldProcessFromPTPMoviePage(provider),
};
})
.filter((result) => result !== null)
.concat({
name: 'ptp',
url: new URL(window.location.href),
processor: getProcessor('ptp'),
shouldProcessFromPTPMoviePage: shouldProcessFromPTPMoviePage('ptp'),
imdbId,
});
if (imdbId) {
results.push({
name: 'letterboxd',
url: new URL(`https://letterboxd.com/imdb/${imdbId}`),
processor: getProcessor('letterboxd'),
shouldProcessFromPTPMoviePage: shouldProcessFromPTPMoviePage('letterboxd'),
imdbId,
});
}
return results;
}
/**
* Queries each link, processes the page returned, and turns each page into a JSON object of information
*/
async function getDetails(links) {
const promises = links.map((data) => {
if (data.shouldProcessFromPTPMoviePage) {
return data.processor(document, data);
}
return (
query(data.url.toString())
.then((page) => data.processor(page, data))
// log error and return null so we can filter it out below
// This is to catch errors in `query` - the processors should catch their own and return null
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
return null;
})
);
});
return Promise.all(promises).then((res) => res.filter((p) => p !== null));
}
/**
* Gets the color the score should be depending on the thresholds defined in the settings
*/
function getThresholdColor(score) {
if (!SETTING_ENABLE_THRESHOLD) {
return 'inherit';
}
const threshold = Object.keys(SETTING_THRESHOLDS)
.sort((a, b) => a - b)
.reduce((result, curr) => {
if (score >= curr) return curr;
return result;
});
return SETTING_THRESHOLDS[threshold];
}
/**
* Calculates the average score for the combined average cell
*/
function getAverageScore(details) {
const settingUserProviders = Object.keys(SETTING_USER_AVG_PROVIDER_INCLUDE);
const settingCriticProviders = Object.keys(
SETTING_CRITIC_AVG_PROVIDER_INCLUDE
);
const providerUserDetails = [];
const providerCriticDetails = [];
for (let i = 0, len = SETTING_RATING_PROVIDER_ORDER.length; i < len; i += 1) {
const providerName = SETTING_RATING_PROVIDER_ORDER[i];
const detail = details.find((d) => d.name === providerName);
if (!detail) {
continue;
}
if (
settingUserProviders.includes(detail.name) &&
detail.user?.count !== null &&
detail.user?.count >= SETTING_USER_AVG_PROVIDER_INCLUDE[providerName] &&
detail.user?.score
) {
providerUserDetails.push(detail);
}
if (
settingCriticProviders.includes(detail.name) &&
detail.critic?.count !== null &&
detail.critic?.count >=
SETTING_CRITIC_AVG_PROVIDER_INCLUDE[providerName] &&
detail.critic?.score
) {
providerCriticDetails.push(detail);
}
}
const totalUser = providerUserDetails.reduce(
(result, provider) => result + (provider?.user?.score || 0),
0
);
const totalCritic = providerCriticDetails.reduce(
(result, provider) => result + (provider?.critic?.score || 0),
0
);
return {
user: {
score: (totalUser / providerUserDetails.length)
.toFixed(2)
.replace(/[.,]00$/, ''),
count: providerUserDetails.length,
url: '#',
display: 'Users',
},
critic: {
score: (totalCritic / providerCriticDetails.length)
.toFixed(2)
.replace(/[.,]00$/, ''),
count: providerCriticDetails.length,
url: '#',
display: 'Critics',
},
custom: {
userProviders: providerUserDetails.map((provider) => provider.name),
criticProviders: providerCriticDetails.map((provider) => provider.name),
},
};
}
/**
* Creates the logo for a provider
*/
function buildLogo(name, url, iconSrc, extraImgStyles = '') {
return `<center style="text-align: center;">
<a target="_blank" class="rating" href="${url}" rel="noreferrer">
<img src="${iconSrc}" style="height:48px!important; width:48px!important; margin:0; padding:0; ${extraImgStyles}" title="${name} Reviews">
</a>
</center>`;
}
/**
* Builds the critic and user rating details
*/
function buildRatings(name, critic, user, custom) {
if (name === 'PTP') {
return `
<span id="ptp_your_rating">${critic.display}: ${
critic.score !== null
? `<span style="color: ${getThresholdColor(critic.score)}">${
critic.score
}%</span>`
: `None`
}</span>
<br />(<a id="star0" href="#edit-vote">${
critic.score !== null ? `Edit` : `Cast`
} ${critic.countType}</a>)
<br /><br />
Users: ${
user.score
? `<span style="color: ${getThresholdColor(user.score)}">${
user.score
}%</span>`
: `None`
}
<br />(<a target="_blank" href="${user.url}">${
user.count ? user.count.toLocaleString() : `No`
} ${user.countType}${user.count === 1 ? `` : `s`}</a>)`;
}
if (name === 'Letterboxd') {
return `
${custom.displays[0]}: ${
custom.numbers[0]
? `<a target="_blank" rel="noopener noreferrer" href="${
custom.urls[0]
}">${custom.numbers[0].toLocaleString()}</a>`
: `None`
}<br />
${custom.displays[1]}: ${
custom.numbers[1]
? `<a target="_blank" rel="noopener noreferrer" href="${
custom.urls[1]
}">${custom.numbers[1].toLocaleString()}</a>`
: `None`
}
<br /><br />
${user.display}: ${
user.score
? `<span style="color: ${getThresholdColor(user.score)}">${
user.score
}%</span>`
: `None`
}<br />
(<a target="_blank" rel="noopener noreferrer" href="${user.url}">${
user.count ? user.count.toLocaleString() : `No`
} ${user.countType}${user.count === 1 ? `` : `s`}</a>)`;
}
if (name === 'Combined Average') {
const userProviders = custom?.userProviders ?
`From ${custom.userProviders.join(', ')}` :
'';
const criticProviders = custom?.criticProviders ?
`From ${custom.criticProviders.join(', ')}` :
'';
return `
${
critic && SETTING_CRITIC_AVG_PROVIDER_NUM !== 0
? `${critic.display}: ${
critic.score &&
critic.count >= SETTING_CRITIC_AVG_PROVIDER_NUM
? `<span style="color: ${getThresholdColor(
critic.score
)}">${critic.score}%</span>`
: `None`
}
<br />(<a href="${critic.url}" title="${criticProviders}">${
critic.count >= SETTING_CRITIC_AVG_PROVIDER_NUM
? `${critic.count.toLocaleString()} providers`
: `Not enough data`
}</a>)
<br /><br />
`
: `<br /><br /><br />`
}
${
user && SETTING_USER_AVG_PROVIDER_NUM !== 0
? `${user.display}: ${
user.score && user.count >= SETTING_USER_AVG_PROVIDER_NUM
? `<span style="color: ${getThresholdColor(user.score)}">${
user.score
}%</span>`
: `None`
}
<br />(<a href="${user.url}" title="${userProviders}">${
user.count >= SETTING_USER_AVG_PROVIDER_NUM
? `${user.count.toLocaleString()} providers`
: `Not enough data`
}</a>)
`
: `<br /><br />`
}
`;
}
return `
${
critic
? `${critic.display}: ${
critic.score
? `<span style="color: ${getThresholdColor(critic.score)}">${
critic.score
}%</span>`
: `None`
}
<br />(<a target="_blank" rel="noopener noreferrer" href="${
critic.url
}">${critic.count ? critic.count.toLocaleString() : `No`} ${
critic.countType
}${critic.count === 1 ? `` : `s`}</a>)
<br /><br />
`
: ``
}
${user.display}: ${
user.score
? `<span style="color: ${getThresholdColor(user.score)}">${
user.score
}%</span>`
: `None`
}
<br />(<a target="_blank" rel="noopener noreferrer" href="${user.url}">${
user.count ? user.count.toLocaleString() : `No`
} ${user.countType}${user.count === 1 ? `` : `s`}</a>)`;
}
/**
* Creates the logo and ratings cell
*/
function buildCell(logo, ratings, isAvgCell = false) {
return `<td style="width: auto; padding: 0; ${
isAvgCell
? 'border-left: 2px solid rgba(255, 255, 255, 0.20); padding-left: 10px;'
: ''
}">
<div style="height: 60px;">
${logo}
</div>
<div style="text-align: center;">
<div style="display: inline-block; text-align: left;">
${ratings}
</div>
<div>
</td>`;
}
/**
* Builds the new rating box based on the information we processed
*/
function buildNewRatingsBox(details) {
const table = document.createElement('div');
table.style = 'padding: 10px 0;';
const cells = [];
for (let i = 0, len = SETTING_RATING_PROVIDER_ORDER.length; i < len; i += 1) {
const detail = details.find(
(d) => d.name === SETTING_RATING_PROVIDER_ORDER[i]
);
if (!detail) {
continue;
}
const {
name,
url,
icon,
critic,
user,
custom
} = detail;
const logo = buildLogo(
name,
url,
icon,
name === 'PTP' && critic?.score === null ?
`filter: grayscale(${SETTING_GRAYSCALE_UNRATED_PTP_FILMS});` :
``
);
const ratings = buildRatings(name, critic, user, custom);
cells.push(buildCell(logo, ratings));
}
if (
SETTING_CRITIC_AVG_PROVIDER_NUM !== 0 ||
SETTING_USER_AVG_PROVIDER_NUM !== 0
) {
// push the avg score in as well
const avgScore = getAverageScore(details);
if (avgScore.critic.count > 0 || avgScore.user.count > 0) {
const logo = `<center style="text-align: center;"><div style="font-size: 60px; line-height: 0.75" title="Combined Average Reviews">✹</div></center>`;
const ratings = buildRatings(
'Combined Average',
avgScore.critic,
avgScore.user,
avgScore.custom
);
cells.push(buildCell(logo, ratings, true));
}
}
table.innerHTML = `<table id="movie-ratings-table" style="margin: auto; width: 100%; table-layout: fixed;">
<tbody>
<tr style="justify-content: space-evenly;">
${cells.join('')}
</tr>
${getRottenTomatoesCriticConsensus(details, cells.length)}
</tbody>
</table>`;
return table;
}
/**
* Replaces the old rating box with a new rating box
*/
function replaceRatingsBox(oldTable, newTable) {
oldTable.parentNode.replaceChild(newTable, oldTable);
}
/**
* Reinitializes the PTP personal rating part of the page
*/
function reinitializeVoter(personalRating) {
// eslint-disable-next-line no-undef
const groupId = unsafeWindow.groupid;
// eslint-disable-next-line no-undef
unsafeWindow.InitializeMoviePageVoting(groupId, personalRating);
}
// Main script runner
(async function main() {
// Make sure there is a rating table on the page
const ratingsTable = document.getElementById('movie-ratings-table');
if (!ratingsTable) {
return;
}
// find the header, and set to "loading"
const header = ratingsTable.parentNode.querySelector(
'.panel__heading__title'
);
header.innerHTML =
'Ratings <span style="font-weight: normal">(Loading Better Rating Box ...)</span>';
// find the imdb link from the page
const imdbId =
document.getElementById('imdb-title-link')?.href?.match(/tt[\d]+/)?.[0] ||
null;
// get all the details from all the providers
const links = getLinks(ratingsTable, imdbId);
const details = await getDetails(links);
const personalRating = details.find((d) => d.name === 'PTP').critic.score;
// create a new ratings box, and replace the old one
const newRatingTable = buildNewRatingsBox(details);
replaceRatingsBox(ratingsTable, newRatingTable);
// reinitialize the personal vote JS
reinitializeVoter(personalRating);
// Set the header back to normal
header.innerHTML = 'Ratings';
})();