Letterboxd External Ratings

Adds ratings of film from external sites to film pages

  1. // ==UserScript==
  2. // @name Letterboxd External Ratings
  3. // @namespace https://github.com/soyguijarro/userscripts
  4. // @description Adds ratings of film from external sites to film pages
  5. // @copyright 2015, Ramón Guijarro (http://soyguijarro.com)
  6. // @homepageURL https://github.com/soyguijarro/userscripts
  7. // @supportURL https://github.com/soyguijarro/userscripts/issues
  8. // @icon https://raw.githubusercontent.com/soyguijarro/userscripts/master/img/letterboxd_icon.png
  9. // @license GPLv3; http://www.gnu.org/licenses/gpl.html
  10. // @version 1.8
  11. // @include *://letterboxd.com/film/*
  12. // @include *://letterboxd.com/film/*/crew/*
  13. // @include *://letterboxd.com/film/*/studios/*
  14. // @include *://letterboxd.com/film/*/genres/*
  15. // @exclude *://letterboxd.com/film/*/views/*
  16. // @exclude *://letterboxd.com/film/*/lists/*
  17. // @exclude *://letterboxd.com/film/*/likes/*
  18. // @exclude *://letterboxd.com/film/*/fans/*
  19. // @exclude *://letterboxd.com/film/*/ratings/*
  20. // @exclude *://letterboxd.com/film/*/reviews/*
  21. // @grant GM_addStyle
  22. // @grant GM_xmlhttpRequest
  23. // @grant unsafeWindow
  24. // ==/UserScript==
  25.  
  26. var ratingsData = { "IMDb": {origRatingMax: 10, isLoaded: false},
  27. "Metascore": {origRatingMax: 100, isLoaded: false},
  28. "Tomatometer": {isLoaded: false} };
  29.  
  30. function updateRatingElt(site) {
  31. var ratingElts = document.querySelectorAll("section.ratings-external a"),
  32. ratingElt = ratingElts[Object.keys(ratingsData).indexOf(site)],
  33. ratingInnerElt = ratingElt.firstElementChild,
  34. ratingData = ratingsData[site];
  35.  
  36. if (ratingData.isLoaded) {
  37. ratingInnerElt.classList.remove("spinner");
  38.  
  39. if (ratingData.origRating && ratingData.origRating !== "" &&
  40. ratingData.origRating !== 0 && !isNaN(ratingData.origRating)) {
  41. if (localStorage.origRatingsMode === "true") {
  42. ratingInnerElt.removeAttribute("class");
  43. ratingInnerElt.textContent = ratingData.origRating +
  44. ((ratingData.origRatingMax) ? ("/" + ratingData.origRatingMax) : "%");
  45. } else {
  46. ratingInnerElt.className = "rating rated-" +
  47. Math.round(ratingData.oneToTenRating);
  48. }
  49. ratingElt.href = ratingData.url;
  50. ratingElt.style.cursor = "pointer";
  51. } else {
  52. ratingInnerElt.removeAttribute("class");
  53. ratingInnerElt.textContent = "N/A";
  54. }
  55. }
  56. }
  57.  
  58. function createRatingsSection(callback) {
  59. var sidebarElt = document.getElementsByClassName("sidebar")[0],
  60. ratingsSectionElt = document.createElement("section"),
  61. modeToggleElt = document.createElement("ul"),
  62. modeToggleInnerElt = document.createElement("li"),
  63. modeToggleInnerInnerElt = document.createElement("a"),
  64. ratingElt,
  65. ratingInnerElt,
  66. cssRules = "section.ratings-external {\
  67. margin-top: 20px;\
  68. }\
  69. section.ratings-external a {\
  70. display: block;\
  71. font-size: 12px;\
  72. line-height: 1.5;\
  73. margin-bottom: 0.5em;\
  74. }\
  75. section.ratings-external span {\
  76. text-align: right;\
  77. position: absolute;\
  78. right: 0;\
  79. color: #6C3;\
  80. }\
  81. section.ratings-external span.spinner {\
  82. background: url('" + getSpinnerImageUrl() + "');\
  83. height: 12px;\
  84. width: 12px;\
  85. margin: 3px 0;\
  86. }";
  87.  
  88. function getSpinnerImageUrl() {
  89. var spinnersObj = unsafeWindow.globals.spinners;
  90.  
  91. for (var prop in spinnersObj) {
  92. if (/spinner_12/.test(prop)) {
  93. return spinnersObj[prop];
  94. }
  95. }
  96. return null;
  97. }
  98.  
  99. function getModeToggleButtonText() {
  100. var ratingsModeName =
  101. (localStorage.origRatingsMode === "true") ? "five-star" : "original";
  102.  
  103. return "Show " + ratingsModeName + " ratings";
  104. }
  105.  
  106. function toggleRatingsMode(evt) {
  107. evt.preventDefault();
  108.  
  109. localStorage.origRatingsMode = !(localStorage.origRatingsMode === "true");
  110. modeToggleInnerInnerElt.textContent = getModeToggleButtonText();
  111.  
  112. for (var i = 0; i < Object.keys(ratingsData).length; i++) {
  113. updateRatingElt(Object.keys(ratingsData)[i]);
  114. }
  115. }
  116.  
  117. // Set up section to be inserted in page
  118. ratingsSectionElt.className = "section ratings-external";
  119.  
  120. // Set up section elements that will contain ratings
  121. for (var i = 0; i < Object.keys(ratingsData).length; i++) {
  122. ratingElt = document.createElement("a");
  123. ratingInnerElt = document.createElement("span");
  124.  
  125. ratingElt.textContent = Object.keys(ratingsData)[i];
  126. ratingElt.className = "rating-green";
  127. ratingInnerElt.className = "spinner";
  128. ratingElt.style.cursor = "default";
  129.  
  130. ratingElt.appendChild(ratingInnerElt);
  131. ratingsSectionElt.appendChild(ratingElt);
  132. }
  133.  
  134. // Set up ratings mode toggle button
  135. modeToggleElt.className = "box-link-list box-links";
  136. modeToggleInnerInnerElt.className = "box-link";
  137. modeToggleInnerInnerElt.href = "#";
  138. modeToggleInnerInnerElt.textContent = getModeToggleButtonText();
  139. modeToggleInnerInnerElt.addEventListener("click", toggleRatingsMode, false);
  140. modeToggleInnerElt.appendChild(modeToggleInnerInnerElt);
  141.  
  142. modeToggleElt.appendChild(modeToggleInnerElt);
  143. ratingsSectionElt.appendChild(modeToggleElt);
  144.  
  145. // Insert section in page
  146. sidebarElt.insertBefore(ratingsSectionElt, sidebarElt.lastElementChild);
  147. GM_addStyle(cssRules);
  148.  
  149. callback();
  150. }
  151.  
  152. function fillRatingsSection() {
  153. var moreDetailsElt = document.querySelector("section.col-main p.text-link"),
  154. imdbIdMatch = moreDetailsElt.innerHTML.
  155. match(/http:\/\/www\.imdb.com\/title\/tt(\d+)\//),
  156. rottenApiReqBaseUrl = "http://api.rottentomatoes.com/api/public/v1.0/",
  157. rottenApiReqParams = "movie_alias.json?type=imdb&id=",
  158. rottenApiReqUrl,
  159. imdbUrl,
  160. imdbId;
  161.  
  162. function updateRatingData(site, origRating, oneToTenRating, url) {
  163. ratingsData[site].origRating = origRating;
  164. ratingsData[site].oneToTenRating = oneToTenRating;
  165. ratingsData[site].url = url;
  166. ratingsData[site].isLoaded = true;
  167.  
  168. updateRatingElt(site);
  169. }
  170.  
  171. function getIMDbAndMetaRatings(res) {
  172. var parser = new DOMParser(),
  173. dom = parser.parseFromString(res.responseText, "text/html"),
  174. ratingsElt = dom.getElementById("title-overview-widget");
  175.  
  176. function getIMDbRating() {
  177. var imdbRating,
  178. imdbRatingElt = ratingsElt.querySelector("span[itemprop=ratingValue]");
  179.  
  180. if (imdbRatingElt) {
  181. imdbRating = parseFloat(imdbRatingElt.textContent);
  182. updateRatingData("IMDb", imdbRating, imdbRating, imdbUrl);
  183. } else {
  184. updateRatingData("IMDb", null);
  185. }
  186. }
  187.  
  188. function getMetaRating() {
  189. var metaRating,
  190. metaRatingElt = ratingsElt.querySelector(".metacriticScore span");
  191.  
  192. if (metaRatingElt) {
  193. metaRating = parseFloat(metaRatingElt.textContent);
  194.  
  195. GM_xmlhttpRequest({
  196. method: "GET",
  197. url: imdbUrl + "criticreviews", // Metacritic reviews page on IMDb
  198. onload: function (res) {
  199. var pageContent,
  200. metaUrl;
  201.  
  202. dom = parser.parseFromString(res.responseText, "text/html");
  203. pageContent = dom.getElementById("main").innerHTML;
  204. metaUrl = pageContent.
  205. match(/<a.*href="(.*?)".*>See all \d+ reviews/)[1];
  206.  
  207. updateRatingData("Metascore", metaRating,
  208. metaRating / 10, metaUrl);
  209. }
  210. });
  211. } else {
  212. updateRatingData("Metascore", null);
  213. }
  214. }
  215.  
  216. if (ratingsElt) {
  217. getIMDbRating();
  218. getMetaRating();
  219. } else {
  220. updateRatingData("IMDb", null);
  221. updateRatingData("Metascore", null);
  222. }
  223. }
  224.  
  225. function getRottenRating(res) {
  226. var json = JSON.parse(res.responseText),
  227. rottenId,
  228. rottenUrl,
  229. rottenRating;
  230.  
  231. if (json) {
  232. if (json.id && json.ratings && !json.error) {
  233. rottenUrl = "http://www.rottentomatoes.com/m/" + json.id;
  234. rottenRating = json.ratings.critics_score;
  235.  
  236. if (rottenRating > 0) {
  237. updateRatingData("Tomatometer", rottenRating,
  238. rottenRating / 10, rottenUrl);
  239. } else {
  240. updateRatingData("Tomatometer", null);
  241. }
  242. } else {
  243. updateRatingData("Tomatometer", null);
  244. }
  245. }
  246. }
  247.  
  248. if (imdbIdMatch) {
  249. imdbUrl = imdbIdMatch[0];
  250. imdbId = imdbIdMatch[1];
  251. rottenApiReqUrl = rottenApiReqBaseUrl + rottenApiReqParams + imdbId;
  252.  
  253. GM_xmlhttpRequest({
  254. method: "GET",
  255. url: imdbUrl,
  256. onload: getIMDbAndMetaRatings
  257. });
  258.  
  259. GM_xmlhttpRequest({
  260. method: "GET",
  261. url: rottenApiReqUrl,
  262. onload: getRottenRating
  263. });
  264. } else {
  265. updateRatingData("IMDb", null);
  266. updateRatingData("Metascore", null);
  267. updateRatingData("Tomatometer", null);
  268. }
  269. }
  270.  
  271. localStorage.origRatingsMode = (localStorage.origRatingsMode || true);
  272. createRatingsSection(fillRatingsSection);