Greasy Fork 还支持 简体中文。

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

目前為 2019-08-25 提交的版本,檢視 最新版本

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