Greasy Fork is available in English.

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

Cài đặt script này?
Script được tác giả gợi ý

Bạn có thế thích Show Rottentomatoes meter - Trakt UI Addon

Cài đặt script này
  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. })()