Show Rottentomatoes meter

Show Rotten Tomatoes score on imdb.com, metacritic.com, letterboxd.com, BoxOfficeMojo, serienjunkies.de, Amazon, Google Play, allmovie.com, Wikipedia, themoviedb.org, movies.com, tvmaze.com, tvguide.com, followshows.com, thetvdb.com, tvnfo.com, save.tv

  1. // ==UserScript==
  2. // @name Show Rottentomatoes meter
  3. // @description Show Rotten Tomatoes score on imdb.com, metacritic.com, letterboxd.com, BoxOfficeMojo, serienjunkies.de, Amazon, Google Play, allmovie.com, Wikipedia, themoviedb.org, movies.com, tvmaze.com, tvguide.com, followshows.com, thetvdb.com, tvnfo.com, save.tv
  4. // @namespace cuzi
  5. // @grant GM_xmlhttpRequest
  6. // @grant GM_setValue
  7. // @grant GM_getValue
  8. // @grant unsafeWindow
  9. // @grant GM.xmlHttpRequest
  10. // @grant GM.setValue
  11. // @grant GM.getValue
  12. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js
  13. // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
  14. // @icon https://raw.githubusercontent.com/hfg-gmuend/openmoji/master/color/72x72/1F345.png
  15. // @version 48
  16. // @connect www.rottentomatoes.com
  17. // @connect algolia.net
  18. // @connect flixster.com
  19. // @connect imdb.com
  20. // @match https://www.rottentomatoes.com/
  21. // @match https://play.google.com/store/movies/details/*
  22. // @match https://www.amazon.ca/*
  23. // @match https://www.amazon.co.jp/*
  24. // @match https://www.amazon.co.uk/*
  25. // @match https://smile.amazon.co.uk/*
  26. // @match https://www.amazon.com.au/*
  27. // @match https://www.amazon.com.mx/*
  28. // @match https://www.amazon.com/*
  29. // @match https://smile.amazon.com/*
  30. // @match https://www.amazon.de/*
  31. // @match https://smile.amazon.de/*
  32. // @match https://www.amazon.es/*
  33. // @match https://www.amazon.fr/*
  34. // @match https://www.amazon.in/*
  35. // @match https://www.amazon.it/*
  36. // @match https://www.imdb.com/title/*
  37. // @match https://www.serienjunkies.de/*
  38. // @match http://www.serienjunkies.de/*
  39. // @match https://www.boxofficemojo.com/movies/*
  40. // @match https://www.boxofficemojo.com/release/*
  41. // @match https://www.allmovie.com/movie/*
  42. // @match https://en.wikipedia.org/*
  43. // @match https://www.fandango.com/*
  44. // @match https://www.themoviedb.org/movie/*
  45. // @match https://www.themoviedb.org/tv/*
  46. // @match https://letterboxd.com/film/*
  47. // @match https://letterboxd.com/film/*/image*
  48. // @match https://www.tvmaze.com/shows/*
  49. // @match https://www.tvguide.com/tvshows/*
  50. // @match https://followshows.com/show/*
  51. // @match https://thetvdb.com/series/*
  52. // @match https://thetvdb.com/movies/*
  53. // @match https://tvnfo.com/tv/*
  54. // @match https://www.metacritic.com/movie/*
  55. // @match https://www.metacritic.com/tv/*
  56. // @match https://www.nme.com/reviews/*
  57. // @match https://itunes.apple.com/*
  58. // @match https://epguides.com/*
  59. // @match https://www.epguides.com/*
  60. // @match https://www.cc.com/*
  61. // @match https://www.amc.com/*
  62. // @match https://www.amcplus.com/*
  63. // @match https://rlsbb.ru/*/
  64. // @match https://www.sho.com/*
  65. // @match https://www.gog.com/*
  66. // @match https://psa.wf/*
  67. // @match https://www.save.tv/*
  68. // @match https://www.wikiwand.com/*
  69. // @match https://trakt.tv/*
  70. // ==/UserScript==
  71.  
  72. /* global GM, $, unsafeWindow */
  73. /* jshint asi: true, esversion: 8 */
  74.  
  75. const scriptName = 'Show Rottentomatoes meter'
  76. const baseURL = 'https://www.rottentomatoes.com'
  77. const baseURLOpenTab = baseURL + '/search/?search={query}'
  78. const algoliaURL = 'https://{domain}-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent={agent}&x-algolia-api-key={sId}&x-algolia-application-id={aId}'
  79. const algoliaAgent = 'Algolia for JavaScript (4.12.0); Browser (lite)'
  80. const flixsterEMSURL = 'https://flixster.com/api/ems/v2/emsId/{emsId}'
  81. const cacheExpireAfterHours = 4
  82. const emojiTomato = String.fromCodePoint(0x1F345)
  83. const emojiGreenApple = String.fromCodePoint(0x1F34F)
  84. const emojiStrawberry = String.fromCodePoint(0x1F353)
  85.  
  86. const emojiPopcorn = '\uD83C\uDF7F'
  87. const emojiGreenSalad = '\uD83E\uDD57'
  88. const emojiNauseated = '\uD83E\uDD22'
  89.  
  90. // Detect dark theme of darkreader.org extension or normal css dark theme from browser
  91. const darkTheme = ('darkreaderScheme' in document.documentElement.dataset && document.documentElement.dataset.darkreaderScheme) || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
  92.  
  93. function minutesSince (time) {
  94. const seconds = ((new Date()).getTime() - time.getTime()) / 1000
  95. return seconds > 60 ? parseInt(seconds / 60) + ' min ago' : 'now'
  96. }
  97.  
  98. function intersection (setA, setB) {
  99. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
  100. const _intersection = new Set()
  101. for (const elem of setB) {
  102. if (setA.has(elem)) {
  103. _intersection.add(elem)
  104. }
  105. }
  106. return _intersection
  107. }
  108.  
  109. function asyncRequest (data) { // No cache (unlike in the Metacritic userscript)
  110. return new Promise(function (resolve, reject) {
  111. const defaultHeaders = {
  112. Referer: data.url,
  113. 'User-Agent': navigator.userAgent
  114. }
  115. const defaultData = {
  116. method: 'GET',
  117. onload: (response) => resolve(response),
  118. onerror: (response) => reject(response)
  119. }
  120. if ('headers' in data) {
  121. data.headers = Object.assign(defaultHeaders, data.headers)
  122. } else {
  123. data.headers = defaultHeaders
  124. }
  125. data = Object.assign(defaultData, data)
  126. console.debug(`${scriptName}: GM.xmlHttpRequest`, data)
  127. GM.xmlHttpRequest(data)
  128. })
  129. }
  130.  
  131. const parseLDJSONCache = {}
  132. function parseLDJSON (keys, condition) {
  133. if (document.querySelector('script[type="application/ld+json"]')) {
  134. const xmlEntitiesElement = document.createElement('div')
  135. const xmlEntitiesPattern = /&(?:#x[a-f0-9]+|#[0-9]+|[a-z0-9]+);?/ig
  136. const xmlEntities = function (s) {
  137. s = s.replace(xmlEntitiesPattern, (m) => {
  138. xmlEntitiesElement.innerHTML = m
  139. return xmlEntitiesElement.textContent
  140. })
  141. return s
  142. }
  143. const decodeXmlEntities = function (jsonObj) {
  144. // Traverse through object, decoding all strings
  145. if (jsonObj !== null && typeof jsonObj === 'object') {
  146. Object.entries(jsonObj).forEach(([key, value]) => {
  147. // key is either an array index or object key
  148. jsonObj[key] = decodeXmlEntities(value)
  149. })
  150. } else if (typeof jsonObj === 'string') {
  151. return xmlEntities(jsonObj)
  152. }
  153. return jsonObj
  154. }
  155.  
  156. const data = []
  157. const scripts = document.querySelectorAll('script[type="application/ld+json"]')
  158. for (let i = 0; i < scripts.length; i++) {
  159. let jsonld
  160. if (scripts[i].innerText in parseLDJSONCache) {
  161. jsonld = parseLDJSONCache[scripts[i].innerText]
  162. } else {
  163. try {
  164. jsonld = JSON.parse(scripts[i].innerText)
  165. parseLDJSONCache[scripts[i].innerText] = jsonld
  166. } catch (e) {
  167. parseLDJSONCache[scripts[i].innerText] = null
  168. continue
  169. }
  170. }
  171. if (jsonld) {
  172. if (Array.isArray(jsonld)) {
  173. data.push(...jsonld)
  174. } else {
  175. data.push(jsonld)
  176. }
  177. }
  178. }
  179. for (let i = 0; i < data.length; i++) {
  180. try {
  181. if (data[i] && data[i] && (typeof condition !== 'function' || condition(data[i]))) {
  182. if (Array.isArray(keys)) {
  183. const r = []
  184. for (let j = 0; j < keys.length; j++) {
  185. r.push(data[i][keys[j]])
  186. }
  187. return decodeXmlEntities(r)
  188. } else if (keys) {
  189. return decodeXmlEntities(data[i][keys])
  190. } else if (typeof condition === 'function') {
  191. return decodeXmlEntities(data[i]) // Return whole object
  192. }
  193. }
  194. } catch (e) {
  195. continue
  196. }
  197. }
  198. return decodeXmlEntities(data)
  199. }
  200. return null
  201. }
  202.  
  203. function askFlixsterEMS (emsId) {
  204. return new Promise(function flixsterEMSRequest (resolve) {
  205. GM.getValue('flixsterEmsCache', '{}').then(function (s) {
  206. const flixsterEmsCache = JSON.parse(s)
  207.  
  208. // Delete algoliaCached values, that are expired
  209. for (const prop in flixsterEmsCache) {
  210. if ((new Date()).getTime() - (new Date(flixsterEmsCache[prop].time)).getTime() > cacheExpireAfterHours * 60 * 60 * 1000) {
  211. delete flixsterEmsCache[prop]
  212. }
  213. }
  214.  
  215. // Check cache or request new content
  216. if (emsId in flixsterEmsCache) {
  217. return resolve(flixsterEmsCache[emsId])
  218. }
  219. const url = flixsterEMSURL.replace('{emsId}', encodeURIComponent(emsId))
  220. GM.xmlHttpRequest({
  221. method: 'GET',
  222. url,
  223. onload: function (response) {
  224. let data = null
  225. try {
  226. data = JSON.parse(response.responseText)
  227. } catch (e) {
  228. console.error(`${scriptName}: flixster ems JSON Error\nURL: ${url}`)
  229. console.error(e)
  230. data = {}
  231. }
  232.  
  233. // Save to flixsterEmsCache
  234. data.time = (new Date()).toJSON()
  235.  
  236. flixsterEmsCache[emsId] = data
  237.  
  238. GM.setValue('flixsterEmsCache', JSON.stringify(flixsterEmsCache))
  239.  
  240. resolve(data)
  241. },
  242. onerror: function (response) {
  243. console.error(`${scriptName}: flixster ems GM.xmlHttpRequest Error: ${response.status}\nURL: ${url}\nResponse:\n${response.responseText}`)
  244. resolve(null)
  245. }
  246. })
  247. })
  248. })
  249. }
  250. async function addFlixsterEMS (orgData) {
  251. const flixsterData = await askFlixsterEMS(orgData.emsId)
  252. if (!flixsterData || !('tomatometer' in flixsterData)) {
  253. return orgData
  254. }
  255. if ('certifiedFresh' in flixsterData.tomatometer && flixsterData.tomatometer.certifiedFresh) {
  256. orgData.meterClass = 'certified_fresh'
  257. }
  258. if ('numReviews' in flixsterData.tomatometer && flixsterData.tomatometer.numReviews) {
  259. orgData.numReviews = flixsterData.tomatometer.numReviews
  260. if ('freshCount' in flixsterData.tomatometer && flixsterData.tomatometer.freshCount != null) {
  261. orgData.freshCount = flixsterData.tomatometer.freshCount
  262. }
  263. if ('rottenCount' in flixsterData.tomatometer && flixsterData.tomatometer.rottenCount != null) {
  264. orgData.rottenCount = flixsterData.tomatometer.rottenCount
  265. }
  266. }
  267. if ('consensus' in flixsterData.tomatometer && flixsterData.tomatometer.consensus) {
  268. orgData.consensus = flixsterData.tomatometer.consensus
  269. }
  270. if ('avgScore' in flixsterData.tomatometer && flixsterData.tomatometer.avgScore != null) {
  271. orgData.avgScore = flixsterData.tomatometer.avgScore
  272. }
  273. if ('userRatingSummary' in flixsterData) {
  274. if ('scoresCount' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.scoresCount) {
  275. orgData.audienceCount = flixsterData.userRatingSummary.scoresCount
  276. } else if ('dtlScoreCount' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.dtlScoreCount) {
  277. orgData.audienceCount = flixsterData.userRatingSummary.dtlScoreCount
  278. }
  279. if ('wtsCount' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.wtsCount) {
  280. orgData.audienceWantToSee = flixsterData.userRatingSummary.wtsCount
  281. } else if ('dtlWtsCount' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.dtlWtsCount) {
  282. orgData.audienceWantToSee = flixsterData.userRatingSummary.dtlWtsCount
  283. }
  284. if ('reviewCount' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.reviewCount) {
  285. orgData.audienceReviewCount = flixsterData.userRatingSummary.reviewCount
  286. }
  287. if ('avgScore' in flixsterData.userRatingSummary && flixsterData.userRatingSummary.avgScore) {
  288. orgData.audienceAvgScore = flixsterData.userRatingSummary.avgScore
  289. }
  290. }
  291. return orgData
  292. }
  293.  
  294. function updateAlgolia () {
  295. // Get algolia data from https://www.rottentomatoes.com/
  296. const algoliaSearch = { aId: null, sId: null }
  297. if (unsafeWindow.RottenTomatoes && 'thirdParty' in unsafeWindow.RottenTomatoes && 'algoliaSearch' in unsafeWindow.RottenTomatoes.thirdParty) {
  298. if (typeof (unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.aId) === 'string' && typeof (unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.sId) === 'string') {
  299. algoliaSearch.aId = unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.aId // x-algolia-application-id
  300. algoliaSearch.sId = unsafeWindow.RottenTomatoes.thirdParty.algoliaSearch.sId // x-algolia-api-key
  301. }
  302. }
  303. if (algoliaSearch.aId) {
  304. GM.setValue('algoliaSearch', JSON.stringify(algoliaSearch)).then(function () {
  305. console.debug(`${scriptName}: Updated algoliaSearch: ${JSON.stringify(algoliaSearch)}`)
  306. })
  307. } else {
  308. console.debug(`${scriptName}: algoliaSearch.aId is ${algoliaSearch.aId}`)
  309. }
  310. }
  311.  
  312. function meterBar (data) {
  313. // Create the "progress" bar with the meter score
  314. let barColor = 'grey'
  315. let bgColor = darkTheme ? '#3e3e3e' : '#ECE4B5'
  316. let color = 'black'
  317. let width = 0
  318. let textInside = ''
  319. let textAfter = ''
  320.  
  321. if (data.meterClass === 'certified_fresh') {
  322. barColor = '#C91B22'
  323. color = 'yellow'
  324. textInside = emojiStrawberry + ' ' + data.meterScore.toLocaleString() + '%'
  325. width = data.meterScore || 0
  326. } else if (data.meterClass === 'fresh') {
  327. barColor = '#C91B22'
  328. color = 'white'
  329. textInside = emojiTomato + ' ' + data.meterScore.toLocaleString() + '%'
  330. width = data.meterScore || 0
  331. } else if (data.meterClass === 'rotten') {
  332. color = 'gray'
  333. barColor = '#94B13C'
  334. if (data.meterScore && data.meterScore > 30) {
  335. textAfter = '<span style="font-size: 15px;padding-top: 2px;display: inline-block;">' + data.meterScore.toLocaleString() + '%</span>'
  336. textInside = '<span style="font-size:13px">' + emojiGreenApple + '</span>'
  337. } else {
  338. textAfter = data.meterScore.toLocaleString() + '% <span style="font-size:13px">' + emojiGreenApple + '</span>'
  339. }
  340. width = data.meterScore || 0
  341. } else {
  342. bgColor = barColor = '#787878'
  343. color = 'silver'
  344. textInside = 'N/A'
  345. width = 100
  346. }
  347.  
  348. let title = 'Critics ' + (typeof data.meterScore === 'number' ? data.meterScore.toLocaleString() : 'N/A') + '% ' + data.meterClass
  349. let avg = ''
  350. if ('avgScore' in data) {
  351. const node = document.createElement('span')
  352. node.innerHTML = data.consensus
  353. title += '\nAverage score: ' + data.avgScore.toLocaleString() + ' / 10'
  354. avg = '<span style="font-weight:bolder">' + data.avgScore.toLocaleString() + '</span>/10'
  355. }
  356. if ('numReviews' in data && typeof data.numReviews === 'number') {
  357. title += ' from ' + data.numReviews.toLocaleString() + ' reviews'
  358. if ('freshCount' in data && data.numReviews > 0) {
  359. const p = parseInt(100 * parseFloat(data.freshCount) / parseFloat(data.numReviews))
  360. title += '\n' + data.freshCount.toLocaleString() + '/' + data.numReviews.toLocaleString() + ' ' + p + '% fresh reviews'
  361. }
  362. if ('rottenCount' in data) {
  363. const p = parseInt(100 * parseFloat(data.rottenCount) / parseFloat(data.numReviews))
  364. title += '\n' + data.rottenCount.toLocaleString() + '/' + data.numReviews.toLocaleString() + ' ' + p + '% rotten reviews'
  365. }
  366. }
  367. if ('consensus' in data) {
  368. const node = document.createElement('span')
  369. node.innerHTML = data.consensus
  370. title += '\n' + node.textContent
  371. }
  372. return '<div title="' + title + '" style="cursor:help;">' +
  373. '<div style="float:left; margin-top:1px; width:100px; overflow: hidden;height: 20px;background-color: ' + bgColor + ';color: ' + color + ';text-align:center; border-radius: 4px;box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);">' +
  374. '<div style="width:' + width + '%; background-color: ' + barColor + '; color: ' + color + '; font-size:14px; font-weight:bold; text-align:center; float:left; height: 100%;line-height: 20px;box-shadow: inset 0 -1px 0 rgba(0,0,0,0.15);transition: width 0.6s ease;">' +
  375. textInside +
  376. '</div>' +
  377. textAfter +
  378. '</div>' +
  379. '<div style="float:left; padding: 3px 0px 0px 3px;">' + avg + '</div>' +
  380. '<div style="clear:left;"></div>' +
  381. '</div>'
  382. }
  383. function audienceBar (data) {
  384. // Create the "progress" bar with the audience score
  385. if (!('audienceScore' in data) || data.audienceScore === null) {
  386. return ''
  387. }
  388.  
  389. let barColor = 'grey'
  390. let bgColor = darkTheme ? '#3e3e3e' : '#ECE4B5'
  391. let color = 'black'
  392. let width = 0
  393. let textInside = ''
  394. let textAfter = ''
  395. let avg = ''
  396.  
  397. if (data.audienceClass === 'red_popcorn') {
  398. barColor = '#C91B22'
  399. color = data.audienceScore > 94 ? 'yellow' : 'white'
  400. textInside = emojiPopcorn + ' ' + data.audienceScore.toLocaleString() + '%'
  401. width = data.audienceScore
  402. } else if (data.audienceClass === 'green_popcorn') {
  403. color = 'gray'
  404. barColor = '#94B13C'
  405. if (data.audienceScore > 30) {
  406. textAfter = '<span style="font-size: 15px;padding-top: 2px;display: inline-block;">' + data.audienceScore.toLocaleString() + '%</span>'
  407. textInside = '<span style="font-size:13px">' + emojiGreenSalad + '</span>'
  408. } else {
  409. textAfter = data.audienceScore.toLocaleString() + '% <span style="font-size:13px">' + emojiNauseated + '</span>'
  410. }
  411. width = data.audienceScore
  412. } else {
  413. bgColor = barColor = '#787878'
  414. color = 'silver'
  415. textInside = 'N/A'
  416. width = 100
  417. }
  418.  
  419. let title = 'Audience ' + (typeof data.audienceScore === 'number' ? data.audienceScore.toLocaleString() : 'N/A') + '% ' + data.audienceClass
  420. const titleLine2 = []
  421. if ('audienceCount' in data && typeof data.audienceCount === 'number') {
  422. titleLine2.push(data.audienceCount.toLocaleString() + ' Votes')
  423. }
  424. if ('audienceReviewCount' in data) {
  425. titleLine2.push(data.audienceReviewCount.toLocaleString() + ' Reviews')
  426. }
  427. if ('audienceAvgScore' in data && typeof data.audienceAvgScore === 'number') {
  428. titleLine2.push('Average score: ' + data.audienceAvgScore.toLocaleString() + ' / 5 stars')
  429. avg = '<span style="font-weight:bolder">' + data.audienceAvgScore.toLocaleString() + '</span>/5'
  430. }
  431. if ('audienceWantToSee' in data && typeof data.audienceWantToSee === 'number') {
  432. titleLine2.push(data.audienceWantToSee.toLocaleString() + ' want to see')
  433. }
  434.  
  435. title = title + (titleLine2 ? ('\n' + titleLine2.join('\n')) : '')
  436. return '<div title="' + title + '" style="cursor:help;">' +
  437. '<div style="float:left; margin-top:1px; width:100px; overflow: hidden;height: 20px;background-color: ' + bgColor + ';color: ' + color + ';text-align:center; border-radius: 4px;box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);">' +
  438. '<div style="width:' + width + '%; background-color: ' + barColor + '; color: ' + color + '; font-size:14px; font-weight:bold; text-align:center; float:left; height: 100%;line-height: 20px;box-shadow: inset 0 -1px 0 rgba(0,0,0,0.15);transition: width 0.6s ease;">' +
  439. textInside +
  440. '</div>' +
  441. textAfter +
  442. '</div>' +
  443. '<div style="float:left; padding: 3px 0px 0px 3px;">' + avg + '</div>' +
  444. '<div style="clear:left;"></div>' +
  445. '</div>'
  446. }
  447.  
  448. const current = {
  449. type: null,
  450. query: null,
  451. year: null
  452. }
  453.  
  454. async function loadMeter (query, type, year) {
  455. // Load data from rotten tomatoes search API or from cache
  456.  
  457. current.type = type
  458. current.query = query
  459. current.year = year
  460.  
  461. const algoliaCache = JSON.parse(await GM.getValue('algoliaCache', '{}'))
  462.  
  463. // Delete algoliaCached values, that are expired
  464. for (const prop in algoliaCache) {
  465. if ((new Date()).getTime() - (new Date(algoliaCache[prop].time)).getTime() > cacheExpireAfterHours * 60 * 60 * 1000) {
  466. delete algoliaCache[prop]
  467. }
  468. }
  469.  
  470. const algoliaSearch = JSON.parse(await GM.getValue('algoliaSearch', '{}'))
  471.  
  472. // Check cache or request new content
  473. if (query in algoliaCache) {
  474. // Use cached response
  475. console.debug(`${scriptName}: Use cached algolia response`)
  476. handleAlgoliaResponse(algoliaCache[query])
  477. } else if ('aId' in algoliaSearch && 'sId' in algoliaSearch) {
  478. // Use algolia.net API
  479. const url = algoliaURL.replace('{domain}', algoliaSearch.aId.toLowerCase()).replace('{aId}', encodeURIComponent(algoliaSearch.aId)).replace('{sId}', encodeURIComponent(algoliaSearch.sId)).replace('{agent}', encodeURIComponent(algoliaAgent))
  480. GM.xmlHttpRequest({
  481. method: 'POST',
  482. url,
  483. data: '{"requests":[{"indexName":"content_rt","query":"' + query.replace('"', '') + '","params":"filters=isEmsSearchable%20%3D%201&hitsPerPage=20"}]}',
  484. onload: function (response) {
  485. // Save to algoliaCache
  486. response.time = (new Date()).toJSON()
  487.  
  488. // Chrome fix: Otherwise JSON.stringify(cache) omits responseText
  489. const newobj = {}
  490. for (const key in response) {
  491. newobj[key] = response[key]
  492. }
  493. newobj.responseText = response.responseText
  494.  
  495. algoliaCache[query] = newobj
  496.  
  497. GM.setValue('algoliaCache', JSON.stringify(algoliaCache))
  498.  
  499. handleAlgoliaResponse(response)
  500. },
  501. onerror: function (response) {
  502. console.error(`${scriptName}: algoliaSearch GM.xmlHttpRequest Error: ${response.status}\nURL: ${url}\nResponse:\n${response.responseText}`)
  503. }
  504. })
  505. } else {
  506. console.error(`${scriptName}: algoliaSearch not configured`)
  507. window.alert(scriptName + ' userscript\n\nYou need to visit www.rottentomatoes.com at least once before the script can work.\n\nThe script needs to read some API keys from the website.')
  508. showMeter('ALGOLIA_NOT_CONFIGURED', new Date())
  509. }
  510. }
  511.  
  512. function matchQuality (title, year, currentSet) {
  513. if (title === current.query && year === current.year) {
  514. return 104 + year
  515. }
  516. if (title.toLowerCase() === current.query.toLowerCase() && year === current.year) {
  517. return 103 + year
  518. }
  519. if (title === current.query && current.year) {
  520. return 102 - Math.abs(year - current.year)
  521. }
  522. if (title.toLowerCase() === current.query.toLowerCase() && current.year) {
  523. return 101 - Math.abs(year - current.year)
  524. }
  525. if (title.replace(/\(.+\)/, '').trim() === current.query && current.year) {
  526. return 100 - Math.abs(year - current.year)
  527. }
  528. if (title === current.query) {
  529. return 8
  530. }
  531. if (title.replace(/\(.+\)/, '').trim() === current.query) {
  532. return 7
  533. }
  534. if (title.startsWith(current.query)) {
  535. return 6
  536. }
  537. if (current.query.indexOf(title) !== -1) {
  538. return 5
  539. }
  540. if (title.indexOf(current.query) !== -1) {
  541. return 4
  542. }
  543. if (current.query.toLowerCase().indexOf(title.toLowerCase()) !== -1) {
  544. return 3
  545. }
  546. if (title.toLowerCase().indexOf(current.query.toLowerCase()) !== -1) {
  547. return 2
  548. }
  549. const titleSet = new Set(title.replace(/[^a-z ]/gi, ' ').split(' '))
  550. const score = intersection(titleSet, currentSet).size - 20
  551. if (year === current.year) {
  552. return score + 1
  553. }
  554. return score
  555. }
  556.  
  557. async function handleAlgoliaResponse (response) {
  558. // Handle GM.xmlHttpRequest response
  559. const rawData = JSON.parse(response.responseText)
  560.  
  561. // Filter according to type
  562. const hits = rawData.results[0].hits.filter(hit => hit.type === current.type)
  563.  
  564. // Change data structure
  565. const arr = []
  566.  
  567. hits.forEach(function (hit) {
  568. const result = {
  569. name: hit.title,
  570. year: parseInt(hit.releaseYear),
  571. url: '/' + (current.type === 'tv' ? 'tv' : 'm') + '/' + ('vanity' in hit ? hit.vanity : hit.title.toLowerCase()),
  572. meterClass: null,
  573. meterScore: null,
  574. audienceClass: null,
  575. audienceScore: null,
  576. emsId: hit.emsId
  577. }
  578. if ('rottenTomatoes' in hit) {
  579. if ('criticsIconUrl' in hit.rottenTomatoes) {
  580. result.meterClass = hit.rottenTomatoes.criticsIconUrl.match(/\/(\w+)\.png/)[1]
  581. }
  582. if ('criticsScore' in hit.rottenTomatoes) {
  583. result.meterScore = hit.rottenTomatoes.criticsScore
  584. }
  585. if ('audienceIconUrl' in hit.rottenTomatoes) {
  586. result.audienceClass = hit.rottenTomatoes.audienceIconUrl.match(/\/(\w+)\.png/)[1]
  587. }
  588. if ('audienceScore' in hit.rottenTomatoes) {
  589. result.audienceScore = hit.rottenTomatoes.audienceScore
  590. }
  591. if ('certifiedFresh' in hit.rottenTomatoes && hit.rottenTomatoes.certifiedFresh) {
  592. result.meterClass = 'certified_fresh'
  593. }
  594. }
  595. arr.push(result)
  596. })
  597.  
  598. // Sort results by closest match
  599. const currentSet = new Set(current.query.replace(/[^a-z ]/gi, ' ').split(' '))
  600. arr.sort(function (a, b) {
  601. if (!Object.prototype.hasOwnProperty.call(a, 'matchQuality')) {
  602. a.matchQuality = matchQuality(a.name, a.year, currentSet)
  603. }
  604. if (!Object.prototype.hasOwnProperty.call(b, 'matchQuality')) {
  605. b.matchQuality = matchQuality(b.name, b.year, currentSet)
  606. }
  607.  
  608. return b.matchQuality - a.matchQuality
  609. })
  610.  
  611. if (arr.length > 0 && arr[0].meterScore) {
  612. // Get more details for first result
  613. arr[0] = await addFlixsterEMS(arr[0])
  614. }
  615.  
  616. if (arr) {
  617. showMeter(arr, new Date(response.time))
  618. } else {
  619. console.debug(`${scriptName}: No results for ${current.query}`)
  620. }
  621. }
  622.  
  623. function showMeter (arr, time) {
  624. // Show a small box in the right lower corner
  625. $('#mcdiv321rotten').remove()
  626. let main, div
  627. div = main = $('<div id="mcdiv321rotten"></div>').appendTo(document.body)
  628. div.css({
  629. position: 'fixed',
  630. bottom: 0,
  631. right: 0,
  632. minWidth: 100,
  633. maxWidth: 400,
  634. maxHeight: '95%',
  635. overflow: 'auto',
  636. backgroundColor: darkTheme ? '#262626' : 'white',
  637. border: darkTheme ? '2px solid #444' : '2px solid #bbb',
  638. borderRadius: ' 6px',
  639. boxShadow: '0 0 3px 3px rgba(100, 100, 100, 0.2)',
  640. color: darkTheme ? 'white' : 'black',
  641. padding: ' 3px',
  642. zIndex: '5010001',
  643. fontFamily: 'Helvetica,Arial,sans-serif'
  644. })
  645.  
  646. const CSS = `<style>
  647. #mcdiv321rotten {
  648. transition:bottom 0.7s, height 0.5s;
  649. }
  650. </style>`
  651.  
  652. $(CSS).appendTo(div)
  653.  
  654. if (arr === 'ALGOLIA_NOT_CONFIGURED') {
  655. $('<div>You need to visit <a href="https://www.rottentomatoes.com/">www.rottentomatoes.com</a> at least once to enable the script.</div>').appendTo(main)
  656. return
  657. }
  658.  
  659. // First result
  660. $('<div class="firstResult"><a style="font-size:small; color:#136CB2; " href="' + baseURL + arr[0].url + '">' + arr[0].name + ' (' + arr[0].year + ')</a>' + meterBar(arr[0]) + audienceBar(arr[0]) + '</div>').appendTo(main)
  661.  
  662. // Shall the following results be collapsed by default?
  663. if ((arr.length > 1 && arr[0].matchQuality > 10) || arr.length > 10) {
  664. $('<span style="color:gray;font-size: x-small">More results...</span>').appendTo(main).click(function () { more.css('display', 'block'); this.parentNode.removeChild(this) })
  665. const more = div = $('<div style="display:none"></div>').appendTo(main)
  666. }
  667.  
  668. // More results
  669. for (let i = 1; i < arr.length; i++) {
  670. $('<div><a style="font-size:small; color:#136CB2; " href="' + baseURL + arr[i].url + '">' + arr[i].name + ' (' + arr[i].year + ')</a>' + meterBar(arr[i]) + audienceBar(arr[i]) + '</div>').appendTo(div)
  671. }
  672.  
  673. // Footer
  674. const sub = $('<div></div>').appendTo(main)
  675. $('<time style="color:#b6b6b6; font-size: 11px;" datetime="' + time + '" title="' + time.toLocaleTimeString() + ' ' + time.toLocaleDateString() + '">' + minutesSince(time) + '</time>').appendTo(sub)
  676. $('<a style="color:#b6b6b6; font-size: 11px;" target="_blank" href="' + baseURLOpenTab.replace('{query}', encodeURIComponent(current.query)) + '" title="Open Rotten Tomatoes">@rottentomatoes.com</a>').appendTo(sub)
  677. $('<span title="Hide me" style="cursor:pointer; float:right; color:#b6b6b6; font-size: 11px; padding-left:5px;padding-top:3px">&#10062;</span>').appendTo(sub).click(function () {
  678. document.body.removeChild(this.parentNode.parentNode)
  679. })
  680. }
  681.  
  682. const Always = () => true
  683. const sites = {
  684. googleplay: {
  685. host: ['play.google.com'],
  686. condition: Always,
  687. products: [
  688. {
  689. condition: () => ~document.location.href.indexOf('/movies/details/'),
  690. type: 'movie',
  691. data: () => document.querySelector('*[itemprop=name]').textContent
  692. }
  693. ]
  694. },
  695. imdb: {
  696. host: ['imdb.com'],
  697. condition: () => !~document.location.pathname.indexOf('/mediaviewer') && !~document.location.pathname.indexOf('/mediaindex') && !~document.location.pathname.indexOf('/videoplayer'),
  698. products: [
  699. {
  700. condition: function () {
  701. const e = document.querySelector("meta[property='og:type']")
  702. if (e && e.content === 'video.movie') {
  703. return true
  704. } else if (document.querySelector('[data-testid="hero__pageTitle"]') && !document.querySelector('[data-testid="hero-subnav-bar-left-block"] a[href*="episodes/"]')) {
  705. return true
  706. }
  707. return false
  708. },
  709. type: 'movie',
  710. data: async function () {
  711. let year = null
  712. let ld = null
  713. if (document.querySelector('script[type="application/ld+json"]')) {
  714. ld = parseLDJSON(['name', 'alternateName', 'datePublished'])
  715. if (ld.length > 2) {
  716. year = parseInt(ld[2].match(/\d{4}/)[0])
  717. }
  718. }
  719.  
  720. const pageNotEnglish = document.querySelector('[for="nav-language-selector"]').textContent.toLowerCase() !== 'en' || !navigator.language.startsWith('en')
  721. const pageNotMovieHomePage = !document.title.match(/(.+?)\s+(\((\d+)\))? - IMDb/)
  722.  
  723. // If the page is not in English or the browser is not in English, request page in English.
  724. // Then the title in <h1> will be the English title and Metacritic always uses the English title.
  725. if (pageNotEnglish || pageNotMovieHomePage) {
  726. // Set language cookie to English, request current page in English, then restore language cookie or expire it if it didn't exist before
  727. const imdbID = document.location.pathname.match(/\/title\/(\w+)/)[1]
  728. const homePageUrl = 'https://www.imdb.com/title/' + imdbID + '/?ref_=nv_sr_1'
  729. const langM = document.cookie.match(/lc-main=([^;]+)/)
  730. const langBefore = langM ? langM[0] : ';expires=Thu, 01 Jan 1970 00:00:01 GMT'
  731. document.cookie = 'lc-main=en-US'
  732. const response = await asyncRequest({
  733. url: homePageUrl,
  734. headers: {
  735. 'Accept-Language': 'en-US,en'
  736. }
  737. }).catch(function (response) {
  738. console.warn('ShowRottentomatoes: Error imdb02\nurl=' + homePageUrl + '\nstatus=' + response.status)
  739. })
  740. document.cookie = 'lc-main=' + langBefore
  741. // Extract <h1> title
  742. const parts = response.responseText.split('</span></h1>')[0].split('>')
  743. const title = parts[parts.length - 1]
  744. if (!year) {
  745. // extract year
  746. const yearM = response.responseText.match(/href="\/title\/\w+\/releaseinfo.*">(\d{4})<\/a>/)
  747. if (yearM) {
  748. year = yearM[1]
  749. }
  750. }
  751. console.debug('ShowRottentomatoes: Movie title from English page:', title, year)
  752. return [title, year]
  753. } else if (ld) {
  754. if (ld.length > 1 && ld[1]) {
  755. console.debug('ShowRottentomatoes: Movie ld+json alternateName', ld[1], year)
  756. return [ld[1], year]
  757. }
  758. console.debug('ShowRottentomatoes: Movie ld+json name', ld[0], year)
  759. return [ld[0], year]
  760. } else {
  761. const m = document.title.match(/(.+?)\s+(\((\d+)\))? - /)
  762. console.debug('ShowRottentomatoes: Movie <title>', [m[1], m[3]])
  763. return [m[1], parseInt(m[3])]
  764. }
  765. }
  766. },
  767. {
  768. condition: function () {
  769. const e = document.querySelector("meta[property='og:type']")
  770. if (e && e.content === 'video.tv_show') {
  771. return true
  772. } else if (document.querySelector('[data-testid="hero-subnav-bar-left-block"] a[href*="episodes/"]')) {
  773. return true
  774. }
  775. return false
  776. },
  777. type: 'tv',
  778. data: async function () {
  779. let year = null
  780. let ld = null
  781. if (document.querySelector('script[type="application/ld+json"]')) {
  782. ld = parseLDJSON(['name', 'alternateName', 'datePublished'])
  783. if (ld.length > 2) {
  784. year = parseInt(ld[2].match(/\d{4}/)[0])
  785. }
  786. }
  787.  
  788. const pageNotEnglish = document.querySelector('[for="nav-language-selector"]').textContent.toLowerCase() !== 'en' || !navigator.language.startsWith('en')
  789. const pageNotMovieHomePage = !document.title.match(/(.+?)\s+\(.+(\d{4})–.{0,4}\) - IMDb/)
  790.  
  791. // If the page is not in English or the browser is not in English, request page in English.
  792. // Then the title in <h1> will be the English title and Metacritic always uses the English title.
  793. if (pageNotEnglish || pageNotMovieHomePage) {
  794. const imdbID = document.location.pathname.match(/\/title\/(\w+)/)[1]
  795. const homePageUrl = 'https://www.imdb.com/title/' + imdbID + '/?ref_=nv_sr_1'
  796. // Set language cookie to English, request current page in English, then restore language cookie or expire it if it didn't exist before
  797. const langM = document.cookie.match(/lc-main=([^;]+)/)
  798. const langBefore = langM ? langM[0] : ';expires=Thu, 01 Jan 1970 00:00:01 GMT'
  799. document.cookie = 'lc-main=en-US'
  800. const response = await asyncRequest({
  801. url: homePageUrl,
  802. headers: {
  803. 'Accept-Language': 'en-US,en'
  804. }
  805. }).catch(function (response) {
  806. console.warn('ShowRottentomatoes: Error imdb03\nurl=' + homePageUrl + '\nstatus=' + response.status)
  807. })
  808. document.cookie = 'lc-main=' + langBefore
  809. // Extract <h1> title
  810. const parts = response.responseText.split('</span></h1>')[0].split('>')
  811. const title = parts[parts.length - 1]
  812. if (!year) {
  813. // extract year
  814. const yearM = response.responseText.match(/href="\/title\/\w+\/releaseinfo.*">(\d{4})/)
  815. if (yearM) {
  816. year = yearM[1]
  817. }
  818. }
  819. console.debug('ShowRottentomatoes: TV title from English page:', title, year)
  820. return [title, year]
  821. } else if (ld) {
  822. if (ld.length > 1 && ld[1]) {
  823. console.debug('ShowRottentomatoes: TV ld+json alternateName', ld[1], year)
  824. return [ld[1], year]
  825. }
  826. console.debug('ShowRottentomatoes: TV ld+json name', ld[0], year)
  827. return [ld[0], year]
  828. } else {
  829. const m = document.title.match(/(.+?)\s+\(.+(\d{4}).+/)
  830. console.debug('ShowRottentomatoes: TV <title>', [m[1], m[2]])
  831. return [m[1], parseInt(m[2])]
  832. }
  833. }
  834. }
  835. ]
  836. },
  837. 'tv.com': {
  838. host: ['www.tv.com'],
  839. condition: () => document.querySelector("meta[property='og:type']"),
  840. products: [{
  841. condition: () => document.querySelector("meta[property='og:type']").content === 'tv_show' && document.querySelector('h1[data-name]'),
  842. type: 'tv',
  843. data: () => document.querySelector('h1[data-name]').dataset.name
  844. }]
  845. },
  846. metacritic: {
  847. host: ['www.metacritic.com'],
  848. condition: () => document.querySelector("meta[property='og:type']"),
  849. products: [{
  850. condition: () => document.querySelector("meta[property='og:type']").content === 'video.movie',
  851. type: 'movie',
  852. data: function () {
  853. let year = null
  854. if (document.querySelector('.release_year')) {
  855. year = parseInt(document.querySelector('.release_year').firstChild.textContent)
  856. } else if (document.querySelector('.release_data .data')) {
  857. year = document.querySelector('.release_data .data').textContent.match(/(\d{4})/)[1]
  858. }
  859.  
  860. return [document.querySelector("meta[property='og:title']").content, year]
  861. }
  862. },
  863. {
  864. condition: () => document.querySelector("meta[property='og:type']").content === 'video.tv_show',
  865. type: 'tv',
  866. data: function () {
  867. let title = document.querySelector("meta[property='og:title']").content
  868. let year = null
  869. if (title.match(/\s\(\d{4}\)$/)) {
  870. year = parseInt(title.match(/\s\((\d{4})\)$/)[1])
  871. title = title.replace(/\s\(\d{4}\)$/, '') // Remove year
  872. } else if (document.querySelector('.release_date')) {
  873. year = document.querySelector('.release_date').textContent.match(/(\d{4})/)[1]
  874. }
  875.  
  876. return [title, year]
  877. }
  878. }
  879. ]
  880. },
  881. serienjunkies: {
  882. host: ['www.serienjunkies.de'],
  883. condition: Always,
  884. products: [{
  885. condition: () => document.getElementById('serienlinksbreit2aktuell'),
  886. type: 'tv',
  887. data: () => document.querySelector('h1').textContent.trim()
  888. },
  889. {
  890. condition: () => document.location.pathname.search(/vod\/film\/.{3,}/) !== -1,
  891. type: 'movie',
  892. data: () => document.querySelector('h1').textContent.trim()
  893. }]
  894. },
  895. amazon: {
  896. host: ['amazon.'],
  897. condition: Always,
  898. products: [
  899. {
  900. condition: () => (document.querySelector('[data-automation-id=title]') && (
  901. document.getElementsByClassName('av-season-single').length ||
  902. document.querySelector('[data-automation-id="num-of-seasons-badge"]') ||
  903. document.getElementById('tab-selector-episodes') ||
  904. document.getElementById('av-droplist-av-atf-season-selector')
  905. )),
  906. type: 'tv',
  907. data: () => document.querySelector('[data-automation-id=title]').textContent.trim()
  908. },
  909. {
  910. condition: () => ((
  911. document.getElementsByClassName('av-season-single').length ||
  912. document.querySelector('[data-automation-id="num-of-seasons-badge"]') ||
  913. document.getElementById('tab-selector-episodes') ||
  914. document.getElementById('av-droplist-av-atf-season-selector')
  915. ) && Array.from(document.querySelectorAll('script[type="text/template"]')).map(e => e.innerHTML.match(/parentTitle"\s*:\s*"(.+?)"/)).some((x) => x != null)),
  916. type: 'tv',
  917. data: () => Array.from(document.querySelectorAll('script[type="text/template"]')).map(e => e.innerHTML.match(/parentTitle"\s*:\s*"(.+?)"/)).filter((x) => x != null)[0][1]
  918. },
  919. {
  920. condition: () => document.querySelector('[data-automation-id=title]'),
  921. type: 'movie',
  922. data: () => document.querySelector('[data-automation-id=title]').textContent.trim().replace(/\[.{1,8}\]/, '')
  923. },
  924. {
  925. condition: () => document.querySelector('#watchNowContainer a[href*="/gp/video/"]'),
  926. type: 'movie',
  927. data: () => document.getElementById('productTitle').textContent.trim()
  928. }
  929. ]
  930. },
  931. BoxOfficeMojo: {
  932. host: ['boxofficemojo.com'],
  933. condition: () => Always,
  934. products: [
  935. {
  936. condition: () => document.location.pathname.startsWith('/release/'),
  937. type: 'movie',
  938. data: function () {
  939. let year = null
  940. const cells = document.querySelectorAll('#body .mojo-summary-values .a-section span')
  941. for (let i = 0; i < cells.length; i++) {
  942. if (~cells[i].innerText.indexOf('Release Date')) {
  943. year = parseInt(cells[i].nextElementSibling.textContent.match(/\d{4}/)[0])
  944. break
  945. }
  946. }
  947. return [document.querySelector('meta[name=title]').content, year]
  948. }
  949. },
  950. {
  951. condition: () => ~document.location.search.indexOf('id=') && document.querySelector('#body table:nth-child(2) tr:first-child b'),
  952. type: 'movie',
  953. data: function () {
  954. let year = null
  955. try {
  956. const tds = document.querySelectorAll('#body table:nth-child(2) tr:first-child table table table td')
  957. for (let i = 0; i < tds.length; i++) {
  958. if (~tds[i].innerText.indexOf('Release Date')) {
  959. year = parseInt(tds[i].innerText.match(/\d{4}/)[0])
  960. break
  961. }
  962. }
  963. } catch (e) { }
  964. return [document.querySelector('#body table:nth-child(2) tr:first-child b').firstChild.textContent, year]
  965. }
  966. }]
  967. },
  968. AllMovie: {
  969. host: ['allmovie.com'],
  970. condition: () => document.querySelector('h2[itemprop=name].movie-title'),
  971. products: [{
  972. condition: () => document.querySelector('h2[itemprop=name].movie-title'),
  973. type: 'movie',
  974. data: () => document.querySelector('h2[itemprop=name].movie-title').firstChild.textContent.trim()
  975. }]
  976. },
  977. 'en.wikipedia': {
  978. host: ['en.wikipedia.org'],
  979. condition: Always,
  980. products: [{
  981. condition: function () {
  982. if (!document.querySelector('.infobox .summary')) {
  983. return false
  984. }
  985. const r = /\d\d\d\d films/
  986. return $('#catlinks a').filter((i, e) => e.firstChild.textContent.match(r)).length
  987. },
  988. type: 'movie',
  989. data: () => document.querySelector('.infobox .summary').firstChild.textContent
  990. },
  991. {
  992. condition: function () {
  993. if (!document.querySelector('.infobox .summary')) {
  994. return false
  995. }
  996. const r = /television series/
  997. return $('#catlinks a').filter((i, e) => e.firstChild.textContent.match(r)).length
  998. },
  999. type: 'tv',
  1000. data: () => document.querySelector('.infobox .summary').firstChild.textContent
  1001. }]
  1002. },
  1003. fandango: {
  1004. host: ['fandango.com'],
  1005. condition: () => document.querySelector("meta[property='og:title']"),
  1006. products: [{
  1007. condition: Always,
  1008. type: 'movie',
  1009. data: () => document.querySelector("meta[property='og:title']").content.match(/(.+?)\s+\(\d{4}\)/)[1].trim()
  1010. }]
  1011. },
  1012. themoviedb: {
  1013. host: ['themoviedb.org'],
  1014. condition: () => document.querySelector("meta[property='og:type']"),
  1015. products: [{
  1016. condition: () => document.querySelector("meta[property='og:type']").content === 'movie' ||
  1017. document.querySelector("meta[property='og:type']").content === 'video.movie',
  1018. type: 'movie',
  1019. data: function () {
  1020. let year = null
  1021. try {
  1022. year = parseInt(document.querySelector('.release_date').innerText.match(/\d{4}/)[0])
  1023. } catch (e) {}
  1024.  
  1025. return [document.querySelector("meta[property='og:title']").content, year]
  1026. }
  1027. },
  1028. {
  1029. condition: () => document.querySelector("meta[property='og:type']").content === 'tv' ||
  1030. document.querySelector("meta[property='og:type']").content === 'tv_series' ||
  1031. document.querySelector("meta[property='og:type']").content.indexOf('tv_show') !== -1,
  1032. type: 'tv',
  1033. data: () => document.querySelector("meta[property='og:title']").content
  1034. }]
  1035. },
  1036. letterboxd: {
  1037. host: ['letterboxd.com'],
  1038. condition: () => unsafeWindow.filmData && 'name' in unsafeWindow.filmData,
  1039. products: [{
  1040. condition: Always,
  1041. type: 'movie',
  1042. data: () => [unsafeWindow.filmData.name, unsafeWindow.filmData.releaseYear]
  1043. }]
  1044. },
  1045. TVmaze: {
  1046. host: ['tvmaze.com'],
  1047. condition: () => document.querySelector('h1'),
  1048. products: [{
  1049. condition: Always,
  1050. type: 'tv',
  1051. data: () => document.querySelector('h1').firstChild.textContent
  1052. }]
  1053. },
  1054. TVGuide: {
  1055. host: ['tvguide.com'],
  1056. condition: Always,
  1057. products: [{
  1058. condition: () => document.location.pathname.startsWith('/tvshows/'),
  1059. type: 'tv',
  1060. data: function () {
  1061. if (document.querySelector('meta[itemprop=name]')) {
  1062. return document.querySelector('meta[itemprop=name]').content
  1063. } else {
  1064. return document.querySelector("meta[property='og:title']").content.split('|')[0]
  1065. }
  1066. }
  1067. }]
  1068. },
  1069. followshows: {
  1070. host: ['followshows.com'],
  1071. condition: Always,
  1072. products: [{
  1073. condition: () => document.querySelector("meta[property='og:type']").content === 'video.tv_show',
  1074. type: 'tv',
  1075. data: () => document.querySelector("meta[property='og:title']").content
  1076. }]
  1077. },
  1078. TheTVDB: {
  1079. host: ['thetvdb.com'],
  1080. condition: Always,
  1081. products: [{
  1082. condition: () => document.location.pathname.startsWith('/series/'),
  1083. type: 'tv',
  1084. data: () => document.getElementById('series_title').firstChild.textContent.trim()
  1085. },
  1086. {
  1087. condition: () => document.location.pathname.startsWith('/movies/'),
  1088. type: 'movie',
  1089. data: () => document.getElementById('series_title').firstChild.textContent.trim()
  1090. }]
  1091. },
  1092. TVNfo: {
  1093. host: ['tvnfo.com'],
  1094. condition: () => document.querySelector('#title #name'),
  1095. products: [{
  1096. condition: Always,
  1097. type: 'tv',
  1098. data: function () {
  1099. const years = document.querySelector('#title #years').textContent.trim()
  1100. const title = document.querySelector('#title #name').textContent.replace(years, '').trim()
  1101. let year = null
  1102. if (years) {
  1103. try {
  1104. year = years.match(/\d{4}/)[0]
  1105. } catch (e) {}
  1106. }
  1107. return [title, year]
  1108. }
  1109. }]
  1110. },
  1111. nme: {
  1112. host: ['nme.com'],
  1113. condition: () => document.location.pathname.startsWith('/reviews/'),
  1114. products: [{
  1115. condition: () => document.querySelector('.tdb-breadcrumbs a[href*="/reviews/film-reviews"]'),
  1116. type: 'movie',
  1117. data: function () {
  1118. let year = null
  1119. try {
  1120. year = parseInt(document.querySelector('*[itemprop=datePublished]').content.match(/\d{4}/)[0])
  1121. } catch (e) {}
  1122.  
  1123. try {
  1124. return [document.title.match(/[‘'](.+?)[’']/)[1], year]
  1125. } catch (e) {
  1126. try {
  1127. return [document.querySelector('h1.tdb-title-text').textContent.match(/[‘'](.+?)[’']/)[1], year]
  1128. } catch (e) {
  1129. return [document.querySelector('h1').textContent.match(/:\s*(.+)/)[1].trim(), year]
  1130. }
  1131. }
  1132. }
  1133. },
  1134. {
  1135. condition: () => document.querySelector('.tdb-breadcrumbs a[href*="/reviews/tv-reviews"]'),
  1136. type: 'tv',
  1137. data: () => document.querySelector('h1.tdb-title-text').textContent.match(/‘(.+?)’/)[1]
  1138. }]
  1139. },
  1140. itunes: {
  1141. host: ['itunes.apple.com'],
  1142. condition: Always,
  1143. products: [{
  1144. condition: () => ~document.location.href.indexOf('/movie/'),
  1145. type: 'movie',
  1146. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  1147. },
  1148. {
  1149. condition: () => ~document.location.href.indexOf('/tv-season/'),
  1150. type: 'tv',
  1151. data: function () {
  1152. let name = parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  1153. if (~name.indexOf(', Season')) {
  1154. name = name.split(', Season')[0]
  1155. }
  1156. return name
  1157. }
  1158. }]
  1159. },
  1160. epguides: {
  1161. host: ['epguides.com'],
  1162. condition: () => document.getElementById('eplist'),
  1163. products: [{
  1164. condition: () => document.getElementById('eplist') && document.querySelector('.center.titleblock h2'),
  1165. type: 'tv',
  1166. data: () => document.querySelector('.center.titleblock h2').textContent.trim()
  1167. }]
  1168. },
  1169. ComedyCentral: {
  1170. host: ['cc.com'],
  1171. condition: () => document.location.pathname.startsWith('/shows/'),
  1172. products: [{
  1173. condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:title']"),
  1174. type: 'tv',
  1175. data: () => document.querySelector("meta[property='og:title']").content.replace('| Comedy Central', '').trim()
  1176. },
  1177. {
  1178. condition: () => document.location.pathname.split('/').length === 3 && document.title.match(/(.+?)\s+-\s+Series/),
  1179. type: 'tv',
  1180. data: () => document.title.match(/(.+?)\s+-\s+Series/)[1]
  1181. }]
  1182. },
  1183. AMC: {
  1184. host: ['amc.com'],
  1185. condition: () => document.location.pathname.startsWith('/shows/'),
  1186. products: [
  1187. {
  1188. condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:type']") && document.querySelector("meta[property='og:type']").content.indexOf('tv_show') !== -1,
  1189. type: 'tv',
  1190. data: () => document.querySelector('.video-card-description h1').textContent.trim()
  1191. }]
  1192. },
  1193. AMCplus: {
  1194. host: ['amcplus.com'],
  1195. condition: () => Always,
  1196. products: [
  1197. {
  1198. condition: () => document.title.match(/Watch .+? |/),
  1199. type: 'tv',
  1200. data: () => document.title.match(/Watch (.+?) |/)[1].trim()
  1201. }]
  1202. },
  1203. RlsBB: {
  1204. host: ['rlsbb.ru'],
  1205. condition: () => document.querySelectorAll('.post').length === 1,
  1206. products: [
  1207. {
  1208. condition: () => document.querySelector('#post-wrapper .entry-meta a[href*="/category/movies/"]'),
  1209. type: 'movie',
  1210. data: () => document.querySelector('h1.entry-title').textContent.match(/(.+?)\s+\d{4}/)[1].trim()
  1211. },
  1212. {
  1213. condition: () => document.querySelector('#post-wrapper .entry-meta a[href*="/category/tv-shows/"]'),
  1214. type: 'tv',
  1215. data: () => document.querySelector('h1.entry-title').textContent.match(/(.+?)\s+S\d{2}/)[1].trim()
  1216. }]
  1217. },
  1218. showtime: {
  1219. host: ['sho.com'],
  1220. condition: Always,
  1221. products: [
  1222. {
  1223. condition: () => parseLDJSON('@type') === 'Movie',
  1224. type: 'movie',
  1225. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  1226. },
  1227. {
  1228. condition: () => parseLDJSON('@type') === 'TVSeries',
  1229. type: 'tv',
  1230. data: () => parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  1231. }]
  1232. },
  1233. gog: {
  1234. host: ['www.gog.com'],
  1235. condition: () => document.querySelector('.productcard-basics__title'),
  1236. products: [{
  1237. condition: () => document.location.pathname.split('/').length > 2 && (
  1238. document.location.pathname.split('/')[1] === 'movie' ||
  1239. document.location.pathname.split('/')[2] === 'movie'),
  1240. type: 'movie',
  1241. data: () => document.querySelector('.productcard-basics__title').textContent
  1242. }]
  1243. },
  1244. psapm: {
  1245. host: ['psa.wf'],
  1246. condition: Always,
  1247. products: [
  1248. {
  1249. condition: () => document.location.pathname.startsWith('/movie/'),
  1250. type: 'movie',
  1251. data: function () {
  1252. const title = document.querySelector('h1').textContent.trim()
  1253. const m = title.match(/(.+)\((\d+)\)$/)
  1254. if (m) {
  1255. return [m[1].trim(), parseInt(m[2])]
  1256. } else {
  1257. return title
  1258. }
  1259. }
  1260. },
  1261. {
  1262. condition: () => document.location.pathname.startsWith('/tv-show/'),
  1263. type: 'tv',
  1264. data: () => document.querySelector('h1').textContent.trim()
  1265. }
  1266. ]
  1267. },
  1268. 'save.tv': {
  1269. host: ['save.tv'],
  1270. condition: () => document.location.pathname.startsWith('/STV/M/obj/archive/'),
  1271. products: [
  1272. {
  1273. condition: () => document.location.pathname.startsWith('/STV/M/obj/archive/'),
  1274. type: 'movie',
  1275. data: function () {
  1276. let title = null
  1277. if (document.querySelector("span[data-bind='text:OrigTitle']")) {
  1278. title = document.querySelector("span[data-bind='text:OrigTitle']").textContent
  1279. } else {
  1280. title = document.querySelector("h2[data-bind='text:Title']").textContent
  1281. }
  1282. let year = null
  1283. if (document.querySelector("span[data-bind='text:ProductionYear']")) {
  1284. year = parseInt(document.querySelector("span[data-bind='text:ProductionYear']").textContent)
  1285. }
  1286. return [title, year]
  1287. }
  1288. }
  1289. ]
  1290. },
  1291. wikiwand: {
  1292. host: ['www.wikiwand.com'],
  1293. condition: Always,
  1294. products: [{
  1295. condition: function () {
  1296. const title = document.querySelector('h1').textContent.toLowerCase()
  1297. const subtitle = document.querySelector('h2[class*="subtitle"]') ? document.querySelector('h2[class*="subtitle"]').textContent.toLowerCase() : ''
  1298. if (title.indexOf('film') === -1 && !subtitle) {
  1299. return false
  1300. }
  1301. return title.indexOf('film') !== -1 ||
  1302. subtitle.indexOf('film') !== -1 ||
  1303. subtitle.indexOf('movie') !== -1
  1304. },
  1305. type: 'movie',
  1306. data: () => document.querySelector('h1').textContent.replace(/\((\d{4} )?film\)/i, '').trim()
  1307. },
  1308. {
  1309. condition: function () {
  1310. const title = document.querySelector('h1').textContent.toLowerCase()
  1311. const subtitle = document.querySelector('h2[class*="subtitle"]') ? document.querySelector('h2[class*="subtitle"]').textContent.toLowerCase() : ''
  1312. if (title.indexOf('tv series') === -1 && !subtitle) {
  1313. return false
  1314. }
  1315. return title.indexOf('tv series') !== -1 ||
  1316. subtitle.indexOf('television') !== -1 ||
  1317. subtitle.indexOf('tv series') !== -1
  1318. },
  1319. type: 'tv',
  1320. data: () => document.querySelector('h1').textContent.replace(/\(tv series\)/i, '').trim()
  1321. }]
  1322. },
  1323. trakt: {
  1324. host: ['trakt.tv'],
  1325. condition: Always,
  1326. products: [
  1327. {
  1328. condition: () => document.location.pathname.startsWith('/movies/'),
  1329. type: 'movie',
  1330. data: function () {
  1331. const title = Array.from(document.querySelector('.summary h1').childNodes).filter(node => node.nodeType === node.TEXT_NODE).map(node => node.textContent).join(' ').trim()
  1332. const year = document.querySelector('.summary h1 .year').textContent
  1333. return [title, year]
  1334. }
  1335. },
  1336. {
  1337. condition: () => document.location.pathname.startsWith('/shows/'),
  1338. type: 'tv',
  1339. data: () => Array.from(document.querySelector('.summary h1').childNodes).filter(node => node.nodeType === node.TEXT_NODE).map(node => node.textContent).join(' ').trim()
  1340. }
  1341. ]
  1342. }
  1343. }
  1344.  
  1345. async function main () {
  1346. let dataFound = false
  1347.  
  1348. for (const name in sites) {
  1349. const site = sites[name]
  1350. if (site.host.some(function (e) { return ~this.indexOf(e) || e === '*' }, document.location.hostname) && site.condition()) {
  1351. for (let i = 0; i < site.products.length; i++) {
  1352. if (site.products[i].condition()) {
  1353. // Try to retrieve item name from page
  1354. let data
  1355. try {
  1356. data = await site.products[i].data()
  1357. } catch (e) {
  1358. data = false
  1359. console.error(`${scriptName}: Error in data() of site='${name}', type='${site.products[i].type}'`)
  1360. console.error(e)
  1361. }
  1362. if (data) {
  1363. if (Array.isArray(data)) {
  1364. if (data[1]) {
  1365. loadMeter(data[0].trim(), site.products[i].type, parseInt(data[1]))
  1366. } else {
  1367. loadMeter(data[0].trim(), site.products[i].type)
  1368. }
  1369. } else {
  1370. loadMeter(data.trim(), site.products[i].type)
  1371. }
  1372. dataFound = true
  1373. }
  1374. break
  1375. }
  1376. }
  1377. break
  1378. }
  1379. }
  1380. return dataFound
  1381. }
  1382.  
  1383. async function adaptForMetaScript () {
  1384. // Move this container above the meta container if the meta container is on the right side
  1385. const rottenC = document.getElementById('mcdiv321rotten')
  1386. const metaC = document.getElementById('mcdiv123')
  1387.  
  1388. if (!metaC || !rottenC) {
  1389. return
  1390. }
  1391. const rottenBounds = rottenC.getBoundingClientRect()
  1392.  
  1393. let bottom = 0
  1394. if (metaC) {
  1395. const metaBounds = metaC.getBoundingClientRect()
  1396. if (Math.abs(metaBounds.right - rottenBounds.right) < 20 && metaBounds.top > 20) {
  1397. bottom += metaBounds.height
  1398. }
  1399. }
  1400.  
  1401. if (bottom > 0) {
  1402. rottenC.style.bottom = bottom + 'px'
  1403. }
  1404. }
  1405.  
  1406. (async function () {
  1407. if (document.location.href === 'https://www.rottentomatoes.com/') {
  1408. updateAlgolia()
  1409. }
  1410.  
  1411. const firstRunResult = await main()
  1412. let lastLoc = document.location.href
  1413. let lastContent = document.body.innerText
  1414. let lastCounter = 0
  1415. async function newpage () {
  1416. if (lastContent === document.body.innerText && lastCounter < 15) {
  1417. window.setTimeout(newpage, 500)
  1418. lastCounter++
  1419. } else {
  1420. lastContent = document.body.innerText
  1421. lastCounter = 0
  1422. const re = await main()
  1423. if (!re) { // No page matched or no data found
  1424. window.setTimeout(newpage, 1000)
  1425. }
  1426. }
  1427. }
  1428. window.setInterval(function () {
  1429. adaptForMetaScript()
  1430. if (document.location.href !== lastLoc) {
  1431. lastLoc = document.location.href
  1432. $('#mcdiv321rotten').remove()
  1433.  
  1434. window.setTimeout(newpage, 1000)
  1435. }
  1436. }, 500)
  1437.  
  1438. if (!firstRunResult) {
  1439. // Initial run had no match, let's try again there may be new content
  1440. window.setTimeout(main, 2000)
  1441. }
  1442. })()