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.5 // @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://ptpimg.me/v1b634.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'; })();