Show Metacritic.com ratings

Show metacritic metascore and user ratings on: Bandcamp, Apple Itunes (Music), Amazon (Music,Movies,TV Shows), IMDb (Movies), Google Play (Music, Movies), TV.com, Steam, Gamespot (PS4, XONE, PC), Rotten Tomatoes, Serienjunkies, BoxOfficeMojo, allmovie.com, movie.com, Wikipedia (en), themoviedb.org, letterboxd, TVmaze, TVGuide, followshows.com, TheTVDB.com, ConsequenceOfSound, Pitchfork, Last.fm, TVnfo, rateyourmusic.com

当前为 2020-03-09 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Show Metacritic.com ratings
  3. // @description Show metacritic metascore and user ratings on: Bandcamp, Apple Itunes (Music), Amazon (Music,Movies,TV Shows), IMDb (Movies), Google Play (Music, Movies), TV.com, Steam, Gamespot (PS4, XONE, PC), Rotten Tomatoes, Serienjunkies, BoxOfficeMojo, allmovie.com, movie.com, Wikipedia (en), themoviedb.org, letterboxd, TVmaze, TVGuide, followshows.com, TheTVDB.com, ConsequenceOfSound, Pitchfork, Last.fm, TVnfo, rateyourmusic.com
  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 http://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
  13. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  14. // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
  15. // @version 53
  16. // @connect metacritic.com
  17. // @connect php-cuzi.herokuapp.com
  18. // @include https://*.bandcamp.com/*
  19. // @include https://play.google.com/store/music/album/*
  20. // @include https://play.google.com/store/movies/details/*
  21. // @include https://music.amazon.com/*
  22. // @include http://www.amazon.com/*
  23. // @include https://www.amazon.com/*
  24. // @include http://www.amazon.co.uk/*
  25. // @include https://www.amazon.co.uk/*
  26. // @include http://www.amazon.fr/*
  27. // @include https://www.amazon.fr/*
  28. // @include http://www.amazon.de/*
  29. // @include https://www.amazon.de/*
  30. // @include http://www.amazon.es/*
  31. // @include https://www.amazon.es/*
  32. // @include http://www.amazon.ca/*
  33. // @include https://www.amazon.ca/*
  34. // @include http://www.amazon.in/*
  35. // @include https://www.amazon.in/*
  36. // @include http://www.amazon.it/*
  37. // @include https://www.amazon.it/*
  38. // @include http://www.amazon.co.jp/*
  39. // @include https://www.amazon.co.jp/*
  40. // @include http://www.amazon.com.mx/*
  41. // @include https://www.amazon.com.mx/*
  42. // @include http://www.amazon.com.au/*
  43. // @include https://www.amazon.com.au/*
  44. // @include http://www.imdb.com/title/*
  45. // @include https://www.imdb.com/title/*
  46. // @include http://store.steampowered.com/app/*
  47. // @include https://store.steampowered.com/app/*
  48. // @include http://www.gamespot.com/*
  49. // @include https://www.gamespot.com/*
  50. // @include http://www.serienjunkies.de/*
  51. // @include https://www.serienjunkies.de/*
  52. // @include http://www.tv.com/shows/*
  53. // @include https://www.rottentomatoes.com/m/*
  54. // @include https://rottentomatoes.com/m/*
  55. // @include https://www.rottentomatoes.com/tv/*
  56. // @include https://rottentomatoes.com/tv/*
  57. // @include https://www.rottentomatoes.com/tv/*/s*/
  58. // @include https://rottentomatoes.com/tv/*/s*/
  59. // @include http://www.boxofficemojo.com/movies/*
  60. // @include https://www.boxofficemojo.com/movies/*
  61. // @include https://www.boxofficemojo.com/release/*
  62. // @include http://www.allmovie.com/movie/*
  63. // @include https://www.allmovie.com/movie/*
  64. // @include https://en.wikipedia.org/*
  65. // @include http://www.movies.com/*/m*
  66. // @include https://www.themoviedb.org/movie/*
  67. // @include https://www.themoviedb.org/tv/*
  68. // @include http://letterboxd.com/film/*
  69. // @include https://letterboxd.com/film/*
  70. // @exclude https://letterboxd.com/film/*/image*
  71. // @include http://www.tvmaze.com/shows/*
  72. // @include https://www.tvmaze.com/shows/*
  73. // @include http://www.tvguide.com/tvshows/*
  74. // @include https://www.tvguide.com/tvshows/*
  75. // @include http://followshows.com/show/*
  76. // @include https://followshows.com/show/*
  77. // @include http://thetvdb.com/*tab=series*
  78. // @include https://thetvdb.com/*tab=series*
  79. // @include http://www.thetvdb.com/*tab=series*
  80. // @include https://www.thetvdb.com/*tab=series*
  81. // @include https://www.thetvdb.com/series/*
  82. // @include http://consequenceofsound.net/*
  83. // @include https://consequenceofsound.net/*
  84. // @include http://pitchfork.com/*
  85. // @include https://pitchfork.com/*
  86. // @include http://www.last.fm/*
  87. // @include https://www.last.fm/*
  88. // @include http://tvnfo.com/s/*
  89. // @include https://tvnfo.com/s/*
  90. // @include http://rateyourmusic.com/release/album/*
  91. // @include https://rateyourmusic.com/release/album/*
  92. // @include https://open.spotify.com/*
  93. // @include https://play.spotify.com/album/*
  94. // @include https://www.nme.com/reviews/*
  95. // @include https://www.albumoftheyear.org/album/*
  96. // @include https://itunes.apple.com/*/movie/*
  97. // @include https://itunes.apple.com/*/album/*
  98. // @include https://music.apple.com/*/album/*
  99. // @include https://itunes.apple.com/*/tv-season/*
  100. // @include http://epguides.com/*
  101. // @include http://www.epguides.com/*
  102. // @include https://sharetv.com/shows/*
  103. // @include https://www.netflix.com/*
  104. // @include http://www.cc.com/*
  105. // @include https://www.tvhoard.com/*
  106. // @include https://www.amc.com/*
  107. // ==/UserScript==
  108.  
  109. /* globals alert, confirm, GM, DOMParser, $, Image, unsafeWindow, parent, Blob */
  110.  
  111. const baseURL = 'https://www.metacritic.com/'
  112.  
  113. const baseURLmusic = 'https://www.metacritic.com/music/'
  114. const baseURLmovie = 'https://www.metacritic.com/movie/'
  115. const baseURLpcgame = 'https://www.metacritic.com/game/pc/'
  116. const baseURLps4 = 'https://www.metacritic.com/game/playstation-4/'
  117. const baseURLxone = 'https://www.metacritic.com/game/xbox-one/'
  118. const baseURLtv = 'https://www.metacritic.com/tv/'
  119.  
  120. const baseURLsearch = 'https://www.metacritic.com/search/{type}/{query}/results'
  121. const baseURLautosearch = 'https://www.metacritic.com/autosearch'
  122.  
  123. const baseURLdatabase = 'https://php-cuzi.herokuapp.com/r.php'
  124. const baseURLwhitelist = 'https://php-cuzi.herokuapp.com/whitelist.php'
  125. const baseURLblacklist = 'https://php-cuzi.herokuapp.com/blacklist.php'
  126.  
  127. const TEMPORARY_BLACKLIST_TIMEOUT = 5 * 60
  128.  
  129. // http://www.designcouch.com/home/why/2013/05/23/dead-simple-pure-css-loading-spinner/
  130. const CSS = '#mcdiv123 .grespinner{height:16px;width:16px;margin:0 auto;position:relative;animation:rotation .6s infinite linear;border-left:6px solid rgba(0,174,239,.15);border-right:6px solid rgba(0,174,239,.15);border-bottom:6px solid rgba(0,174,239,.15);border-top:6px solid rgba(0,174,239,.8);border-radius:100%}@keyframes rotation{from{transform:rotate(0)}to{transform:rotate(359deg)}}#mcdiv123searchresults .result{font:12px arial,helvetica,serif;border-top-width:1px;border-top-color:#ccc;border-top-style:solid;padding:5px}#mcdiv123searchresults .result .result_type{display:inline}#mcdiv123searchresults .result .result_wrap{float:left;width:100%}#mcdiv123searchresults .result .has_score{padding-left:42px}#mcdiv123searchresults .result .basic_stats{height:1%;overflow:hidden}#mcdiv123searchresults .result h3{font-size:14px;font-weight:700}#mcdiv123searchresults .result a{color:#09f;font-weight:700;text-decoration:none}#mcdiv123searchresults .metascore_w.game.seventyfive,#mcdiv123searchresults .metascore_w.positive,#mcdiv123searchresults .metascore_w.score_favorable,#mcdiv123searchresults .metascore_w.score_outstanding,#mcdiv123searchresults .metascore_w.sixtyone{background-color:#6c3}#mcdiv123searchresults .metascore_w.forty,#mcdiv123searchresults .metascore_w.game.fifty,#mcdiv123searchresults .metascore_w.mixed,#mcdiv123searchresults .metascore_w.score_mixed{background-color:#fc3}#mcdiv123searchresults .metascore_w.negative,#mcdiv123searchresults .metascore_w.score_terrible,#mcdiv123searchresults .metascore_w.score_unfavorable{background-color:red}#mcdiv123searchresults a.metascore_w,#mcdiv123searchresults span.metascore_w{display:inline-block}#mcdiv123searchresults .result .metascore_w{color:#fff!important;font-family:Arial,Helvetica,sans-serif;font-size:17px;font-style:normal!important;font-weight:700!important;height:2em;line-height:2em;text-align:center;vertical-align:middle;width:2em;float:left;margin:0 0 0 -42px}#mcdiv123searchresults .result .more_stats{font-size:10px;color:#444}#mcdiv123searchresults .result .release_date .data{font-weight:700;color:#000}#mcdiv123searchresults ol,#mcdiv123searchresults ul{list-style:none}#mcdiv123searchresults .result li.stat{background:0 0;display:inline;float:left;margin:0;padding:0 6px 0 0;white-space:nowrap}#mcdiv123searchresults .result .deck{margin:3px 0 0}#mcdiv123searchresults .result .basic_stat{display:inline;float:right;overflow:hidden;width:100%}'
  131.  
  132. var myDOMParser = null
  133. function domParser () {
  134. if (myDOMParser === null) {
  135. myDOMParser = new DOMParser()
  136. }
  137. return myDOMParser
  138. }
  139.  
  140. async function versionUpdate () {
  141. const version = parseInt(await GM.getValue('version', 0))
  142. if (version <= 51) {
  143. // Reset database
  144. await GM.setValue('map', '{}')
  145. await GM.setValue('black', '[]')
  146. await GM.setValue('hovercache', '{}')
  147. await GM.setValue('requestcache', '{}')
  148. await GM.setValue('searchcache', '{}')
  149. await GM.setValue('autosearchcache', '{}')
  150. await GM.setValue('temporaryblack', '{}')
  151. }
  152. if (version < 52) {
  153. await GM.setValue('version', 52)
  154. }
  155. }
  156.  
  157. function getHostname (url) {
  158. const a = document.createElement('a')
  159. a.href = url
  160. return a.hostname
  161. }
  162. function absoluteMetaURL (url) {
  163. if (url.startsWith('https://')) {
  164. return url
  165. }
  166. if (url.startsWith('http://')) {
  167. return 'https' + url.substr(4)
  168. }
  169. if (url.startsWith('//')) {
  170. return baseURL + url.substr(2)
  171. }
  172. if (url.startsWith('/')) {
  173. return baseURL + url.substr(1)
  174. }
  175. return baseURL + url
  176. }
  177.  
  178. const parseLDJSONCache = {}
  179. function parseLDJSON (keys, condition) {
  180. if (document.querySelector('script[type="application/ld+json"]')) {
  181. const data = []
  182. const scripts = document.querySelectorAll('script[type="application/ld+json"]')
  183. for (let i = 0; i < scripts.length; i++) {
  184. let jsonld
  185. if (scripts[i].innerText in parseLDJSONCache) {
  186. jsonld = parseLDJSONCache[scripts[i].innerText]
  187. } else {
  188. try {
  189. jsonld = JSON.parse(scripts[i].innerText)
  190. parseLDJSONCache[scripts[i].innerText] = jsonld
  191. } catch (e) {
  192. parseLDJSONCache[scripts[i].innerText] = null
  193. continue
  194. }
  195. }
  196. if (jsonld) {
  197. if (Array.isArray(jsonld)) {
  198. data.push(...jsonld)
  199. } else {
  200. data.push(jsonld)
  201. }
  202. }
  203. }
  204. for (let i = 0; i < data.length; i++) {
  205. try {
  206. if (data[i] && data[i] && (typeof condition !== 'function' || condition(data[i]))) {
  207. if (Array.isArray(keys)) {
  208. const r = []
  209. for (let j = 0; j < keys.length; j++) {
  210. r.push(data[i][keys[j]])
  211. }
  212. return r
  213. } else if (keys) {
  214. return data[i][keys]
  215. } else if (typeof condition === 'function') {
  216. return data[i] // Return whole object
  217. }
  218. }
  219. } catch (e) {
  220. continue
  221. }
  222. }
  223. return data
  224. }
  225. return null
  226. }
  227.  
  228. function name2metacritic (s) {
  229. return s.normalize('NFKD').replace(/\//g, '').replace(/[\u0300-\u036F]/g, '').replace(/&/g, 'and').replace(/\W+/g, ' ').toLowerCase().trim().replace(/\W+/g, '-')
  230. }
  231. function minutesSince (time) {
  232. const seconds = ((new Date()).getTime() - time.getTime()) / 1000
  233. return seconds > 60 ? parseInt(seconds / 60) + ' min ago' : 'now'
  234. }
  235. function randomStringId () {
  236. const id10 = () => Math.floor((1 + Math.random()) * 0x10000000000).toString(16).substring(1)
  237. return id10() + id10() + id10() + id10() + id10() + id10()
  238. }
  239. function fixMetacriticURLs (html) {
  240. return html.replace(/<a /g, '<a target="_blank" ').replace(/href="\//g, 'href="' + baseURL).replace(/src="\//g, 'src="' + baseURL)
  241. }
  242. function searchType2metacritic (type) {
  243. return ({
  244. movie: 'movie',
  245. pcgame: 'game',
  246. xonegame: 'game',
  247. ps4game: 'game',
  248. music: 'album',
  249. tv: 'tv'
  250. })[type]
  251. }
  252. function metacritic2searchType (type) {
  253. return ({
  254. Album: 'music',
  255. TV: 'tv',
  256. Movie: 'movie',
  257. 'PC Game': 'pcgame',
  258. 'PS4 Game': 'ps4game',
  259. 'XONE Game': 'onegame',
  260. 'WIIU Game': 'xxxxx',
  261. '3DS Game': 'xxxx'
  262. })[type]
  263. }
  264.  
  265. function balloonAlert (message, timeout, title, css, click) {
  266. let header
  267. if (title) {
  268. header = '<div style="background:rgb(220,230,150); padding: 2px 12px;">' + title + '</div>'
  269. } else if (title === false) {
  270. header = ''
  271. } else {
  272. header = '<div style="background:rgb(220,230,150); padding: 2px 12px;">Userscript alert</div>'
  273. }
  274. const div = $('<div>' + header + '<div style="padding:5px">' + message.split('\n').join('<br>') + '</div></div>')
  275. div.css({
  276. position: 'fixed',
  277. top: 10,
  278. left: 10,
  279. maxWidth: 200,
  280. zIndex: '2147483601',
  281. background: 'rgb(240,240,240)',
  282. border: '2px solid yellow',
  283. borderRadius: '6px',
  284. boxShadow: '0 0 3px 3px rgba(100, 100, 100, 0.2)',
  285. fontFamily: 'sans-serif',
  286. color: 'black'
  287. })
  288. if (css) {
  289. div.css(css)
  290. }
  291. div.appendTo(document.body)
  292.  
  293. if (click) {
  294. div.click(function (ev) {
  295. $(this).hide(500)
  296. click.call(this, ev)
  297. })
  298. }
  299.  
  300. if (!click) {
  301. const close = $('<div title="Close" style="cursor:pointer; position:absolute; top:0px; right:3px;">&#10062;</div>').appendTo(div)
  302. close.click(function () {
  303. $(this.parentNode).hide(1000)
  304. })
  305. }
  306.  
  307. if (timeout && timeout > 0) {
  308. window.setTimeout(function () {
  309. div.hide(3000)
  310. }, timeout)
  311. }
  312. return div
  313. }
  314.  
  315. function filterUniversalUrl (url) {
  316. try {
  317. url = url.match(/http.+/)[0]
  318. } catch (e) { }
  319.  
  320. try {
  321. url = url.replace(/https?:\/\/(www.)?/, '')
  322. } catch (e) { }
  323.  
  324. if (url.indexOf('#') !== -1) {
  325. url = url.split('#')[0]
  326. }
  327.  
  328. if (url.startsWith('imdb.com/') && url.match(/(imdb\.com\/\w+\/\w+\/)/)) {
  329. // Remove movie subpage from imdb url
  330. return url.match(/(imdb\.com\/\w+\/\w+\/)/)[1]
  331. } else if (url.startsWith('thetvdb.com/')) {
  332. // Do nothing with thetvdb.com urls
  333. return url
  334. } else if (url.startsWith('boxofficemojo.com/') && url.indexOf('id=') !== -1) {
  335. // Keep the important id= on
  336. try {
  337. const parts = url.split('?')
  338. const page = parts[0] + '?'
  339. const idparam = parts[1].match(/(id=.+?)(\.|&)/)[1]
  340. return page + idparam
  341. } catch (e) {
  342. return url
  343. }
  344. } else {
  345. // Default: Remove parameters
  346. return url.split('?')[0].split('&')[0]
  347. }
  348. }
  349.  
  350. async function addToMap (url, metaurl) {
  351. const data = JSON.parse(await GM.getValue('map', '{}'))
  352.  
  353. url = filterUniversalUrl(url)
  354. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  355.  
  356. data[url] = metaurl
  357.  
  358. await GM.setValue('map', JSON.stringify(data));
  359.  
  360. (new Image()).src = baseURLwhitelist + '?docurl=' + encodeURIComponent(url) + '&metaurl=' + encodeURIComponent(metaurl) + '&ref=' + encodeURIComponent(randomStringId())
  361. return [url, metaurl]
  362. }
  363.  
  364. async function removeFromMap (url) {
  365. const data = JSON.parse(await GM.getValue('map', '{}'))
  366.  
  367. url = filterUniversalUrl(url)
  368. if (url in data) {
  369. delete data[url]
  370. await GM.setValue('map', JSON.stringify(data))
  371. }
  372. }
  373.  
  374. async function addToTemporaryBlacklist (metaurl) {
  375. const data = JSON.parse(await GM.getValue('temporaryblack', '{}'))
  376.  
  377. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  378. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/')
  379. metaurl = metaurl.replace(/^\/+/, '')
  380.  
  381. data[metaurl] = (new Date()).toJSON()
  382.  
  383. // Remove old entries
  384. const now = (new Date()).getTime()
  385. const timeout = TEMPORARY_BLACKLIST_TIMEOUT * 1000
  386. for (const prop in data) {
  387. if (now - (new Date(data[prop].time)).getTime() > timeout) {
  388. delete data[prop]
  389. }
  390. }
  391.  
  392. await GM.setValue('temporaryblack', JSON.stringify(data))
  393.  
  394. return true
  395. }
  396.  
  397. async function isTemporaryBlacklisted (metaurl) {
  398. const data = JSON.parse(await GM.getValue('temporaryblack', '{}'))
  399.  
  400. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  401. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/')
  402. metaurl = metaurl.replace(/^\/+/, '')
  403.  
  404. if (metaurl in data) {
  405. const now = (new Date()).getTime()
  406. const timeout = TEMPORARY_BLACKLIST_TIMEOUT * 1000
  407. if (now - (new Date(data[metaurl])).getTime() < timeout) {
  408. return true
  409. }
  410. }
  411. return false
  412. }
  413.  
  414. async function addToBlacklist (url, metaurl) {
  415. const data = JSON.parse(await GM.getValue('black', '[]'))
  416.  
  417. url = filterUniversalUrl(url)
  418. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  419.  
  420. data.push([url, metaurl])
  421.  
  422. await GM.setValue('black', JSON.stringify(data));
  423.  
  424. (new Image()).src = baseURLblacklist + '?docurl=' + encodeURIComponent(url) + '&metaurl=' + encodeURIComponent(metaurl) + '&ref=' + encodeURIComponent(randomStringId())
  425. return [url, metaurl]
  426. }
  427.  
  428. async function removeFromBlacklist (docurl, metaurl) {
  429. docurl = filterUniversalUrl(docurl)
  430. docurl = docurl.replace(/https?:\/\/(www.)?/, '')
  431.  
  432. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  433. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/') // remove double slash
  434. metaurl = metaurl.replace(/^\/+/, '') // remove starting slash
  435.  
  436. const data = JSON.parse(await GM.getValue('black', '[]')) // [ [docurl0, metaurl0] , [docurl1, metaurl1] , ... ]
  437. const found = []
  438. for (let i = 0; i < data.length; i++) {
  439. if (data[i][0] === docurl && data[i][1] === metaurl) {
  440. found.push(i)
  441. }
  442. }
  443. for (let i = found.length - 1; i >= 0; i--) {
  444. data.pop(i)
  445. }
  446.  
  447. await GM.setValue('black', JSON.stringify(data))
  448. }
  449.  
  450. async function isBlacklistedUrl (docurl, metaurl) {
  451. docurl = filterUniversalUrl(docurl)
  452. docurl = docurl.replace(/https?:\/\/(www.)?/, '')
  453.  
  454. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  455. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/') // remove double slash
  456. metaurl = metaurl.replace(/^\/+/, '') // remove starting slash
  457.  
  458. const data = JSON.parse(await GM.getValue('black', '[]')) // [ [docurl0, metaurl0] , [docurl1, metaurl1] , ... ]
  459. for (let i = 0; i < data.length; i++) {
  460. if (data[i][0] === docurl && data[i][1] === metaurl) {
  461. return true
  462. }
  463. }
  464. return false
  465. }
  466.  
  467. let listenForHotkeysActive = false
  468. function listenForHotkeys (code, cb) {
  469. // Call cb() as soon as the code sequence was typed
  470. if (listenForHotkeysActive) {
  471. return
  472. }
  473. listenForHotkeysActive = true
  474. let i = 0
  475. $(document).bind('keydown.listenForHotkeys', function (ev) {
  476. if (document.activeElement === document.body) {
  477. if (ev.key !== code[i]) {
  478. i = 0
  479. } else {
  480. i++
  481. if (i === code.length) {
  482. ev.preventDefault()
  483. $(document).unbind('keydown.listenForHotkeys')
  484. cb()
  485. }
  486. }
  487. }
  488. })
  489. }
  490.  
  491. function waitForHotkeysMETA () {
  492. listenForHotkeys('meta', (ev) => openSearchBox())
  493. }
  494.  
  495. function asyncRequest (data) {
  496. return new Promise(async function (resolve, reject) {
  497. const cachedValue = await isInRequestCache(data)
  498. if (cachedValue) {
  499. return resolve(cachedValue)
  500. }
  501. const defaultHeaders = {
  502. Referer: data.url,
  503. // Host: getHostname(data.url),
  504. 'User-Agent': navigator.userAgent
  505. }
  506. const defaultData = {
  507. method: 'GET',
  508. onload: function (response) {
  509. storeInRequestCache(data, response)
  510. resolve(response)
  511. },
  512. onerror: (response) => reject(response)
  513. }
  514. if ('headers' in data) {
  515. data.headers = Object.assign(defaultHeaders, data.headers)
  516. } else {
  517. data.headers = defaultHeaders
  518. }
  519.  
  520. data = Object.assign(defaultData, data)
  521. GM.xmlHttpRequest(data)
  522. })
  523. }
  524.  
  525. async function handleJSONredirect (response) {
  526. let blacklistedredirect = false
  527. const j = JSON.parse(response.responseText)
  528.  
  529. // Blacklist items from database received?
  530. if ('blacklist' in j && j.blacklist && j.blacklist.length) {
  531. // Save new blacklist items
  532. const data = JSON.parse(await GM.getValue('black', '[]'))
  533. for (let i = 0; i < j.blacklist.length; i++) {
  534. const saveDocurl = j.blacklist[i].docurl
  535. const saveMetaurl = j.blacklist[i].metaurl
  536.  
  537. data.push([saveDocurl, saveMetaurl])
  538. if (j.jsonRedirect === '/' + saveMetaurl) {
  539. // Redirect is blacklisted!
  540. blacklistedredirect = true
  541. }
  542. }
  543. await GM.setValue('black', JSON.stringify(data))
  544. }
  545. if (blacklistedredirect) {
  546. // Redirect was blacklisted, show nothing
  547. console.log('ShowMetacriticRatings: Redirect was blacklisted -> show nothing')
  548. return null
  549. } else {
  550. // Load redirect
  551. current.metaurl = absoluteMetaURL(j.jsonRedirect)
  552. response = await asyncRequest({
  553. method: 'POST',
  554. url: current.metaurl,
  555. data: 'hoverinfo=1',
  556. headers: {
  557. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  558. 'X-Requested-With': 'XMLHttpRequest'
  559. }
  560. }).catch(function (response) {
  561. console.log('ShowMetacriticRatings: Error 01')
  562. })
  563. return response
  564. }
  565. }
  566.  
  567. function extractHoverFromFullPage (response) {
  568. let html = 'ShowMetacriticRatings:<br>Error occured in extractHoverFromFullPage()'
  569. try {
  570. // Try parsing HTML
  571. const doc = domParser().parseFromString(response.responseText, 'text/html')
  572. doc.querySelector('.product_page_title h1')
  573. doc.querySelector('.summary_img')
  574. doc.querySelectorAll('.details_section')
  575. doc.querySelectorAll('#nav_to_metascore .distribution')
  576.  
  577. let pageUrl = ''
  578. let imgSrc = ''
  579. let imgAlt = ''
  580. let title = ''
  581. let publisher = ''
  582. let releaseDate = ''
  583. let starring = ''
  584. let criticsScore = ''
  585. let criticsClass = ''
  586. let criticsNumber = ''
  587. let criticsCharts = ''
  588. let userScore = ''
  589. let userClass = ''
  590. let userNumber = ''
  591. let userCharts = ''
  592.  
  593. pageUrl = response.finalUrl + (response.finalUrl.endsWith('/') ? '' : '/')
  594. imgSrc = doc.querySelector('.summary_img').src
  595. imgAlt = doc.querySelector('.summary_img').alt
  596. title = doc.querySelector('.product_page_title h1').textContent
  597. if (doc.querySelector('.details_section .distributor a')) { publisher = doc.querySelector('.details_section .distributor a').textContent }
  598.  
  599. if (doc.querySelector('.details_section .release_date span:nth-child(2)')) {
  600. const date = doc.querySelector('.details_section .release_date span:nth-child(2)').textContent
  601. releaseDate = `
  602. <div class="summary_detail release_data">
  603. <span class="label">Release Date:</span>
  604. <span class="data">${date}</span>
  605. </div>`
  606. }
  607.  
  608. if (doc.querySelector('.details_section.summary_cast span:nth-child(2)')) {
  609. const stars = doc.querySelector('.details_section.summary_cast span:nth-child(2)').innerHTML
  610. starring = `
  611. <div>
  612. <div class="summary_detail product_credits">
  613. <span class="label">Starring:</span>
  614. <span class="data">
  615. ${stars}
  616. </span>
  617. </div>
  618. </div>`
  619. }
  620.  
  621. criticsClass = 'metascore_w medium tbd'
  622. criticsScore = 'tbd'
  623. userClass = 'metascore_w medium user tbd'
  624. userScore = 'tbd'
  625.  
  626. if (doc.querySelector('.score_details .based_on')) {
  627. criticsNumber = doc.querySelector('.score_details .based_on').textContent.match(/\d+/)
  628. } else {
  629. criticsNumber = 'By'
  630. }
  631. if (doc.querySelector('.user_score_summary .based_on')) {
  632. userNumber = doc.querySelector('.user_score_summary .based_on').textContent.match(/\d+/)
  633. } else {
  634. userNumber = 'User'
  635. }
  636.  
  637. // Remove text from distribution charts:
  638. let label = doc.querySelector('#nav_to_metascore .charts .label.fl')
  639. while (label) {
  640. label.parentNode.title = label.textContent.trim() + ' ' + label.parentNode.querySelector('.count').textContent.trim()
  641. label.remove()
  642. label = doc.querySelector('#nav_to_metascore .charts .label.fl')
  643. }
  644. const scores = doc.querySelectorAll('#nav_to_metascore .distribution .metascore_w')
  645. if (scores.length === 2) {
  646. criticsScore = scores[0].innerText
  647. criticsClass = scores[0].className.replace('larger', 'medium')
  648. scores[0].parentNode.parentNode.querySelector('.charts').style.width = '40px'
  649. criticsCharts = '<td class="meta">' + scores[0].parentNode.parentNode.querySelector('.charts').outerHTML + '</td>'
  650. userScore = scores[1].innerText
  651. userClass = scores[1].className.replace('larger', 'medium')
  652. scores[1].parentNode.parentNode.querySelector('.charts').style.width = '40px'
  653. userCharts = '<td class="usr">' + scores[1].parentNode.parentNode.querySelector('.charts').outerHTML + '</td>'
  654. } else if (scores.length === 1) {
  655. if (scores[0].className.indexOf('user') === -1) {
  656. criticsScore = scores[0].innerText
  657. criticsClass = scores[0].className.replace('larger', 'medium')
  658. scores[0].parentNode.parentNode.querySelector('.charts').style.width = '40px'
  659. criticsCharts = '<td class="meta">' + scores[0].parentNode.parentNode.querySelector('.charts').outerHTML + '</td>'
  660. } else {
  661. userScore = scores[0].innerText
  662. userClass = scores[0].className.replace('larger', 'medium')
  663. scores[0].parentNode.parentNode.querySelector('.charts').style.width = '40px'
  664. userCharts = '<td class="usr">' + scores[0].parentNode.parentNode.querySelector('.charts').outerHTML + '</td>'
  665. }
  666. }
  667.  
  668. html = `
  669. <div class="hoverinfo">
  670. <div class="hover_left">
  671. <div class="product_image_wrapper">
  672. <a target="_blank" href="${pageUrl}">
  673. <img class="product_image large_image" src="${imgSrc}" alt="${imgAlt}" />
  674. </a>
  675. </div>
  676. </div>
  677. <div class="hover_right">
  678. <h2 class="product_title">
  679. <a target="_blank" href="${pageUrl}">${title}</a>
  680. </h2>
  681. <div>
  682. <div class="summary_detail publisher">
  683. <span class="data">${publisher}</span>
  684. <span>&nbsp;|&nbsp;&nbsp;</span>
  685. </div>
  686. ${releaseDate}
  687. <div class="clr"></div>
  688. </div>
  689. ${starring}
  690. <div class="hr">
  691. &nbsp;
  692. </div>
  693.  
  694. <table class="hover_scores ">
  695. <tr>
  696. <td class="meta num">
  697. <a target="_blank" class="metascore_anchor" href="${pageUrl}#nav_to_metascore">
  698. <span class="${criticsClass}">${criticsScore}</span>
  699. </a>
  700. </td>
  701. <td class="meta txt">
  702. <div class="metascore_label">Metascore</div>
  703. <div class="metascore_review_count">
  704. <a target="_blank" href="${pageUrl}#nav_to_metascore">
  705. <span>${criticsNumber}</span> critics
  706. </a>
  707. </div>
  708. </td>
  709. ${criticsCharts}
  710. <td class="usr num">
  711.  
  712. <a target="_blank" class="metascore_anchor" href="${pageUrl}#nav_to_metascore">
  713. <span class="${userClass}">${userScore}</span>
  714. </a>
  715.  
  716. </td>
  717. <td class="usr txt">
  718. <div class="userscore_label">User Score</div>
  719. <div class="userscore_review_count">
  720. <a target="_blank" href="${pageUrl}#nav_to_metascore">
  721. <span>${userNumber}</span> Ratings
  722. </a>
  723. </div>
  724. </td>
  725. ${userCharts}
  726. </tr>
  727. </table>
  728.  
  729. </div>
  730.  
  731. <div class="clr"></div>
  732. </div>
  733. `
  734. } catch (e) {
  735. console.log('ShowMetacriticRatings: Error parsing HTML: ' + e)
  736.  
  737. // fallback to cutting out the relevant parts
  738.  
  739. let parts = response.responseText.split('class="score_details')
  740. const textPart = '<div class="' + parts[1].split('</div>')[0] + '</div>'
  741.  
  742. let titleText = '<div class="product_page_title' + response.responseText.split('class="product_page_title')[1].split('</div>')[0]
  743. titleText = titleText.split('<h1>').join('<h1 style="padding:0px; margin:2px">') + '</div>'
  744.  
  745. parts = response.responseText.split('id="nav_to_metascore"')
  746. let metaScorePart = '<div ' + parts[1].split('<div class="subsection_title"')[0] + '</div></div>'
  747.  
  748. metaScorePart = metaScorePart.split('href="">').join('href="' + response.finalUrl + '">')
  749. metaScorePart = metaScorePart.split('section_title bold">').join('section_title bold">' + titleText)
  750.  
  751. html = metaScorePart.split('<div class="distribution">').join(textPart + '<div class="distribution">')
  752.  
  753. if (html.indexOf('products_module') !== -1) {
  754. // Critic reviews are not available for this Series yet -> Cut the preview for other series
  755. html = html.split('products_module')[0] + '"></div>'
  756. }
  757.  
  758. if (html.length > 5000) {
  759. // Probably something went wrong, let's cut the response to prevent too long content
  760. console.log('ShowMetacriticRatings: Cutting response to 5000 chars')
  761. html = html.substr(0, 5000)
  762. }
  763. }
  764. return html
  765. }
  766.  
  767. async function storeInRequestCache (requestData, response) {
  768. if ('onload' in requestData) {
  769. delete requestData.onload
  770. }
  771. if ('onerror' in requestData) {
  772. delete requestData.onerror
  773. }
  774. const newkey = JSON.stringify(requestData)
  775. const cache = JSON.parse(await GM.getValue('requestcache', '{}'))
  776. const now = (new Date()).getTime()
  777. const timeout = 15 * 60 * 1000
  778. for (const prop in cache) {
  779. // Delete cached values, that are older than 15 minutes
  780. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  781. delete cache[prop]
  782. }
  783. }
  784.  
  785. const newobj = {}
  786. for (const key in response) {
  787. newobj[key] = response[key]
  788. }
  789. newobj.responseText = '' + response.responseText
  790. newobj.cached = true
  791. if (!('time' in newobj)) {
  792. newobj.time = (new Date()).toJSON()
  793. }
  794.  
  795. cache[newkey] = newobj
  796.  
  797. await GM.setValue('requestcache', JSON.stringify(cache))
  798. }
  799.  
  800. async function isInRequestCache (requestData) {
  801. if ('onload' in requestData) {
  802. delete requestData.onload
  803. }
  804. if ('onerror' in requestData) {
  805. delete requestData.onerror
  806. }
  807. const key = JSON.stringify(requestData)
  808.  
  809. const cache = JSON.parse(await GM.getValue('requestcache', '{}'))
  810. const now = (new Date()).getTime()
  811. const timeout = 15 * 60 * 1000
  812. for (const prop in cache) {
  813. // Delete cached values, that are older than 15 minutes
  814. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  815. delete cache[prop]
  816. }
  817. }
  818.  
  819. if (key in cache) {
  820. return cache[key]
  821. } else {
  822. return false
  823. }
  824. }
  825.  
  826. async function storeInHoverCache (metaurl, response, orgMetaUrl) {
  827. const cache = JSON.parse(await GM.getValue('hovercache', '{}'))
  828. const now = (new Date()).getTime()
  829. const timeout = 2 * 60 * 60 * 1000
  830. for (const prop in cache) {
  831. // Delete cached values, that are older than 2 hours
  832. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  833. delete cache[prop]
  834. }
  835. }
  836.  
  837. const newobj = {}
  838. for (const key in response) {
  839. newobj[key] = response[key]
  840. }
  841. newobj.responseText = '' + response.responseText
  842. newobj.cached = true
  843. if (!('time' in newobj)) {
  844. newobj.time = (new Date()).toJSON()
  845. }
  846.  
  847. cache[metaurl] = newobj
  848. if (orgMetaUrl && orgMetaUrl !== metaurl) { // Store redirect
  849. cache[orgMetaUrl] = { time: (new Date()).toJSON(), redirect: metaurl }
  850. }
  851.  
  852. await GM.setValue('hovercache', JSON.stringify(cache))
  853. }
  854.  
  855. async function isInHoverCache (metaurl) {
  856. const cache = JSON.parse(await GM.getValue('hovercache', '{}'))
  857. const now = (new Date()).getTime()
  858. const timeout = 2 * 60 * 60 * 1000
  859. for (const prop in cache) {
  860. // Delete cached values, that are older than 2 hours
  861. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  862. delete cache[prop]
  863. }
  864. }
  865.  
  866. function resolveRedirects (cacheEntry) {
  867. if (cacheEntry.redirect) {
  868. const newkey = cacheEntry.redirect
  869. if (newkey in cache) {
  870. const value = cache[newkey]
  871. delete cache[newkey]
  872. return resolveRedirects(value)
  873. }
  874. } else {
  875. return cacheEntry
  876. }
  877. return false
  878. }
  879.  
  880. if (metaurl in cache) {
  881. const value = cache[metaurl]
  882. delete cache[metaurl]
  883. return resolveRedirects(value)
  884. } else {
  885. return false
  886. }
  887. }
  888.  
  889. async function loadHoverInfo () {
  890. const cacheResponse = await isInHoverCache(current.metaurl)
  891. if (cacheResponse !== false) {
  892. return cacheResponse
  893. }
  894.  
  895. const requestURL = baseURLdatabase
  896. const requestParams = 'm=' + encodeURIComponent(current.docurl) + '&a=' + encodeURIComponent(current.metaurl)
  897.  
  898. let response = await asyncRequest({
  899. method: 'POST',
  900. url: requestURL,
  901. data: requestParams,
  902. headers: {
  903. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
  904. }
  905. }).catch(function (response) {
  906. console.log('ShowMetacriticRatings: Error 02\nurl=' + requestURL + '\nparams=' + requestParams + '\nstatus=' + response.status)
  907. })
  908.  
  909. if (response.responseText.indexOf('"jsonRedirect"') !== -1) {
  910. response = await handleJSONredirect(response)
  911. }
  912. if (response.responseText.indexOf('<title>500 Page') !== -1) {
  913. // Hover info not available for this url, try again with GET
  914. response = await asyncRequest({ url: current.metaurl }).catch(function (response) {
  915. console.log('ShowMetacriticRatings: Error 03\nurl=' + current.metaurl + '\nstatus=' + response.status)
  916. })
  917.  
  918. const newobj = {}
  919. for (const key in response) {
  920. newobj[key] = response[key]
  921. }
  922. newobj.responseText = extractHoverFromFullPage(response)
  923. response = newobj
  924. }
  925.  
  926. if (!('time' in response)) {
  927. response.time = (new Date()).toJSON()
  928. }
  929. if (response.status === 200 && response.responseText) {
  930. return response
  931. } else {
  932. throw new Error('ShowMetacriticRatings: loadHoverInfo()\nUrl: ' + response.finalUrl + '\nStatus: ' + response.status)
  933. }
  934. }
  935.  
  936. const current = {
  937. metaurl: false,
  938. docurl: false,
  939. type: false,
  940. data: [], // Array of raw search keys
  941. searchTerm: false
  942. }
  943.  
  944. async function loadMetacriticUrl (fromSearch) {
  945. if (!current.metaurl) {
  946. alert('ShowMetacriticRatings: Error 04')
  947. return
  948. }
  949. const orgMetaUrl = current.metaurl
  950. if (await isBlacklistedUrl(document.location.href, current.metaurl)) {
  951. waitForHotkeysMETA()
  952. return
  953. }
  954.  
  955. if (await isTemporaryBlacklisted(current.metaurl)) {
  956. console.log('ShowMetacriticRatings: isTemporaryBlacklisted=true')
  957. waitForHotkeysMETA()
  958. return
  959. }
  960.  
  961. const response = await loadHoverInfo().catch((response) => fromSearch ? null : startSearch())
  962.  
  963. if (await isBlacklistedUrl(document.location.href, current.metaurl)) {
  964. waitForHotkeysMETA()
  965. return
  966. }
  967.  
  968. if (typeof response !== 'undefined') {
  969. showHoverInfo(response, orgMetaUrl)
  970. } else {
  971. waitForHotkeysMETA()
  972. }
  973. }
  974.  
  975. async function startSearch () {
  976. waitForHotkeysMETA()
  977.  
  978. const cache = JSON.parse(await GM.getValue('autosearchcache', '{}'))
  979. const now = (new Date()).getTime()
  980. const timeout = 2 * 60 * 60 * 1000
  981. for (const prop in cache) {
  982. // Delete cached values, that are older than 2 hours
  983. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  984. delete cache[prop]
  985. }
  986. }
  987.  
  988. if (current.type === 'music') {
  989. current.searchTerm = current.data[0]
  990. } else {
  991. current.searchTerm = current.data.join(' ')
  992. }
  993. let response
  994. if (current.searchTerm in cache) {
  995. response = cache[current.searchTerm]
  996. } else {
  997. response = await asyncRequest({
  998. method: 'POST',
  999. url: baseURLautosearch,
  1000. data: 'search_term=' + encodeURIComponent(current.searchTerm) + '&image_size=98&search_each=1&sort_type=popular',
  1001. headers: {
  1002. Referer: current.metaurl,
  1003. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  1004. // Host: 'www.metacritic.com',
  1005. 'User-Agent': 'MetacriticUserscript Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0',
  1006. 'X-Requested-With': 'XMLHttpRequest'
  1007. }
  1008. })
  1009. response = {
  1010. time: (new Date()).toJSON(),
  1011. json: JSON.parse(response.responseText)
  1012. }
  1013. cache[current.searchTerm] = response
  1014. await GM.setValue('autosearchcache', JSON.stringify(cache))
  1015. }
  1016.  
  1017. if (!response || !('json' in response)) {
  1018. alert('ShowMetacriticRatings: Error 05')
  1019. }
  1020. const data = response.json
  1021. let multiple = false
  1022. if (data && data.autoComplete && data.autoComplete.results && data.autoComplete.results.length) {
  1023. // Remove data with wrong type
  1024. data.autoComplete = data.autoComplete.results
  1025.  
  1026. const newdata = []
  1027. data.autoComplete.forEach(function (result) {
  1028. if (metacritic2searchType(result.refType) === current.type) {
  1029. newdata.push(result)
  1030. }
  1031. })
  1032. data.autoComplete = newdata
  1033. if (data.autoComplete.length === 0) {
  1034. // No results
  1035. console.log('ShowMetacriticRatings: No results (after filtering by type) for searchTerm=' + current.searchTerm)
  1036. } else if (data.autoComplete.length === 1) {
  1037. // One result, let's show it
  1038. if (!await isBlacklistedUrl(document.location.href, absoluteMetaURL(data.autoComplete[0].url))) {
  1039. current.metaurl = absoluteMetaURL(data.autoComplete[0].url)
  1040. loadMetacriticUrl(true)
  1041. return
  1042. }
  1043. } else {
  1044. // More than one result
  1045. multiple = true
  1046. console.log('ShowMetacriticRatings: Multiple results for searchTerm=' + current.searchTerm)
  1047. const exactMatches = []
  1048. data.autoComplete.forEach(function (result, i) { // Try to find the correct result by matching the search term to exactly one movie title
  1049. if (current.searchTerm === result.name) {
  1050. exactMatches.push(result)
  1051. }
  1052. })
  1053. if (exactMatches.length === 1) {
  1054. // Only one exact match, let's show it
  1055. console.log('ShowMetacriticRatings: Only one exact match for searchTerm=' + current.searchTerm)
  1056. if (!await isBlacklistedUrl(document.location.href, absoluteMetaURL(exactMatches[0].url))) {
  1057. current.metaurl = absoluteMetaURL(exactMatches[0].url)
  1058. loadMetacriticUrl(true)
  1059. return
  1060. }
  1061. }
  1062. }
  1063. } else {
  1064. console.log('ShowMetacriticRatings: No results (at all) for searchTerm=' + current.searchTerm)
  1065. }
  1066. // HERE: multiple results or no result. The user may type "meta" now
  1067. if (multiple) {
  1068. balloonAlert('Multiple metacritic results. Type &#34;meta&#34; for manual search.', 10000, false, { bottom: 5, top: 'auto', maxWidth: 400, paddingRight: 5 }, () => openSearchBox(true))
  1069. }
  1070. }
  1071.  
  1072. function openSearchBox (search) {
  1073. let query
  1074. if (current.type === 'music') {
  1075. query = current.data[0]
  1076. } else {
  1077. query = current.data.join(' ')
  1078. }
  1079. $('#mcdiv123').remove()
  1080. const div = $('<div id="mcdiv123"></div>').appendTo(document.body)
  1081. div.css({
  1082. position: 'fixed',
  1083. bottom: 0,
  1084. left: 0,
  1085. minWidth: 300,
  1086. maxHeight: '80%',
  1087. maxWidth: 640,
  1088. overflow: 'auto',
  1089. backgroundColor: '#fff',
  1090. border: '2px solid #bbb',
  1091. borderRadius: ' 6px',
  1092. boxShadow: '0 0 3px 3px rgba(100, 100, 100, 0.2)',
  1093. color: '#000',
  1094. padding: ' 3px',
  1095. zIndex: '2147483601'
  1096. })
  1097. $('<input type="text" size="60" id="mcisearchquery" style="background:white;color:black;">').appendTo(div).focus().val(query).on('keypress', function (e) {
  1098. const code = e.keyCode || e.which
  1099. if (code === 13) { // Enter key
  1100. searchBoxSearch(e, $('#mcisearchquery').val())
  1101. }
  1102. })
  1103. $('<button id="mcisearchbutton" style="background:silver;color:black;">').text('Search').appendTo(div).click((ev) => searchBoxSearch(ev, $('#mcisearchquery').val()))
  1104. }
  1105. async function searchBoxSearch (ev, query) {
  1106. if (!query) { // Use values from search form
  1107. query = current.searchTerm
  1108. }
  1109.  
  1110. const type = searchType2metacritic(current.type)
  1111.  
  1112. const style = document.createElement('style')
  1113. style.type = 'text/css'
  1114. style.innerHTML = CSS
  1115. document.head.appendChild(style)
  1116.  
  1117. const div = $('#mcdiv123')
  1118. const loader = $('<div style="width:20px; height:20px;display:inline-block" class="grespinner"></div>').appendTo($('#mcisearchbutton'))
  1119.  
  1120. const url = baseURLsearch.replace('{type}', encodeURIComponent(type)).replace('{query}', encodeURIComponent(query))
  1121.  
  1122. const response = await asyncRequest({
  1123. url: url,
  1124. data: 'search_term=' + encodeURIComponent(current.searchTerm) + '&image_size=98&search_each=1&sort_type=popular',
  1125. headers: {
  1126. Referer: url,
  1127. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  1128. // Host: 'www.metacritic.com',
  1129. 'User-Agent': 'MetacriticUserscript ' + navigator.userAgent
  1130. }
  1131. }).catch(function (response) {
  1132. alert('Search failed!\n' + response.finalUrl + '\nStatus: ' + response.status + '\n' + response.responseText ? response.responseText.substring(0, 500) : 'Empty response')
  1133. })
  1134.  
  1135. const results = []
  1136. if (!~response.responseText.indexOf('No search results found.')) {
  1137. const d = $('<html>').html(response.responseText)
  1138. d.find('ul.search_results.module .result').each(function () {
  1139. results.push(this.innerHTML)
  1140. })
  1141. }
  1142.  
  1143. if (results && results.length > 0) {
  1144. // Show results
  1145. loader.remove()
  1146.  
  1147. const accept = function (ev) {
  1148. const parentDiv = $(this.parentNode)
  1149. const a = parentDiv.find("a[href*='metacritic.com']")
  1150. const metaurl = a.attr('href')
  1151. const docurl = document.location.href
  1152.  
  1153. const resultDivParent = parentDiv.parent()
  1154. resultDivParent.html('')
  1155. resultDivParent.append(loader)
  1156.  
  1157. removeFromBlacklist(docurl, metaurl).then(function () {
  1158. addToMap(docurl, metaurl).then(function () {
  1159. current.metaurl = metaurl
  1160. loadMetacriticUrl().then(() => loader.remove())
  1161. })
  1162. })
  1163. }
  1164. const denyAll = function (ev) {
  1165. const docurl = document.location.href
  1166. $('#mcdiv123searchresults').find("div.result a[href*='metacritic.com']").each(function () {
  1167. addToBlacklist(docurl, this.href)
  1168. })
  1169. }
  1170.  
  1171. const resultdiv = $('#mcdiv123searchresults').length ? $('#mcdiv123searchresults').html('') : $('<div id="mcdiv123searchresults"></div>').css('max-width', '95%').appendTo(div)
  1172. results.forEach(function (html) {
  1173. const singleresult = $('<div class="result"></div>').html(fixMetacriticURLs(html) + '<div style="clear:left"></div>').appendTo(resultdiv)
  1174. $('<span title="Assist us: This is the correct entry!" style="cursor:pointer; color:green; font-size: 13px;">&check;</span>').prependTo(singleresult).click(accept)
  1175. })
  1176. resultdiv.find('.metascore_w.album').removeClass('album') // Remove some classes
  1177. resultdiv.find('.must-see').remove() // Remove some elements
  1178.  
  1179. const sub = $('#mcdiv123 .sub').length ? $('#mcdiv123 .sub').html('') : $('<div class="sub"></div>').appendTo(div)
  1180. $('<a style="color:#b6b6b6; font-size: 11px;" target="_blank" href="' + url + '" title="Open Metacritic">' + decodeURI(url.replace('https://www.', '@')) + '</a>').appendTo(sub)
  1181. $('<span title="Hide me" style="cursor:pointer; float:right; color:#b6b6b6; font-size: 11px;">&#10062;</span>').appendTo(sub).click(function () {
  1182. document.body.removeChild(this.parentNode.parentNode)
  1183. })
  1184. $('<span title="Assist us: None of the above is the correct item!" style="cursor:pointer; float:right; color:crimson; font-size: 11px;">&cross;</span>').appendTo(sub).click(function () { if (confirm('None of the above is the correct item\nConfirm?')) denyAll() })
  1185. } else {
  1186. // No results
  1187. loader.remove()
  1188. const resultdiv = $('#mcdiv123searchresults').length ? $('#mcdiv123searchresults').html('') : $('<div id="mcdiv123searchresults"></div>').appendTo(div)
  1189. resultdiv.html('No search results.')
  1190.  
  1191. const sub = $('#mcdiv123 .sub').length ? $('#mcdiv123 .sub').html('') : $('<div class="sub"></div>').appendTo(div)
  1192. $('<a style="color:#b6b6b6; font-size: 11px;" target="_blank" href="' + url + '" title="Open Metacritic">' + decodeURI(url.replace('https://www.', '@')) + '</a>').appendTo(sub)
  1193. $('<span title="Hide me" style="cursor:pointer; float:right; color:#b6b6b6; font-size: 11px;">&#10062;</span>').appendTo(sub).click(function () {
  1194. document.body.removeChild(this.parentNode.parentNode)
  1195. })
  1196. }
  1197. }
  1198.  
  1199. function showHoverInfo (response, orgMetaUrl) {
  1200. const html = fixMetacriticURLs(response.responseText)
  1201. const time = new Date(response.time)
  1202. const url = response.finalUrl
  1203.  
  1204. $('#mcdiv123').remove()
  1205. const div = $('<div id="mcdiv123"></div>').appendTo(document.body)
  1206. div.css({
  1207. position: 'fixed',
  1208. bottom: 0,
  1209. left: 0,
  1210. minWidth: 300,
  1211. backgroundColor: '#fff',
  1212. border: '2px solid #bbb',
  1213. borderRadius: ' 6px',
  1214. boxShadow: '0 0 3px 3px rgba(100, 100, 100, 0.2)',
  1215. color: '#000',
  1216. padding: ' 3px',
  1217. zIndex: '2147483601'
  1218. })
  1219.  
  1220. // Functions for communication between page and iframe
  1221. // Mozilla can access parent.document
  1222. // Chrome can use postMessage()
  1223. let frameStatus = false // if this remains false, loading the frame content failed. A reason could be "Content Security Policy"
  1224. function loadExternalImage (url, myframe) {
  1225. // Load external image, bypass CSP
  1226. GM.xmlHttpRequest({
  1227. method: 'GET',
  1228. url: url,
  1229. responseType: 'arraybuffer',
  1230. onload: function (response) {
  1231. myframe.contentWindow.postMessage({
  1232. mcimessage_imgLoaded: true,
  1233. mcimessage_imgData: response.response,
  1234. mcimessage_imgOrgSrc: url
  1235. }, '*')
  1236. }
  1237. })
  1238. }
  1239. const functions = {
  1240. parent: function () {
  1241. const f = parent.document.getElementById('mciframe123')
  1242. let lastdiff = -200000
  1243. window.addEventListener('message', function (e) {
  1244. if (typeof e.data !== 'object') {
  1245. return
  1246. } else if ('mcimessage0' in e.data) {
  1247. frameStatus = true // Frame content was loaded successfully
  1248. } else if ('mcimessage1' in e.data) {
  1249. f.style.width = parseInt(f.style.width) + 10 + 'px'
  1250. if (e.data.heightdiff === lastdiff) {
  1251. f.style.height = parseInt(f.style.height) + 5 + 'px'
  1252. }
  1253. lastdiff = e.data.heightdiff
  1254. } else if ('mcimessage2' in e.data) {
  1255. f.style.height = parseInt(f.style.height) + 15 + 'px'
  1256. f.style.width = '400px'
  1257. } else if ('mcimessage_loadImg' in e.data) {
  1258. loadExternalImage(e.data.mcimessage_imgUrl, f)
  1259. } else {
  1260. return
  1261. }
  1262. f.contentWindow.postMessage({
  1263. mcimessage3: true,
  1264. mciframe123_clientHeight: f.clientHeight,
  1265. mciframe123_clientWidth: f.clientWidth
  1266. }, '*')
  1267. })
  1268. },
  1269. frame: function () {
  1270. parent.postMessage({ mcimessage0: true }, '*') // Loading frame content was successfull
  1271.  
  1272. let i = 0
  1273. window.addEventListener('message', function (e) {
  1274. if (typeof e.data === 'object' && 'mcimessage_imgLoaded' in e.data) {
  1275. // Load external image
  1276. const arrayBufferView = new Uint8Array(e.data.mcimessage_imgData)
  1277. const blob = new Blob([arrayBufferView], { type: 'image/jpeg' })
  1278. const urlCreator = window.URL || window.webkitURL
  1279. const imageUrl = urlCreator.createObjectURL(blob)
  1280. const img = failedImages[e.data.mcimessage_imgOrgSrc]
  1281. img.src = imageUrl
  1282. }
  1283.  
  1284. if (!('mcimessage3' in e.data)) return
  1285.  
  1286. if (e.data.mciframe123_clientHeight < document.body.scrollHeight && i < 100) {
  1287. parent.postMessage({ mcimessage1: 1, heightdiff: document.body.scrollHeight - e.data.mciframe123_clientHeight }, '*')
  1288. i++
  1289. }
  1290. if (i >= 100) {
  1291. parent.postMessage({ mcimessage2: 1 }, '*')
  1292. i = 0
  1293. }
  1294. })
  1295. parent.postMessage({ mcimessage1: 1, heightdiff: -100000 }, '*')
  1296. }
  1297.  
  1298. }
  1299.  
  1300. const css = `#hover_div .clr { clear: both}
  1301. #hover_div .fl{float: left}
  1302. #hover_div { background-color: #fff; color: #666; font-family:Arial,Helvetica,sans-serif; font-size:12px; font-weight:400; font-style:normal;}
  1303. #hover_div .hoverinfo .hover_left { float: left}
  1304. #hover_div .hoverinfo .product_image_wrapper { color: #999; font-size: 6px; font-weight: normal; min-height: 98px; min-width: 98px;}
  1305. #hover_div .hoverinfo .product_image_wrapper a { color: #999; font-size: 6px; font-weight: normal;}
  1306. #hover_div a * { cursor: pointer}
  1307. #hover_div a { color: #09f; font-weight: bold;}
  1308. #hover_div a:link, #hover_div a:visited { text-decoration: none;}
  1309. #hover_div a:hover { text-decoration: underline;}
  1310. #hover_div .hoverinfo .hover_right { float: left; margin-left: 15px; max-width: 395px;}
  1311. #hover_div .hoverinfo .product_title { color: #333; font-family: georgia,serif; font-size: 24px; line-height: 26px; margin-bottom: 10px;}
  1312. #hover_div .hoverinfo .product_title a { color:#333; font-family: georgia,serif; font-size: 24px;}
  1313. #hover_div .hoverinfo .summary_detail.publisher, .hoverinfo .summary_detail.release_data { float: left}
  1314. #hover_div .hoverinfo .summary_detail { font-size: 11px; margin-bottom: 10px;}
  1315. #hover_div .hoverinfo .summary_detail.product_credits a { color: #999; font-weight: normal; }
  1316. #hover_div .hoverinfo .hr { background-color: #ccc; height: 2px; margin: 15px 0 10px;}
  1317. #hover_div .hoverinfo .hover_scores { width: 100%; border-collapse: collapse; border-spacing: 0;}
  1318. #hover_div .hoverinfo .hover_scores td.num { width: 39px}
  1319. #hover_div .hoverinfo .hover_scores td { vertical-align: middle}
  1320. #hover_div caption, #hover_div th, #hover_div td { font-weight: normal; text-align: left;}
  1321. #hover_div .metascore_anchor, #hover_div a.metascore_w { text-decoration: none !important}
  1322. #hover_div span.metascore_w, #hover_div a.metascore_w { display: inline-block; padding:0px;}
  1323. .metascore_w { background-color: transparent; color: #fff !important; font-family: Arial,Helvetica,sans-serif; font-size: 17px; font-style: normal !important; font-weight: bold !important; height: 2em; line-height: 2em; text-align: center; vertical-align: middle; width: 2em;}
  1324. #hover_div .metascore, #hover_div .metascore a, #hover_div .avguserscore, #hover_div .avguserscore a { color: #fff}
  1325. #hover_div .critscore, #hover_div .critscore a, #hover_div .userscore, #hover_div .userscore a { color: #333}
  1326. .score_tbd { background: #eaeaea; color: #333; font-size: 14px;}
  1327. #hover_div .score_tbd a { color: #333}
  1328. .negative, .score_terrible, .score_unfavorable, .carousel_set a.product_terrible:hover, .carousel_set a.product_unfavorable:hover { background-color: #f00}
  1329. .mixed, .neutral, .score_mixed, .carousel_set a.product_mixed:hover { background-color: #fc3; color: #333;}
  1330. #hover_div .score_mixed a { color: #333}
  1331. .positive, .score_favorable, .score_outstanding, .carousel_set a.product_favorable:hover, .carousel_set a.product_outstanding:hover { background-color: #6c3}
  1332. .critscore_terrible, .critscore_unfavorable { border-color: #f00}
  1333. .critscore_mixed { border-color: #fc3}
  1334. .critscore_favorable, .critscore_outstanding { border-color: #6c3}
  1335. .metascore .score_total, .userscore .score_total { display: none; visibility: hidden;}
  1336. .hoverinfo .metascore_label, .hoverinfo .userscore_label { font-size: 12px; font-weight: bold; line-height: 16px; margin-top: 2%;}
  1337. .hoverinfo .metascore_review_count, .hoverinfo .userscore_review_count { font-size: 11px}
  1338. .hoverinfo .hover_scores td { vertical-align: middle}
  1339. .hoverinfo .hover_scores td.num { width: 39px}
  1340. .hoverinfo .hover_scores td.usr.num { padding-left: 20px}
  1341. .metascore_anchor, a.metascore_w { text-decoration: none !important}
  1342. .metascore_w.album { padding-top:0px; !important}
  1343. .metascore_w.user { border-radius: 55%; color: #fff;}
  1344. .metascore_anchor, .metascore_w.album { padding: 0px;!important, padding-top: 0px;!important}
  1345. a.metascore_w { text-decoration: none!important}
  1346. .metascore_anchor:hover { text-decoration: none!important}
  1347. .metascore_w:hover { text-decoration: none!important}
  1348. span.metascore_w, a.metascore_w { display: inline-block}
  1349. .metascore_w.xlarge, .metascore_w.xl { font-size: 42px}
  1350. .metascore_w.large, .metascore_w.lrg { font-size: 25px}
  1351. .m .metascore_w.medium, .m .metascore_w.med { font-size: 19px}
  1352. .metascore_w.med_small { font-size: 14px}
  1353. .metascore_w.small, .metascore_w.sm { font-size: 12px}
  1354. .metascore_w.tiny { height: 1.9em; font-size: 11px; line-height: 1.9em;}
  1355. .metascore_w.user { border-radius: 55%; color: #fff;}
  1356. .metascore_w.user.small, .metascore_w.user.sm { font-size: 11px}
  1357. .metascore_w.tbd, .metascore_w.score_tbd { color: #000!important; background-color: #ccc;}
  1358. .metascore_w.tbd.hide_tbd, .metascore_w.score_tbd.hide_tbd { visibility: hidden}
  1359. .metascore_w.tbd.no_tbd, .metascore_w.score_tbd.no_tbd { display: none}
  1360. .metascore_w.noscore::before, .metascore_w.score_noscore::before { content: '\u2022\u2022\u2022'}
  1361. .metascore_w.noscore, .metascore_w.score_noscore { color: #fff!important; background-color: #ccc;}
  1362. .metascore_w.rip, .metascore_w.score_rip { border-radius: 4px; color: #fff!important; background-color: #999;}
  1363. .metascore_w.negative, .metascore_w.score_terrible, .metascore_w.score_unfavorable { background-color: #f00}
  1364. .metascore_w.mixed, .metascore_w.forty, .metascore_w.game.fifty, .metascore_w.score_mixed { background-color: #fc3}
  1365. .metascore_w.positive, .metascore_w.sixtyone, .metascore_w.game.seventyfive, .metascore_w.score_favorable, .metascore_w.score_outstanding { background-color: #6c3}
  1366. .metascore_w.indiv { height: 1.9em; width: 1.9em; font-size: 15px; line-height: 1.9em;}
  1367. .metascore_w.indiv.large, .metascore_w.indiv.lrg { font-size: 24px}
  1368. .m .metascore_w.indiv.medium, .m .metascore_w.indiv.med { font-size: 16px}
  1369. .metascore_w.indiv.small, .metascore_w.indiv.sm { font-size: 11px}
  1370. .metascore_w.indiv.perfect { padding-right: 1px}
  1371. .hover_esite { display:none; }
  1372. .promo_amazon .esite_btn { margin: 3px 0 0 7px;}
  1373. .esite_amazon { background-color: #fdc354; border: 1px solid #aaa;}
  1374. .esite_label_wrapper { display:none;}
  1375. .esite_btn { border-radius: 4px; color: #222; font-size: 12px; height: 40px; line-height: 40px; width: 120px;}
  1376. .chart{background-color:inherit!important;margin-top:-3px}
  1377. .chart_bg{width:100%;border-top:3px solid rgba(150,150,150,0.3)}
  1378. .chart .bar{width:100%;height:3px}
  1379. .chart .count{font-size:10px}`
  1380.  
  1381. let framesrc = 'data:text/html,'
  1382. framesrc += encodeURIComponent('<!DOCTYPE html>\
  1383. <html lang="en">\
  1384. <head>\
  1385. <meta charset="utf-8">\
  1386. <title>Metacritic info</title>\
  1387. <style>body { margin:0px; padding:0px; background:white; }' + css +
  1388. '\
  1389. </style>\
  1390. <script>\
  1391. const failedImages = {};\
  1392. function detectCSP(img) {\
  1393. if(img.complete && (!img.naturalWidth || !img.naturalHeight)) {\
  1394. return true;\
  1395. }\
  1396. return false;\
  1397. }\
  1398. function findCSPerrors() {\
  1399. const imgs = document.querySelectorAll("img");\
  1400. for(let i = 0; i < imgs.length; i++) {\
  1401. if(imgs[i].complete && detectCSP(imgs[i])) {\
  1402. fixCSP(imgs[i]);\
  1403. }\
  1404. }\
  1405. }\
  1406. function fixCSP(img) {\
  1407. console.log("ShowMetacriticRatings(iFrame): Loading image failed. Bypassing CSP...");\
  1408. failedImages[img.src] = img;\
  1409. parent.postMessage({"mcimessage_loadImg":true, "mcimessage_imgUrl": img.src},"*"); \
  1410. }\
  1411. function on_load() {\
  1412. (' + functions.frame.toString() + ')();\
  1413. window.setTimeout(findCSPerrors, 500);\
  1414. \
  1415. }\
  1416. </script>\
  1417. </head>\
  1418. <body onload="on_load();">\
  1419. <div style="border:0px solid; display:block; position:relative; border-radius:0px; padding:0px; margin:0px; box-shadow:none;" class="hover_div" id="hover_div">\
  1420. <div class="hover_content">' + html + '</div>\
  1421. </div>\
  1422. </body>\
  1423. </html>')
  1424.  
  1425. const frame = $('<iframe></iframe>').appendTo(div)
  1426. frame.attr('id', 'mciframe123')
  1427. frame.attr('src', framesrc)
  1428. frame.attr('scrolling', 'auto')
  1429. frame.css({
  1430. width: 380,
  1431. height: 150,
  1432. border: 'none'
  1433. })
  1434.  
  1435. window.setTimeout(function () {
  1436. if (!frameStatus) { // Loading frame content failed.
  1437. // Directly inject the html without an iframe (this may break the site or the metacritic)
  1438. console.log('ShowMetacriticRatings: Loading iframe content failed. Injecting directly.')
  1439. $('head').append('<style>' + css + '</style>')
  1440. const noframe = $('<div style="border:0px solid; display:block; position:relative; border-radius:0px; padding:0px; margin:0px; box-shadow:none;" class="hover_div" id="hover_div">\
  1441. <div class="hover_content">' + html + '</div>\
  1442. </div>')
  1443. frame.replaceWith(noframe)
  1444. }
  1445. }, 2000)
  1446.  
  1447. functions.parent()
  1448.  
  1449. const sub = $('<div></div>').appendTo(div)
  1450. $('<time style="color:#b6b6b6; font-size: 11px;" datetime="' + time + '" title="' + time.toLocaleTimeString() + ' ' + time.toLocaleDateString() + '">' + minutesSince(time) + '</time>').appendTo(sub)
  1451. $('<a style="color:#b6b6b6; font-size: 11px;" target="_blank" href="' + url + '" title="Open Metacritic">' + decodeURI(url.replace('https://www.', '@')) + '</a>').appendTo(sub)
  1452. $('<span title="Hide me" style="cursor:pointer; float:right; color:#b6b6b6; font-size: 11px; padding-left:5px;">&#10062;</span>').data('url', current.metaurl).appendTo(sub).click(function () {
  1453. const metaurl = $(this).data('url')
  1454. addToTemporaryBlacklist(metaurl)
  1455. document.body.removeChild(this.parentNode.parentNode)
  1456. })
  1457.  
  1458. $('<span title="Assist us: This is the correct entry!" style="cursor:pointer; float:right; color:green; font-size: 11px;">&check;</span>').data('url', current.metaurl).appendTo(sub).click(function () {
  1459. const docurl = document.location.href
  1460. const metaurl = $(this).data('url')
  1461. addToMap(docurl, metaurl).then(function (r) {
  1462. balloonAlert('Thanks for your submission!\n\nSaved as a correct entry.\n\n' + r[0] + '\n' + r[1], 6000, 'Success')
  1463. })
  1464. })
  1465. $('<span title="Assist us: This is NOT the correct entry!" style="cursor:pointer; float:right; color:crimson; font-size: 11px;">&cross;</span>').data('url', current.metaurl).appendTo(sub).click(function () {
  1466. if (!confirm('This is NOT the correct entry!\n\nAdd to blacklist?')) return
  1467. const docurl = document.location.href
  1468. const metaurl = $(this).data('url')
  1469. addToBlacklist(docurl, metaurl).then(function (r) {
  1470. balloonAlert('Thanks for your submission!\n\nSaved to blacklist.\n\n' + r[0] + '\n' + r[1], 6000, 'Success')
  1471. })
  1472.  
  1473. openSearchBox(true)
  1474. })
  1475.  
  1476. // Store response in cache:
  1477. if (!('cached' in response)) {
  1478. storeInHoverCache(current.metaurl, response, orgMetaUrl)
  1479. }
  1480. }
  1481.  
  1482. const metacritic = {
  1483. mapped: function metacriticMapped (docurl, metaurl, type) {
  1484. // url was in the map/whitelist
  1485. current.data = []
  1486. current.docurl = docurl
  1487. current.metaurl = metaurl
  1488. current.type = type
  1489. current.searchTerm = null
  1490. loadMetacriticUrl()
  1491. },
  1492. music: function metacriticMusic (docurl, artistname, albumname) {
  1493. current.data = [albumname.trim(), artistname.trim()]
  1494. artistname = name2metacritic(artistname)
  1495. albumname = albumname.replace('&', ' ')
  1496. albumname = name2metacritic(albumname)
  1497. current.docurl = docurl
  1498. current.metaurl = baseURLmusic + albumname + '/' + artistname
  1499. current.type = 'music'
  1500. current.searchTerm = albumname + '/' + artistname
  1501. loadMetacriticUrl()
  1502. },
  1503. movie: function metacriticMovie (docurl, moviename) {
  1504. current.data = [moviename.trim()]
  1505. moviename = name2metacritic(moviename)
  1506. current.docurl = docurl
  1507. current.metaurl = baseURLmovie + moviename
  1508. current.type = 'movie'
  1509. current.searchTerm = moviename
  1510. loadMetacriticUrl()
  1511. },
  1512. tv: function metacriticTv (docurl, seriesname) {
  1513. current.data = [seriesname.trim()]
  1514. seriesname = name2metacritic(seriesname)
  1515. current.docurl = docurl
  1516. current.metaurl = baseURLtv + seriesname
  1517. current.type = 'tv'
  1518. current.searchTerm = seriesname
  1519. loadMetacriticUrl()
  1520. },
  1521. pcgame: function metacriticPcgame (docurl, gamename) {
  1522. current.data = [gamename.trim()]
  1523. gamename = name2metacritic(gamename)
  1524. current.docurl = docurl
  1525. current.metaurl = baseURLpcgame + gamename
  1526. current.type = 'pcgame'
  1527. current.searchTerm = gamename
  1528. loadMetacriticUrl()
  1529. },
  1530. ps4game: function metacriticPs4game (docurl, gamename) {
  1531. current.data = [gamename.trim()]
  1532. gamename = name2metacritic(gamename)
  1533. current.docurl = docurl
  1534. current.metaurl = baseURLps4 + gamename
  1535. current.type = 'ps4game'
  1536. current.searchTerm = gamename
  1537. loadMetacriticUrl()
  1538. },
  1539. xonegame: function metacriticXonegame (docurl, gamename) {
  1540. current.data = [gamename.trim()]
  1541. gamename = name2metacritic(gamename)
  1542. current.docurl = docurl
  1543. current.metaurl = baseURLxone + gamename
  1544. current.type = 'xonegame'
  1545. current.searchTerm = gamename
  1546. loadMetacriticUrl()
  1547. }
  1548. }
  1549.  
  1550. const Always = () => true
  1551. const sites = {
  1552. bandcamp: {
  1553. host: ['bandcamp.com'],
  1554. condition: () => unsafeWindow && unsafeWindow.TralbumData && unsafeWindow.TralbumData.current,
  1555. products: [{
  1556. condition: Always,
  1557. type: 'music',
  1558. data: () => [unsafeWindow.TralbumData.artist, unsafeWindow.TralbumData.current.title]
  1559. }]
  1560. },
  1561. itunes: {
  1562. host: ['itunes.apple.com'],
  1563. condition: Always,
  1564. products: [{
  1565. condition: () => ~document.location.href.indexOf('/movie/'),
  1566. type: 'movie',
  1567. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  1568. },
  1569. {
  1570. condition: () => ~document.location.href.indexOf('/tv-season/'),
  1571. type: 'tv',
  1572. data: function () {
  1573. let name = parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  1574. if (~name.indexOf(', Season')) {
  1575. name = name.split(', Season')[0]
  1576. }
  1577. return name
  1578. }
  1579. },
  1580. {
  1581. condition: () => ~document.location.href.indexOf('/album/'),
  1582. type: 'music',
  1583. data: function () {
  1584. const ld = parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum'))
  1585. const album = ld[0]
  1586. const artist = ld[1].name
  1587. return [artist, album]
  1588. }
  1589. }]
  1590. },
  1591. 'music.apple': {
  1592. host: ['music.apple.com'],
  1593. condition: Always,
  1594. products: [{
  1595. condition: () => ~document.location.href.indexOf('/album/'),
  1596. type: 'music',
  1597. data: function () {
  1598. const ld = parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum'))
  1599. const album = ld[0]
  1600. const artist = ld[1].name
  1601. return [artist, album]
  1602. }
  1603. }]
  1604. },
  1605. googleplay: {
  1606. host: ['play.google.com'],
  1607. condition: Always,
  1608. products: [
  1609. {
  1610. condition: () => ~document.location.href.indexOf('/album/'),
  1611. type: 'music',
  1612. data: () => [document.querySelector('[itemprop="byArtist"] meta[itemprop="name"]').content, document.querySelector('[itemtype="https://schema.org/MusicAlbum"] meta[itemprop="name"]').content]
  1613. },
  1614. {
  1615. condition: () => ~document.location.href.indexOf('/movies/details/'),
  1616. type: 'movie',
  1617. data: () => document.querySelector('*[itemprop=name]').textContent
  1618. }
  1619. ]
  1620. },
  1621. imdb: {
  1622. host: ['imdb.com'],
  1623. condition: () => !~document.location.pathname.indexOf('/mediaviewer') && !~document.location.pathname.indexOf('/mediaindex') && !~document.location.pathname.indexOf('/videoplayer'),
  1624. products: [
  1625. {
  1626. condition: function () {
  1627. const e = document.querySelector("meta[property='og:type']")
  1628. if (e) {
  1629. return e.content === 'video.movie'
  1630. }
  1631. return false
  1632. },
  1633. type: 'movie',
  1634. data: function () {
  1635. if (document.querySelector("meta[property='og:title']") && document.querySelector("meta[property='og:title']").content) { // English/Worldwide title, this is the prefered title for search
  1636. let name = document.querySelector("meta[property='og:title']").content.trim()
  1637. if (name.indexOf('- IMDb') !== -1) {
  1638. name = name.replace('- IMDb', '').trim()
  1639. }
  1640. name = name.replace(/\(\d{4}\)/, '').trim()
  1641. return name
  1642. } else if (document.querySelector('.originalTitle') && document.querySelector('.title_wrapper h1')) { // Use English title 2018
  1643. return document.querySelector('.title_wrapper h1').firstChild.data.trim()
  1644. } else if (document.querySelector('script[type="application/ld+json"]')) { // Use original language title
  1645. return parseLDJSON('name')
  1646. } else if (document.querySelector('h1[itemprop=name]')) { // Movie homepage (New design 2015-12)
  1647. return document.querySelector('h1[itemprop=name]').firstChild.textContent.trim()
  1648. } else if (document.querySelector('*[itemprop=name] a') && document.querySelector('*[itemprop=name] a').firstChild.data) { // Subpage of a move
  1649. return document.querySelector('*[itemprop=name] a').firstChild.data.trim()
  1650. } else if (document.querySelector('.title-extra[itemprop=name]')) { // Movie homepage: sub-/alternative-/original title
  1651. return document.querySelector('.title-extra[itemprop=name]').firstChild.textContent.replace(/"/g, '').trim()
  1652. } else { // Movie homepage (old design)
  1653. return document.querySelector('*[itemprop=name]').firstChild.textContent.trim()
  1654. }
  1655. }
  1656. },
  1657. {
  1658. condition: function () {
  1659. const e = document.querySelector("meta[property='og:type']")
  1660. if (e) {
  1661. return e.content === 'video.tv_show'
  1662. }
  1663. return false
  1664. },
  1665. type: 'tv',
  1666. data: function () {
  1667. if (document.querySelector('*[itemprop=name]')) {
  1668. return document.querySelector('*[itemprop=name]').textContent
  1669. } else {
  1670. const jsonld = JSON.parse(document.querySelector('script[type="application/ld+json"]').innerText)
  1671. return jsonld.name
  1672. }
  1673. }
  1674. }
  1675. ]
  1676. },
  1677. steam: {
  1678. host: ['store.steampowered.com'],
  1679. condition: () => document.querySelector('*[itemprop=name]'),
  1680. products: [{
  1681. condition: Always,
  1682. type: 'pcgame',
  1683. data: () => document.querySelector('*[itemprop=name]').textContent
  1684. }]
  1685. },
  1686. 'tv.com': {
  1687. host: ['www.tv.com'],
  1688. condition: () => document.querySelector("meta[property='og:type']"),
  1689. products: [{
  1690. condition: () => document.querySelector("meta[property='og:type']").content === 'tv_show' && document.querySelector('h1[data-name]'),
  1691. type: 'tv',
  1692. data: () => document.querySelector('h1[data-name]').dataset.name
  1693. }]
  1694. },
  1695. rottentomatoes: {
  1696. host: ['rottentomatoes.com'],
  1697. condition: Always,
  1698. products: [{
  1699. condition: () => document.location.pathname.startsWith('/m/'),
  1700. type: 'movie',
  1701. data: () => document.querySelector('h1').firstChild.textContent
  1702. },
  1703. {
  1704. condition: () => document.location.pathname.startsWith('/tv/'),
  1705. type: 'tv',
  1706. data: () => unsafeWindow.BK.TvSeriesTitle
  1707. }
  1708. ]
  1709. },
  1710. serienjunkies: {
  1711. host: ['www.serienjunkies.de'],
  1712. condition: Always,
  1713. products: [{
  1714. condition: () => Always,
  1715. type: 'tv',
  1716. data: () => parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  1717. }]
  1718. },
  1719. gamespot: {
  1720. host: ['gamespot.com'],
  1721. condition: () => document.querySelector('[itemprop=device]'),
  1722. products: [
  1723. {
  1724. condition: () => ~$('[itemprop=device]').text().indexOf('PC'),
  1725. type: 'pcgame',
  1726. data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
  1727. },
  1728. {
  1729. condition: () => ~$('[itemprop=device]').text().indexOf('PS4'),
  1730. type: 'ps4game',
  1731. data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
  1732. },
  1733. {
  1734. condition: () => ~$('[itemprop=device]').text().indexOf('XONE'),
  1735. type: 'xonegame',
  1736. data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
  1737. }
  1738. ]
  1739. },
  1740. amazon: {
  1741. host: ['amazon.'],
  1742. condition: Always,
  1743. products: [
  1744. {
  1745. condition: () => document.location.hostname === 'music.amazon.com' && document.location.pathname.startsWith('/albums/') && document.querySelector('.viewTitle'), // "Amazon Music Unlimited" page
  1746. type: 'music',
  1747. data: function () {
  1748. const artist = document.querySelector('.artistLink').textContent.trim()
  1749. let title = document.querySelector('.viewTitle').textContent.trim()
  1750. title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
  1751. if (artist && title) {
  1752. return [artist, title]
  1753. }
  1754. return false
  1755. }
  1756. },
  1757. {
  1758. condition: function () { // "Normal amazon" page
  1759. try {
  1760. if (document.querySelector('.nav-categ-image').alt.toLowerCase().indexOf('musi') !== -1) {
  1761. return true
  1762. }
  1763. } catch (e) {}
  1764. const music = ['Music', 'Musique', 'Musik', 'Música', 'Musica', '音楽']
  1765. return music.some(function (s) {
  1766. if (~document.title.indexOf(s)) {
  1767. return true
  1768. } else {
  1769. return false
  1770. }
  1771. })
  1772. },
  1773. type: 'music',
  1774. data: function () {
  1775. let artist = false
  1776. let title = false
  1777. if (document.querySelector('#ProductInfoArtistLink')) {
  1778. artist = document.querySelector('#ProductInfoArtistLink').textContent.trim()
  1779. } else if (document.querySelector('#bylineInfo .author>*')) {
  1780. artist = document.querySelector('#bylineInfo .author>*').textContent.trim()
  1781. }
  1782.  
  1783. if (document.querySelector('#dmusicProductTitle_feature_div')) {
  1784. title = document.querySelector('#dmusicProductTitle_feature_div').textContent.trim()
  1785. title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
  1786. } else if (document.querySelector('#productTitle')) {
  1787. title = document.querySelector('#productTitle').textContent.trim()
  1788. title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
  1789. }
  1790. return [artist, title]
  1791. }
  1792. },
  1793. {
  1794. condition: () => (document.querySelector('[data-automation-id=title]') && (document.getElementsByClassName('av-season-single').length || document.querySelector('[data-automation-id="num-of-seasons-badge"]'))),
  1795. type: 'tv',
  1796. data: () => document.querySelector('[data-automation-id=title]').textContent.trim()
  1797. },
  1798. {
  1799. condition: () => document.querySelector('[data-automation-id=title]'),
  1800. type: 'movie',
  1801. data: () => document.querySelector('[data-automation-id=title]').textContent.trim()
  1802. }
  1803. ]
  1804. },
  1805. BoxOfficeMojo: {
  1806. host: ['boxofficemojo.com'],
  1807. condition: () => Always,
  1808. products: [
  1809. {
  1810. condition: () => document.location.pathname.startsWith('/release/'),
  1811. type: 'movie',
  1812. data: () => document.querySelector('meta[name=title]').content
  1813. },
  1814. {
  1815. // Old page design
  1816. condition: () => ~document.location.search.indexOf('id=') && document.querySelector('#body table:nth-child(2) tr:first-child b'),
  1817. type: 'movie',
  1818. data: () => document.querySelector('#body table:nth-child(2) tr:first-child b').firstChild.data
  1819. }]
  1820. },
  1821. AllMovie: {
  1822. host: ['allmovie.com'],
  1823. condition: () => document.querySelector('h2[itemprop=name].movie-title'),
  1824. products: [{
  1825. condition: () => document.querySelector('h2[itemprop=name].movie-title'),
  1826. type: 'movie',
  1827. data: () => document.querySelector('h2[itemprop=name].movie-title').firstChild.data.trim()
  1828. }]
  1829. },
  1830. 'en.wikipedia': {
  1831. host: ['en.wikipedia.org'],
  1832. condition: Always,
  1833. products: [{
  1834. condition: function () {
  1835. if (!document.querySelector('.infobox .summary')) {
  1836. return false
  1837. }
  1838. const r = /\d\d\d\d films/
  1839. return $('#catlinks a').filter((i, e) => e.firstChild.data.match(r)).length
  1840. },
  1841. type: 'movie',
  1842. data: () => document.querySelector('.infobox .summary').firstChild.data
  1843. },
  1844. {
  1845. condition: function () {
  1846. if (!document.querySelector('.infobox .summary')) {
  1847. return false
  1848. }
  1849. const r = /television series/
  1850. return $('#catlinks a').filter((i, e) => e.firstChild.data.match(r)).length
  1851. },
  1852. type: 'tv',
  1853. data: () => document.querySelector('.infobox .summary').firstChild.data
  1854. }]
  1855. },
  1856. 'movies.com': {
  1857. host: ['movies.com'],
  1858. condition: () => document.querySelector("meta[property='og:title']"),
  1859. products: [{
  1860. condition: Always,
  1861. type: 'movie',
  1862. data: () => document.querySelector("meta[property='og:title']").content
  1863. }]
  1864. },
  1865. themoviedb: {
  1866. host: ['themoviedb.org'],
  1867. condition: () => document.querySelector("meta[property='og:type']"),
  1868. products: [{
  1869. condition: () => document.querySelector("meta[property='og:type']").content === 'movie',
  1870. type: 'movie',
  1871. data: () => document.querySelector("meta[property='og:title']").content
  1872. },
  1873. {
  1874. condition: () => document.querySelector("meta[property='og:type']").content === 'tv' || document.querySelector("meta[property='og:type']").content === 'tv_series',
  1875. type: 'tv',
  1876. data: () => document.querySelector("meta[property='og:title']").content
  1877. }]
  1878. },
  1879. letterboxd: {
  1880. host: ['letterboxd.com'],
  1881. condition: () => unsafeWindow.filmData && 'name' in unsafeWindow.filmData,
  1882. products: [{
  1883. condition: Always,
  1884. type: 'movie',
  1885. data: () => unsafeWindow.filmData.name
  1886. }]
  1887. },
  1888. TVmaze: {
  1889. host: ['tvmaze.com'],
  1890. condition: () => document.querySelector('h1'),
  1891. products: [{
  1892. condition: Always,
  1893. type: 'tv',
  1894. data: () => document.querySelector('h1').firstChild.data
  1895. }]
  1896. },
  1897. TVGuide: {
  1898. host: ['tvguide.com'],
  1899. condition: Always,
  1900. products: [{
  1901. condition: () => document.location.pathname.startsWith('/tvshows/'),
  1902. type: 'tv',
  1903. data: function () {
  1904. if (document.querySelector('meta[itemprop=name]')) {
  1905. return document.querySelector('meta[itemprop=name]').content
  1906. } else {
  1907. return document.querySelector("meta[property='og:title']").content.split('|')[0]
  1908. }
  1909. }
  1910. }]
  1911. },
  1912. followshows: {
  1913. host: ['followshows.com'],
  1914. condition: Always,
  1915. products: [{
  1916. condition: () => document.querySelector("meta[property='og:type']").content === 'video.tv_show',
  1917. type: 'tv',
  1918. data: () => document.querySelector("meta[property='og:title']").content
  1919. }]
  1920. },
  1921. TheTVDB: {
  1922. host: ['thetvdb.com'],
  1923. condition: Always,
  1924. products: [{
  1925. condition: () => document.location.pathname.startsWith('/series/') || ~document.location.search.indexOf('tab=series'),
  1926. type: 'tv',
  1927. data: () => document.getElementById('series_title').firstChild.data.trim()
  1928. }]
  1929. },
  1930. ConsequenceOfSound: {
  1931. host: ['consequenceofsound.net'],
  1932. condition: () => document.querySelector('#main-content .review-summary'),
  1933. products: [{
  1934. condition: () => document.title.match(/(.+?)\s+\u2013\s+(.+?) \| Album Review/),
  1935. type: 'music',
  1936. data: function () {
  1937. const m = document.title.match(/(.+?)\s+\u2013\s+(.+?) \| Album Review/)
  1938. return [m[1], m[2]]
  1939. }
  1940. }]
  1941. },
  1942. Pitchfork: {
  1943. host: ['pitchfork.com'],
  1944. condition: () => ~document.location.href.indexOf('/reviews/albums/'),
  1945. products: [{
  1946. condition: () => document.querySelector('.single-album-tombstone'),
  1947. type: 'music',
  1948. data: function () {
  1949. let artist
  1950. let album
  1951. if (document.querySelector('.single-album-tombstone .artists')) {
  1952. artist = document.querySelector('.single-album-tombstone .artists').innerText.trim()
  1953. } else if (document.querySelector('.single-album-tombstone .artist-list')) {
  1954. artist = document.querySelector('.single-album-tombstone .artist-list').innerText.trim()
  1955. }
  1956. if (document.querySelector('.single-album-tombstone h1.review-title')) {
  1957. album = document.querySelector('.single-album-tombstone h1.review-title').innerText.trim()
  1958. } else if (document.querySelector('.single-album-tombstone h1')) {
  1959. album = document.querySelector('.single-album-tombstone h1').innerText.trim()
  1960. }
  1961.  
  1962. return [artist, album]
  1963. }
  1964. }]
  1965. },
  1966. 'Last.fm': {
  1967. host: ['last.fm'],
  1968. condition: () => document.querySelector('*[data-page-resource-type]') && document.querySelector('*[data-page-resource-type]').dataset.pageResourceType === 'album',
  1969. products: [{
  1970. condition: () => document.querySelector('*[data-page-resource-type]').dataset.pageResourceName,
  1971. type: 'music',
  1972. data: function () {
  1973. const artist = document.querySelector('*[data-page-resource-type]').dataset.pageResourceArtistName
  1974. const album = document.querySelector('*[data-page-resource-type]').dataset.pageResourceName
  1975. return [artist, album]
  1976. }
  1977. }]
  1978. },
  1979. TVNfo: {
  1980. host: ['tvnfo.com'],
  1981. condition: () => document.querySelector('#tvsign'),
  1982. products: [{
  1983. condition: Always,
  1984. type: 'tv',
  1985. data: () => document.querySelector('.heading h1').textContent.trim()
  1986. }]
  1987. },
  1988. rateyourmusic: {
  1989. host: ['rateyourmusic.com'],
  1990. condition: () => document.querySelector("meta[property='og:type']"),
  1991. products: [{
  1992. condition: () => document.querySelector("meta[property='og:type']").content === 'music.album',
  1993. type: 'music',
  1994. data: function () {
  1995. const artist = document.querySelector('.section_main_info .artist').innerText.trim()
  1996. const album = document.querySelector('.section_main_info .album_title').innerText.trim()
  1997. return [artist, album]
  1998. }
  1999. }]
  2000. },
  2001. spotify_webplayer: {
  2002. host: ['open.spotify.com'],
  2003. condition: Always,
  2004. products: [{
  2005. condition: () => document.querySelector('#main .main-view-container .content.album'),
  2006. type: 'music',
  2007. data: function () {
  2008. const artist = document.querySelector("#main .media-bd div a[href*='artist']").textContent
  2009. const album = document.querySelector('#main .media-bd h2').textContent
  2010. return [artist, album]
  2011. }
  2012. },
  2013. {
  2014. condition: () => document.location.pathname.startsWith('/album/') && document.querySelector("meta[property='og:type']").content === 'music.album',
  2015. type: 'music',
  2016. data: function () {
  2017. const artist = ''
  2018. const album = document.querySelector("meta[property='og:title']").content
  2019. return [artist, album]
  2020. }
  2021. }]
  2022. },
  2023. spotify: {
  2024. host: ['play.spotify.com'],
  2025. condition: Always,
  2026. products: [{
  2027. condition: () => document.location.pathname.startsWith('/album/'),
  2028. type: 'music',
  2029. data: function () {
  2030. const artist = document.querySelector('.context_landing p.secondary-title').textContent
  2031. const album = document.querySelector('.context_landing p.primary-title').textContent
  2032. return [artist, album]
  2033. }
  2034. }]
  2035. },
  2036. nme: {
  2037. host: ['nme.com'],
  2038. condition: () => document.location.pathname.startsWith('/reviews/'),
  2039. products: [
  2040. {
  2041. condition: () => document.location.pathname.startsWith('/reviews/movie/'),
  2042. type: 'movie',
  2043. data: function () {
  2044. try {
  2045. return document.querySelector('.title-primary').textContent.match(/‘(.+?)’/)[1]
  2046. } catch (e) {
  2047. return document.querySelector('h1').textContent.match(/:\s*(.+)/)[1].trim()
  2048. }
  2049. }
  2050. },
  2051. {
  2052. condition: () => document.location.pathname.startsWith('/reviews/album/'),
  2053. type: 'music',
  2054. data: () => document.querySelector('.title-primary').textContent.match(/\s*(.+?)\s*.\s*‘(.+?)’/).slice(1)
  2055. }]
  2056. },
  2057. albumoftheyear: {
  2058. host: ['albumoftheyear.org'],
  2059. condition: Always,
  2060. products: [{
  2061. condition: () => document.location.pathname.startsWith('/album/'),
  2062. type: 'music',
  2063. data: function () {
  2064. const artist = document.querySelector('*[itemprop=byArtist] *[itemprop=name]').textContent
  2065. const album = document.querySelector('.albumTitle *[itemprop=name]').textContent
  2066. return [artist, album]
  2067. }
  2068. }]
  2069. },
  2070. epguides: {
  2071. host: ['epguides.com'],
  2072. condition: () => document.getElementById('TVHeader'),
  2073. products: [{
  2074. condition: () => document.getElementById('TVHeader') && document.querySelector('body>div#header h1'),
  2075. type: 'tv',
  2076. data: () => document.querySelector('body>div#header h1').textContent.trim()
  2077. }]
  2078. },
  2079. ShareTV: {
  2080. host: ['sharetv.com'],
  2081. condition: () => document.location.pathname.startsWith('/shows/'),
  2082. products: [{
  2083. condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:title']"),
  2084. type: 'tv',
  2085. data: () => document.querySelector("meta[property='og:title']").content
  2086. }]
  2087. },
  2088. /*
  2089. netflix: {
  2090. host: ['netflix.com'],
  2091. condition: !(document.querySelector('.button-nfplayerPlay') || document.querySelector('.nf-big-play-pause') || document.querySelector('.AkiraPlayer video')),
  2092.  
  2093. // TODO
  2094. // https://www.netflix.com/de/title/70264888
  2095. // https://www.netflix.com/de/title/70178217
  2096. // https://www.netflix.com/de/title/70305892 ## Movie
  2097. // https://www.netflix.com/de-en/title/80108495 ## No meta
  2098.  
  2099. products: [{
  2100. condition: () => parseLDJSON('@type') === 'Movie',
  2101. type: 'movie',
  2102. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  2103. },
  2104. {
  2105. condition: () => parseLDJSON('@type') === 'TVSeries',
  2106. type: 'tv',
  2107. data: () => parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  2108. }]
  2109. },
  2110. */
  2111. ComedyCentral: {
  2112. host: ['cc.com'],
  2113. condition: () => document.location.pathname.startsWith('/shows/'),
  2114. products: [{
  2115. condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:title']"),
  2116. type: 'tv',
  2117. data: () => document.querySelector("meta[property='og:title']").content
  2118. }]
  2119. },
  2120. TVHoard: {
  2121. host: ['tvhoard.com'],
  2122. condition: Always,
  2123. products: [{
  2124. condition: () => document.location.pathname.split('/').length === 3 && document.location.pathname.split('/')[1] === 'titles' && !document.querySelector('app-root title-secondary-details-panel .seasons') && document.querySelector('app-root title-page-container h1.title a'),
  2125. type: 'movie',
  2126. data: () => document.querySelector('app-root title-page-container h1.title a').textContent.trim()
  2127. },
  2128. {
  2129. condition: () => document.location.pathname.split('/').length === 3 && document.location.pathname.split('/')[1] === 'titles' && document.querySelector('app-root title-secondary-details-panel .seasons') && document.querySelector('app-root title-page-container h1.title a'),
  2130. type: 'tv',
  2131. data: () => document.querySelector('app-root title-page-container h1.title a').textContent.trim()
  2132. }]
  2133. },
  2134. AMC: {
  2135. host: ['amc.com'],
  2136. condition: () => document.location.pathname.startsWith('/shows/'),
  2137. products: [
  2138. {
  2139. condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:type']") && document.querySelector("meta[property='og:type']").content === 'tv_show',
  2140. type: 'tv',
  2141. data: () => document.querySelector("meta[property='og:title']").content
  2142. }]
  2143. }
  2144.  
  2145. }
  2146.  
  2147. async function main () {
  2148. let dataFound = false
  2149.  
  2150. let map = false
  2151.  
  2152. for (const name in sites) {
  2153. const site = sites[name]
  2154. if (site.host.some(function (e) { return ~this.indexOf(e) }, document.location.hostname) && site.condition()) {
  2155. for (let i = 0; i < site.products.length; i++) {
  2156. if (site.products[i].condition()) {
  2157. // Check map for a match
  2158. if (map === false) {
  2159. map = JSON.parse(await GM.getValue('map', '{}'))
  2160. }
  2161. const docurl = filterUniversalUrl(document.location.href)
  2162. if (docurl in map) {
  2163. // Found in map, show result
  2164. const metaurl = map[docurl]
  2165. metacritic.mapped.apply(undefined, [docurl, absoluteMetaURL(metaurl), site.products[i].type])
  2166. break
  2167. }
  2168. // Try to retrieve item name from page
  2169. let data
  2170. try {
  2171. data = site.products[i].data()
  2172. } catch (e) {
  2173. data = false
  2174. console.log('ShowMetacriticRatings: ' + e)
  2175. }
  2176. if (data) {
  2177. const params = [docurl]
  2178. if (Array.isArray(data)) {
  2179. params.push(...data)
  2180. } else {
  2181. params.push(data)
  2182. }
  2183. metacritic[site.products[i].type].apply(undefined, params)
  2184. dataFound = true
  2185. }
  2186. break
  2187. }
  2188. }
  2189. break
  2190. }
  2191. }
  2192. return dataFound
  2193. }
  2194.  
  2195. (async function () {
  2196. await versionUpdate()
  2197. const firstRunResult = await main()
  2198. let lastLoc = document.location.href
  2199. const lastContent = document.body.innerText
  2200. let lastCounter = 0
  2201. async function newpage () {
  2202. if (lastContent === document.body.innerText && lastCounter < 15) {
  2203. window.setTimeout(newpage, 500)
  2204. lastCounter++
  2205. } else {
  2206. lastCounter = 0
  2207. const re = await main()
  2208. if (!re) { // No page matched or no data found
  2209. window.setTimeout(newpage, 1000)
  2210. }
  2211. }
  2212. }
  2213. window.setInterval(function () {
  2214. if (document.location.href !== lastLoc) {
  2215. lastLoc = document.location.href
  2216. $('#mcdiv123').remove()
  2217.  
  2218. window.setTimeout(newpage, 1000)
  2219. }
  2220. }, 500)
  2221.  
  2222. if (!firstRunResult) {
  2223. // Initial run had no match, let's try again there may be new content
  2224. window.setTimeout(main, 2000)
  2225. }
  2226. })()