// ==UserScript==
// @name Add movie ratings to IMDB links [By Pharaoh2k]
// @description Adds movie ratings and number of voters to links on IMDB. Modified version of http://userscripts.org/scripts/show/96884
// @author StackOverflow community (especially Brock Adams)
// @version 2023-01-28-01-Pharaoh2k
// @license MIT
// @match *://www.imdb.com/*
// @grant GM_xmlhttpRequest
// TODO: Remove this
// @grant unsafeWindow
// @grant GM_addStyle
// @require http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// @namespace https://greasyfork.org/users/2427
// @derived-from https://greasyfork.org/en/scripts/2033-add-imdb-rating-votes-next-to-all-imdb-movie-series-links-improved
// ==/UserScript==
// Special Thanks to Brock Adams for this script: http://stackoverflow.com/questions/23974801/gm-xmlhttprequest-data-is-being-placed-in-the-wrong-places/23992742
var maxLinksAtATime = 100; //-- Pages can have 100's of links to fetch. Don't spam server or browser.
var skipEpisodes = true; //-- I only want to see ratings for movies or TV shows, not TV episodes.
var showAsStar = false; //-- Use IMDB star instead of colored div, less info but more consistent with the rest of the site.
var addRatingToTitle = true; //-- Adds the rating to the browser's title bar (so rating will appear in browser bookmarks).
var showMetaScore = true; //-- When the metascore is available, show it
var useLightBackground = false; //-- If you prefer the site to have a light grey background
if (useLightBackground) {
GM_addStyle('.ipc-page-background { background: #e3e2dd !important; color: black !important; }');
// You could also try #262626 for a dark grey but not black background
}
// Nov 2022 design has `display: flex` to make all/some info flow downwards, which causes our rating to appear below the link, instead of after it
// TODO: A better solution might be to replace the <a> with a <div> containing the <a> and our rating
// TODO: Or we could try putting the rating inside the link
// TODO: Or we could make the rating float after the link, using position: absolute
GM_addStyle(`
/* For the "Known For" section */
.ipc-primary-image-list-card__content-top {
flex-direction: row;
}
/* For the "Credits" section */
.ipc-metadata-list-summary-item__tc {
display: initial;
}
`);
// The old iMDB site exposed jQuery, but the new one does not
//var $ = unsafeWindow.$;
// This was exposed by the @require
var $ = jQuery;
var fetchedLinkCnt = 0;
//const ratingSelectorNew = '.ipc-button > div > div > div > div > span:first-child';
//const ratingSelectorNew = '.ipc-button > div > div > div > div > span:first-child';
const ratingSelectorNew = "*[data-testid='hero-rating-bar__aggregate-rating__score'] > span:nth-child(1)";
const voteCountSelectorNew = "*[data-testid='hero-rating-bar__aggregate-rating__score'] + div + div";
function processIMDB_Links() {
//--- Get only links that could be to IMBD movie/TV pages.
var linksToIMBD_Shows = document.querySelectorAll("a[href*='/title/']");
var lastLinkProcessed;
for (var J = 0, L = linksToIMBD_Shows.length; J < L; J++) {
const currentLink = linksToIMBD_Shows[J];
/*--- Strict tests for the correct IMDB link to keep from spamming the page
with erroneous results.
*/
if (!/^(?:www\.)?IMDB\.com$/i.test(currentLink.hostname) ||
!/^\/title\/tt\d+\/?$/i.test(currentLink.pathname)
)
continue;
// I am beginning to think a whitelist might be better than this blacklist!
// Skip if in Bio
if ($(currentLink).hasClass("ipc-md-link")) {
continue;
}
// Skip thumbnails on the search results page
if ($(currentLink).closest('.primary_photo').length) {
continue;
}
// Skip thumbnails in the six recommendations area of a title page
if ($(currentLink).closest('.rec_item, .rec_poster').length) {
continue;
}
// Skip top-rated episodes on the right-hand sidebar of TV series pages; they already display a rating anyway!
if ($(currentLink).closest('#top-rated-episodes-rhs').length) {
continue;
}
// Skip thumbnail of title at top of Season page
if ($(currentLink).find(':only-child').prop('tagName') === 'IMG') {
continue;
}
// Skip the thumbnail of each episode on a season page (episode names still processed)
if ($(currentLink).closest('.image').length) {
continue;
}
// Skip thumbnails in "Known For" section of actor pages
if ($(currentLink).closest('.known-for, .knownfor-title').length && $(currentLink).find('img').length) {
continue;
}
// Skip links to character pages
// || currentLink.href.includes('/characters/')
if ($(currentLink).closest('td.character').length) {
continue;
}
// Skip episodes on actor pages
if (skipEpisodes && $(currentLink).closest('.filmo-episodes').length) {
continue;
}
// On an episode page, skip the next/previous buttons
if ($(currentLink).closest('.bp_item').length) {
continue;
}
// New layout 2021
// The thumbnails on the "More like this" video cards
if ($(currentLink).closest('.ipc-lockup-overlay').length) {
continue;
}
if (typeof lastLinkProcessed !== 'undefined') {
lastLinkProcessed = /[^\?]*/.exec(lastLinkProcessed)[0];
// console.log("lastLinkProcessed="+lastLinkProcessed);
}
// console.log("currentLink.href="+currentLink.href.split('?')[0]);
continueBttn.style.display = 'inline';
continueBttn.style.top = '0px';
continueBttn.style.left = '50%';
continueBttn.style.position = 'fixed';
continueBttn.style.height = '30px';
continueBttn.style.width = '170px';
continueBttn.style.color = 'black';
continueBttn.style.zIndex = '1000';
continueBttn.style.backgroundColor = 'rgba(245, 245, 149, 0.7)';
continueBttn.style.boxShadow = '0 6px 6px rgb(0 0 0 / 60%)';
currentLink.parentNode.insertBefore(continueBttn, currentLink);
// Nov 2022: In the list of titles for an actor, there are now two <a>s in each row.
// if (lastLinkProcessed && currentLink.href === lastLinkProcessed.href) {
if (lastLinkProcessed === currentLink.href.split('?')[0]) {
currentLink.setAttribute("data-gm-fetched", "true");
continue;
}
if (!currentLink.getAttribute("data-gm-fetched")) {
if (fetchedLinkCnt >= maxLinksAtATime) {
//--- Position the "continue" button.
break;
}
//fetchTargetLink (currentLink); //-- AJAX-in the ratings for a given link.
// Stagger the fetches, so we don't overwhelm IMDB's servers (or trigger any throttles they might have)
// Needs currentLink to be a const, or a closure around it
setTimeout(() => fetchTargetLink(currentLink), 300 * fetchedLinkCnt);
//---Mark the link with a data attribute, so we know it's been fetched.
currentLink.setAttribute("data-gm-fetched", "true");
lastLinkProcessed = currentLink;
fetchedLinkCnt++;
}
}
}
function fetchTargetLink(linkNode) {
//--- This function provides a closure so that the callbacks can work correctly.
//console.log("Fetching " + linkNode.href + ' for ', linkNode);
/*--- Must either call AJAX in a closure or pass a context.
But Tampermonkey does not implement context correctly!
(Tries to JSON serialize a DOM node.)
*/
GM_xmlhttpRequest({
method: 'get',
url: linkNode.href,
//context: linkNode,
onload: function(response) {
prependIMDB_Rating(response, linkNode);
},
onload: function(response) {
prependIMDB_Rating(response, linkNode);
},
onabort: function(response) {
prependIMDB_Rating(response, linkNode);
}
});
}
function prependIMDB_Rating(resp, targetLink) {
var isError = true;
var ratingTxt = "** Unknown Error!";
var colnumber = 0;
var justrate = 'RATING_NOT_FOUND';
if (resp.status != 200 && resp.status != 304) {
ratingTxt = '** ' + resp.status + ' Error!';
} else {
// Example value: ["Users rated this 8.5/10 (", "8.5/10"]
//var ratingM = resp.responseText.match (/Users rated this (.*) \(/);
// Example value: ["(1,914 votes) -", "1,914"]
//var votesM = resp.responseText.match (/\((.*) votes\) -/);
var doc = document.createElement('div');
doc.innerHTML = resp.responseText;
var elem = doc.querySelector('.title-overview .vital .ratingValue strong');
var ratingT, votesT;
if (elem) {
// Old site
var title = elem && elem.title || '';
ratingT = title.replace(/ based on .*$/, '');
votesT = title.replace(/.* based on /, '').replace(/ user ratings/, '');
} else {
// New site
var ratingElem = doc.querySelector(ratingSelectorNew);
ratingT = ratingElem && ratingElem.textContent || '';
var votesElem = doc.querySelector(voteCountSelectorNew);
votesT = votesElem && votesElem.textContent || '';
//console.log('ratingElem', ratingElem);
//console.log('votesElem', votesElem);
if (votesT.slice(-1) == 'K') {
votesT = String(1000 * votesT.slice(0, -1));
} else if (votesT.slice(-1) == 'M') {
votesT = String(1000000 * votesT.slice(0, -1));
}
// Add in commas (to match old format)
votesT = votesT.replace(/(\d)(\d\d\d)(\d\d\d)$/, '$1,$2,$3').replace(/(\d)(\d\d\d$)/, '$1,$2');
//console.log('votesT:', votesT);
}
// The code below expects arrays (originally returned by string match)
var ratingM = [ratingT, ratingT + "/10"];
var votesM = [votesT, votesT];
//console.log('ratingM', ratingM);
//console.log('votesM', votesM);
// This doesn't work on the new version of the site
//if (/\(awaiting \d+ votes\)|\(voting begins after release\)|in development,/i.test (resp.responseText) ) {
// hopefully this will work better
if (ratingT == '' || votesT == '') {
ratingTxt = "NR";
isError = false;
colnumber = 0;
} else {
if (ratingM && ratingM.length > 1 && votesM && votesM.length > 1) {
isError = false;
justrate = ratingM[1].substr(0, ratingM[1].indexOf("/"));
// Let's try the metascore instead
// Not all movied have a metascore
var metaScoreElem = showMetaScore && doc.querySelector('.score-meta');
//var metaScore = metaScoreElem && (Number(metaScoreElem.textContent) / 10).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 });
var metaScore = metaScoreElem && metaScoreElem.textContent;
var metaScoreColor = metaScoreElem && metaScoreElem.style.backgroundColor;
var votes = votesM[1];
var votesNum = Number(votes.replace(',', '', 'g'));
var commas_found = (votes.match(/,/g) || []).length;
if (commas_found === 1) {
votes = votes.replace(/,\d\d\d$/, 'k');
} else if (commas_found === 2) {
votes = votes.replace(/,\d\d\d,\d\d\d$/, 'M');
}
// ratingTxt = ratingM[1] + " - " + votesM[1];
// We use the element style to override IMDB's reset
ratingTxt = "<strong style=\"font-weight: bolder\">" + justrate + "</strong>" + " / " + votes;
//ratingTxt = "<strong>" + (metaScoreElem ? metaScore : justrate) + "</strong>" + " / " + votes;
//ratingTxt = "<strong>" + (metaScoreElem ? metaScore : justrate) + "</strong>" + " / " + votes + (metaScoreElem ? " (" + justrate + "i)" : "" );
//ratingTxt = "<strong>" + justrate + "</strong>" + " / " + votes + (metaScoreElem ? " (<strong>" + metaScore + "</strong> meta)" : "" );
colnumber = Math.round(justrate);
// If metaScore was found, use that for the colour instead of the IMDB rating. But since metascores are lower than imdb scores, add 1.5.
//colnumber = Math.round(metaScoreElem ? metaScore / 10 + 1.5 : justrate);
//if (metaScoreElem) {
// justRate = metaScore / 10;
//}
}
}
}
//console.log('ratingTxt', ratingTxt);
//console.log('justrate', justrate);
// NOTE: I switched from <b> to <strong> simply because on Season pages, the rating injected after episode titles was getting uglified by an IMDB CSS rule: .list_item .info b { font-size: 15px; }
//targetLink.setAttribute("title", "Rated " + ratingTxt.replace(/<\/*strong>/g,'').replace(/\//,'by') + " users." );
targetLink.setAttribute("title", `Rated ${justrate} by ${votes} users.`);
if (!(justrate > 0)) {
return;
}
// Slowly approach full opacity as votesNum increases. 10,000 votes results in opacity 0.5 (actually 0.6 when adjusted).
var opacity = 1 - 1 / (1 + 0.0001 * votesNum);
// Actually let's not start from 0; we may still want to see the numbers!
opacity = 0.2 + 0.8 * opacity;
// Don't use too many decimal points; it's ugly!
//opacity = Math.round(opacity * 10000) / 10000;
opacity = opacity.toFixed(3);
var colors = ["#Faa", "#Faa", "#Faa", "#Faa", "#Faa", "#F88", "#Faa", "#ff7", "#7e7", "#5e5", "#0e0", "#ddd"];
var bgCol = colors[colnumber];
//var hue = justrate <= 6 ? 0 : justrate <= 8 ? 120 * (justrate - 6) / 2 : 120;
//var bgCol = `hsla(${hue}, 100%, 60%, ${opacity})`;
var resltSpan = document.createElement("span");
// resltSpan.innerHTML = '<b><font style="border-radius: 5px;padding: 1px;border: #575757 solid 1px; background-color:' + color[colnumber] + ';">' + ' [' + ratingTxt + '] </font></b> ';
// resltSpan.innerHTML = '<b><font style="background-color:' + justrate + '">' + ' [' + ratingTxt + '] </font></b> ';
// I wanted vertical padding 1px but then the element does not fit in the "also liked" area, causing the top border to disappear! Although reducing the font size to 70% is an alternative.
resltSpan.innerHTML = ' <font style="font-weight: normal;font-size: 80%;opacity: ' + opacity + ';border-radius: 3px;padding: 0.1em 0.6em;border: rgba(0,0,0,0.1) solid 1px; background-color:' + bgCol + ';color: black;">' + '' + ratingTxt + '</font>';
if (showAsStar) {
resltSpan.innerHTML = `
<div class="ipl-rating-star" style="font-weight: normal">
<span class="ipl-rating-star__star">
<svg class="ipl-icon ipl-star-icon " xmlns="http://www.w3.org/2000/svg" fill="#000000" height="24" viewBox="0 0 24 24" width="24">
<path d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path>
<path d="M0 0h24v24H0z" fill="none"></path>
</svg>
</span>
<span class="ipl-rating-star__rating">${justrate}</span>
</div>
`;
}
if (isError)
resltSpan.style.color = 'red';
//var targetLink = resp.context;
//console.log ("targetLink: ", targetLink);
// The "More like this" cards have a vertical flowing grid, so if we want rating and metascore to appear next to each other, they will need a container
var container = document.createElement('div');
container.style.display = 'inline-block';
container.appendChild(resltSpan);
//targetLink.parentNode.insertBefore (container, targetLink);
targetLink.parentNode.insertBefore(container, targetLink.nextSibling);
if (metaScoreElem) {
// I am reluctant to move an element from another document into this one, multiple times.
// Therefore we create a new element, like the original.
var newMetaScoreElem = document.createElement(metaScoreElem.tagName);
//newMetaScoreElem.outerHTML = metaScoreElem.outerHTML;
newMetaScoreElem.className = metaScoreElem.className;
newMetaScoreElem.textContent = metaScoreElem.textContent;
newMetaScoreElem.style.backgroundColor = metaScoreElem.style.backgroundColor;
// Missing despite the class. It seems some pages don't include the .score-meta CSS
newMetaScoreElem.style.color = 'white';
newMetaScoreElem.style.padding = '2px';
//resltSpan.parentNode.insertBefore (newMetaScoreElem, resltSpan.nextSibling);
//resltSpan.parentNode.insertBefore (document.createTextNode(' '), resltSpan.nextSibling);
container.appendChild(document.createTextNode(' '));
container.appendChild(newMetaScoreElem);
}
}
//--- Create the continue button
var continueBttn = document.createElement("button");
continueBttn.innerHTML = "Get more ratings";
continueBttn.addEventListener("click", function() {
fetchedLinkCnt = 0;
continueBttn.style.display = 'none';
processIMDB_Links();
},
false
);
processIMDB_Links();
if (addRatingToTitle) {
setTimeout(function() {
// Selectors for old site and new site
var foundRating = document.querySelectorAll('.ratingValue [itemprop=ratingValue], ' + ratingSelectorNew);
if (foundRating.length >= 1) {
var rating = foundRating[0].textContent;
if (rating.match(/^[0-9]\.[0-9]$/)) {
document.title = `(${rating}) ` + document.title;
}
}
}, 2000);
}