RottenTomatoes Utility Library (custom API)

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

目前为 2021-03-11 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/389810/909672/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.8
  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. let year = parseInt(_parse('.h3.year, .movie_title .h3.subtle, .meta-row .meta-value time', /(\d{4})/, doc));
  135.  
  136. // Find the javascript snippet storing the tomatometer/score info.
  137. // Everything is named different for TV shows for some stupid reason.
  138. let m = text.match(/root\.RottenTomatoes\.context\.scoreInfo = ({.+});/);
  139. m = m || text.match(/root\.RottenTomatoes\.context\.scoreboardCriticInfo = ({.+});/);
  140. let dataString = m[1];
  141. let scoreInfo = jsonParse(dataString);
  142. let all = scoreInfo.tomatometerAllCritics ?? scoreInfo.all ?? false,
  143. top = scoreInfo.tomatometerTopCritics ?? scoreInfo.top ?? {};
  144. console.log('scoreInfo', scoreInfo);
  145.  
  146. // TV consensus is stored in a totally different object :/
  147. m = text.match(/root\.RottenTomatoes\.context\.result =\s+({.+});\n/);
  148. console.log('m[1]', m?.[1]);
  149. let fixedJson = m?.[1].replace(/:undefined/g, ':null');
  150. console.log('fixedJson', fixedJson);
  151. let contextResult = m && jsonParse(fixedJson);
  152.  
  153. if (all) {
  154. // Try field names used for movie data, then TV show data.
  155. const data = {
  156. id: id,
  157. score: all.score ?? all.tomatometer ?? -1,
  158. rating: all.avgScore ?? all.averageRating ?? -1,
  159. votes: all.numberOfReviews ?? all.totalCount ?? all.ratingCount ?? all.reviewCount ?? 0,
  160. consensus: all.consensus ?? (contextResult?.seasonData?.tomatometer?.consensus) ?? doc.querySelector('.what-to-know__section-body > span, .mop-ratings-wrap__text--concensus')?.innerHTML, // TV consensus is stored in a totally different object :/
  161. state: all.tomatometerState ?? all.state,
  162. topScore: parseInt(top.score ?? top.tomatometer ?? -1),
  163. topRating: parseFloat(top.avgScore ?? top.averageRating ?? -1),
  164. topVotes: top.numberOfReviews ?? top.totalCount ?? top.ratingCount ?? top.reviewCount ?? 0,
  165. year: year,
  166. fetched: new Date()
  167. };
  168.  
  169. console.log('found data', data);
  170. resolve(data);
  171. } else {
  172. reject('error getting rt info for id ' + id);
  173. }
  174. }
  175. });
  176. });
  177. }
  178.  
  179. function getRtInfoFromTitle(title, tv, year) {
  180. return getRtIdFromTitle(title, tv, year).then((id) => {
  181. return getRtInfoFromId(id);
  182. })
  183. }