RottenTomatoes Utility Library (custom API)

Utility library for Rotten Tomatoes. Provides an API for grabbing info from rottentomatoes.com

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/389810/959077/RottenTomatoes%20Utility%20Library%20%28custom%20API%29.js

  1. // ==UserScript==
  2. // @name RottenTomatoes Utility Library (custom API)
  3. // @namespace driver8.net
  4. // @version 0.1.11
  5. // @description Utility library for Rotten Tomatoes. Provides an API for grabbing info from rottentomatoes.com
  6. // @author driver8
  7. // @grant GM_xmlhttpRequest
  8. // @connect rottentomatoes.com
  9. // ==/UserScript==
  10.  
  11. console.log('hi rt api lib');
  12.  
  13. const MAX_YEAR_DIFF = 2;
  14. const MAX_RESULTS = 100;
  15.  
  16. function _parse(query, regex, doc) {
  17. doc = doc || document;
  18. try {
  19. let text = doc.querySelector(query).textContent.trim();
  20. if (regex) {
  21. text = text.match(regex)[1];
  22. }
  23. return text.trim();
  24. } catch (e) {
  25. console.log('error', e);
  26. return '';
  27. }
  28. };
  29.  
  30. function jsonParse(j) {
  31. try {
  32. let result = JSON.parse(j);
  33. return result;
  34. } catch(e) {
  35. //console.log('RT error', e);
  36. return null;
  37. }
  38. }
  39.  
  40. function getRtIdFromTitle(title, tv, year) {
  41. tv = tv || false;
  42. year = parseInt(year) || 1800;
  43. return new Promise(function(resolve, reject) {
  44. GM_xmlhttpRequest({
  45. method: 'GET',
  46. responseType: 'json',
  47. url: `https://www.rottentomatoes.com/api/private/v2.0/search/?limit=${MAX_RESULTS}&q=${title}`,
  48. onload: (resp) => {
  49. let movies = tv ? resp.response.tvSeries : resp.response.movies;
  50. if (!Array.isArray(movies) || movies.length < 1) {
  51. console.log('no search results');
  52. reject('no results');
  53. return;
  54. }
  55.  
  56. let sorted = movies.concat();
  57. if (year && sorted) {
  58. sorted.sort((a, b) => {
  59. if (Math.abs(a.year - year) !== Math.abs(b.year - year)) {
  60. // Prefer closest year to the given one
  61. return Math.abs(a.year - year) - Math.abs(b.year - year);
  62. } else {
  63. return b.year - a.year; // In a tie, later year should come first
  64. }
  65. });
  66. }
  67. //console.log('sorted', sorted);
  68.  
  69. // Search for matches with exact title in order of proximity by year
  70. let bestMatch, closeMatch;
  71. for (let m of sorted) {
  72. m.title = m.title || m.name;
  73. if (m.title.toLowerCase() === title.toLowerCase()) {
  74. bestMatch = bestMatch || m;
  75. console.log('bestMatch', bestMatch);
  76. // RT often includes original titles in parentheses for foreign films, so only check if they start the same
  77. } else if (m.title.toLowerCase().startsWith(title.toLowerCase())) {
  78. closeMatch = closeMatch || m;
  79. console.log('closeMatch', closeMatch);
  80. }
  81. if (bestMatch && closeMatch) {
  82. break;
  83. }
  84. }
  85. //console.log('sorted', sorted, 'bestMatch', bestMatch, 'closeMatch', closeMatch, movies);
  86.  
  87. // Fall back on closest year match if within 2 years, or whatever the first result was.
  88. // RT years are often one year later than imdb, or even two
  89. function yearComp(imdb, rt) {
  90. return rt - imdb <= MAX_YEAR_DIFF && imdb - rt < MAX_YEAR_DIFF;
  91. }
  92. if (year && (!bestMatch || !yearComp(year, bestMatch.year))) {
  93. if (closeMatch && yearComp(year, closeMatch.year)) {
  94. bestMatch = closeMatch;
  95. } else if (yearComp(year, sorted[0].year)) {
  96. bestMatch = sorted[0];
  97. }
  98. }
  99. bestMatch = bestMatch || closeMatch || movies[0];
  100.  
  101. if (bestMatch) {
  102. let id = bestMatch && bestMatch.url.replace(/\/s\d{2}\/?$/, ''); // remove season suffix from tv matches
  103. console.log('found id', id);
  104. resolve(id);
  105. } else {
  106. console.log('no match found on rt');
  107. reject('no suitable match');
  108. }
  109. }
  110. });
  111. });
  112. }
  113.  
  114. function getRtInfoFromId(id) {
  115. return new Promise(function(resolve, reject) {
  116. if (!id || typeof id !== 'string' || id.length < 3) {
  117. console.log('invalid id');
  118. reject('invalid id');
  119. }
  120. let url = 'https://www.rottentomatoes.com' + id + (id.startsWith('/tv/') ? '/s01' : ''); // Look up season 1 for TV shows
  121. //console.log('url', url);
  122. GM_xmlhttpRequest({
  123. method: 'GET',
  124. //responseType: 'document',
  125. url: url,
  126. onload: (resp) => {
  127. //console.log('resp', resp);
  128. let text = resp.responseText;
  129. //console.log('text', text);
  130.  
  131. // Create DOM from responseText
  132. const doc = document.implementation.createHTMLDocument().documentElement;
  133. doc.innerHTML = text;
  134. //console.log('doc', doc);
  135. let year = parseInt(_parse('.h3.year, .movie_title .h3.subtle, .meta-row .meta-value time', /(\d{4})/, doc));
  136.  
  137. // Find the javascript snippet storing the tomatometer/score info.
  138. // Everything is named different for TV shows for some stupid reason.
  139. let m = text.match(/root\.RottenTomatoes\.context\.scoreInfo = ({.+});/);
  140. m = m || text.match(/root\.RottenTomatoes\.context\.scoreboardCriticInfo = ({.+});/);
  141. let dataString = m?.[1];
  142. let scoreInfo = dataString && jsonParse(dataString);
  143. let scorescript = doc.querySelector('#score-details-json');
  144. let sb = scorescript && JSON.parse(scorescript.innerHTML);
  145. let scoreboard = sb?.scoreboard;
  146. let all = scoreInfo?.tomatometerAllCritics ?? scoreInfo?.all ?? sb?.modal.tomatometerScoreAll ?? {},
  147. top = scoreInfo?.tomatometerTopCritics ?? scoreInfo?.top ?? sb?.modal.tomatometerScoreTop ?? {};
  148. //console.log('scoreInfo', scoreInfo);
  149. //console.log('scoreboard', scoreboard);
  150.  
  151. // TV consensus is stored in a totally different object :/
  152. m = text.match(/root\.RottenTomatoes\.context\.result =\s+({.+});\n/);
  153. //console.log('m[1]', m?.[1]);
  154. let fixedJson = m?.[1].replace(/:undefined/g, ':null');
  155. //console.log('fixedJson', fixedJson);
  156. let contextResult = m && jsonParse(fixedJson);
  157. //console.log('contextResult', contextResult);
  158. let sd = contextResult?.seasonData;
  159.  
  160. if (all) {
  161. // Try field names used for movie data, then TV show data.
  162. const data = {
  163. id: id,
  164. score: parseInt(all?.score ?? all?.tomatometer ?? scoreboard?.tomatometerScore ?? sd?.tomatometerScoreAll?.score ?? -1),
  165. rating: parseFloat(all?.avgScore ?? all?.averageRating ?? sd?.tomatometerScoreAll?.averageRating ?? -1),
  166. votes: parseInt(all?.numberOfReviews ?? all?.totalCount ?? all?.ratingCount ?? all?.reviewCount ?? scoreboard?.tomatometerCount ?? sd?.tomatometerScoreAll?.reviewCount ?? 0),
  167. consensus: all?.consensus ?? sd?.tomatometerScoreAll?.consensus ?? doc.querySelector('.what-to-know__section-body > span, .mop-ratings-wrap__text--concensus')?.innerHTML, // TV consensus is stored in a totally different object :/
  168. state: all?.tomatometerState ?? all?.state ?? scoreboard?.tomatometerState ?? sd?.tomatometerScoreAll?.state,
  169. topScore: parseInt(top?.score ?? top?.tomatometer ?? sd?.tomatometerScoreTop?.score ?? -1),
  170. topRating: parseFloat(top?.avgScore ?? top?.averageRating ?? sd?.tomatometerScoreAll?.averageRating ?? -1),
  171. topVotes: parseInt(top?.numberOfReviews ?? top?.totalCount ?? top?.ratingCount ?? top?.reviewCount ?? sd?.tomatometerScoreAll?.reviewCount ?? 0),
  172. year: year,
  173. fetched: new Date()
  174. };
  175.  
  176. console.log('found data', data);
  177. resolve(data);
  178. } else {
  179. reject('error getting rt info for id ' + id);
  180. }
  181. }
  182. });
  183. });
  184. }
  185.  
  186. function getRtInfoFromTitle(title, tv, year) {
  187. return getRtIdFromTitle(title, tv, year).then((id) => {
  188. return getRtInfoFromId(id);
  189. })
  190. }