Greasy Fork is available in English.

Add movie ratings to IMDB links [adopted]

Adds movie ratings and number of voters to links on IMDB. Modified version of http://userscripts.org/scripts/show/96884

  1. // ==UserScript==
  2. // @name Add movie ratings to IMDB links [adopted]
  3. // @description Adds movie ratings and number of voters to links on IMDB. Modified version of http://userscripts.org/scripts/show/96884
  4. // @author StackOverflow community (especially Brock Adams)
  5. // @version 2015-11-24-41-joeytwiddle
  6. // @license MIT
  7. // @match *://www.imdb.com/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_addStyle
  10. // @require http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
  11. // @namespace https://greasyfork.org/users/8615
  12. // @derived-from https://greasyfork.org/en/scripts/2033-add-imdb-rating-votes-next-to-all-imdb-movie-series-links-improved
  13. // ==/UserScript==
  14. // Special Thanks to Brock Adams for this script: http://stackoverflow.com/questions/23974801/gm-xmlhttprequest-data-is-being-placed-in-the-wrong-places/23992742
  15.  
  16. var maxLinksAtATime = 100; //-- Pages can have 100's of links to fetch. Don't spam server or browser.
  17. var skipEpisodes = true; //-- I only want to see ratings for movies or TV shows, not TV episodes.
  18. var showAsStar = false; //-- Use IMDB star instead of colored div, less info but more consistent with the rest of the site.
  19. var addRatingToTitle = true; //-- Adds the rating to the browser's title bar (so rating will appear in browser bookmarks).
  20. var showMetaScore = true; //-- When the metascore is available, show it
  21. var useLightBackground = false; //-- If you prefer the site to have a light grey background
  22. var showGetMoreRatings = false; //-- Turn this on if the "See more" button stops pulling down ratings
  23.  
  24. if (useLightBackground) {
  25. GM_addStyle('.ipc-page-background { background: #e3e2dd !important; color: black !important; }');
  26. // You could also try #262626 for a dark grey but not black background
  27. }
  28.  
  29. // 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
  30. // TODO: A better solution might be to replace the <a> with a <div> containing the <a> and our rating
  31. // TODO: Or we could try putting the rating inside the link
  32. // TODO: Or we could make the rating float after the link, using position: absolute
  33. GM_addStyle(`
  34. /* For the "Known For" section */
  35. .ipc-primary-image-list-card__content-top {
  36. flex-direction: row;
  37. }
  38.  
  39. /* For the "Credits" section */
  40. .ipc-metadata-list-summary-item__tc {
  41. display: initial;
  42. }
  43. `);
  44.  
  45. // The old iMDB site exposed jQuery, but the new one does not
  46. //var $ = unsafeWindow.$;
  47. // This was exposed by the @require
  48. var $ = jQuery;
  49.  
  50. var fetchedLinkCnt = 0;
  51.  
  52. //const ratingSelectorNew = '.ipc-button > div > div > div > div > span:first-child';
  53. //const ratingSelectorNew = '.ipc-button > div > div > div > div > span:first-child';
  54. const ratingSelectorNew = "*[data-testid='hero-rating-bar__aggregate-rating__score'] > span:nth-child(1)";
  55. const voteCountSelectorNew = "*[data-testid='hero-rating-bar__aggregate-rating__score'] + div + div";
  56.  
  57. function processIMDB_Links () {
  58. //--- Get only links that could be to IMBD movie/TV pages.
  59. var linksToIMBD_Shows = document.querySelectorAll ("a[href*='/title/']");
  60.  
  61. var lastLinkProcessed;
  62.  
  63. for (var J = 0, L = linksToIMBD_Shows.length; J < L; J++) {
  64. const currentLink = linksToIMBD_Shows[J];
  65.  
  66. /*--- Strict tests for the correct IMDB link to keep from spamming the page
  67. with erroneous results.
  68. */
  69. if ( ! /^(?:www\.)?IMDB\.com$/i.test (currentLink.hostname)
  70. || ! /^\/title\/tt\d+\/?$/i.test (currentLink.pathname)
  71. )
  72. continue;
  73.  
  74. // I am beginning to think a whitelist might be better than this blacklist!
  75.  
  76. // Skip if in Bio
  77. if ($(currentLink).hasClass('ipc-md-link')) {
  78. continue;
  79. }
  80.  
  81. // Skip thumbnails on the search results page
  82. if ($(currentLink).closest('.primary_photo').length) {
  83. continue;
  84. }
  85.  
  86. // Skip thumbnails in the six recommendations area of a title page
  87. if ($(currentLink).closest('.rec_item, .rec_poster').length) {
  88. continue;
  89. }
  90.  
  91. // Skip top-rated episodes on the right-hand sidebar of TV series pages; they already display a rating anyway!
  92. if ($(currentLink).closest('#top-rated-episodes-rhs').length) {
  93. continue;
  94. }
  95.  
  96. // Skip thumbnail of title at top of Season page
  97. if ($(currentLink).find(':only-child').prop('tagName') === 'IMG') {
  98. continue;
  99. }
  100.  
  101. // Skip the thumbnail of each episode on a season page (episode names still processed)
  102. if ($(currentLink).closest('.image').length) {
  103. continue;
  104. }
  105.  
  106. // Skip thumbnails in "Known For" section of actor pages
  107. if ($(currentLink).closest('.known-for, .knownfor-title').length && $(currentLink).find('img').length) {
  108. continue;
  109. }
  110.  
  111. // Skip links to character pages
  112. // || currentLink.href.includes('/characters/')
  113. if ($(currentLink).closest('td.character').length) {
  114. continue;
  115. }
  116.  
  117. // Skip episodes on actor pages
  118. if (skipEpisodes && $(currentLink).closest('.filmo-episodes').length) {
  119. continue;
  120. }
  121.  
  122. // On an episode page, skip the next/previous buttons
  123. if ($(currentLink).closest('.bp_item').length) {
  124. continue;
  125. }
  126.  
  127. // New layout 2021
  128.  
  129. // The thumbnails on the "More like this" video cards
  130. if ($(currentLink).closest('.ipc-lockup-overlay').length) {
  131. continue;
  132. }
  133.  
  134. // Nov 2022: In the list of titles for an actor, there are now two <a>s in each row.
  135. if (lastLinkProcessed && currentLink.href === lastLinkProcessed.href) {
  136. continue;
  137. }
  138.  
  139. // Even if we skip this link, we remember it, to prevent duplicate ratings appearing when we "Get more ratings"
  140. lastLinkProcessed = currentLink;
  141.  
  142. if (! currentLink.getAttribute ("data-gm-fetched") ){
  143. if (fetchedLinkCnt >= maxLinksAtATime){
  144. //--- Position the "continue" button.
  145. continueBttn.style.display = 'inline';
  146. currentLink.parentNode.insertBefore (continueBttn, currentLink);
  147. break;
  148. }
  149.  
  150. //fetchTargetLink (currentLink); //-- AJAX-in the ratings for a given link.
  151.  
  152. // Stagger the fetches, so we don't overwhelm IMDB's servers (or trigger any throttles they might have)
  153. // Needs currentLink to be a const, or a closure around it
  154. setTimeout(() => fetchTargetLink(currentLink), 300 * fetchedLinkCnt);
  155.  
  156. //---Mark the link with a data attribute, so we know it's been fetched.
  157. currentLink.setAttribute ("data-gm-fetched", "true");
  158. fetchedLinkCnt++;
  159. }
  160. }
  161. }
  162.  
  163. function fetchTargetLink (linkNode) {
  164. //--- This function provides a closure so that the callbacks can work correctly.
  165.  
  166. //console.log("Fetching " + linkNode.href + ' for ', linkNode);
  167.  
  168. /*--- Must either call AJAX in a closure or pass a context.
  169. But Tampermonkey does not implement context correctly!
  170. (Tries to JSON serialize a DOM node.)
  171. */
  172. GM_xmlhttpRequest ( {
  173. method: 'get',
  174. url: linkNode.href,
  175. //context: linkNode,
  176. onload: function (response) {
  177. prependIMDB_Rating (response, linkNode);
  178. },
  179. onload: function (response) {
  180. prependIMDB_Rating (response, linkNode);
  181. },
  182. onabort: function (response) {
  183. prependIMDB_Rating (response, linkNode);
  184. }
  185. } );
  186. }
  187.  
  188. function prependIMDB_Rating (resp, targetLink) {
  189. var isError = true;
  190. var ratingTxt = "** Unknown Error!";
  191. var colnumber = 0;
  192. var justrate = 'RATING_NOT_FOUND';
  193.  
  194. if (resp.status != 200 && resp.status != 304) {
  195. ratingTxt = '** ' + resp.status + ' Error!';
  196. }
  197. else {
  198. // Example value: ["Users rated this 8.5/10 (", "8.5/10"]
  199. //var ratingM = resp.responseText.match (/Users rated this (.*) \(/);
  200. // Example value: ["(1,914 votes) -", "1,914"]
  201. //var votesM = resp.responseText.match (/\((.*) votes\) -/);
  202.  
  203. var doc = document.createElement('div');
  204. doc.innerHTML = resp.responseText;
  205. var elem = doc.querySelector('.title-overview .vital .ratingValue strong');
  206.  
  207. var ratingT, votesT;
  208. if (elem) {
  209. // Old site
  210. var title = elem && elem.title || '';
  211.  
  212. ratingT = title.replace(/ based on .*$/, '');
  213. votesT = title.replace(/.* based on /, '').replace(/ user ratings/, '');
  214. } else {
  215. // New site
  216. var ratingElem = doc.querySelector(ratingSelectorNew);
  217. ratingT = ratingElem && ratingElem.textContent || '';
  218.  
  219. var votesElem = doc.querySelector(voteCountSelectorNew);
  220. votesT = votesElem && votesElem.textContent || '';
  221.  
  222. //console.log('ratingElem', ratingElem);
  223. //console.log('votesElem', votesElem);
  224.  
  225. if (votesT.slice(-1) == 'K') {
  226. votesT = String(1000 * votesT.slice(0, -1));
  227. } else if (votesT.slice(-1) == 'M') {
  228. votesT = String(1000000 * votesT.slice(0, -1));
  229. }
  230. // Add in commas (to match old format)
  231. votesT = votesT.replace(/(\d)(\d\d\d)(\d\d\d)$/, '$1,$2,$3').replace(/(\d)(\d\d\d$)/, '$1,$2');
  232. //console.log('votesT:', votesT);
  233. }
  234. // The code below expects arrays (originally returned by string match)
  235. var ratingM = [ratingT, ratingT + "/10"];
  236. var votesM = [votesT, votesT];
  237.  
  238. //console.log('ratingM', ratingM);
  239. //console.log('votesM', votesM);
  240.  
  241. // This doesn't work on the new version of the site
  242. //if (/\(awaiting \d+ votes\)|\(voting begins after release\)|in development,/i.test (resp.responseText) ) {
  243. // hopefully this will work better
  244. if (ratingT == '' || votesT == '') {
  245. ratingTxt = "NR";
  246. isError = false;
  247. colnumber = 0;
  248. } else {
  249. if (ratingM && ratingM.length > 1 && votesM && votesM.length > 1) {
  250. isError = false;
  251.  
  252. justrate = ratingM[1].substr(0, ratingM[1].indexOf("/"));
  253. // For countries which use ',' instead of '.' for decimal point
  254. justrate = justrate.replace(',', '.');
  255.  
  256. // Let's try the metascore instead
  257. // Not all movied have a metascore
  258. var metaScoreElem = showMetaScore && doc.querySelector('.score-meta');
  259. //var metaScore = metaScoreElem && (Number(metaScoreElem.textContent) / 10).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 });
  260. var metaScore = metaScoreElem && metaScoreElem.textContent;
  261. var metaScoreColor = metaScoreElem && metaScoreElem.style.backgroundColor;
  262.  
  263. var votes = votesM[1];
  264. var votesNum = Number(votes.replace(',', '', 'g'));
  265. var commas_found = (votes.match(/,/g) || []).length;
  266. if (commas_found === 1) {
  267. votes = votes.replace(/,\d\d\d$/, 'k');
  268. } else if (commas_found === 2) {
  269. votes = votes.replace(/,\d\d\d,\d\d\d$/, 'M');
  270. }
  271.  
  272. // ratingTxt = ratingM[1] + " - " + votesM[1];
  273. // We use the element style to override IMDB's reset
  274. ratingTxt = "<strong style=\"font-weight: bolder\">" + justrate + "</strong>" + " / " + votes;
  275. //ratingTxt = "<strong>" + (metaScoreElem ? metaScore : justrate) + "</strong>" + " / " + votes;
  276. //ratingTxt = "<strong>" + (metaScoreElem ? metaScore : justrate) + "</strong>" + " / " + votes + (metaScoreElem ? " (" + justrate + "i)" : "" );
  277. //ratingTxt = "<strong>" + justrate + "</strong>" + " / " + votes + (metaScoreElem ? " (<strong>" + metaScore + "</strong> meta)" : "" );
  278. colnumber = Math.round(justrate);
  279. // 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.
  280. //colnumber = Math.round(metaScoreElem ? metaScore / 10 + 1.5 : justrate);
  281.  
  282. //if (metaScoreElem) {
  283. // justRate = metaScore / 10;
  284. //}
  285. }
  286. }
  287. }
  288.  
  289. //console.log('ratingTxt', ratingTxt);
  290. //console.log('justrate', justrate);
  291.  
  292. // 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; }
  293. //targetLink.setAttribute("title", "Rated " + ratingTxt.replace(/<\/*strong>/g,'').replace(/\//,'by') + " users." );
  294. targetLink.setAttribute("title", `Rated ${justrate} by ${votes} users.`);
  295.  
  296. if (!(justrate > 0)) {
  297. return;
  298. }
  299.  
  300.  
  301. // Slowly approach full opacity as votesNum increases. 10,000 votes results in opacity 0.5 (actually 0.6 when adjusted).
  302. var opacity = 1 - 1 / (1 + 0.0001 * votesNum);
  303. // Actually let's not start from 0; we may still want to see the numbers!
  304. opacity = 0.2 + 0.8*opacity;
  305. // Don't use too many decimal points; it's ugly!
  306. //opacity = Math.round(opacity * 10000) / 10000;
  307. opacity = opacity.toFixed(3);
  308.  
  309. var colors = ["#Faa", "#Faa","#Faa", "#Faa","#Faa", "#F88","#Faa", "#ff7","#7e7", "#5e5", "#0e0", "#ddd"];
  310. var bgCol = colors[colnumber];
  311. //var hue = justrate <= 6 ? 0 : justrate <= 8 ? 120 * (justrate - 6) / 2 : 120;
  312. //var bgCol = `hsla(${hue}, 100%, 60%, ${opacity})`;
  313. var resltSpan = document.createElement ("span");
  314. // resltSpan.innerHTML = '<b><font style="border-radius: 5px;padding: 1px;border: #575757 solid 1px; background-color:' + color[colnumber] + ';">' + ' [' + ratingTxt + '] </font></b>&nbsp;';
  315. // resltSpan.innerHTML = '<b><font style="background-color:' + justrate + '">' + ' [' + ratingTxt + '] </font></b>&nbsp;';
  316. // 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.
  317. resltSpan.innerHTML = '&nbsp;<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>';
  318.  
  319. if (showAsStar) {
  320. resltSpan.innerHTML = `
  321. <div class="ipl-rating-star" style="font-weight: normal">
  322. <span class="ipl-rating-star__star">
  323. <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">
  324. <path d="M0 0h24v24H0z" fill="none"></path>
  325. <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>
  326. <path d="M0 0h24v24H0z" fill="none"></path>
  327. </svg>
  328. </span>
  329. <span class="ipl-rating-star__rating">${justrate}</span>
  330. </div>
  331. `;
  332. }
  333.  
  334. if (isError)
  335. resltSpan.style.color = 'red';
  336.  
  337. //var targetLink = resp.context;
  338. //console.log ("targetLink: ", targetLink);
  339.  
  340. // 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
  341. var container = document.createElement('div');
  342. container.style.display = 'inline-block';
  343. container.appendChild(resltSpan);
  344.  
  345. //targetLink.parentNode.insertBefore (container, targetLink);
  346. targetLink.parentNode.insertBefore (container, targetLink.nextSibling);
  347.  
  348. if (metaScoreElem) {
  349. // I am reluctant to move an element from another document into this one, multiple times.
  350. // Therefore we create a new element, like the original.
  351. var newMetaScoreElem = document.createElement(metaScoreElem.tagName);
  352. //newMetaScoreElem.outerHTML = metaScoreElem.outerHTML;
  353. newMetaScoreElem.className = metaScoreElem.className;
  354. newMetaScoreElem.textContent = metaScoreElem.textContent;
  355. newMetaScoreElem.style.backgroundColor = metaScoreElem.style.backgroundColor;
  356. // Missing despite the class. It seems some pages don't include the .score-meta CSS
  357. newMetaScoreElem.style.color = 'white';
  358. newMetaScoreElem.style.padding = '2px';
  359. //resltSpan.parentNode.insertBefore (newMetaScoreElem, resltSpan.nextSibling);
  360. //resltSpan.parentNode.insertBefore (document.createTextNode(' '), resltSpan.nextSibling);
  361. container.appendChild(document.createTextNode(' '));
  362. container.appendChild(newMetaScoreElem);
  363. }
  364. }
  365.  
  366. //--- Create the continue button
  367. var continueBttn = document.createElement ("button");
  368. continueBttn.innerHTML = "Get more ratings";
  369.  
  370. continueBttn.addEventListener ("click", function (){
  371. fetchedLinkCnt = 0;
  372. continueBttn.style.display = 'none';
  373. processIMDB_Links ();
  374. },
  375. false
  376. );
  377.  
  378. if (showGetMoreRatings) {
  379. // Old way: Show the "Get more ratings" button at the top of the screen, so the user can click it after loading more titles
  380. // Now the site hides some of the Credits of an individual behind a "See all" button
  381. // Ideally we would trigger automatically when the new results are loaded
  382. // But until then, we present this button, so the user can manually trigger a refetch
  383. // Styling from Pharaoh2k
  384. continueBttn.style.display = 'inline';
  385. continueBttn.style.top = '0px';
  386. continueBttn.style.left = '50%';
  387. continueBttn.style.position = 'fixed';
  388. //continueBttn.style.height = '30px';
  389. //continueBttn.style.width = '170px';
  390. //continueBttn.style.color = 'black';
  391. continueBttn.style.zIndex = '1000';
  392. //continueBttn.style.backgroundColor = 'rgba(245, 245, 149, 0.7)';
  393. //continueBttn.style.boxShadow = '0 6px 6px rgb(0 0 0 / 60%)';
  394. continueBttn.style.boxShadow = '0 6px 6px #0004';
  395. continueBttn.style.cursor = 'pointer';
  396. // Borrow styling from the website
  397. continueBttn.className = 'ipc-btn ipc-btn--theme-base ipc-btn--core-accent1';
  398. continueBttn.style.padding = '0 1rem';
  399. document.body.appendChild(continueBttn);
  400. } else {
  401. var seeMoreButton = document.querySelector('.ipc-see-more__button');
  402. if (seeMoreButton) {
  403. // New way: Load the new ratings automatically after the "See more" button is clicked
  404. seeMoreButton.addEventListener('click', function (){
  405. setTimeout(() => { continueBttn.click() }, 3000);
  406. });
  407. }
  408. }
  409.  
  410. processIMDB_Links ();
  411.  
  412. if (addRatingToTitle) {
  413. setTimeout(function () {
  414. // Selectors for old site and new site
  415. var foundRating = document.querySelectorAll('.ratingValue [itemprop=ratingValue], ' + ratingSelectorNew);
  416. if (foundRating.length >= 1) {
  417. var rating = foundRating[0].textContent;
  418. if (rating.match(/^[0-9]\.[0-9]$/)) {
  419. document.title = `(${rating}) ` + document.title;
  420. }
  421. }
  422. }, 2000);
  423. }