Greasy Fork is available in English.

MoreMovieRatings

Show IMDb ratings on Douban, and vice versa

  1. // ==UserScript==
  2. // @name MoreMovieRatings
  3. // @namespace http://www.jayxon.com/
  4. // @version 0.7.3
  5. // @description Show IMDb ratings on Douban, and vice versa
  6. // @description:zh-CN 豆瓣和IMDb互相显示评分
  7. // @author JayXon
  8. // @match *://movie.douban.com/subject/*
  9. // @match *://www.douban.com/personage/*
  10. // @match *://www.imdb.com/title/tt*
  11. // @match *://letterboxd.com/film/*
  12. // @grant GM.xmlHttpRequest
  13. // @connect api.douban.com
  14. // @connect movie.douban.com
  15. // @connect p.media-imdb.com
  16. // @connect www.omdbapi.com
  17. // ==/UserScript==
  18.  
  19. 'use strict';
  20.  
  21. function getURL_GM(url, headers, data) {
  22. return new Promise(resolve => GM.xmlHttpRequest({
  23. method: data ? 'POST' : 'GET',
  24. url: url,
  25. headers: headers,
  26. data: data,
  27. onload: function (response) {
  28. if (response.status >= 200 && response.status < 400) {
  29. resolve(response.responseText);
  30. } else {
  31. console.error(`Error getting ${url}:`, response.status, response.statusText, response.responseText);
  32. resolve();
  33. }
  34. },
  35. onerror: function (response) {
  36. console.error(`Error during GM.xmlHttpRequest to ${url}:`, response.statusText);
  37. resolve();
  38. }
  39. }));
  40. }
  41.  
  42. async function getJSON_GM(url, headers, post_data) {
  43. const data = await getURL_GM(url, headers, post_data);
  44. if (data) {
  45. return JSON.parse(data);
  46. }
  47. }
  48.  
  49. async function getJSONP_GM(url, headers, post_data) {
  50. const data = await getURL_GM(url, headers, post_data);
  51. if (data) {
  52. const end = data.lastIndexOf(')');
  53. const [, json] = data.substring(0, end).split('(', 2);
  54. return JSON.parse(json);
  55. }
  56. }
  57.  
  58. async function getJSON(url) {
  59. try {
  60. const response = await fetch(url);
  61. if (response.status >= 200 && response.status < 400)
  62. return await response.json();
  63. console.error(`Error fetching ${url}:`, response.status, response.statusText, await response.text());
  64. }
  65. catch (e) {
  66. console.error(`Error fetching ${url}:`, e);
  67. }
  68. }
  69.  
  70. async function getIMDbInfo(id) {
  71. const keys = ['40700ff1', '4ee790e0', 'd82cb888', '386234f9', 'd58193b6', '15c0aa3f'];
  72. const apikey = keys[Math.floor(Math.random() * keys.length)];
  73. const omdbapi_url = `https://www.omdbapi.com/?tomatoes=true&apikey=${apikey}&i=${id}`;
  74. const imdb_url = `https://p.media-imdb.com/static-content/documents/v1/title/${id}/ratings%3Fjsonp=imdb.rating.run:imdb.api.title.ratings/data.json`;
  75. let [omdb_data, imdb_data] = await Promise.all([getJSON(omdbapi_url), getJSONP_GM(imdb_url)]);
  76. omdb_data = omdb_data || {};
  77. if (imdb_data && imdb_data.resource) {
  78. const resource = imdb_data.resource;
  79. if (resource.rating) {
  80. omdb_data.imdbRating = resource.rating;
  81. }
  82. if (resource.ratingCount) {
  83. omdb_data.imdbVotes = resource.ratingCount;
  84. }
  85. if (resource.ratingsHistograms && resource.ratingsHistograms["IMDb Users"]) {
  86. omdb_data.histogram = resource.ratingsHistograms["IMDb Users"].histogram;
  87. }
  88. if (resource.topRank) {
  89. omdb_data.topRank = resource.topRank;
  90. }
  91. }
  92. return omdb_data;
  93. }
  94.  
  95. async function getDoubanAPI(query) {
  96. return await getJSON_GM(`https://api.douban.com/v2/${query}`, {
  97. "Content-Type": "application/x-www-form-urlencoded; charset=utf8",
  98. }, "apikey=0ab215a8b1977939201640fa14c66bab");
  99. }
  100.  
  101. async function getDoubanInfo(id) {
  102. const data = await getDoubanAPI(`movie/imdb/${id}`);
  103. if (data) {
  104. if (isEmpty(data.alt))
  105. return;
  106. const url = data.alt.replace('/movie/', '/subject/') + '/';
  107. return { url, rating: data.rating, title: data.title };
  108. }
  109. // Fallback to search.
  110. const search = await getJSON_GM(`https://movie.douban.com/j/subject_suggest?q=${id}`);
  111. if (search && search.length > 0 && search[0].id) {
  112. const abstract = await getJSON_GM(`https://movie.douban.com/j/subject_abstract?subject_id=${search[0].id}`);
  113. const average = abstract && abstract.subject && abstract.subject.rate ? abstract.subject.rate : '?';
  114. return {
  115. url: `https://movie.douban.com/subject/${search[0].id}/`,
  116. rating: { numRaters: '', max: 10, average },
  117. title: search[0].title,
  118. };
  119. }
  120. }
  121.  
  122. function isEmpty(s) {
  123. return !s || s === 'N/A';
  124. }
  125.  
  126. function insertDoubanRatingDiv(parent, title, rating, link, num_raters, histogram) {
  127. let star = (5 * Math.round(rating)).toString();
  128. if (star.length == 1)
  129. star = '0' + star;
  130. if (typeof rating === 'number')
  131. rating = rating.toFixed(1);
  132. let histogram_html = '';
  133. if (histogram) {
  134. histogram_html += '<div class="ratings-on-weight">';
  135. const max = Math.max(...Object.values(histogram));
  136. for (let i = 10; i > 0; i--) {
  137. const percent = histogram[i] * 100 / num_raters;
  138. histogram_html += `<div class="item">
  139. <span class="stars${i} starstop" style="width:18px;text-align:center">${i}</span>
  140. <div class="power" style="width:${64 / max * histogram[i]}px"></div>
  141. <span class="rating_per">${percent.toFixed(1)}%</span>
  142. <br>
  143. </div>`;
  144. }
  145. histogram_html += '</div>';
  146. }
  147. parent.insertAdjacentHTML('beforeend',
  148. `<div class="rating_logo">${title}</div>
  149. <div class="rating_self clearfix">
  150. <strong class="ll rating_num">${rating}</strong>
  151. <div class="rating_right">
  152. <div class="ll bigstar${star}"></div>
  153. <div style="clear: both" class="rating_sum"><a href=${link} target=_blank>${num_raters.toString().replace(/,/g, '')}人评价</a></div>
  154. </div>
  155. </div>` + histogram_html);
  156. }
  157.  
  158. function insertDoubanInfo(name, value) {
  159. const info = document.querySelector('#info');
  160. if (info) {
  161. if (info.lastElementChild.nodeName != 'BR')
  162. info.insertAdjacentHTML('beforeend', '<br>');
  163. info.insertAdjacentHTML('beforeend', `<span class="pl">${name}:</span> ${value}<br>`);
  164. }
  165. }
  166.  
  167. function insertLetterboxdRating(ratings, title, title_href, rating, link, num_raters, histogram) {
  168. let new_rating = ratings.cloneNode(true);
  169. new_rating.querySelector('a[href$="/fans/"]')?.remove();
  170. const title_element = new_rating.querySelector('a[href$="/ratings/"]');
  171. title_element.textContent = title;
  172. title_element.href = title_href;
  173. title_element.target = '_blank';
  174. let average_element = new_rating.querySelector('a.display-rating');
  175. if (!average_element) {
  176. const average_span = document.createElement('span');
  177. average_span.classList.add('average-rating');
  178. average_element = document.createElement('a');
  179. average_element.classList.add('tooltip');
  180. average_element.classList.add('display-rating');
  181. average_span.append(average_element);
  182. new_rating.querySelector('.rating-histogram').before(average_span);
  183. }
  184. const average_rating = rating / 2;
  185. average_element.textContent = average_rating.toFixed(1);
  186. average_element.href = link;
  187. average_element.target = '_blank';
  188. average_element.title = `Weighted average of ${average_rating.toFixed(2)} based on ${new Intl.NumberFormat("en-US").format(num_raters)} ratings`;
  189. if (histogram) {
  190. const histogram_elements = new_rating.querySelectorAll('li i');
  191. const max = Math.max(...Object.values(histogram));
  192. for (let i = 10; i > 0; i--) {
  193. const current = histogram[i];
  194. const percent = current * 100 / num_raters;
  195. if (!histogram_elements[i - 1].previousSibling) {
  196. histogram_elements[i - 1].before(histogram_elements[i - 1].parentNode.dataset.originalTitle.replace(/No/, current) + `(${~~percent}%)`)
  197. } else {
  198. histogram_elements[i - 1].previousSibling.textContent = histogram_elements[i - 1].previousSibling.textContent.replace(/^[\d,]+/, current).replace(/\d+%/, ~~percent + '%');
  199. }
  200. histogram_elements[i - 1].style = `height: ${Math.max(44 / max * current, 1)}px`;
  201. }
  202. }
  203. ratings.after(new_rating);
  204. }
  205.  
  206. function linkifyIMDbNode(text_node, url_base) {
  207. const id = text_node.textContent.trim();
  208. let a = document.createElement('a');
  209. a.href = url_base + id;
  210. a.target = '_blank';
  211. a.appendChild(document.createTextNode(id));
  212. text_node.replaceWith(a);
  213. a.insertAdjacentText('beforebegin', ' ');
  214. return id;
  215. }
  216.  
  217. (async () => {
  218. let host = location.hostname;
  219. if (host === 'movie.douban.com') {
  220. let sectl = document.getElementById('interest_sectl');
  221. if (!sectl) {
  222. // No rating, might be censored, try to recover using API
  223. const douban_id = location.href.match(/douban\.com\/subject\/(\d+)/)[1];
  224. if (!douban_id)
  225. return;
  226.  
  227. // Insert related div back in
  228. const subjectwrap = document.querySelector('.subjectwrap');
  229. const subject = document.querySelector('.subject');
  230. if (!subjectwrap || !subject)
  231. return;
  232. sectl = document.createElement('div');
  233. sectl.id = 'interest_sectl';
  234. subjectwrap.insertBefore(sectl, subject.nextSibling);
  235. const rating_wrap = document.createElement('div');
  236. rating_wrap.className = 'rating_wrap';
  237. sectl.appendChild(rating_wrap);
  238.  
  239. const data = await getDoubanAPI(`movie/${douban_id}`)
  240.  
  241. if (data && data.rating && !isEmpty(data.rating.average)) {
  242. insertDoubanRatingDiv(rating_wrap, '豆瓣评分', data.rating.average, `https://movie.douban.com/subject/${douban_id}/collections`, data.rating.numRaters);
  243. rating_wrap.title = '此条目的豆瓣评分已被和谐,MoreMovieRatings恢复了部分评分';
  244. }
  245. // Move it down to leave space for fixed rating.
  246. if (document.querySelector('#movie-rating-iframe'))
  247. sectl.style.marginTop = '96px';
  248. }
  249.  
  250. // Douban stops linking to IMDb, so find the text node instead and retore the link.
  251. const imdb_text = [...document.querySelectorAll('#info > span.pl')].find(s => s.innerText.trim() == 'IMDb:');
  252. if (!imdb_text) {
  253. console.log('IMDb id not available');
  254. return;
  255. }
  256. const text_node = imdb_text.nextSibling;
  257. const id = linkifyIMDbNode(text_node, 'https://www.imdb.com/title/');
  258.  
  259. const data = await getIMDbInfo(id);
  260. if (!data)
  261. return;
  262. if (isEmpty(data.imdbRating) && isEmpty(data.Metascore)) {
  263. console.log('MoreMovieRatings: No ratings found');
  264. return;
  265. }
  266. const ratings = document.createElement('div');
  267. ratings.style.padding = '15px 0';
  268. ratings.style.borderTop = '1px solid #eaeaea';
  269. let rating_wrap = document.querySelector('.friends_rating_wrap');
  270. if (!rating_wrap)
  271. rating_wrap = document.querySelector('.rating_wrap');
  272. sectl.insertBefore(ratings, rating_wrap.nextSibling);
  273. ratings.className = 'rating_wrap clearbox';
  274. // Reduce whitespace
  275. sectl.style.marginBottom = document.querySelector('.colbutt') ? '-136px' : '-154px';
  276. const rec_sec = document.querySelector('.rec-sec');
  277. if (rec_sec)
  278. rec_sec.style.width = '488px';
  279. const interest_sect_level = document.querySelector('#interest_sect_level');
  280. if (interest_sect_level)
  281. interest_sect_level.style.width = '488px';
  282. // IMDb
  283. if (!isEmpty(data.imdbRating)) {
  284. insertDoubanRatingDiv(ratings, 'IMDb评分', data.imdbRating, `https://www.imdb.com/title/${id}/ratings`, data.imdbVotes, data.histogram);
  285. // IMDb Top 250
  286. if (!isEmpty(data.topRank) && data.topRank <= 250) {
  287. // inject css if needed
  288. if (document.getElementsByClassName('top250').length === 0) {
  289. const style = document.createElement('style');
  290. style.innerHTML = '.top250{background:url(https://img1.doubanio.com/f/movie/f8a7b5e23d00edee6b42c6424989ce6683aa2fff/pics/movie/top250_bg.png) no-repeat;width:150px;font:12px Helvetica,Arial,sans-serif;margin:5px 0;color:#744900}.top250 span{display:inline-block;text-align:center;height:18px;line-height:18px}.top250 a,.top250 a:link,.top250 a:hover,.top250 a:active,.top250 a:visited{color:#744900;text-decoration:none;background:none}.top250-no{width:34%}.top250-link{width:66%}';
  291. document.head.appendChild(style);
  292. }
  293. let after = document.getElementById('dale_movie_subject_top_icon');
  294. if (!after)
  295. after = document.querySelector('h1');
  296. after.insertAdjacentHTML('beforebegin', `<div class="top250"><span class="top250-no">No.${data.topRank}</span><span class="top250-link"><a href="https://www.imdb.com/chart/top">IMDb Top 250</a></span></div>`);
  297. [].forEach.call(document.getElementsByClassName('top250'), function (e) {
  298. e.style.display = 'inline-block';
  299. });
  300. }
  301. }
  302.  
  303. // Metascore
  304. if (!isEmpty(data.Metascore)) {
  305. const metascore = parseInt(data.Metascore);
  306. let metacolor;
  307. if (metascore >= 60)
  308. metacolor = '#6c3';
  309. else if (metascore >= 40)
  310. metacolor = '#fc3';
  311. else
  312. metacolor = '#f00';
  313. ratings.insertAdjacentHTML('beforeend',
  314. `<br>Metascore:
  315. <span style="background-color: ${metacolor}; color: #fff; height: 24px; width: 24px; line-height: 24px; vertical-align: middle; display: inline-block; text-align: center; font-weight: bold">
  316. ${data.Metascore}
  317. </span>`
  318. );
  319. }
  320.  
  321. // Rotten Tomatoes
  322. let tomato_score = null;
  323. for (let i in data.Ratings) {
  324. if (data.Ratings[i].Source == 'Rotten Tomatoes') {
  325. tomato_score = data.Ratings[i].Value;
  326. break;
  327. }
  328. }
  329. if (tomato_score) {
  330. ratings.insertAdjacentHTML('beforeend', '<br>');
  331.  
  332. const tomatoimg = {
  333. 'certified': 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAEyElEQVR4AdWTY4DkSABGXyXpTqYxRk+PT2sLZ9u2bdu2bdvmeG27b8dWWkhSZxO/7sWo7xX53yP4C/yGGtx7M/f+20xWtivNE5mOBAGicp4zZ+4C53Pg3X8l0DVh3H+695GjDuLwxqjklsd8lBbaDN/Y+vZaP89FcaHDqDyn9dQbkxcBz/xtwaAC19CZz3jm1a4X+gfVOhcfH+G9SjejBqeonaMzYpDFgy96OHi3GMUBG78Oi2fw3lG3mwcAsT8VDP4m/FV97pd9GB/WGDS1KRQVSE4+IMxDs3OZEjTJVGwkMHJQikPPz+SW80z6TMGSma7lwNA/FBiaMFY+m9V307uanpUuOfmgKO9U6l+HR4lZKue9HKQs6HDqZm3oikN/WLAipHHZPX5KAjbnHh7htefFPcCZfI/Gzzh3d//NxaNj+hYtBg++4qG8THLkPjGqm7O5e3ohfQv6aGkQxNI34ooxK8nwwdANbIZtlGLUJjajh1sEjzHOGHXowE1A629a0Pt2Vuym9zRji/EpSgocKsocLvyigh7Nz4LVCTxzenAhMXYsYJdglHHlvWwV6MaMCl76OI0PanROODDCp2/LH1vxo2Byhb5Z9XPu2qNu9JHhl1xwdJgQmTxYE6AkIVmUpaFKgcsFAUUwfrrJ/CIXnjGCC8vX8dS7GmZEECxwGO6jFQj+QnDqlhlX335H8goFAaqksdbLxx/ks9faGHqGxsW7ZeCNSbRei/WjvJz2Qi+jVsWYm6XSMtTNTic30tCnMHyjFMl13h+zNX5cUEjHUnCkgHZB5u1+DvYluW+cQXxsBkWmxa5zTQY1WNwmFELFGsPXCEYOOIysj6MZfoacY2IlVaTb4TeDLEAIW0UCqToDxxaIPJWzvQauiAuJRqo4RSoe46DP+5kxwYcEpABUSHyShn52GGwBUe23gvqQtU5J6EgNFFNF1UDRBcKrIoJ+SElEdwzFLSiwYKMVMRQBErAERBQHY5WOXurQFib+G8H8hvgnyoCXL9bGsRokVliiNMdRZBy1O4qqqCTaTGhPIWLg9Fnca6cYprkwHQdHKuQuTRKIChbPEp/8RhBOOD33vas0dRZdUrzDhdvyxGMPc8TRx4G0KdtgI6ZOHE9HcwPX3HgTjz76KG9VVhOq+ozLH36IpfPn49EVojc4HHPErjQ3zO35jaB4WP7F9WPLi70rY8xpNJm9cAVjmsMYHi+azCXc20V1kyTUESPl7uHFV98jsMlwTDNBTTPsd9Il335vcATqpOBRwNE/CrICaUcUb2js3dJuo/TbrP3oCzq7ljKz3WL9nDcZ1OlQvtmevL8uwsKuJPH+TGa0xhk+qBB/YQXvrYvy8hM3EdjvEtpMSVZMEl7acefCytZLRGbAc9wxD41+tDeUojIkiX/cCgKMybnEpnei+DUc00IIkFIihIIQgmQq+e2zy+OGpETNMbC/mQRZBsVjM9huTx+rq8IIAN3jOmT/28c//1FlBDuRAkBLOM1OOBni7yBxlOy0LSxVQqZOsSLJlomFdU+v3kkD8Aa8g1r7QF3aRv/KvhoBQ4Bi/gG2Y5+lKNrx/lxPhdgukJbwGkMmHD+iDQD/oLxPfGWZ1wA6/Hc86d7di7fccJbb5R7N/56vAEDvDGwghbBBAAAAAElFTkSuQmCC',
  334. 'fresh': 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAFBElEQVR4Aa2VA7TszBpEd6c7yfiY13q2bdu2bdu2bdu2bdtHGGei7u9l8Ju4tVaNsKtaieJsyA917djNjrzk9z/a/hzwKc6BDGdDpbo/e3i1es3NYPtrrX+1K0B0XAI8rUJtvKA6E+6J/9XV57/I9PX/mGX/A77D2ZQ6xRuFBzgAE+r6ngvO3ODoFVfuV58LyvZPvSs1zt+wP/3W/56YRPnuf3698xGgc45GIChvam/1KqsXnLnYwQvN3nTPhWeulXStIXO0kjb/+vh/zEVue/AZUvPi1f8kgx+ttT8GJGd7BABTvlm5/NX3vlEuM3/TxaUSTdH8+7d9yv/oUGsY6pecJlguMb8z2M7/3Pvd5k+3Pv/DP+y8D/jXWQYESgXXPDh7p/lbHnhcfqh6QV02DFKo4fhnU6hVNIcWNO4fEUEno1sExg2fcGvw73984l8v/M5vN98CJKcbMOXpmdtecvWljasu3fNif4459p+UPx30+f6NF9ChR9bLiaqG0PeY++uA+759g1rHsjal+d2hkJ9dosLar7ff88Wv/euRwPYpAspKlZ+8uvzWix1o3PFyu5YlrSAsrODpt1vk9/tC7vyONbpW+MpN5hkcrnCjLzS5ywe2yX2FlwstDd89EvCTne6HX/aX/90DiE4MeHh95glPm5t7fqWqyBYNa4s+67OGv84FfPlgmUN/6/O4r7ao/zvn+0XjN99/menA48kv/C9hIuRG4TswuaPvK17YbT4NeDaAdwE/uPADGtMPV3VNuuwTzfv8YUbzw1mffxrFdX/X5cH/TPCXA9pHQq7Ycdzxg9sMrBCXNZ5jpMyDQRHqKcV9a1MP/fygf/HCmJuUardaDM2yqiqkoalO+1y3EXD9IMTzFH7FJ1tIiHQKqdCZN1z5rwl/+GWfqKKY3RUEheIkzXt64abl6q2BX5orh+Wr4wMlUKGCqoaZMt5CHVUyJO2EfLODF+dQUtgSeL7H5X4VQS6Id0o4gAOuGJavCWD2G3MIBSiFGAWeQrRGygGqXoIhWHuFx9+pwkkAB9YzlIJMn4SXCTxFmNFmH4ApKVXCAVYgH1uSDNcejJ/7KZLZwoLkbvw9inIuoECGhglCSIFYhIE4H8A0rdtdzlklFlQkSD9HBTHWOZTWSJpDO0H1LcQCiYCVERg1AU9aJwgRMED4n827AOZ3efLrC2bBRaQn2HaGNuDsMCxHlMLlDgq4a2fQdhAJOBA1BmeT1hHD1kJfHD3n+FUa/xbAfDmOPnvrSv2O0negwUqOShwq9MBTkDskLty20BRcIghCJpw0HQzBhZ2li9DKUr4fDz4zCeh/7odp/N3LB6Ur03GQgnQtEqoTA4hB+oIkQgYjeMKkNePWXWfp2Jx2nvHrOPrRr7Pk0wAK4CrFlnrX3MrHpjxvGgAP0JOvRRAL1o3hyaR1xLCxG8HbtoC7bNR8LYk7n8iy2wJfOsXF7o7Vxj1eOr3w+lCpEsJIMrKQAhlCDEQiI/ecK2xpuXHrVhKzlSXZ16x9MPDm071c36Rcu9XTp+ZecNj4xxyQTxYxnrTuU1gsPetonwDOEpppwj/z/G8/FnkS8MEzveEcMv7R+9amH3bdcuWWs57ZOxA32R1CT9x4ngu38rQAx/w3zf73B3Gf+BO8Cvjj6d9wTkdHjH+BS4Wla1zQhFde0PqCGuaK9n4zy9LNLGn+O0//8A/rvrsG3wB+xxno/+N5rMoDguFXAAAAAElFTkSuQmCC',
  335. 'rotten': 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAFZ0lEQVR4Ac1UA5Qj7RYMhwjGtk10upOsbTtr28YoM6uxbRtr29Zv22a9O3q265w6za+uwPtr0Jfo+npGWmuDBzsUmDmYTOYL+GLevwsSCwNmbVzQq02XGbRcZZBWF/VDgMoumc/ni/51dT5PMGK2e8uJ+yR+TYHGSyw6bjI4Uhn9i8zGaNi/rC/SEZqu14a+X3eRRWEnsYNFEV2zmxVwCTRf+fv/CsVCqaFcP5zS6ScQ8vX+qrCugdhW31jHpfvHiUt8LpWdYJHZqEBmgwLZTQoklSpg5Sqd1v+/qZ3xcM3G0GuZdWO+0BYM/yh0iFO+SFdo9mfFnfzMVmw7EvLGnszwDwKUtomWzpJpuzKivslq4nC4gkV6HYcFO0Le0zEQ2/MIBjK9wPXaqPcbLo6hGo3H2YfTUXFyAryibVP+RNzS0WRIZn3El513GFSeY5Fcx/5q7iSZbOYombJOG/nN0SolDpVzCB7kkNt/xj3Mam9KtZLqokRqjRpZTYNRc3Ykxi8JfC4QCoz/wECI0mpj01UFyk+zyGjhkNKoQuQo57Ie426yhTvSGRJRImK4U1FPjcRCySiN14XDFRwSilloiXRFer0KU9YEfUx1sfnD9PjI55SdYlByksORWhZJ5PHymIiPKYopcnuTieuTon44WsVhV2b096M1nicmL/O9ElvA/qIt5ZBUxuJgGaWxkkgRRY/1eEbz8ocRiPVEVpOW+1zKP9brfWwJBy0d2put+GVnGvNrQhGLeGJiCQvyGgeJ9B1JNSpoy5WIp/9jCllM3xAKmbXxvj8/tdQ9oUMcMpfFhn+SSIdIDIml5F05RwIs4sjb35LEY4hT14ciYqTHNz6MwzOXQOsrEkvDWPJewvtroC6xDVDbpx8oYn89VMshsYolT8nbavK8pptKpDQpsSE5Guau8nahjtCFz+cJe8bzr4EicLawN2KlFgbBFtSiW1Ijv87q4JDazCGtpY/NbM81o40MVzJQTHA7ae4snSqxMlLrmeh6UfcY/NnF4BxovnZ5TMhre3Iiv9qaFvHpgaLo7zNau0XZPnJIb+0mS+IsyDByqV5ZnQrElDDYmBL5w+xtQW+rpnlesPSQL/+DiZbbGY/clhb+Sd7xnoPE3mtmO4ucrl5vM0k0u5Mj0to4zaH8ghJVl1WouqRG3dXBaLk1DJ33RuH4gwlIrh4OP7VDqUDUF03wEMei5AYFCfaIEVnkHe8VyyCPS86qUHpWjfLzA9F4YyjiC5U/RI5wur5iT8T7px9NxenHM3DmyWycfzYXF15ocOe1xcisHQtbP/NtNO2hPEtX6ZyY4sivKy5wKD6jQs1lNfI6VVBO9bjoq7TPT6sf/N3Jx5Nw/P5UXH5pDtbsY94X6QlDdQzF0Zu0qvdvv7oYF19aiEsvLcD55wt6ruceaDBoms81O1/z/bzu/R400P5o0fEhaL89FmUnR4MZ59ZG3SEX6Yls1sYwL918ZSF5Nx/XX1mEmhNTYOko2csjWHuaripun4irLy9A15056LgxG81XZhCnY8zCwFfdI2xo8gnd+bJykU4KG+gQa+9lqhGKBEa9q1hgsmBz+PUbJHz2iQZnHmsoCg0mzA19nb65dDvHjHPvKOmaiOKuSUij1CRXjcburEHwYR0vmzlIdvH+FlQTPKpPPZiD1qszUHd+CkU5HbNWRf1I0z+gp72pPQfO8Lm3PFaBRfsZTN8agbCRbu/pGekMobY1/5sGTCwN1Uv2Mh/HFQ3r8WzO+mjYeZi108TKfn84bbzNdjr4W5abOUgTxboiP94/AjIyyD3KrtMl2OaWsZnBnh7P/p0QUcGltiarjC0MD4j1hNG8/yf8BrCAoJdN16WUAAAAAElFTkSuQmCC',
  336. 'full': 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAFK0lEQVR4AZTNQ4DsWAAF0JsXFJIygm/btm3btoXN2LZt27bRtm2nKprGdlr3mYegndA0ocMhV5gQilzdO+CmJ25c9uonL576dtwIaSLQEo/b4aUag/9Ju8CC2SMXvvP8kQ/uuW7Vfdu3bdkxdfLkWZNHKdPO7Z5288wJvRY8eOuORy4cmn4ZgNUpQJF8XWZM6DH7zMEFF7o52bFrZvQ53G/kVNnrZ4WqyiJj0crVM568afkbC4bZ9nf30SMH9vYM7TDg8/C+2y/NffiFOzZ8On6oNM0VFk04esLUSmCodfaKzDRaL/sFvoCd93Xvj5lzV8zZu2XCoQ4BTo7hH7pp3RMrlk9f6g0rdF3BP+AYjYS72VGUGgPG0hES/SgvZlBTrsGqL4TXU0sU0d6dt3N8m4AblOeOwd0fmT1JWuUMjQYIi/rKepSn/IyKrBgwQg9wPAOV6Ql/r/7wSjx0tQJ15dWYMm7IotsPL76Xp2mhVWC34D2y4uj0LXzXYdCqs2GoVRBHzIKnx1BUF1fBrImBFjUg9ekFWktDpLYCFiuDYRlYhKLnDnLt3m8XTrYKjAczla7gYA/2hIpy1FQVorqkGLSgINyrJyJVpYCpw2YzQTu6gqK8cCsKNLMIzpAXLkcAM3jHvFaBCEEDc9fnqNx9A7TPEsDWmjDqMgEOYCUFoVETUakZSE7JBngB4AREc9Nh/ZgG9sbPYLv6PqKa2dAqkMLR8XB5YL70O6wz74E++gmc1/6GyOnXoF33HYw7f4brmVgID/4I9fRLIOc+gLbrbXDXJUB7NgYmKyCGpf9qFUiHlSo8fDfI0H6wiAVU69C/y4P2bgzURz5Bwy3vgn74ezje/hvW+3HQv4gBVanDpCyQwb3geuRupFpmYqtAXl1NTtTnjRBZApFEuF9+GsL9N4MKCiDdJNj3bodt72ZYXYKAT2g8u63xzjMgsghalhAN+CK5tTU5rQIFqppXEBtbClEEGlQwo0bAcWAvYLM3IgEI99zW2O4AFQoBNq7prPHOyP8qK4cEWK4Aip42Y3tFyR5CDJNRsImY09j2t+2wzVJbVY+prmnQSfHp3vMEbBhiY4175dexE4bDfwRMYdKv1R3uvhOzXKA8H2MtqVwOKxUYk7wIBfl8EtZ+gFkssbGmX/vDmcL4HwELa+f9334fmttvwypJNJsjUylMoZjErbFYazBSYotFVDqNms0wSmBjTffX3wdbj3/bKsJWo97W112HtaCnUyRgSwWMUmhrURa01glAA2I6QxnLVtOp1zrA5t8A1Pv9miqXkPkCejzBAKZcTEz11sgatJLoUikB6MkUlSsgqxVqg/4fOze77mTSWksp1DVXI12PxKRYRmuJwiSQBBZDJSA8DxOXDYUQncm4uRMwULI38YOZufEGhOsiAV0uoZRBaoNKAApVKiKAyHFRcdlpMFoMpRzsBDgi6nmDgW/vuQfpeFjAlEoordFSIqVAJi0ooxOAA/fchT8ceI4QvZ2AhWXS/O67YD0eI6OQDKCvvZZIa1Q6g8pkENpgr7mWJC/cEE4m1L75NphZO9oJAKKfzp/73Dt5kmWrzeqzL5GLBWo7XZ0hdjjEpMAs5qzjvFWzjXfyFHvOn/sCiP4LgO/Wq9ffN+L5oF4T/UceZ/X9D6TSsHzgYRYPPJQA5j/8RPfRx/HjMh8Z+eK369Wr/+fQX705Dp58ehrc+7YMXz6wWR88G4U/n2+1m6earT+OhZuzezerve+J8JVnp8F9b4z9J4DV3xn9CYgbvHRBBzqoAAAAAElFTkSuQmCC',
  337. 'spilled': 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAEXklEQVR4Ae2UQ6MjWRiG33NOpVJIUhX7KpdtjW3btpfzG2Y9mxE2Y9tm27Z9jSRlTKaGbaz7LfP5jBM6kgiOQ5SCyxTFCeOnpa9L5sOtS2b2vw7gexxE3GHpFEwQOSUS4zOpjFpKZ5XmbE5tbatUWjip2gZhRw+ciBKLRcd/8/76YQALD+sBH2aR7smpyxvfd2Tz6VJzU1ux0trekkwlE+lkTpEkWTbtKtGMUSxa/wH29m3Gip+SsHSKbPeueW89t/JyAEMHBSTSQuW6+5tePfusc87oKJ0Lx7VQ1QYwPLYLo7VeaMYQdHMMrufAhw9BYujfImPl7zI8l6BtiubPn7PkTgBvHQAgBPS2J7o/PPW8wrXETsL3AI6FIAoKZFFBTM4iKmcgNc55TsacFW9i9+AibFqQwe6NPBgF5LgNHWtfBPDoPjkIiyxWaIpMa+4MnT2t63JMb38UPjzwnADWgOyvmjaE35a8ANsGasMMjPoglMDQGMS00PnF2xsUAKP/Alo6Yme19agXqaqY6O3rxXztUxjWGCy7Bt2swrQ12LYRbI5nw3bq0N29IE4EpkZAGAAgCBPxWJLjiLQPIF2QxmcL0kT4PFatmwutPhOUAoRQUMYQHAkJzhnh4MMGEz24dgi+R0EI4CPYAT4VKSP7uM01SjArRbhUrabjunOfQnvTGbBtG5RSMEIARkB8EkD4sIB122bhk9+ehO+GIIoyQHzADwBgjOcIAdsHYFtu3bY93bE89A/ugcRvhuvaoIwDoyzYaLBRCEIMtfoIPN8DYRxURQGhPnxQ+B6BSTkfjWUfQHXE2l0btXdnCyK+/vVpGIYZWEUaG0BAKQEJQkRBwMCFGGQlFHgkRRX4YKAE4BqgvjqxPM939gEM9hoblYTWUqpwmDHpBkysXIKRai8MqwbTqsO2dZh2HYb5Z+J1GNYoeodXgcLGxHYeYeZAtxXsHCLQBrQR1/GNfQDbN47NMgyn2j4+fJ9ujGQkMYGonIYgRMCHJHCcEPQECEGIA6q1Pjz79r0Yre3E2VNGkRHqsHgev65T4Lqt2rd0g3XQTr705uaXuyZGH5T4JCgNwXHNv5stCllqQKUkVKUQHOev+BBbdy3Hw1fciS41jj2bF4AQAeFkS+/Fd794NoD1+Fv/ZnzXlvpsQUa+p3ty1xnTb2PlwkSSTVQg8BEYZg39Q9uweccCrNn4E+raACyXQIaBKc05REsTURvYiWg8JYyf0Fnp7xvEgqVbN45VdYfsO4YJpyb5rkxO6Sw3FdtLTbm2YqnU1N7W1dwYd4lYLK6KgiQ2QomvZ74Es7oV906ahpbWqUDYgM8SQSn7zgC+XUA++fHXxT+Qo5n9gsipUYXPp9JqUyIdbS2VcxXGG20O65vQU0hHWtTSxrOnTu+CuUsaHBrarVaupG99OPPNWXOXzyI4ToVFFm/rjF1BeGRH99o/P3DLRVcrvB6fu3DtPCtU1mbNXTFzeGRsGCd0JP0BLHO0MJZ4Kw0AAAAASUVORK5CYII='
  338. };
  339. // Currently no way to know if it's certified or not
  340. let fresh;
  341. if (parseInt(tomato_score) >= 60)
  342. fresh = 'fresh';
  343. else
  344. fresh = 'rotten';
  345. const tomato_url = data.tomatoURL.replace('http://', 'https://');
  346. ratings.insertAdjacentHTML('beforeend',
  347. "<a href=" + tomato_url + " target=_blank style='background:none'><span style='background: url(data:image/png;base64," + tomatoimg[fresh] + ") no-repeat; background-size: cover; width: 18px; height: 18px; margin: 0 2px; vertical-align: middle; display: inline-block'></span></a>" +
  348. "<span style='vertical-align: middle; display: inline-block; line-height: 18px'>" + tomato_score + "</span>"
  349. );
  350. if (!isEmpty(data.tomatoUserMeter)) {
  351. let userimage;
  352. if (parseFloat(data.tomatoUserRating) >= 3.5)
  353. userimage = "full";
  354. else
  355. userimage = "spilled";
  356.  
  357. ratings.insertAdjacentHTML('beforeend',
  358. "<a href=" + tomato_url + " target=_blank style='background:none'><span style='background: url(data:image/png;base64," + tomatoimg[userimage] + ") no-repeat; background-size: cover; width: 18px; height: 18px; margin: 0 2px; vertical-align: middle; display: inline-block'></span></a>" +
  359. "<span style='vertical-align: middle; display: inline-block; line-height: 18px'>" + data.tomatoUserMeter + "%</span>"
  360. );
  361. }
  362. }
  363.  
  364. // MPAA Rating
  365. if (!isEmpty(data.Rated)) {
  366. insertDoubanInfo('MPAA评级', data.Rated);
  367. }
  368.  
  369. // Box office
  370. if (!isEmpty(data.BoxOffice)) {
  371. insertDoubanInfo('票房', data.BoxOffice);
  372. }
  373. } else if (host === 'www.douban.com') {
  374. // www.douban.com/personage/*
  375. let person_id_node = [...document.querySelectorAll('span.value')].find(s => s.innerText.trim().match(/^nm\d+/));
  376. if (person_id_node) {
  377. linkifyIMDbNode(person_id_node, 'https://www.imdb.com/name/');
  378. }
  379. } else if (host === 'www.imdb.com') {
  380. const id = location.href.match(/tt\d+/);
  381. if (!id)
  382. return;
  383. const data = await getDoubanInfo(id);
  384. if (!data)
  385. return;
  386. const imdb_rating = document.querySelector('.rating-bar__base-button');
  387. if (!imdb_rating) {
  388. console.log('rating bar not found, IMDb UI updated again?');
  389. return;
  390. }
  391.  
  392. let douban_rating = imdb_rating.cloneNode(true);
  393. douban_rating.firstElementChild.textContent = 'Douban RATING';
  394. douban_rating.children[1].href = data.url;
  395. douban_rating.children[1].target = '_blank';
  396. douban_rating.children[1].title = data.title;
  397. const rating_div = douban_rating.querySelector('div[data-testid="hero-rating-bar__aggregate-rating__score"]');
  398. rating_div.firstElementChild.textContent = data.rating.average;
  399. rating_div.nextElementSibling.nextElementSibling.textContent = new Intl.NumberFormat("en-US", {
  400. notation: 'compact',
  401. }).format(data.rating.numRaters);
  402. imdb_rating.insertAdjacentElement('beforebegin', douban_rating);
  403. } else if (host === 'letterboxd.com') {
  404. const imdb_link = document.querySelector('.text-link a[href*="://www.imdb.com/"]');
  405. if (!imdb_link)
  406. return;
  407. const id = imdb_link.href.match(/tt\d+/);
  408.  
  409. const [imdb_data, douban_data] = await Promise.all([getIMDbInfo(id), getDoubanInfo(id)]);
  410. const ratings = document.querySelector('.ratings-histogram-chart');
  411. if (!ratings) {
  412. console.log('ratings section not found');
  413. return;
  414. }
  415.  
  416. if (imdb_data && !isEmpty(imdb_data.imdbRating)) {
  417. insertLetterboxdRating(ratings, 'IMDb', 'https://www.imdb.com/title/' + id, imdb_data.imdbRating, `https://www.imdb.com/title/${id}/ratings`, imdb_data.imdbVotes, imdb_data.histogram)
  418. }
  419.  
  420. if (douban_data) {
  421. insertLetterboxdRating(ratings, 'Douban', douban_data.url, douban_data.rating.average, douban_data.url + 'comments', douban_data.rating.numRaters, null)
  422. }
  423. }
  424. })();