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), Steam, Gamespot (PS4, XONE, PC), Rotten Tomatoes, Serienjunkies, BoxOfficeMojo, allmovie.com, fandango.com, Wikipedia (en), themoviedb.org, letterboxd, TVmaze, TVGuide, followshows.com, TheTVDB.com, ConsequenceOfSound, Pitchfork, Last.fm, TVnfo, rateyourmusic.com, GOG, Epic Games Store, save.tv

Install this script?
Author's suggested script

You may also like Show Rottentomatoes meter.

Install this script
  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), Steam, Gamespot (PS4, XONE, PC), Rotten Tomatoes, Serienjunkies, BoxOfficeMojo, allmovie.com, fandango.com, Wikipedia (en), themoviedb.org, letterboxd, TVmaze, TVGuide, followshows.com, TheTVDB.com, ConsequenceOfSound, Pitchfork, Last.fm, TVnfo, rateyourmusic.com, GOG, Epic Games Store, save.tv
  4. // @namespace cuzi
  5. // @icon https://www.metacritic.com/a/img/favicon.svg
  6. // @supportURL https://github.com/cvzi/Metacritic-userscript/issues
  7. // @contributionURL https://buymeacoff.ee/cuzi
  8. // @contributionURL https://ko-fi.com/cuzicvzi
  9. // @grant unsafeWindow
  10. // @grant GM.xmlHttpRequest
  11. // @grant GM.setValue
  12. // @grant GM.getValue
  13. // @grant GM.registerMenuCommand
  14. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js
  15. // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
  16. // @antifeature tracking When a metacritic rating is displayed, we may store the url of the current website and the metacritic url in our database. Log files are temporarily retained by our database hoster Cloudflare Workers® and contain your IP address and browser configuration.
  17. // @version 106
  18. // @connect metacritic.com
  19. // @connect backend.metacritic.com
  20. // @connect met.acritic.workers.dev
  21. // @connect imdb.com
  22. // @match https://*.bandcamp.com/*
  23. // @match https://play.google.com/store/music/album/*
  24. // @match https://play.google.com/store/movies/details/*
  25. // @match https://music.amazon.com/*
  26. // @match https://www.amazon.ca/*
  27. // @match https://www.amazon.co.jp/*
  28. // @match https://www.amazon.co.uk/*
  29. // @match https://smile.amazon.co.uk/*
  30. // @match https://www.amazon.com.au/*
  31. // @match https://www.amazon.com.mx/*
  32. // @match https://www.amazon.com/*
  33. // @match https://smile.amazon.com/*
  34. // @match https://www.amazon.de/*
  35. // @match https://smile.amazon.de/*
  36. // @match https://www.amazon.es/*
  37. // @match https://www.amazon.fr/*
  38. // @match https://www.amazon.in/*
  39. // @match https://www.amazon.it/*
  40. // @match https://www.imdb.com/title/*
  41. // @match https://store.steampowered.com/app/*
  42. // @match https://www.gamespot.com/*
  43. // @match http://www.serienjunkies.de/*
  44. // @match https://www.serienjunkies.de/*
  45. // @match https://www.rottentomatoes.com/m/*
  46. // @match https://rottentomatoes.com/m/*
  47. // @match https://www.rottentomatoes.com/tv/*
  48. // @match https://rottentomatoes.com/tv/*
  49. // @match https://www.rottentomatoes.com/tv/*
  50. // @match https://rottentomatoes.com/tv/*
  51. // @match https://www.boxofficemojo.com/movies/*
  52. // @match https://www.boxofficemojo.com/release/*
  53. // @match https://www.allmovie.com/movie/*
  54. // @match https://en.wikipedia.org/*
  55. // @match https://www.fandango.com/*
  56. // @match https://flixster.com/movie/*
  57. // @match https://www.themoviedb.org/movie/*
  58. // @match https://www.themoviedb.org/tv/*
  59. // @match https://letterboxd.com/film/*
  60. // @match https://www.tvmaze.com/shows/*
  61. // @match https://www.tvguide.com/tvshows/*
  62. // @match https://followshows.com/show/*
  63. // @match https://thetvdb.com/series/*
  64. // @match https://thetvdb.com/movies/*
  65. // @match https://consequenceofsound.net/*
  66. // @match https://consequence.net/*
  67. // @match https://pitchfork.com/*
  68. // @match https://www.last.fm/*
  69. // @match https://tvnfo.com/tv/*
  70. // @match https://rateyourmusic.com/release/album/*
  71. // @match https://open.spotify.com/*
  72. // @match https://play.spotify.com/album/*
  73. // @match https://www.nme.com/reviews/*
  74. // @match https://www.albumoftheyear.org/album/*
  75. // @match https://itunes.apple.com/*
  76. // @match https://music.apple.com/*
  77. // @match https://epguides.com/*
  78. // @match https://www.epguides.com/*
  79. // @match https://www.netflix.com/*
  80. // @match https://www.cc.com/*
  81. // @match https://www.amc.com/*
  82. // @match https://www.amcplus.com/*
  83. // @match https://rlsbb.ru/*/
  84. // @match https://newalbumreleases.net/*
  85. // @match https://www.sho.com/*
  86. // @match https://www.epicgames.com/store/*
  87. // @match https://store.epicgames.com/*
  88. // @match https://www.gog.com/*
  89. // @match https://www.allmusic.com/album/*
  90. // @match https://www.steamgifts.com/giveaway/*
  91. // @match https://psa.wf/*
  92. // @match https://www.save.tv/*
  93. // @match https://www.wikiwand.com/*
  94. // @match https://trakt.tv/*
  95. // @match http://localhost:7878/*
  96. // ==/UserScript==
  97.  
  98. /* globals alert, confirm, GM, DOMParser, $, Image, unsafeWindow, parent, Blob, failedImages */
  99. /* jshint asi: true, esversion: 8 */
  100.  
  101. const scriptName = 'Show Metacritic.com ratings'
  102.  
  103. const baseURL = 'https://www.metacritic.com/'
  104.  
  105. const baseURLmusic = 'https://www.metacritic.com/music/'
  106. const baseURLmovie = 'https://www.metacritic.com/movie/'
  107. const baseURLpcgame = 'https://www.metacritic.com/game/'
  108. const baseURLps4 = 'https://www.metacritic.com/game/'
  109. const baseURLxone = 'https://www.metacritic.com/game/'
  110. const baseURLtv = 'https://www.metacritic.com/tv/'
  111.  
  112. const baseURLsearch = 'https://backend.metacritic.com/finder/metacritic/search/{query}/web?apiKey={apiKey}&componentName=search-tabs&componentDisplayName=Search+Page+Tab+Filters&componentType=FilterConfig&mcoTypeId={type}&offset=0&limit=30'
  113.  
  114. const baseURLdatabase = 'https://met.acritic.workers.dev/r.php'
  115. const baseURLwhitelist = 'https://met.acritic.workers.dev/whitelist.php'
  116. const baseURLblacklist = 'https://met.acritic.workers.dev/blacklist.php'
  117.  
  118. const TEMPORARY_BLACKLIST_TIMEOUT = 5 * 60
  119.  
  120. const windowPositions = [
  121. {
  122. bottom: 0,
  123. left: 0
  124. },
  125. {
  126. bottom: 0,
  127. right: 0
  128. },
  129. {
  130. top: 0,
  131. right: 0
  132. },
  133. {
  134. top: 0,
  135. left: 0
  136. }
  137. ]
  138.  
  139. // Detect dark theme of darkreader.org extension
  140. const darkTheme = 'darkreaderScheme' in document.documentElement.dataset && document.documentElement.dataset.darkreaderScheme
  141.  
  142. let myDOMParser = null
  143. function domParser () {
  144. if (myDOMParser === null) {
  145. myDOMParser = new DOMParser()
  146. }
  147. return myDOMParser
  148. }
  149.  
  150. async function versionUpdate () {
  151. const version = parseInt(await GM.getValue('version', 0))
  152. if (version <= 105) {
  153. // Reset database
  154. await GM.setValue('map', '{}')
  155. await GM.setValue('black', '[]')
  156. await GM.setValue('hovercache', '{}')
  157. await GM.setValue('requestcache', '{}')
  158. await GM.setValue('temporaryblack', '{}')
  159. await GM.setValue('searchcache', false) // Unused
  160. await GM.setValue('autosearchcache', false) // Unused
  161. }
  162. if (version < 106) {
  163. await GM.setValue('version', 106)
  164. }
  165. }
  166.  
  167. const BOX_CSS_DARK_THEME = `
  168. #mcdiv123 {
  169. position: fixed;
  170. background-color: #262626;
  171. border: 2px solid #313131;
  172. color: white;
  173. }
  174.  
  175. #mcisearchquery {
  176. background: #262626;
  177. color: white;
  178. }
  179.  
  180. #mcisearchbutton {
  181. background: rgb(56, 56, 56);
  182. color: white;
  183. border: 2px solid white;
  184. }
  185. #mcdiv123 .grespinner {
  186. border-left: 6px solid rgba(0,174,239,.15);
  187. border-right: 6px solid rgba(0,174,239,.15);
  188. border-bottom: 6px solid rgba(0,174,239,.15);
  189. border-top: 6px solid rgba(0,174,239,.8);
  190. }
  191.  
  192. #mcdiv123searchresults .result {
  193. border-top-color: #525252;
  194. }
  195.  
  196.  
  197. #mcdiv123searchresults .result .mcdiv123_score_badge {
  198. color: white;
  199. }
  200.  
  201.  
  202. #mcdiv123searchresults .result .mcdiv_release_date {
  203. color: silver
  204. }
  205.  
  206. .mcdiv123_image_placeholder {
  207. background: rgb(64, 64, 64);
  208. }
  209.  
  210. #mcdiv123searchresults .result a {
  211. color: #09f;
  212. }
  213.  
  214. #mcdiv123searchresults .mcdiv_desc {
  215. scrollbar-color: #003c09 #00ce7a;
  216. }
  217. #mcdiv123searchresults .mcdiv_desc::-webkit-scrollbar-thumb {
  218. background-color: #003c09;
  219. }
  220. `
  221.  
  222. const BOX_CSS = `
  223. #mcdiv123 {
  224. position: fixed;
  225. background-color: #fff;
  226. border: 2px solid #bbb;
  227. border-radius: 6px;
  228. box-shadow: 0 0 3px 3px rgba(100, 100, 100, 0.2);
  229. color: #000;
  230. min-width: 150;
  231. max-height: 80%;
  232. max-width: 640;
  233. overflow: auto;
  234. padding: 3px;
  235. z-index: 2147483601;
  236. }
  237.  
  238. #mcisearchquery {
  239. background: white;
  240. color: black;
  241. width: 450px;
  242. display: inline;
  243. }
  244.  
  245. #mcisearchbutton {
  246. background: silver;
  247. color: black;
  248. border: 2px solid black;
  249. padding: 3px;
  250. display: inline;
  251. margin: 0px 5px;
  252. cursor: pointer;
  253. }
  254.  
  255. /* http://www.designcouch.com/home/why/2013/05/23/dead-simple-pure-css-loading-spinner/ */
  256. #mcdiv123 .grespinner {
  257. display: inline-block;
  258. height: 20px;
  259. width: 20px;
  260. margin: 0 auto;
  261. position: relative;
  262. animation: rotation .6s infinite linear;
  263. border-left: 6px solid rgba(0,174,239,.15);
  264. border-right: 6px solid rgba(0,174,239,.15);
  265. border-bottom: 6px solid rgba(0,174,239,.15);
  266. border-top: 6px solid rgba(0,174,239,.8);
  267. border-radius: 100%
  268. }
  269.  
  270. @keyframes rotation {
  271. from {
  272. transform: rotate(0)
  273. }
  274.  
  275. to {
  276. transform: rotate(359deg)
  277. }
  278. }
  279.  
  280. #mcdiv123searchresults {
  281. font-size: 12px;
  282. max-width: 95%
  283. }
  284.  
  285. .mcdiv123_correct_entry {
  286. cursor: pointer;
  287. color: green;
  288. font-size: 25px;
  289. margin-top: 10px;
  290. }
  291. .mcdiv123_correct_entry:hover {
  292. color: #41fd41;
  293. }
  294.  
  295. .mcdiv123_incorrect {
  296. cursor: pointer;
  297. float: right;
  298. color: crimson;
  299. font-size: 11px;
  300. }
  301. .mcdiv123_incorrect {
  302. cursor: pointer;
  303. float: right;
  304. color: crimson;
  305. font-size: 15px;
  306. margin-right: 10px;
  307. }
  308. .mcdiv123_incorrect:hover {
  309. cursor: pointer;
  310. float: right;
  311. color: crimson;
  312. font-size: 15px;
  313. margin-right: 10px;
  314. border:2px solid white;
  315. }
  316. .mcdiv123_incorrect:hover {
  317. border-color: crimson;
  318. }
  319.  
  320. #mcdiv123searchresults .result {
  321. font: 12px arial,helvetica,serif;
  322. border-top-width: 1px;
  323. border-top-color: #ccc;
  324. border-top-style: solid;
  325. padding: 5px
  326. }
  327.  
  328. .mcdiv123_cover {
  329. max-width: 200px;
  330. max-height: 140px;
  331. }
  332.  
  333. #mcdiv123searchresults .result .mcdiv123_score_badge {
  334. display: inline-block;
  335. margin: 3px;
  336. font-weight: 600;
  337. border-radius: 6px;
  338. color: black;
  339. padding: 5px;
  340. }
  341.  
  342. #mcdiv123searchresults .result .floatleft {
  343. float: left;
  344. }
  345.  
  346. #mcdiv123searchresults .result .clearleft {
  347. clear: left;
  348. }
  349.  
  350. #mcdiv123searchresults .result .resultcontent {
  351. max-width: 360px;
  352. margin-left: 10px;
  353. }
  354.  
  355. #mcdiv123searchresults .result .mcdiv_release_date {
  356. color: silver
  357. }
  358.  
  359. .mcdiv123_image_placeholder {
  360. width: 82px;
  361. height: 82px;
  362. background: rgb(64, 64, 64);
  363. border-radius: 8px;
  364. }
  365.  
  366. #mcdiv123searchresults .result a {
  367. color: #09f;
  368. font-weight: 700;
  369. text-decoration: none
  370. }
  371.  
  372. #mcdiv123searchresults .mcdiv_desc {
  373. max-height:120px;
  374. overflow-y: auto;
  375. scrollbar-color: #d9d9d9 #eee;
  376. scrollbar-width: thin;
  377. }
  378.  
  379. @media (prefers-color-scheme: dark) {
  380. ${BOX_CSS_DARK_THEME}
  381. }
  382.  
  383. ${
  384. darkTheme ? BOX_CSS_DARK_THEME : ''
  385. }
  386. `
  387.  
  388. async function acceptGDPR (showDialog) {
  389. if (showDialog === true) {
  390. await GM.setValue('gdpr', null)
  391. return acceptGDPR()
  392. }
  393. return new Promise(function (resolve) {
  394. GM.getValue('gdpr', null).then(function (value) {
  395. if (value === true) {
  396. return resolve(true)
  397. }
  398. if (value === false) {
  399. return resolve(false)
  400. }
  401. const html = '<h1>Privacy Policy for &quot;Show Metacritic.com ratings&quot;</h1><h2>General Data Protection Regulation (GDPR)</h2><p>We are a Data Controller of your information.</p> <p>&quot;Show Metacritic.com ratings&quot; legal basis for collecting and using the personal information described in this Privacy Policy depends on the Personal Information we collect and the specific context in which we collect the information:</p><ul> <li>&quot;Show Metacritic.com ratings&quot; needs to perform a contract with you</li> <li>You have given &quot;Show Metacritic.com ratings&quot; permission to do so</li> <li>Processing your personal information is in &quot;Show Metacritic.com ratings&quot; legitimate interests</li> <li>&quot;Show Metacritic.com ratings&quot; needs to comply with the law</li></ul> <p>&quot;Show Metacritic.com ratings&quot; will retain your personal information only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use your information to the extent necessary to comply with our legal obligations, resolve disputes, and enforce our policies.</p> <p>If you are a resident of the European Economic Area (EEA), you have certain data protection rights. If you wish to be informed what Personal Information we hold about you and if you want it to be removed from our systems, please contact us. Our Privacy Policy was generated with the help of <a href="https://www.gdprprivacypolicy.net/">GDPR Privacy Policy Generator</a> and the <a href="https://www.app-privacy-policy.com">App Privacy Policy Generator</a>.</p><p>In certain circumstances, you have the following data protection rights:</p><ul> <li>The right to access, update or to delete the information we have on you.</li> <li>The right of rectification.</li> <li>The right to object.</li> <li>The right of restriction.</li> <li>The right to data portability</li> <li>The right to withdraw consent</li></ul><h2>Log Files</h2><p>&quot;Show Metacritic.com ratings&quot; follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services\' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users\' movement on the website, and gathering demographic information.</p><h2>Privacy Policies</h2><P>You may consult this list to find the Privacy Policy for each of the advertising partners of &quot;Show Metacritic.com ratings&quot;.</p><p>Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on &quot;Show Metacritic.com ratings&quot;, which are sent directly to users\' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.</p><p>Note that &quot;Show Metacritic.com ratings&quot; has no access to or control over these cookies that are used by third-party advertisers.</p><h2>Third Party Privacy Policies</h2><p>&quot;Show Metacritic.com ratings&quot;\'s Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options.List of these Privacy Policies and their links: <ul> <li>Cloudflare Workers®: <a href="https://www.cloudflare.com/privacypolicy/">https://www.cloudflare.com/privacypolicy/</a></li> <li>www.metacritic.com: <a href="https://privacy.cbs/">https://privacy.cbs/</a></li></ul></p><p>You can choose to disable cookies through your individual browser options.</p><h2>Children\'s Information</h2><p>Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, and/or monitor and guide their online activity.</p><p>&quot;Show Metacritic.com ratings&quot; does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.</p><h2>Online Privacy Policy Only</h2><p>Our Privacy Policy created at GDPRPrivacyPolicy.net) applies only to our online activities and is valid for users of our program with regards to the information that they shared and/or collect in &quot;Show Metacritic.com ratings&quot;. This policy is not applicable to any information collected offline or via channels other than this program. <a href="https://gdprprivacypolicy.net">Our GDPR Privacy Policy</a> was generated from the GDPR Privacy Policy Generator.</p><h2>Contact</h2><p>Contact us via github <a href="https://github.com/cvzi/Metacritic-userscript">https://github.com/cvzi/Metacritic-userscript</a> or email cuzi@openmail.cc</p><h2>Consent</h2><p>By using our program ("userscript"), you hereby consent to our Privacy Policy and agree to its terms.</p>'
  402. const div = document.body.appendChild(document.createElement('div'))
  403. div.innerHTML = html
  404. div.style = 'z-index:9999;position:absolute;min-height:100%;top:0px; left:0px; right:0px; padding:10px; background:white; color:black; font-family:serif; font-size:16px'
  405. div.appendChild(document.createElement('br'))
  406. const acceptButton = div.appendChild(document.createElement('button'))
  407. acceptButton.setAttribute('style', 'color:black;background:#e5e4e4;border:2px #bbb outset;margin:5px;padding:2px 10px;font-size:16px;font-family:sans-serif;cursor:pointer')
  408. acceptButton.appendChild(document.createTextNode('Accept'))
  409. acceptButton.addEventListener('click', function () {
  410. div.remove()
  411. resolve(true)
  412. GM.setValue('gdpr', true)
  413. })
  414. const declineButton = div.appendChild(document.createElement('button'))
  415. declineButton.setAttribute('style', 'color:black;background:#e5e4e4;border:2px #bbb outset;margin:5px;padding:2px 10px;font-size:16px;font-family:sans-serif;cursor:pointer')
  416. declineButton.appendChild(document.createTextNode('Decline'))
  417. declineButton.addEventListener('click', function () {
  418. alert('You may uninstall the userscript now.')
  419. div.remove()
  420. resolve(false)
  421. GM.setValue('gdpr', false)
  422. })
  423. const space = div.appendChild(document.createElement('div'))
  424. space.style = 'height:2000px;'
  425. div.scrollIntoView()
  426. window.setTimeout(function () {
  427. alert('ShowMetacriticRatings:\n\nWhen you use this script, data will be sent to our database and to metacritic.com. This data includes the url of the website that you are browsing, the metacritic page url, your IP adress, browser configuration and language preferences. We only store the url of the website and the metacritic url and no personal information. Log files are temporarily retained and contain your IP address. We have no control over which data is stored by metacritic.com and our hoster heroku.com, see their respective privacy policies for more information (see "Third Party Privacy Policies").\n\nPlease read and accept our privacy policy now or uninstall this userscript.')
  428. }, 20)
  429. })
  430. })
  431. }
  432.  
  433. function delay (ms) {
  434. return new Promise(function (resolve) {
  435. window.setTimeout(() => resolve(), ms)
  436. })
  437. }
  438.  
  439. function absoluteMetaURL (url) {
  440. if (url.startsWith('https://')) {
  441. return url
  442. }
  443. if (url.startsWith('http://')) {
  444. return 'https' + url.substr(4)
  445. }
  446. if (url.startsWith('//')) {
  447. return baseURL + url.substr(2)
  448. }
  449. if (url.startsWith('/')) {
  450. return baseURL + url.substr(1)
  451. }
  452. url = url.replace('/game/pc/', '/game/').replace(/\/game\/playstation-\d\//, '/game/').replace('/game/xbox-one/', '/game/')
  453. return baseURL + url
  454. }
  455.  
  456. const parseLDJSONCache = {}
  457. function parseLDJSON (keys, condition) {
  458. if (document.querySelector('script[type="application/ld+json"]')) {
  459. const xmlEntitiesElement = document.createElement('div')
  460. const xmlEntitiesPattern = /&(?:#x[a-f0-9]+|#[0-9]+|[a-z0-9]+);?/ig
  461. const xmlEntities = function (s) {
  462. s = s.replace(xmlEntitiesPattern, (m) => {
  463. xmlEntitiesElement.innerHTML = m
  464. return xmlEntitiesElement.textContent
  465. })
  466. return s
  467. }
  468. const decodeXmlEntities = function (jsonObj) {
  469. // Traverse through object, decoding all strings
  470. if (jsonObj !== null && typeof jsonObj === 'object') {
  471. Object.entries(jsonObj).forEach(([key, value]) => {
  472. // key is either an array index or object key
  473. jsonObj[key] = decodeXmlEntities(value)
  474. })
  475. } else if (typeof jsonObj === 'string') {
  476. return xmlEntities(jsonObj)
  477. }
  478. return jsonObj
  479. }
  480.  
  481. const data = []
  482. const scripts = document.querySelectorAll('script[type="application/ld+json"]')
  483. for (let i = 0; i < scripts.length; i++) {
  484. let jsonld
  485. if (scripts[i].innerText in parseLDJSONCache) {
  486. jsonld = parseLDJSONCache[scripts[i].innerText]
  487. } else {
  488. try {
  489. jsonld = JSON.parse(scripts[i].innerText)
  490. parseLDJSONCache[scripts[i].innerText] = jsonld
  491. } catch (e) {
  492. parseLDJSONCache[scripts[i].innerText] = null
  493. continue
  494. }
  495. }
  496. if (jsonld) {
  497. if (Array.isArray(jsonld)) {
  498. data.push(...jsonld)
  499. } else {
  500. data.push(jsonld)
  501. }
  502. }
  503. }
  504. for (let i = 0; i < data.length; i++) {
  505. try {
  506. if (data[i] && data[i] && (typeof condition !== 'function' || condition(data[i]))) {
  507. if (Array.isArray(keys)) {
  508. const r = []
  509. for (let j = 0; j < keys.length; j++) {
  510. r.push(data[i][keys[j]])
  511. }
  512. return decodeXmlEntities(r)
  513. } else if (keys) {
  514. return decodeXmlEntities(data[i][keys])
  515. } else if (typeof condition === 'function') {
  516. return decodeXmlEntities(data[i]) // Return whole object
  517. }
  518. }
  519. } catch (e) {
  520. continue
  521. }
  522. }
  523. return decodeXmlEntities(data)
  524. }
  525. return null
  526. }
  527.  
  528. function name2metacritic (s) {
  529. const mc = s.normalize('NFKD').replace(/\//g, '').replace(/[\u0300-\u036F]/g, '').replace(/&/g, 'and').replace(/\W+/g, ' ').toLowerCase().trim().replace(/\W+/g, '-')
  530. if (!mc) {
  531. throw new Error("name2metacritic converted '" + s + "' to empty string")
  532. }
  533. return mc
  534. }
  535. function minutesSince (time) {
  536. const seconds = ((new Date()).getTime() - time.getTime()) / 1000
  537. return seconds > 60 ? parseInt(seconds / 60) + ' min ago' : 'now'
  538. }
  539. function randomStringId () {
  540. const id10 = () => Math.floor((1 + Math.random()) * 0x10000000000).toString(16).substring(1)
  541. return id10() + id10() + id10() + id10() + id10() + id10()
  542. }
  543. function fixMetacriticURLs (html) {
  544. return html.replace(/<a /g, '<a target="_blank" ').replace(/href="\//g, 'href="' + baseURL).replace(/src="\//g, 'src="' + baseURL)
  545. }
  546. function searchType2fandomProdApigee (type) {
  547. return ({
  548. tv: '1',
  549. movie: '2',
  550. pcgame: '13',
  551. xonegame: '13',
  552. ps4game: '13',
  553. music: '4' // TODO this is probably wrong, music seems to be unsupported at the moment
  554. })[type]
  555. }
  556. function fandomProdApigee2metacriticUrl (type) {
  557. return ({
  558. 1: 'tv',
  559. 2: 'movie',
  560. 13: 'game',
  561. 4: 'music' // TODO this is probably wrong, music seems to be unsupported at the moment
  562. })[type]
  563. }
  564.  
  565. function badgeColor (score, type = '') {
  566. const colors = {
  567. universalAcclaim: '#6c3',
  568. generallyFavorable: '#00ce7a',
  569. mixedOrAverage: '#ffbd3f',
  570. generallyUnfavorable: '#ff6874',
  571. overwhelmingDislike: '#f00',
  572. tbd: '#fff'
  573. }
  574.  
  575. if (type.indexOf('game') !== -1) {
  576. if (score > 89) {
  577. return colors.universalAcclaim
  578. }
  579. if (score > 74) {
  580. return colors.generallyFavorable
  581. }
  582. if (score > 49) {
  583. return colors.mixedOrAverage
  584. }
  585. if (score > 19) {
  586. return colors.generallyUnfavorable
  587. }
  588. if (score > 0) {
  589. return colors.overwhelmingDislike
  590. }
  591. return colors.tbd
  592. } else {
  593. if (score > 80) {
  594. return colors.universalAcclaim
  595. }
  596. if (score > 60) {
  597. return colors.generallyFavorable
  598. }
  599. if (score > 39) {
  600. return colors.mixedOrAverage
  601. }
  602. if (score > 19) {
  603. return colors.generallyUnfavorable
  604. }
  605. if (score > 0) {
  606. return colors.overwhelmingDislike
  607. }
  608. return colors.tbd
  609. }
  610. }
  611.  
  612. function replaceBrackets (str) {
  613. str = str.replace(/\([^(]*\)/g, '')
  614. str = str.replace(/\[[^\]]*\]/g, '')
  615. return str.trim()
  616. }
  617. function removeSymbols (str) {
  618. str = str.replace(/[^\s0-9A-Za-zÀ-ÖØ-öø-ÿ]*/gi, '').trim()
  619. return str.trim()
  620. }
  621. const dashRegExp = /[-\u2010\u2011\u2012\u2013\u2014\u2015\uFE58\uFE63\uFF0D]/
  622. function removeAnythingAfterDash (str) {
  623. str = str.split(dashRegExp)[0]
  624. return str.trim()
  625. }
  626.  
  627. function broadenSearch (data, step, type) {
  628. if (type === 'pcgame') {
  629. if (step > 0) {
  630. data[0] = replaceBrackets(data[0])
  631. } else if (step > 1) {
  632. data[0] = removeSymbols(data[0])
  633. } else if (step > 2) {
  634. data[0] = removeAnythingAfterDash(data[0])
  635. }
  636. } else {
  637. data = data.map(removeSymbols)
  638. }
  639. return data
  640. }
  641.  
  642. function balloonAlert (message, timeout, title, css, click) {
  643. let header
  644. if (title) {
  645. header = '<div style="background:rgb(220,230,150); padding: 2px 12px;">' + title + '</div>'
  646. } else if (title === false) {
  647. header = ''
  648. } else {
  649. header = '<div style="background:rgb(220,230,150); padding: 2px 12px;">Userscript alert</div>'
  650. }
  651. const div = $('<div>' + header + '<div style="padding:5px">' + message.split('\n').join('<br>') + '</div></div>')
  652. div.css({
  653. position: 'fixed',
  654. top: 10,
  655. left: 10,
  656. maxWidth: 200,
  657. zIndex: '2147483601',
  658. background: 'rgb(240,240,240)',
  659. border: '2px solid yellow',
  660. borderRadius: '6px',
  661. boxShadow: '0 0 3px 3px rgba(100, 100, 100, 0.2)',
  662. fontFamily: 'sans-serif',
  663. color: 'black'
  664. })
  665. if (css) {
  666. div.css(css)
  667. }
  668. div.appendTo(document.body)
  669.  
  670. if (click) {
  671. div.click(function (ev) {
  672. $(this).hide(500)
  673. click.call(this, ev)
  674. })
  675. }
  676.  
  677. if (!click) {
  678. const close = $('<div title="Close" style="cursor:pointer; position:absolute; top:0px; right:3px;">&#10062;</div>').appendTo(div)
  679. close.click(function () {
  680. $(this.parentNode).hide(1000)
  681. })
  682. }
  683.  
  684. if (timeout && timeout > 0) {
  685. window.setTimeout(function () {
  686. div.hide(3000)
  687. }, timeout)
  688. }
  689. return div
  690. }
  691.  
  692. function filterUniversalUrl (url) {
  693. try {
  694. url = url.match(/http.+/)[0]
  695. } catch (e) { }
  696.  
  697. try {
  698. url = url.replace(/https?:\/\/(www.)?/, '')
  699. } catch (e) { }
  700.  
  701. if (url.indexOf('#') !== -1) {
  702. url = url.split('#')[0]
  703. }
  704.  
  705. if (url.startsWith('imdb.com/') && url.match(/(imdb\.com\/\w+\/\w+\/)/)) {
  706. // Remove movie subpage from imdb url
  707. return url.match(/(imdb\.com\/\w+\/\w+\/)/)[1]
  708. } else if (url.startsWith('boxofficemojo.com/') && url.indexOf('id=') !== -1) {
  709. // Keep the important id= on
  710. try {
  711. const parts = url.split('?')
  712. const page = parts[0] + '?'
  713. const idparam = parts[1].match(/(id=.+?)(\.|&)/)[1]
  714. return page + idparam
  715. } catch (e) {
  716. return url
  717. }
  718. } else {
  719. // Default: Remove parameters
  720. return url.split('?')[0].split('&')[0]
  721. }
  722. }
  723.  
  724. async function addToMap (url, metaurl) {
  725. const data = JSON.parse(await GM.getValue('map', '{}'))
  726.  
  727. url = filterUniversalUrl(url)
  728. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  729.  
  730. data[url] = metaurl
  731.  
  732. await GM.setValue('map', JSON.stringify(data));
  733.  
  734. (new Image()).src = baseURLwhitelist + '?docurl=' + encodeURIComponent(url) + '&metaurl=' + encodeURIComponent(metaurl) + '&ref=' + encodeURIComponent(randomStringId())
  735. return [url, metaurl]
  736. }
  737.  
  738. async function addToTemporaryBlacklist (metaurl) {
  739. const data = JSON.parse(await GM.getValue('temporaryblack', '{}'))
  740.  
  741. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  742. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/')
  743. metaurl = metaurl.replace(/^\/+/, '')
  744.  
  745. data[metaurl] = (new Date()).toJSON()
  746.  
  747. // Remove old entries
  748. const now = (new Date()).getTime()
  749. const timeout = TEMPORARY_BLACKLIST_TIMEOUT * 1000
  750. for (const prop in data) {
  751. if (now - (new Date(data[prop].time)).getTime() > timeout) {
  752. delete data[prop]
  753. }
  754. }
  755.  
  756. await GM.setValue('temporaryblack', JSON.stringify(data))
  757.  
  758. return true
  759. }
  760.  
  761. async function removeFromTemporaryBlacklist (metaurl) {
  762. const data = JSON.parse(await GM.getValue('temporaryblack', '{}'))
  763.  
  764. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  765. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/')
  766. metaurl = metaurl.replace(/^\/+/, '')
  767.  
  768. if (metaurl in data) {
  769. delete data[metaurl]
  770. await GM.setValue('temporaryblack', JSON.stringify(data))
  771. }
  772. }
  773.  
  774. async function isTemporaryBlacklisted (metaurl) {
  775. const data = JSON.parse(await GM.getValue('temporaryblack', '{}'))
  776.  
  777. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  778. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/')
  779. metaurl = metaurl.replace(/^\/+/, '')
  780.  
  781. if (metaurl in data) {
  782. const now = (new Date()).getTime()
  783. const timeout = TEMPORARY_BLACKLIST_TIMEOUT * 1000
  784. if (now - (new Date(data[metaurl])).getTime() < timeout) {
  785. return true
  786. }
  787. }
  788. return false
  789. }
  790.  
  791. async function addToBlacklist (url, metaurl) {
  792. const data = JSON.parse(await GM.getValue('black', '[]'))
  793.  
  794. url = filterUniversalUrl(url)
  795. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  796.  
  797. data.push([url, metaurl])
  798.  
  799. await GM.setValue('black', JSON.stringify(data));
  800.  
  801. (new Image()).src = baseURLblacklist + '?docurl=' + encodeURIComponent(url) + '&metaurl=' + encodeURIComponent(metaurl) + '&ref=' + encodeURIComponent(randomStringId())
  802. return [url, metaurl]
  803. }
  804.  
  805. async function removeFromBlacklist (docurl, metaurl) {
  806. docurl = filterUniversalUrl(docurl)
  807. docurl = docurl.replace(/https?:\/\/(www.)?/, '')
  808.  
  809. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  810. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/') // remove double slash
  811. metaurl = metaurl.replace(/^\/+/, '') // remove starting slash
  812.  
  813. const data = JSON.parse(await GM.getValue('black', '[]')) // [ [docurl0, metaurl0] , [docurl1, metaurl1] , ... ]
  814. const found = []
  815. for (let i = 0; i < data.length; i++) {
  816. if (data[i][0] === docurl && data[i][1] === metaurl) {
  817. found.push(i)
  818. }
  819. }
  820. for (let i = found.length - 1; i >= 0; i--) {
  821. data.pop(i)
  822. }
  823.  
  824. await GM.setValue('black', JSON.stringify(data))
  825. }
  826.  
  827. async function isBlacklistedUrl (docurl, metaurl) {
  828. docurl = filterUniversalUrl(docurl)
  829. docurl = docurl.replace(/https?:\/\/(www.)?/, '')
  830.  
  831. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  832. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/') // remove double slash
  833. metaurl = metaurl.replace(/^\/+/, '') // remove starting slash
  834.  
  835. const data = JSON.parse(await GM.getValue('black', '[]')) // [ [docurl0, metaurl0] , [docurl1, metaurl1] , ... ]
  836. for (let i = 0; i < data.length; i++) {
  837. if (data[i][0] === docurl && data[i][1] === metaurl) {
  838. return true
  839. }
  840. }
  841. return false
  842. }
  843.  
  844. let listenForHotkeysActive = false
  845. function listenForHotkeys (code, cb) {
  846. // Call cb() as soon as the code sequence was typed
  847. if (listenForHotkeysActive) {
  848. return
  849. }
  850. listenForHotkeysActive = true
  851. let i = 0
  852. $(document).bind('keydown.listenForHotkeys', function (ev) {
  853. if (document.activeElement === document.body) {
  854. if (ev.key !== code[i]) {
  855. i = 0
  856. } else {
  857. i++
  858. if (i === code.length) {
  859. ev.preventDefault()
  860. $(document).unbind('keydown.listenForHotkeys')
  861. cb()
  862. }
  863. }
  864. }
  865. })
  866. }
  867.  
  868. function waitForHotkeysMETA () {
  869. listenForHotkeys('meta', (ev) => openSearchBox())
  870. }
  871.  
  872. async function handleJSONredirect (response) {
  873. let blacklistedredirect = false
  874. const j = JSON.parse(response.responseText)
  875.  
  876. // Blacklist items from database received?
  877. if ('blacklist' in j && j.blacklist && j.blacklist.length) {
  878. // Save new blacklist items
  879. const data = JSON.parse(await GM.getValue('black', '[]'))
  880. for (let i = 0; i < j.blacklist.length; i++) {
  881. const saveDocurl = j.blacklist[i].docurl
  882. const saveMetaurl = j.blacklist[i].metaurl
  883.  
  884. data.push([saveDocurl, saveMetaurl])
  885. if (j.jsonRedirect === '/' + saveMetaurl) {
  886. // Redirect is blacklisted!
  887. blacklistedredirect = true
  888. }
  889. }
  890. await GM.setValue('black', JSON.stringify(data))
  891. }
  892. if (blacklistedredirect) {
  893. // Redirect was blacklisted, show nothing
  894. console.debug('ShowMetacriticRatings: Redirect was blacklisted -> show nothing')
  895. return null
  896. } else {
  897. // Load redirect
  898. current.metaurl = absoluteMetaURL(j.jsonRedirect)
  899. response = await asyncRequest({
  900. url: current.metaurl
  901. }).catch(function (response) {
  902. console.error('ShowMetacriticRatings: Error 01')
  903. })
  904. return response
  905. }
  906. }
  907.  
  908. function extractHoverFromFullPage (response) {
  909. let html = 'ShowMetacriticRatings:<br>Error occured in extractHoverFromFullPage()'
  910. try {
  911. // Try parsing HTML
  912. const doc = domParser().parseFromString(response.responseText, 'text/html')
  913.  
  914. let content = null
  915. // Try to get the review containers from the bottom of the page below the actors
  916. const carouselItems = doc.querySelectorAll('.c-reviewsSection_carouselContainer .c-reviewsOverview_overviewDetails')
  917. if (carouselItems.length > 0) {
  918. content = Array.from(carouselItems).map(e => e.outerHTML).join('\n\n')
  919. } else {
  920. // Fallback: Try to get the review containers from the right side of the page next to the poster/screenshot
  921. content = doc.querySelector('.c-productHero_scoreInfo').innerHTML
  922. }
  923.  
  924. // Get the current platform title:
  925. if (doc.querySelector('.c-gamePlatformLogo title')) {
  926. content = `<div class="mci_current_platform_title">Platform: ${doc.querySelector('.c-gamePlatformLogo title').textContent}</div>\n\n${content}`
  927. }
  928.  
  929. // Get the game row with the other platform scores
  930. const gameRow = doc.querySelector('.c-PageProductGame_row')
  931. if (gameRow) {
  932. // Get the currently selected platform
  933. const latestCriticReviewsLink = doc.querySelector('a.c-sectionHeader_urlLink[href*="platform="]')
  934. let platform = null
  935. if (latestCriticReviewsLink) {
  936. platform = latestCriticReviewsLink.href.match(/platform=([^&]+)/)[1]
  937. content += `\n\n<input type="hidden" id="mci_current_platform" value="${platform}"/>`
  938. }
  939.  
  940. // Remove platforms that don't have a score
  941. gameRow.querySelectorAll('.c-gamePlatformTile[to]').forEach(e => e.remove())
  942. // Remove the currently selected platform
  943. if (platform) {
  944. gameRow.querySelectorAll(`a.c-gamePlatformTile[href*="platform=${platform}"]`).forEach(e => e.remove())
  945. }
  946. // Replace the platform icon with the platform name
  947. gameRow.querySelectorAll('.c-gamePlatformTile-description').forEach(e => {
  948. e.textContent = e.querySelector('svg title').textContent
  949. })
  950. content += `\n\n<div class="game_row_5456d45" style="display:none">${gameRow.innerHTML}</div>`
  951. }
  952.  
  953. if (!content) {
  954. throw new Error('No content found')
  955. }
  956.  
  957. html = `
  958. <div id="hover_div_a20230915">
  959.  
  960. ${content}
  961.  
  962. </div>
  963. `
  964. } catch (e) {
  965. console.warn('ShowMetacriticRatings: Error parsing HTML: ' + e)
  966. // fallback to cutting out the relevant parts
  967. const parts = response.responseText.split('c-productHero_score-container')
  968.  
  969. html = '<div class="' + parts[1].split('c-ratingReviewWrapper')[0] + '"></div></div>'
  970. if (html.length > 5000) {
  971. // Probably something went wrong, let's cut the response to prevent too long content
  972. console.warn('ShowMetacriticRatings: Cutting response to 5000 chars')
  973. html = html.substring(0, 5000)
  974. }
  975. }
  976. return html
  977. }
  978.  
  979. function asyncRequest (data) {
  980. return new Promise(function (resolve, reject) {
  981. isInRequestCache(data).then(function (cachedValue) {
  982. if (cachedValue) {
  983. console.debug(`${scriptName}: asyncRequest() Cache hit for`, data)
  984. return window.setTimeout(() => resolve(cachedValue), 10)
  985. }
  986. const defaultHeaders = {
  987. Referer: data.url,
  988. 'User-Agent': navigator.userAgent
  989. }
  990. const defaultData = {
  991. method: 'GET',
  992. onload: function (response) {
  993. storeInRequestCache(data, response)
  994. resolve(response)
  995. },
  996. onerror: (response) => reject(response)
  997. }
  998. if ('headers' in data) {
  999. data.headers = Object.assign(defaultHeaders, data.headers)
  1000. } else {
  1001. data.headers = defaultHeaders
  1002. }
  1003. data = Object.assign(defaultData, data)
  1004. console.debug(`${scriptName}: asyncRequest() GM.xmlHttpRequest`, data)
  1005. GM.xmlHttpRequest(data)
  1006. })
  1007. })
  1008. }
  1009.  
  1010. async function storeInRequestCache (requestData, response) {
  1011. const newkey = JSON.stringify({
  1012. url: requestData.url,
  1013. method: requestData.method || 'GET',
  1014. data: requestData.data || null
  1015. })
  1016. const cache = JSON.parse(await GM.getValue('requestcache', '{}'))
  1017. const now = (new Date()).getTime()
  1018. const timeout = 15 * 60 * 1000
  1019. for (const prop in cache) {
  1020. // Delete cached values, that are older than 15 minutes
  1021. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  1022. delete cache[prop]
  1023. }
  1024. }
  1025.  
  1026. const newobj = {}
  1027. for (const key in response) {
  1028. newobj[key] = response[key]
  1029. }
  1030. newobj.responseText = '' + response.responseText
  1031. newobj.cached = true
  1032. if (!('time' in newobj)) {
  1033. newobj.time = (new Date()).toJSON()
  1034. }
  1035.  
  1036. cache[newkey] = newobj
  1037.  
  1038. await GM.setValue('requestcache', JSON.stringify(cache))
  1039. }
  1040.  
  1041. async function isInRequestCache (requestData) {
  1042. const key = JSON.stringify({
  1043. url: requestData.url,
  1044. method: requestData.method || 'GET',
  1045. data: requestData.data || null
  1046. })
  1047.  
  1048. const cache = JSON.parse(await GM.getValue('requestcache', '{}'))
  1049. const now = (new Date()).getTime()
  1050. const timeout = 15 * 60 * 1000
  1051. for (const prop in cache) {
  1052. // Delete cached values, that are older than 15 minutes
  1053. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  1054. delete cache[prop]
  1055. }
  1056. }
  1057.  
  1058. if (key in cache) {
  1059. return cache[key]
  1060. } else {
  1061. return false
  1062. }
  1063. }
  1064.  
  1065. async function storeInHoverCache (metaurl, response, orgMetaUrl) {
  1066. const cache = JSON.parse(await GM.getValue('hovercache', '{}'))
  1067. const now = (new Date()).getTime()
  1068. const timeout = 2 * 60 * 60 * 1000
  1069. for (const prop in cache) {
  1070. // Delete cached values, that are older than 2 hours
  1071. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  1072. delete cache[prop]
  1073. }
  1074. }
  1075.  
  1076. const newobj = {}
  1077. for (const key in response) {
  1078. newobj[key] = response[key]
  1079. }
  1080. newobj.responseText = '' + response.responseText
  1081. newobj.cached = true
  1082. if (!('time' in newobj)) {
  1083. newobj.time = (new Date()).toJSON()
  1084. }
  1085.  
  1086. cache[metaurl] = newobj
  1087. if (orgMetaUrl && orgMetaUrl !== metaurl) { // Store redirect
  1088. cache[orgMetaUrl] = { time: (new Date()).toJSON(), redirect: metaurl }
  1089. }
  1090.  
  1091. await GM.setValue('hovercache', JSON.stringify(cache))
  1092. }
  1093.  
  1094. async function isInHoverCache (metaurl) {
  1095. const cache = JSON.parse(await GM.getValue('hovercache', '{}'))
  1096. const now = (new Date()).getTime()
  1097. const timeout = 2 * 60 * 60 * 1000
  1098. for (const prop in cache) {
  1099. // Delete cached values, that are older than 2 hours
  1100. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  1101. delete cache[prop]
  1102. }
  1103. }
  1104.  
  1105. function resolveRedirects (cacheEntry) {
  1106. if (cacheEntry.redirect) {
  1107. const newkey = cacheEntry.redirect
  1108. if (newkey in cache) {
  1109. const value = cache[newkey]
  1110. delete cache[newkey]
  1111. return resolveRedirects(value)
  1112. }
  1113. } else {
  1114. return cacheEntry
  1115. }
  1116. return false
  1117. }
  1118.  
  1119. if (metaurl in cache) {
  1120. const value = cache[metaurl]
  1121. delete cache[metaurl]
  1122. return resolveRedirects(value)
  1123. } else {
  1124. return false
  1125. }
  1126. }
  1127.  
  1128. async function loadHoverInfo () {
  1129. const cacheResponse = await isInHoverCache(current.metaurl)
  1130. if (cacheResponse !== false) {
  1131. console.debug(`ShowMetacriticRatings: loadHoverInfo () ${current.metaurl} found in hover cache`)
  1132. if (cacheResponse.responseText.indexOf('"jsonRedirect"') !== -1) {
  1133. return await handleJSONredirect(cacheResponse)
  1134. }
  1135. return cacheResponse
  1136. }
  1137. const requestURL = baseURLdatabase
  1138. const requestParams = 'm=' + encodeURIComponent(current.docurl) + '&a=' + encodeURIComponent(current.metaurl)
  1139.  
  1140. let response = await asyncRequest({
  1141. method: 'POST',
  1142. url: requestURL,
  1143. data: requestParams,
  1144. headers: {
  1145. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
  1146. }
  1147. }).catch(function (response) {
  1148. console.warn('ShowMetacriticRatings: Error 02\nurl=' + requestURL + '\nparams=' + requestParams + '\nstatus=' + response.status)
  1149. })
  1150.  
  1151. if (response.responseText && response.responseText.indexOf('"jsonRedirect"') !== -1) {
  1152. response = await handleJSONredirect(response)
  1153. }
  1154.  
  1155. if (response.status >= 500) {
  1156. // Metacritic server error, try again after 2 seconds
  1157. console.warn('ShowMetacriticRatings: Metacritic server error\nwait 2s for retry\nurl=' + current.metaurl + '\nstatus=' + response.status)
  1158. await delay(2000)
  1159. response = await asyncRequest({ url: current.metaurl }).catch(function (response) {
  1160. console.warn('ShowMetacriticRatings: Error 06\nurl=' + current.metaurl + '\nstatus=' + response.status)
  1161. })
  1162. if (response.status > 300) {
  1163. console.warn('ShowMetacriticRatings: Metacritic server error. Error 07. Retry failed as well.\nurl=' + current.metaurl + '\nstatus=' + response.status)
  1164. } else {
  1165. const newobj = {}
  1166. for (const key in response) {
  1167. newobj[key] = response[key]
  1168. }
  1169. newobj.responseText = extractHoverFromFullPage(response)
  1170. response = newobj
  1171. }
  1172. }
  1173.  
  1174. // Extract relevant data from HTML
  1175. if (!('time' in response)) {
  1176. response.time = (new Date()).toJSON()
  1177. }
  1178. if (response.status === 200 && response.responseText) {
  1179. const newobj = {}
  1180. for (const key in response) {
  1181. newobj[key] = response[key]
  1182. }
  1183. newobj.responseText = extractHoverFromFullPage(response)
  1184. response = newobj
  1185. return response
  1186. } else {
  1187. const error = new Error('ShowMetacriticRatings: loadHoverInfo()\nUrl: ' + response.finalUrl + '\nStatus: ' + response.status)
  1188. error.status = response.status
  1189. error.responseText = response.responseText
  1190. throw error
  1191. }
  1192. }
  1193.  
  1194. function changePosition () {
  1195. // Cycle through positions
  1196. GM.getValue('position', JSON.stringify(windowPositions[0])).then(function (s) {
  1197. let index
  1198. for (index = 0; index < windowPositions.length; index++) {
  1199. if (JSON.stringify(windowPositions[index]) === s) {
  1200. break
  1201. }
  1202. }
  1203. const nextIndex = (index + 1) % windowPositions.length
  1204. GM.setValue('position', JSON.stringify(windowPositions[nextIndex])).then(function () {
  1205. document.location.reload()
  1206. })
  1207. })
  1208. }
  1209.  
  1210. function onSizeChanged () {
  1211. GM.getValue('size', 100).then(function (size) {
  1212. if (size && size !== 100) {
  1213. size = parseInt(size)
  1214. $('#mcdiv123').css('transform', `scale(${size}%)`)
  1215. }
  1216. })
  1217. }
  1218.  
  1219. function changeSizeEnlarge () {
  1220. GM.getValue('size', 100).then((size) => {
  1221. GM.setValue('size', parseInt(size) + 5).then(onSizeChanged)
  1222. })
  1223. }
  1224.  
  1225. function changeSizeShrink () {
  1226. GM.getValue('size', 100).then((size) => {
  1227. GM.setValue('size', parseInt(size) - 5).then(onSizeChanged)
  1228. })
  1229. }
  1230.  
  1231. const current = {
  1232. metaurl: false,
  1233. docurl: false,
  1234. type: false,
  1235. data: [], // Array of raw search keys
  1236. searchTerm: false,
  1237. product: null,
  1238. broadenCounter: 0
  1239. }
  1240.  
  1241. async function onBlacklistedPage () {
  1242. GM.registerMenuCommand('Show Metacritic.com ratings - Remove from Blacklist', () => removeFromBlacklistAndReload())
  1243. }
  1244.  
  1245. async function removeFromBlacklistAndReload () {
  1246. await removeFromBlacklist(current.docurl, current.metaurl)
  1247. await removeFromTemporaryBlacklist(current.metaurl)
  1248. main()
  1249. }
  1250.  
  1251. async function loadMetacriticUrl (fromSearch) {
  1252. if (!current.metaurl) {
  1253. alert('ShowMetacriticRatings: Error 04')
  1254. return
  1255. }
  1256. const orgMetaUrl = current.metaurl
  1257. if (await isBlacklistedUrl(document.location.href, current.metaurl)) {
  1258. waitForHotkeysMETA()
  1259. onBlacklistedPage()
  1260. return
  1261. }
  1262.  
  1263. if (await isTemporaryBlacklisted(current.metaurl)) {
  1264. console.debug(`ShowMetacriticRatings: loadMetacriticUrl(fromSearch=${fromSearch}) ${current.metaurl} is temporary blacklisted`)
  1265. waitForHotkeysMETA()
  1266. onBlacklistedPage()
  1267. return
  1268. }
  1269.  
  1270. const response = await loadHoverInfo().catch(async function (response) {
  1271. if (response instanceof Error || (response && response.stack && response.message)) {
  1272. if (!fromSearch && ('status' in response && response.status === 404)) {
  1273. console.debug('ShowMetacriticRatings: loadMetacriticUrl(): status=404', response)
  1274. // No results
  1275. let broadenFct = broadenSearch // global broadenSearch function is the default
  1276. if ('broaden' in current.product) {
  1277. // try product 'broaden'-function if it is defined
  1278. broadenFct = current.product.broaden
  1279. }
  1280. const newData = await broadenFct(current.data.slice(0), ++current.broadenCounter, current.type)
  1281. if (JSON.stringify(newData) !== JSON.stringify(current.data)) {
  1282. current.data = newData
  1283. metacritic[current.type](current.docurl, current.product, ...newData)
  1284. } else if (JSON.stringify(newData) === JSON.stringify(current.data)) {
  1285. // Same data as before, try once again to broaden
  1286. const newData2 = await broadenFct(current.data.slice(0), ++current.broadenCounter, current.type)
  1287. if (JSON.stringify(newData2) !== JSON.stringify(current.data)) {
  1288. current.data = newData2
  1289. metacritic[current.type](current.docurl, current.product, ...newData2)
  1290. } else {
  1291. console.debug('ShowMetacriticRatings: loadMetacriticUrl(): ' + ('broaden' in current.product ? 'product specific' : 'global') + " 'broaden search' did not change after " + current.broadenCounter + ' steps')
  1292. }
  1293. } else {
  1294. console.debug("ShowMetacriticRatings: loadMetacriticUrl(): Unexpected result from 'broaden'-function: ", newData)
  1295. }
  1296. } else {
  1297. console.error(`ShowMetacriticRatings: loadMetacriticUrl(fromSearch=${fromSearch}) current.metaurl = ${current.metaurl}. Error in loadHoverInfo():\n`, response)
  1298. }
  1299. }
  1300.  
  1301. if (!fromSearch) {
  1302. startSearch()
  1303. }
  1304. })
  1305.  
  1306. if (await isBlacklistedUrl(document.location.href, current.metaurl)) {
  1307. waitForHotkeysMETA()
  1308. onBlacklistedPage()
  1309. return
  1310. }
  1311.  
  1312. if (typeof response !== 'undefined') {
  1313. showHoverInfo(response, orgMetaUrl)
  1314. } else {
  1315. waitForHotkeysMETA()
  1316. }
  1317. }
  1318.  
  1319. async function startSearch () {
  1320. waitForHotkeysMETA()
  1321.  
  1322. if (current.type === 'music') {
  1323. current.searchTerm = current.data[0]
  1324. } else {
  1325. current.searchTerm = current.data.join(' ')
  1326. }
  1327. const items = await fandomProdApigeeSearch(current.searchTerm, current.type)
  1328.  
  1329. if (!items) {
  1330. alert('ShowMetacriticRatings: Error 05 item=', items)
  1331. }
  1332.  
  1333. let multiple = false
  1334. if (items.length === 0) {
  1335. // No results
  1336. console.debug('ShowMetacriticRatings: No results for searchTerm=' + current.searchTerm)
  1337. } else if (items.length === 1) {
  1338. // One result, let's show it
  1339. const itemURL = absoluteMetaURL(items[0].metacriticUrl)
  1340. if (!await isBlacklistedUrl(document.location.href, itemURL)) {
  1341. current.metaurl = itemURL
  1342. loadMetacriticUrl(true)
  1343. return
  1344. } else {
  1345. onBlacklistedPage()
  1346. return
  1347. }
  1348. } else {
  1349. // More than one result
  1350. multiple = true
  1351. console.debug('ShowMetacriticRatings: Multiple results for searchTerm=' + current.searchTerm)
  1352. const exactMatches = []
  1353. items.forEach(function (result, i) { // Try to find the correct result by matching the search term to exactly one movie title
  1354. if (current.searchTerm.toLowerCase() === result.title.toLowerCase()) {
  1355. exactMatches.push(result)
  1356. }
  1357. })
  1358. if (exactMatches.length === 0) {
  1359. // Try to be a bit more fuzzy
  1360. items.forEach(function (result, i) {
  1361. if (removeSymbols(current.searchTerm.toLowerCase()) === removeSymbols(result.title.toLowerCase())) {
  1362. exactMatches.push(result)
  1363. }
  1364. })
  1365. }
  1366. if (exactMatches.length === 1) {
  1367. // Only one exact match, let's show it
  1368. console.debug('ShowMetacriticRatings: Only one exact match for searchTerm=' + current.searchTerm)
  1369. const itemURL = absoluteMetaURL(exactMatches[0].metacriticUrl)
  1370. if (!await isBlacklistedUrl(document.location.href, itemURL)) {
  1371. current.metaurl = itemURL
  1372. loadMetacriticUrl(true)
  1373. return
  1374. } else {
  1375. onBlacklistedPage()
  1376. return
  1377. }
  1378. }
  1379. }
  1380.  
  1381. // HERE: multiple results or no result. The user may type "meta" now
  1382. if (multiple) {
  1383. balloonAlert('Multiple metacritic results. Type &#34;meta&#34; for manual search.', 10000, false, { bottom: 5, top: 'auto', maxWidth: 400, paddingRight: 5, cursor: 'pointer' }, () => openSearchBox(true))
  1384. }
  1385. }
  1386.  
  1387. function openSearchBox (search) {
  1388. let query
  1389. if (current.type === 'music') {
  1390. query = current.data[0]
  1391. } else {
  1392. query = current.data.join(' ')
  1393. }
  1394. $('#mcdiv123').remove()
  1395.  
  1396. const div = $('<div id="mcdiv123"></div>').appendTo(document.body)
  1397. div.css({
  1398. minWidth: 300,
  1399. bottom: 0,
  1400. left: 0
  1401. })
  1402.  
  1403. GM.getValue('position', false).then(function (s) {
  1404. if (s) {
  1405. div.css({
  1406. top: '',
  1407. left: '',
  1408. bottom: '',
  1409. right: ''
  1410. })
  1411. div.css(JSON.parse(s))
  1412. }
  1413. })
  1414.  
  1415. $('<input type="text" id="mcisearchquery">').appendTo(div).focus().val(query).on('keypress', function (e) {
  1416. const code = e.keyCode || e.which
  1417. if (code === 13) { // Enter key
  1418. searchBoxSearch(e, $('#mcisearchquery').val())
  1419. }
  1420. })
  1421. $('<button id="mcisearchbutton">').text('Search').appendTo(div).click((ev) => searchBoxSearch(ev, $('#mcisearchquery').val()))
  1422. }
  1423.  
  1424. async function getFandomProdApigeeApiKey () {
  1425. let apiKey = await GM.getValue('fandomProdApigeeKey', false)
  1426. if (!apiKey) {
  1427. apiKey = await findFandomProdApigeeApiKey()
  1428. }
  1429.  
  1430. const lastUpdate = await GM.getValue('fandomProdApigeeTime', false)
  1431. if (!lastUpdate || (new Date()).getTime() - (new Date(lastUpdate)).getTime() > 7 * 24 * 60 * 60 * 1000) {
  1432. // Update api key once a week
  1433. const newApiKey = await findFandomProdApigeeApiKey()
  1434. if (newApiKey) {
  1435. apiKey = newApiKey
  1436. }
  1437. }
  1438.  
  1439. if (!apiKey) {
  1440. console.debug('ShowMetacriticRatings: Fallback to hard-coded api key')
  1441. apiKey = '1MOZgmNFxvmljaQR1X9KAij9Mo4xAY3u'
  1442. }
  1443. return apiKey
  1444. }
  1445.  
  1446. async function findFandomProdApigeeApiKey () {
  1447. // Get a new Api key from the metacritic website search results page
  1448. const url = 'https://www.metacritic.com/search/Fly/'
  1449. try {
  1450. const response = await asyncRequest({ url })
  1451. const m = response.responseText.match(/\?apiKey=(\w{20,})/)
  1452. if (m) {
  1453. const apiKey = m[1]
  1454. console.debug('ShowMetacriticRatings: Api key updated', apiKey)
  1455. await GM.setValue('fandomProdApigeeKey', apiKey)
  1456. await GM.setValue('fandomProdApigeeTime', (new Date()).toJSON())
  1457. return apiKey
  1458. }
  1459. } catch (e) {
  1460. console.error('ShowMetacriticRatings: findFandomProdApigeeApiKey() Error:', e)
  1461. }
  1462. console.error('ShowMetacriticRatings: Could not find fandomProdApigee api key')
  1463. return false
  1464. }
  1465.  
  1466. async function fandomProdApigeeSearch (query, searchType) {
  1467. const apiKey = await getFandomProdApigeeApiKey()
  1468.  
  1469. const type = searchType2fandomProdApigee(searchType)
  1470. const url = baseURLsearch.replace('{type}', encodeURIComponent(type)).replace('{query}', encodeURIComponent(query)).replace('{apiKey}', encodeURIComponent(apiKey))
  1471.  
  1472. const response = await asyncRequest({ url })
  1473.  
  1474. if (response.status !== 200) {
  1475. console.error('ShowMetacriticRatings: fandomProdApigeeSearch() response != 200: ', response)
  1476. }
  1477.  
  1478. const obj = JSON.parse(response.responseText)
  1479. return obj.data.items.map(item => {
  1480. // Improve results by adding the metacritic url
  1481. let itemUrl = 'criticScoreSummary' in item && 'url' in item.criticScoreSummary ? item.criticScoreSummary.url : null
  1482. if (!itemUrl) {
  1483. itemUrl = `${baseURL}${fandomProdApigee2metacriticUrl(item.typeId)}/${item.slug}/`
  1484. }
  1485. item.metacriticUrl = itemUrl.replace('/critic-reviews/', '/')
  1486. return item
  1487. })
  1488. }
  1489.  
  1490. async function searchBoxSearch (ev, query) {
  1491. if (!query) { // Use values from search form
  1492. query = current.searchTerm
  1493. }
  1494.  
  1495. const div = $('#mcdiv123')
  1496. div.css({
  1497. minWidth: '550px'
  1498. })
  1499. const loader = $('<div class="grespinner"></div>').appendTo($('#mcisearchbutton'))
  1500.  
  1501. const resultItems = await fandomProdApigeeSearch(query, current.type).catch(function (response) {
  1502. alert('Search failed!\n' + response.finalUrl + '\nStatus: ' + response.status + '\n' + response.responseText ? response.responseText.substring(0, 500) : 'Empty response')
  1503. })
  1504.  
  1505. const results = []
  1506. resultItems.forEach(item => {
  1507. let img = `<svg class="mcdiv123_image_placeholder" viewBox="0 0 176 40"">
  1508. <path d="M17.2088 32.937L20.6188 29.527L14.0522 22.9604C13.7757 22.6839 13.4762 22.3383 13.3149 21.9466C12.9462 21.1632 12.7849 19.942 13.6835 19.0434C14.7895 17.9375 16.2641 18.3983 17.6926 19.8268L24.0058 26.14L27.4159 22.73L20.8262 16.1403C20.5497 15.8638 20.2271 15.4491 20.0659 15.1034C19.6281 14.2049 19.6511 13.0758 20.4576 12.2694C21.5866 11.1404 23.0612 11.5551 24.6971 13.191L30.8259 19.3199L34.236 15.9099L27.6002 9.27409C24.2362 5.91013 21.0796 6.02534 18.9138 8.19118C18.0843 9.02065 17.5774 9.8962 17.324 10.887C17.1166 11.7395 17.0475 12.6841 17.2318 13.6979L17.1857 13.744C15.5268 13.0528 13.6374 13.4675 12.1859 14.9191C10.2504 16.8545 10.3196 18.9052 10.55 20.1033L10.4809 20.1724L8.79888 18.813L5.84965 21.7622C6.88648 22.7069 8.1307 23.859 9.53619 25.2645L17.2088 32.937V32.937Z"></path> <path d="M19.9822 8.05032e-06C14.6789 0.00472041 9.59462 2.11554 5.84741 5.86828C2.10021 9.62102 -0.00310998 14.7084 3.45157e-06 20.0117C0.00307557 25.315 2.11239 30.4 5.86407 34.1484C9.61575 37.8968 14.7026 40.0016 20.006 40C25.3093 39.9984 30.3949 37.8906 34.1443 34.14C37.8938 30.3893 40.0001 25.3031 40 19.9998V19.9764C39.9938 14.6731 37.8814 9.58935 34.1275 5.8432C30.3736 2.09705 25.2855 -0.00474688 19.9822 8.05032e-06ZM19.8908 4.27438C24.0447 4.27063 28.0301 5.91689 30.9704 8.85113C33.9107 11.7854 35.5652 15.7673 35.57 19.9212V19.9393C35.57 24.0932 33.9201 28.0769 30.9833 31.0145C28.0465 33.9522 24.0632 35.6031 19.9093 35.6043C15.7555 35.6055 11.7712 33.9569 8.83271 31.0209C5.89421 28.085 4.24207 24.1022 4.23964 19.9484C4.23727 15.7946 5.88474 11.8099 8.81975 8.87064C11.7548 5.93134 15.737 4.27808 19.8908 4.27438Z"></path> <path d="M46.5464 27.9426H51.1377V19.1013C51.1377 18.7291 51.1687 18.2948 51.3238 17.9225C51.603 17.147 52.3165 16.2163 53.5264 16.2163C55.0154 16.2163 55.6979 17.5192 55.6979 19.4426V27.9426H60.2891V19.0703C60.2891 18.6981 60.3512 18.2017 60.4753 17.8605C60.7855 16.9608 61.561 16.2163 62.6468 16.2163C64.1669 16.2163 64.8804 17.4882 64.8804 19.6908V27.9426H69.4716V19.0083C69.4716 14.4791 67.2691 12.4316 64.353 12.4316C63.2362 12.4316 62.3056 12.6798 61.468 13.1762C60.7545 13.6105 60.072 14.1999 59.5136 15.0065H59.4516C58.8001 13.4243 57.249 12.4316 55.2946 12.4316C52.6888 12.4316 51.3548 13.8587 50.7034 14.8203H50.6103L50.3932 12.7729H46.4224C46.4844 14.1068 46.5464 15.72 46.5464 17.6123V27.9426V27.9426Z"></path> <path d="M85.8077 21.8623C85.8697 21.5211 85.9628 20.8075 85.9628 20.001C85.9628 16.2473 84.1015 12.4316 79.2 12.4316C73.9263 12.4316 71.5376 16.6816 71.5376 20.5284C71.5376 25.2747 74.4847 28.2838 79.6343 28.2838C81.6817 28.2838 83.5741 27.9426 85.1252 27.3221L84.5047 24.1269C83.2328 24.5302 81.9299 24.7473 80.3168 24.7473C78.1142 24.7473 76.1909 23.8167 76.0358 21.8623H85.8077ZM76.0047 18.636C76.1288 17.3641 76.9354 15.5649 78.9208 15.5649C81.0923 15.5649 81.5887 17.4882 81.5887 18.636H76.0047Z"></path> <path d="M88.617 9.48442V12.7727H86.6006V16.2472H88.617V22.4516C88.617 24.5921 89.0513 26.0501 89.9199 26.9498C90.6645 27.7253 91.9363 28.2837 93.4564 28.2837C94.7904 28.2837 95.9071 28.0976 96.5276 27.8494L96.4966 24.2819C96.1553 24.3439 95.69 24.4059 95.1006 24.4059C93.6736 24.4059 93.2393 23.5684 93.2393 21.7381V16.2472H96.6207V12.7727H93.2393V8.42969L88.617 9.48442V9.48442Z"></path> <path d="M111.213 18.9773C111.213 15.4097 109.6 12.4316 104.543 12.4316C101.782 12.4316 99.704 13.1762 98.6492 13.7656L99.5179 16.8057C100.511 16.1853 102.155 15.6579 103.706 15.6579C106.032 15.6579 106.467 16.8057 106.467 17.6123V17.8294C101.1 17.7984 97.5635 19.6908 97.5635 23.6305C97.5635 26.0502 99.3938 28.2838 102.465 28.2838C104.264 28.2838 105.815 27.6324 106.808 26.4225H106.901L107.18 27.9426H111.43C111.275 27.105 111.213 25.709 111.213 24.251V18.9773V18.9773ZM106.622 22.4207C106.622 22.6999 106.591 22.9791 106.529 23.2273C106.219 24.1889 105.257 24.9645 104.078 24.9645C103.023 24.9645 102.217 24.3751 102.217 23.1652C102.217 21.3349 104.14 20.7455 106.622 20.7765V22.4207V22.4207Z"></path> <path d="M125.003 24.0648C124.289 24.3751 123.421 24.5612 122.304 24.5612C120.008 24.5612 118.147 23.1032 118.147 20.3112C118.116 17.8294 119.729 16.0612 122.211 16.0612C123.452 16.0612 124.289 16.2784 124.848 16.5265L125.592 13.0211C124.6 12.6488 123.235 12.4316 121.994 12.4316C116.348 12.4316 113.308 16.0612 113.308 20.4973C113.308 25.2747 116.441 28.2838 121.342 28.2838C123.142 28.2838 124.724 27.9426 125.561 27.5703L125.003 24.0648Z"></path> <path d="M127.373 27.9426H132.088V20.2492C132.088 19.8769 132.119 19.5046 132.181 19.1944C132.491 17.7364 133.67 16.8057 135.407 16.8057C135.935 16.8057 136.338 16.8678 136.679 16.9608V12.4937C136.338 12.4316 136.121 12.4316 135.686 12.4316C134.228 12.4316 132.367 13.3623 131.592 15.5649H131.468L131.312 12.7729H127.249C127.311 14.0758 127.373 15.5338 127.373 17.7674V27.9426V27.9426Z"></path> <path d="M143.042 27.9424V12.7727H138.327V27.9424H143.042ZM140.685 6.16504C139.165 6.16504 138.172 7.18877 138.203 8.55373C138.172 9.85665 139.165 10.9114 140.654 10.9114C142.205 10.9114 143.197 9.85665 143.197 8.55373C143.166 7.18877 142.205 6.16504 140.685 6.16504Z"></path> <path d="M146.661 9.48442V12.7727H144.645V16.2472H146.661V22.4516C146.661 24.5921 147.095 26.0501 147.964 26.9498C148.708 27.7253 149.98 28.2837 151.5 28.2837C152.834 28.2837 153.951 28.0976 154.572 27.8494L154.541 24.2819C154.199 24.3439 153.734 24.4059 153.145 24.4059C151.718 24.4059 151.283 23.5684 151.283 21.7381V16.2472H154.665V12.7727H151.283V8.42969L146.661 9.48442Z"></path> <path d="M161.316 27.9424V12.7727H156.6V27.9424H161.316ZM158.958 6.16504C157.438 6.16504 156.445 7.18877 156.476 8.55373C156.445 9.85665 157.438 10.9114 158.927 10.9114C160.478 10.9114 161.471 9.85665 161.471 8.55373C161.44 7.18877 160.478 6.16504 158.958 6.16504V6.16504Z"></path> <path d="M175.11 24.0648C174.396 24.3751 173.528 24.5612 172.411 24.5612C170.115 24.5612 168.254 23.1032 168.254 20.3112C168.223 17.8294 169.836 16.0612 172.318 16.0612C173.559 16.0612 174.396 16.2784 174.955 16.5265L175.699 13.0211C174.707 12.6488 173.342 12.4316 172.101 12.4316C166.455 12.4316 163.415 16.0612 163.415 20.4973C163.415 25.2747 166.548 28.2838 171.449 28.2838C173.248 28.2838 174.831 27.9426 175.668 27.5703L175.11 24.0648Z"></path>
  1509. </svg>`
  1510. if (item.images.length > 0) {
  1511. img = `<img class="mcdiv123_cover" src="https://www.metacritic.com/a/img/${item.images[0].bucketType}${item.images[0].bucketPath}">`
  1512. }
  1513.  
  1514. let score = ''
  1515. if ('criticScoreSummary' in item && 'score' in item.criticScoreSummary && item.criticScoreSummary.score > 0) {
  1516. const bgColor = badgeColor(item.criticScoreSummary.score, item.type)
  1517. score = `<div class="mcdiv123_score_badge" style="background: ${bgColor}">${item.criticScoreSummary.score}</div>`
  1518. }
  1519.  
  1520. results.push(`
  1521. <div>
  1522. <div class="floatleft">
  1523. ${img}
  1524. </div>
  1525. <div class="floatleft resultcontent">
  1526. <a style="font-size:17px" href="${item.metacriticUrl}">
  1527. ${item.title}
  1528. </a>
  1529. <span style="font-weight:800;">${item.premiereYear ? item.premiereYear : (item.releaseDate ? item.releaseDate.substring(0, 4) : '')}</span>
  1530. ${score}
  1531. <div>
  1532. ${item.genres.map(g => g.name).join(' • ')}
  1533. <br>
  1534. <span class="mcdiv_release_date">${item.releaseDate ? item.releaseDate : ''}</span>
  1535. <div class="mcdiv_desc">
  1536. ${item.description}
  1537. </div>
  1538. </div>
  1539. </div>
  1540. <div class="floatleft mcdiv123_correct_entry" title="Assist us: This is the correct entry!">&check;</div>
  1541. <div class="clear:left"></div>
  1542. </div>
  1543. `
  1544. )
  1545. })
  1546.  
  1547. const websiteSearchUrl = `${baseURL}search/${encodeURIComponent(query)}/`
  1548.  
  1549. if (results && results.length > 0) {
  1550. // Show results
  1551. loader.remove()
  1552.  
  1553. const accept = function (ev) {
  1554. const parentDiv = $(this).closest('.result')
  1555. const a = parentDiv.find("a[href*='metacritic.com']")
  1556. const metaurl = a.attr('href')
  1557. const docurl = document.location.href
  1558.  
  1559. const resultDivParent = parentDiv.parent()
  1560. resultDivParent.html('')
  1561. resultDivParent.append(loader)
  1562.  
  1563. removeFromBlacklist(docurl, metaurl).then(function () {
  1564. addToMap(docurl, metaurl).then(function () {
  1565. current.metaurl = metaurl
  1566. loadMetacriticUrl().then(() => loader.remove())
  1567. })
  1568. })
  1569. }
  1570. const denyAll = function (ev) {
  1571. const docurl = document.location.href
  1572. $('#mcdiv123searchresults').find("div.result a[href*='metacritic.com']").each(function () {
  1573. addToBlacklist(docurl, this.href)
  1574. })
  1575. }
  1576.  
  1577. const resultdiv = $('#mcdiv123searchresults').length
  1578. ? $('#mcdiv123searchresults').html('')
  1579. : $('<div id="mcdiv123searchresults"></div>').appendTo(div)
  1580. results.forEach(function (html) {
  1581. const singleresult = $('<div class="result"></div>').html(fixMetacriticURLs(html) + '<div style="clear:left"></div>').appendTo(resultdiv)
  1582. singleresult.find('.mcdiv123_correct_entry').click(accept)
  1583. })
  1584. resultdiv.find('.metascore_w.album').removeClass('album') // Remove some classes
  1585. resultdiv.find('.must-see').remove() // Remove some elements
  1586.  
  1587. const sub = $('#mcdiv123 .sub').length ? $('#mcdiv123 .sub').html('') : $('<div class="sub"></div>').appendTo(div)
  1588. $('<a style="color:#b6b6b6; font-size: 11px;" target="_blank" href="' + websiteSearchUrl + '" title="Open Metacritic">' + decodeURI(websiteSearchUrl.replace('https://www.', '')) + '</a>').appendTo(sub)
  1589. $('<span title="Hide me" style="cursor:pointer; float:right; color:#b6b6b6; font-size: 11px;">&#10062;</span>').appendTo(sub).click(function () {
  1590. document.body.removeChild(this.parentNode.parentNode)
  1591. })
  1592. $('<span class="mcdiv123_incorrect" title="Assist us: None of the above is the correct item!">&cross;</span>').appendTo(sub).click(function () { if (confirm('None of the above is the correct item\nConfirm?')) denyAll() })
  1593. } else {
  1594. // No results
  1595. loader.remove()
  1596. const resultdiv = $('#mcdiv123searchresults').length ? $('#mcdiv123searchresults').html('') : $('<div id="mcdiv123searchresults"></div>').appendTo(div)
  1597. resultdiv.html('No search results.')
  1598.  
  1599. const sub = $('#mcdiv123 .sub').length ? $('#mcdiv123 .sub').html('') : $('<div class="sub"></div>').appendTo(div)
  1600. $('<a style="color:#b6b6b6; font-size: 11px;" target="_blank" href="' + websiteSearchUrl + '" title="Open Metacritic">' + decodeURI(websiteSearchUrl.replace('https://www.', '')) + '</a>').appendTo(sub)
  1601. $('<span title="Hide me" style="cursor:pointer; float:right; color:#b6b6b6; font-size: 11px;">&#10062;</span>').appendTo(sub).click(function () {
  1602. document.body.removeChild(this.parentNode.parentNode)
  1603. })
  1604. }
  1605. }
  1606.  
  1607. function showHoverInfo (response, orgMetaUrl) {
  1608. const html = fixMetacriticURLs(response.responseText)
  1609. const time = new Date(response.time)
  1610. const url = response.finalUrl
  1611.  
  1612. $('#mcdiv123').remove()
  1613. const div = $('<div id="mcdiv123"></div>').appendTo(document.body)
  1614. div.css({
  1615. bottom: 0,
  1616. left: 0
  1617. })
  1618.  
  1619. div.css('transform-origin', 'bottom left')
  1620. GM.getValue('position', false).then(function (s) {
  1621. if (s) {
  1622. div.css({
  1623. top: '',
  1624. left: '',
  1625. bottom: '',
  1626. right: ''
  1627. })
  1628. const parsedPosition = JSON.parse(s)
  1629. div.css(parsedPosition)
  1630. div.css('transform-origin', Object.keys(parsedPosition).join(' '))
  1631. }
  1632. })
  1633. onSizeChanged()
  1634.  
  1635. // Functions for communication between page and iframe
  1636. // Mozilla can access parent.document
  1637. // Chrome can use postMessage()
  1638. let frameStatus = false // if this remains false, loading the frame content failed. A reason could be "Content Security Policy"
  1639. async function tryToLoadMoreMetacriticDetails (myframe, myelement, platforms) {
  1640. console.log('ShowMetacriticRatings: tryToLoadMoreMetacriticDetails current', current)
  1641. if (!current.metaurl) {
  1642. return
  1643. }
  1644.  
  1645. let url = current.metaurl
  1646. if (url.endsWith('/')) {
  1647. url = url + 'details/'
  1648. } else {
  1649. url = url + '/details/'
  1650. }
  1651. url = url.replace('/game/pc/', '/game/').replace('/game/playstation-4/', '/game/').replace('/game/xbox-one/', '/game/')
  1652.  
  1653. const response = await asyncRequest({ url })
  1654. const doc = domParser().parseFromString(response.responseText, 'text/html')
  1655.  
  1656. const titleA = doc.querySelector('.c-productSubpageHeader_back')
  1657. if (titleA) {
  1658. titleA.querySelectorAll('.c-productSubpageHeader_backIcon').forEach(e => e.remove())
  1659. }
  1660. const titleHTML = titleA ? titleA.outerHTML : ''
  1661.  
  1662. let image = doc.querySelector('picture img')
  1663. if (!image) {
  1664. image = doc.createElement('img')
  1665. }
  1666.  
  1667. if (!image.getAttribute('src') && doc.querySelector('meta[name="twitter:image"]')) {
  1668. console.log('Using fallback image', doc.querySelector('meta[name="twitter:image"]').content)
  1669. image.src = doc.querySelector('meta[name="twitter:image"]').content
  1670. image.style.maxHeight = `${image.height}px`
  1671. image.removeAttribute('height')
  1672. image.removeAttribute('width')
  1673. } else if (!image.getAttribute('src') && response.responseText.match(/"image":"https:\/\/www.metacritic.com\/[^""]+/)) {
  1674. const m = response.responseText.match(/"image":"(https:\/\/www.metacritic.com\/[^""]+)/)
  1675. if (m) {
  1676. image.src = m[1]
  1677. image.style.maxHeight = `${image.height}px`
  1678. image.removeAttribute('height')
  1679. image.removeAttribute('width')
  1680. }
  1681. }
  1682. image.style.display = ''
  1683. const imageHTML = image.outerHTML
  1684.  
  1685. let detailsTable = Array.from(doc.querySelectorAll('.c-movieDetails_sectionContainer,.c-productionDetailsTv_sectionContainer,.c-gameDetails_sectionContainer'))
  1686. .map(e => Array.from(e.children).map(e => e.textContent.trim()))
  1687.  
  1688. detailsTable = detailsTable.filter(columns => {
  1689. if (columns[0].search(/release date/i) !== -1) {
  1690. return true
  1691. }
  1692. if (columns[0].search(/genres/i) !== -1) {
  1693. return true
  1694. }
  1695. if (columns[0].search(/developer/i) !== -1) {
  1696. return true
  1697. }
  1698. if (columns[0].search(/publisher/i) !== -1) {
  1699. return true
  1700. }
  1701. if (columns[0].search(/seasons/i) !== -1) {
  1702. return true
  1703. }
  1704. if (columns[0].search(/production/i) !== -1) {
  1705. return true
  1706. }
  1707. if (columns[0].search(/platforms/i) !== -1) {
  1708. return true
  1709. }
  1710.  
  1711. return false
  1712. }).map(columns => columns.join(': ').replace(/:\s*:\s*/, ': '))
  1713.  
  1714. const html = imageHTML + '<br>' + titleHTML + '<br>' + detailsTable.join('<br>')
  1715.  
  1716. if (myframe) {
  1717. myframe.contentWindow.postMessage({
  1718. mcimessage_addhtml: true,
  1719. mcimessage_element_id: 'metacritic_extra_data',
  1720. mcimessage_element_style: 'display:none;',
  1721. mcimessage_html: html
  1722. }, '*')
  1723.  
  1724. // Wait to show the extra data to avoid making the frame to big
  1725. window.setTimeout(function () {
  1726. myframe.contentWindow.postMessage({
  1727. mcimessage_showelement: true,
  1728. mcimessage_selector: '#metacritic_extra_data'
  1729. }, '*')
  1730. myframe.contentWindow.postMessage({
  1731. mcimessage_showelement: true,
  1732. mcimessage_selector: '.game_row_5456d45'
  1733. }, '*')
  1734. }, 1000)
  1735. } else {
  1736. const extraDiv = myelement.appendChild(document.createElement('div'))
  1737. extraDiv.setAttribute('id', 'metacritic_extra_data')
  1738. extraDiv.setAttribute('style', 'display:none;')
  1739. extraDiv.innerHTML = html
  1740. window.setTimeout(() => { extraDiv.style.display = '' }, 500)
  1741.  
  1742. // For gamesshow the other platforms (Wait to show the extra data to avoid making the frame to big)
  1743. window.setTimeout(() => {
  1744. myelement.querySelectorAll('.game_row_5456d45').forEach(e => { e.style.display = '' })
  1745. myelement.scrollTo(0, 0)
  1746. }, 500)
  1747. }
  1748.  
  1749. // For games, try to load user score for other platforms
  1750. platforms.forEach(async function (platformCriticsUrl) {
  1751. const url = platformCriticsUrl.replace('/critic-reviews', '/user-reviews')
  1752. const response = await asyncRequest({ url })
  1753. const doc = domParser().parseFromString(response.responseText, 'text/html')
  1754.  
  1755. const userScoreNode = doc.querySelector('.c-ScoreCardLeft_scoreContent_number')
  1756.  
  1757. if (myframe) {
  1758. myframe.contentWindow.postMessage({
  1759. mcimessage_appendchild: true,
  1760. mcimessage_selector: `.game_row_5456d45 a[href*="${platformCriticsUrl}"]`,
  1761. mcimessage_html: userScoreNode.outerHTML
  1762. }, '*')
  1763. } else {
  1764. myelement.querySelector(`.game_row_5456d45 a[href*="${platformCriticsUrl}"]`).appendChild(userScoreNode)
  1765. myelement.scrollTo(0, 0)
  1766. }
  1767. })
  1768. }
  1769.  
  1770. function loadExternalImage (url, myframe) {
  1771. // Load external image, bypass CSP
  1772. GM.xmlHttpRequest({
  1773. method: 'GET',
  1774. url,
  1775. responseType: 'arraybuffer',
  1776. onload: function (response) {
  1777. myframe.contentWindow.postMessage({
  1778. mcimessage_imgLoaded: true,
  1779. mcimessage_imgData: response.response,
  1780. mcimessage_imgOrgSrc: url
  1781. }, '*')
  1782. }
  1783. })
  1784. }
  1785. const functions = {
  1786. parent: function () {
  1787. const f = parent.document.getElementById('mciframe123')
  1788. let lastdiff = -200000
  1789. window.addEventListener('message', function (e) {
  1790. if (typeof e.data !== 'object') {
  1791. return
  1792. } else if ('mcimessage0' in e.data) {
  1793. frameStatus = true // Frame content was loaded successfully
  1794. let platforms = []
  1795. if ('mcimessage_platforms' in e.data) {
  1796. platforms = e.data.mcimessage_platforms
  1797. }
  1798. tryToLoadMoreMetacriticDetails(f, null, platforms)
  1799. } else if ('mcimessage1' in e.data) {
  1800. f.style.width = parseInt(f.style.width) + 5 + 'px'
  1801. if (e.data.heightdiff === lastdiff) {
  1802. f.style.height = parseInt(f.style.height) + 10 + 'px'
  1803. }
  1804. lastdiff = e.data.heightdiff
  1805. } else if ('mcimessage2' in e.data) {
  1806. f.style.height = parseInt(f.style.height) + 10 + 'px'
  1807. } else if ('mcimessage_loadImg' in e.data && e.data.mcimessage_imgUrl) {
  1808. loadExternalImage(e.data.mcimessage_imgUrl, f)
  1809. } else {
  1810. return
  1811. }
  1812. if (f.contentWindow != null) {
  1813. f.contentWindow.postMessage({
  1814. mcimessage3: true,
  1815. mciframe123_clientHeight: f.clientHeight,
  1816. mciframe123_clientWidth: f.clientWidth
  1817. }, '*')
  1818. }
  1819. })
  1820. },
  1821. frame: function () {
  1822. let currentPlatform = 'playstation'
  1823. if (document.getElementById('mci_current_platform')) {
  1824. currentPlatform = document.getElementById('mci_current_platform').value
  1825. }
  1826.  
  1827. const platforms = Array.from(document.querySelectorAll('.game_row_5456d45 a[href^="https://www.metacritic.com/game/"][href*="critic-reviews"]'))
  1828. .filter(a => !a.href.includes(`platform=${currentPlatform}`)).map(a => a.href.toString())
  1829.  
  1830. parent.postMessage({ mcimessage0: true, mcimessage_platforms: platforms }, '*') // Loading frame content was successfull
  1831.  
  1832. let i = 0
  1833. window.addEventListener('message', function (e) {
  1834. if (typeof e.data === 'object' && 'mcimessage_imgLoaded' in e.data) {
  1835. // Load external image
  1836. const arrayBufferView = new Uint8Array(e.data.mcimessage_imgData)
  1837. const blob = new Blob([arrayBufferView], { type: 'image/jpeg' })
  1838. const urlCreator = window.URL || window.webkitURL
  1839. const imageUrl = urlCreator.createObjectURL(blob)
  1840. const img = failedImages[e.data.mcimessage_imgOrgSrc]
  1841. img.src = imageUrl
  1842. }
  1843. if (typeof e.data === 'object' && 'mcimessage_addhtml' in e.data) {
  1844. const div = document.body.appendChild(document.createElement('div'))
  1845. div.setAttribute('id', e.data.mcimessage_element_id)
  1846. div.setAttribute('style', e.data.mcimessage_element_style)
  1847. div.innerHTML = e.data.mcimessage_html
  1848. }
  1849. if (typeof e.data === 'object' && 'mcimessage_showelement' in e.data) {
  1850. document.querySelectorAll(e.data.mcimessage_selector).forEach(node => { node.style.display = '' })
  1851. }
  1852. if (typeof e.data === 'object' && 'mcimessage_appendchild' in e.data) {
  1853. const node = document.body.querySelector(e.data.mcimessage_selector).appendChild(document.createElement('div'))
  1854. node.outerHTML = e.data.mcimessage_html
  1855. }
  1856.  
  1857. if (!('mcimessage3' in e.data)) return
  1858.  
  1859. if (e.data.mciframe123_clientHeight < document.body.scrollHeight && i < 100) {
  1860. parent.postMessage({ mcimessage2: 1 }, '*')
  1861. i++
  1862. }
  1863. if (i >= 100) {
  1864. parent.postMessage({ mcimessage1: 1, heightdiff: document.body.scrollHeight - e.data.mciframe123_clientHeight }, '*')
  1865. i = 0
  1866. }
  1867. })
  1868. parent.postMessage({ mcimessage1: 1, heightdiff: -100000 }, '*')
  1869. }
  1870.  
  1871. }
  1872.  
  1873. const css = `
  1874. #hover_div_a20230915{font-family:sans-serif;color:#262626;font-size:1rem;line-height:1.625rem}#hover_div_a202309:hover15 a,#hover_div_a20230915 a:hover{text-decoration:none}#hover_div_a20230915 a:hover{color:#09f}#hover_div_a20230915 a{color:#000;text-decoration:none;}#hover_div_a20230915 a:focus{color:grey}#hover_div_a20230915 .g-border-black,#hover_div_a20230915 .g-border-gray100{border-color:#000}#hover_div_a20230915 .g-color-black,#hover_div_a20230915 .g-color-gray100{color:#000}#hover_div_a20230915 .g-border-gray98{border-color:#191919}#hover_div_a20230915 .g-color-gray98{color:#191919}#hover_div_a20230915 .g-border-gray90{border-color:#262626}#hover_div_a20230915 .g-color-gray90{color:#262626}#hover_div_a20230915 .g-border-gray80{border-color:#404040}#hover_div_a20230915 .g-color-gray80{color:#404040}#hover_div_a20230915 .g-border-gray70{border-color:#666}#hover_div_a20230915 .g-color-gray70{color:#666}#hover_div_a20230915 .g-border-gray60{border-color:grey}#hover_div_a20230915 .g-color-gray60{color:grey}#hover_div_a20230915 .g-border-gray50{border-color:#999}#hover_div_a20230915 .g-color-gray50{color:#999}#hover_div_a20230915 .g-border-gray40{border-color:#bfbfbf}#hover_div_a20230915 .g-color-gray40{color:#bfbfbf}#hover_div_a20230915 .g-border-gray30{border-color:#d8d8d8}#hover_div_a20230915 .g-color-gray30{color:#d8d8d8}#hover_div_a20230915 .g-border-gray20{border-color:#e6e6e6}#hover_div_a20230915 .g-color-gray20{color:#e6e6e6}#hover_div_a20230915 .g-border-gray10{border-color:#f2f2f2}#hover_div_a20230915 .g-color-gray10{color:#f2f2f2}#hover_div_a20230915 .g-border-gray0,#hover_div_a20230915 .g-border-white{border-color:#fff}#hover_div_a20230915 .g-color-gray0,#hover_div_a20230915 .g-color-white{color:#fff}#hover_div_a20230915 .g-border-red{border-color:#eb0036}#hover_div_a20230915 .g-color-red{color:#eb0036}#hover_div_a20230915 .g-border-green{border-color:#01b44f}#hover_div_a20230915 .g-color-green{color:#01b44f}#hover_div_a20230915 .g-width-large{width:1.5rem}#hover_div_a20230915 .g-height-large{height:1.5rem}#hover_div_a20230915 .g-width-100{width:100%}#hover_div_a20230915 .g-height-100{height:100%}#hover_div_a20230915 .g-text-large{font-size:1.5rem;line-height:2rem}#hover_div_a20230915 .g-text-xxsmall{font-size:xx-small}#hover_div_a20230915 .g-text-bold{font-weight:700}#hover_div_a20230915 .g-text-link{text-decoration:underline}#hover_div_a20230915 .u-block{display:block}#hover_div_a20230915 .u-flexbox{display:flex}#hover_div_a20230915 .u-flexbox-column{display:flex;flex-direction:column}#hover_div_a20230915 .u-flexbox-justifyCenter{justify-content:center}#hover_div_a20230915 .u-flexbox-alignCenter{align-items:center}#hover_div_a20230915 .u-grid{display:grid;grid-gap:0;grid-gap:var(--grid-gap,0)}#hover_div_a20230915 .u-grid-2column{-ms-grid-columns:50% 50%;display:grid;grid-template:auto/repeat(2,1fr)}#hover_div_a20230915 .u-grid-3column{-ms-grid-columns:33.3% 33.3% 33.3%;display:grid;grid-template:auto/repeat(3,1fr)}#hover_div_a20230915 .u-grid-4column{-ms-grid-columns:25% 25% 25% 25%;display:grid;grid-template:auto/repeat(4,1fr)}#hover_div_a20230915 .u-grid-5column{-ms-grid-columns:20% 20% 20% 20% 20%;display:grid;grid-template:auto/repeat(5,1fr)}#hover_div_a20230915 .u-grid-7column{-ms-grid-columns:14.2857% 14.2857% 14.2857% 14.2857% 14.2857% 14.2857% 14.2857%;display:grid;grid-template:auto/repeat(7,1fr)}#hover_div_a20230915 .u-grid-column-span2{grid-column-end:span 2}#hover_div_a20230915 .u-grid-column-span3{grid-column-end:span 3}#hover_div_a20230915 .u-grid-column-span4{grid-column-end:span 4}#hover_div_a20230915 .u-text-center{text-align:center}#hover_div_a20230915 .c-siteReviewScore_large{border-radius:0.5rem;height:4rem;width:4rem;font-size:2rem}#hover_div_a20230915 .c-siteReviewScore_user{border-radius:50%}#hover_div_a20230915 .c-reviewsStats{padding:1rem 0;grid-template-columns:1fr 1fr 1fr;justify-content:space-evenly;font-size:0.75rem;line-height:1.25rem}#hover_div_a20230915 div[class^=c-reviewsStats_]:first-child,#hover_div_a20230915 div[class^=c-reviewsStats_]:nth-child(2){border-right:0.0625rem solid #d8d8d8}#hover_div_a20230915 .c-ScoreCardGraph{overflow:hidden;white-space:nowrap}#hover_div_a20230915 .c-ScoreCardGraph > div{margin-left:0.25rem;padding:0 0.25rem;text-align:right;height:0.5rem;min-width:2rem;line-height:1rem}#hover_div_a20230915 .c-ScoreCardGraph > div:first-child{margin-left:0}#hover_div_a20230915 .c-ScoreCardGraph_scoreTitle{letter-spacing:0.25rem}#hover_div_a20230915 .c-ScoreCardGraph_scoreSentiment{color:#00ce7a}#hover_div_a20230915 .c-ScoreCardGraph_scoreGraphPositive{background:#00ce7a;border-radius:0.25rem 0 0 0.25rem}#hover_div_a20230915 .c-ScoreCardGraph_scoreGraphNeutral{background:#ffbd3f}#hover_div_a20230915 .c-ScoreCardGraph_scoreGraphNegative{background:#ff6874;border-radius:0 0.25rem 0.25rem 0}#hover_div_a20230915 .gray{background:#bfbfbf;height:1rem;display:inline-block}#hover_div_a20230915 .c-ScoreCard_scoreContent{display:flex;align-content:flex-start;flex-wrap:nowrap;grid-gap:10px;gap:10px;width:100%;justify-content:space-between;align-items:stretch}#hover_div_a20230915 .c-ScoreCard_scoreContent_text{line-height:normal;display:flex;flex-direction:column;justify-content:space-between}#hover_div_a20230915 .c-ScoreCard_scoreContent_number > .c-siteReviewScore_background-critic_large,#hover_div_a20230915 .c-ScoreCard_scoreContent_number > .c-siteReviewScore_background-critic_large .c-siteReviewScore_large{width:4rem;height:4rem}#hover_div_a20230915 .c-ScoreCard_scoreSentiment{font-size:1rem;line-height:1.25rem;text-transform:capitalize}#hover_div_a20230915 .c-ScoreCard_scoreTitle{letter-spacing:0.25rem}#hover_div_a20230915 .c-reviewsOverview_overviewDetails{grid-template-columns:1fr 1fr;grid-gap:1.25rem;border-top:1px solid #262626;margin-top:auto;padding:2px}#hover_div_a20230915 .c-reviewsOverview_overviewDetails:first-child{border-top:0 solid #262626}#hover_div_a20230915 .c-siteReviewScore_green{background:#00ce7a}#hover_div_a20230915 .c-siteReviewScore_yellow{background:#ffbd3f}#hover_div_a20230915 .c-siteReviewScore_red{background:#ff6874}#hover_div_a20230915 .c-siteReviewScore_grey{background:#404040}#hover_div_a20230915 .c-siteReviewScore_tbdCritic,#hover_div_a20230915 .c-siteReviewScore_tbdUser{border-width:0.125rem;border-style:solid}#hover_div_a20230915 .o-inlineScore{border-radius:0.25rem;font-size:1.25rem;font-weight:700;color:#404040;width:2.5rem;height:2.5rem;display:inline-flex;justify-content:center;align-items:center;text-decoration:none!important}#hover_div_a20230915 .o-inlineScore-green{background:#00ce7a}#hover_div_a20230915 .o-inlineScore-yellow{background:#ffbd3f}#hover_div_a20230915 .o-inlineScore-red{background:#ff6874}#hover_div_a20230915 .o-inlineScore-tbd{border:1px solid grey}#hover_div_a20230915 .u-pointer{cursor:pointer}#hover_div_a20230915 .c-siteReviewScore_green{background:#00ce7a}#hover_div_a20230915 .c-siteReviewScore_yellow{background:#ffbd3f}#hover_div_a20230915 .c-siteReviewScore_red{background:#ff6874}#hover_div_a20230915 .c-siteReviewScore_grey{background:#404040}#hover_div_a20230915 .c-siteReviewScore_tbdCritic,#hover_div_a20230915 .c-siteReviewScore_tbdUser{border-width:0.125rem;border-style:solid}#hover_div_a20230915{max-width:440px}
  1875.  
  1876. .mci_current_platform_title {
  1877. padding:0px;
  1878. margin: -8px 0px -5px 0px;
  1879. font-size: 12px;
  1880. }
  1881.  
  1882. .game_row_5456d45 .c-gamePlatformTile {
  1883. border-radius: .game_row_5456d45 .375rem;
  1884. box-shadow: 0 .1875rem .625rem rgba(0,0,0,.16);
  1885. gap: .game_row_5456d45 .75rem;
  1886. grid-template-columns: 1fr 70px 70px;
  1887. padding: 1rem;
  1888. }
  1889.  
  1890. .game_row_5456d45 .c-gamePlatformTile-description {
  1891. gap: .game_row_5456d45.75rem;
  1892. grid-template-rows: 1fr;
  1893. }
  1894.  
  1895. .game_row_5456d45 .g-outer-spacing-right-xsmall {
  1896. margin-right: .25rem;
  1897. }
  1898.  
  1899. .game_row_5456d45 .c-siteReviewScore_medium {
  1900. border-radius: 6px;
  1901. font-size: 2.25rem;
  1902. height: 4rem;
  1903. line-height: 2.5rem;
  1904. width: 4rem;
  1905. }
  1906.  
  1907. .game_row_5456d45 .c-siteReviewScore_yellow {
  1908. background: #ffbd3f;
  1909. }
  1910.  
  1911. .game_row_5456d45 .u-flexbox-alignCenter {
  1912. align-items: center;
  1913. }
  1914.  
  1915. .game_row_5456d45 .u-flexbox-justifyCenter {
  1916. justify-content: center;
  1917. }
  1918.  
  1919. .game_row_5456d45 .u-flexbox-column {
  1920. display: flex;
  1921. flex-direction: column;
  1922. }
  1923.  
  1924. .game_row_5456d45 .g-text-bold {
  1925. font-weight: 700;
  1926. }
  1927.  
  1928. .game_row_5456d45 .g-color-gray90 {
  1929. color: #262626;
  1930. }
  1931.  
  1932. .game_row_5456d45 .c-siteReviewScore_medium {
  1933. font-size: 2.25rem;
  1934. line-height: 2.5rem;
  1935. }
  1936. `
  1937.  
  1938. const cssDark = `
  1939. html {
  1940. scrollbar-color: #003c09 #00ce7a;
  1941. }
  1942. *::-webkit-scrollbar-thumb {
  1943. background-color: #003c09;
  1944. }
  1945. body {
  1946. background:#262626;
  1947. color:white;
  1948. }
  1949.  
  1950. #metacritic_extra_data {
  1951. color:white;
  1952. }
  1953. #metacritic_extra_data a:hover {
  1954. color: white;
  1955. }
  1956. #metacritic_extra_data a {
  1957. color: #5799ef;
  1958. text-decoration: none;
  1959. }
  1960.  
  1961. #hover_div_a20230915 {
  1962. color: #d1d1d1;
  1963. }
  1964. #hover_div_a20230915 a:hover {
  1965. color: #09f;
  1966. }
  1967. #hover_div_a20230915 a {
  1968. color: #ffffff;
  1969. text-decoration: none;
  1970. }
  1971. #hover_div_a20230915 a:focus {
  1972. color: rgb(184, 184, 184);
  1973. }
  1974. #hover_div_a20230915 .g-border-black,
  1975. #hover_div_a20230915 .g-border-gray100 {
  1976. border-color: #ffffff;
  1977. }
  1978. #hover_div_a20230915 .g-color-black,
  1979. #hover_div_a20230915 .g-color-gray100 {
  1980. color: #ffffff;
  1981. }
  1982. #hover_div_a20230915 .g-border-gray98 {
  1983. border-color: #d6d6d6;
  1984. }
  1985. #hover_div_a20230915 .g-color-gray98 {
  1986. color: #d6d6d6;
  1987. }
  1988. #hover_div_a20230915 .g-border-gray90 {
  1989. border-color: #d3d3d3;
  1990. }
  1991. #hover_div_a20230915 .g-color-gray90 {
  1992. color: #d3d3d3;
  1993. }
  1994. #hover_div_a20230915 .g-border-gray80 {
  1995. border-color: #404040;
  1996. }
  1997. #hover_div_a20230915 .g-color-gray80 {
  1998. color: #404040;
  1999. }
  2000. #hover_div_a20230915 .g-border-gray70 {
  2001. border-color: #666;
  2002. }
  2003. #hover_div_a20230915 .g-color-gray70 {
  2004. color: #666;
  2005. }
  2006. #hover_div_a20230915 .g-border-gray60 {
  2007. border-color: grey;
  2008. }
  2009. #hover_div_a20230915 .g-color-gray60 {
  2010. color: grey;
  2011. }
  2012. #hover_div_a20230915 .c-reviewsOverview_overviewDetails {
  2013. border-top: 1px solid #d1d1d1;
  2014. }
  2015. #hover_div_a20230915 .c-reviewsOverview_overviewDetails:first-child {
  2016. border-top: 0 solid #d1d1d1;
  2017. }
  2018. #hover_div_a20230915 .c-siteReviewScore_grey {
  2019. background: #404040;
  2020. }
  2021. #hover_div_a20230915 .o-inlineScore {
  2022. color: #404040;
  2023. }
  2024. #hover_div_a20230915 .o-inlineScore-tbd {
  2025. border: 1px solid grey;
  2026. }
  2027. #hover_div_a20230915 .u-pointer {
  2028. cursor: pointer;
  2029. }
  2030. #hover_div_a20230915 .c-siteReviewScore_grey {
  2031. background: #404040;
  2032. }
  2033. `
  2034.  
  2035. let framesrc = 'data:text/html,'
  2036. framesrc += encodeURIComponent(`<!DOCTYPE html>
  2037. <html lang="en">
  2038. <head>
  2039. <meta charset="utf-8">
  2040. <title>Metacritic info</title>
  2041. <style>
  2042. html {
  2043. scrollbar-width: thin;
  2044. scrollbar-color: #dfdfdf white;
  2045. }
  2046. /* Chrome, Edge, and Safari */
  2047. *::-webkit-scrollbar {
  2048. width: 10px;
  2049. }
  2050.  
  2051. *::-webkit-scrollbar-track {
  2052. background: transparent;
  2053. }
  2054.  
  2055. *::-webkit-scrollbar-thumb {
  2056. background-color: #dfdfdf;
  2057. border-radius: 10px;
  2058. border: 0px none transparent;
  2059. }
  2060.  
  2061. body {
  2062. margin:0px;
  2063. padding:0px;
  2064. background:white;
  2065. }
  2066.  
  2067. ${css}
  2068.  
  2069. #metacritic_extra_data {
  2070. font-family: sans-serif;
  2071. color:black;
  2072. }
  2073.  
  2074. #metacritic_extra_data a:hover {
  2075. color: #054585;
  2076. }
  2077. #metacritic_extra_data a {
  2078. color: #06c;
  2079. text-decoration: none;
  2080. }
  2081.  
  2082. @media (prefers-color-scheme: dark) {
  2083. ${cssDark}
  2084. }
  2085.  
  2086. ${
  2087. darkTheme ? cssDark : ''
  2088. }
  2089.  
  2090. </style>
  2091. <script>
  2092. const failedImages = {};
  2093. function detectCSP(img) {
  2094. if(img.complete && (!img.naturalWidth || !img.naturalHeight)) {
  2095. return true;
  2096. }
  2097. return false;
  2098. }
  2099. function findCSPerrors() {
  2100. const imgs = document.querySelectorAll("img");
  2101. for(let i = 0; i < imgs.length; i++) {
  2102. if(imgs[i].complete && detectCSP(imgs[i])) {
  2103. fixCSP(imgs[i]);
  2104. }
  2105. }
  2106. }
  2107. function fixCSP(img) {
  2108. console.debug("ShowMetacriticRatings(iFrame): Loading image failed. Bypassing CSP...", img);
  2109. if (img.getAttribute('src')) {
  2110. failedImages[img.src] = img;
  2111. parent.postMessage({"mcimessage_loadImg":true, "mcimessage_imgUrl": img.src},"*");
  2112. }
  2113. }
  2114. function on_load() {
  2115. (${functions.frame.toString()})();
  2116. window.setTimeout(findCSPerrors, 500);
  2117. }
  2118. </script>
  2119. </head>
  2120. <body onload="on_load();">
  2121. <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">
  2122. <div class="hover_content">${html}</div>
  2123. </div>
  2124. </body>
  2125. </html>`)
  2126.  
  2127. const frame = $('<iframe></iframe>')
  2128. frame.attr('id', 'mciframe123')
  2129. frame.attr('src', framesrc)
  2130. frame.attr('scrolling', 'auto')
  2131. frame.css({
  2132. width: 440,
  2133. height: 110,
  2134. border: 'none',
  2135. opacity: '0.1',
  2136. transition: 'opacity 1s'
  2137. })
  2138. frame.appendTo(div)
  2139. window.setTimeout(function () {
  2140. frame.css('opacity', '1.0')
  2141. }, 1000)
  2142.  
  2143. window.setTimeout(function () {
  2144. if (!frameStatus) { // Loading frame content failed.
  2145. // Directly inject the html without an iframe (this may break the site or the metacritic)
  2146. console.debug('ShowMetacriticRatings: Loading iframe content failed. Injecting directly.')
  2147. $('head').append(`<style>${css}\n\n@media (prefers-color-scheme: dark) {\n${cssDark}\n}\n</style>`)
  2148. 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">
  2149. <div class="hover_content">${html}</div>
  2150. </div>`)
  2151. frame.replaceWith(noframe)
  2152.  
  2153. const frameElement = noframe[0]
  2154.  
  2155. let currentPlatform = 'playstation'
  2156. if (frameElement.querySelector('#mci_current_platform')) {
  2157. currentPlatform = frameElement.querySelector('#mci_current_platform').value
  2158. }
  2159. const platforms = Array.from(frameElement.querySelectorAll('.game_row_5456d45 a[href^="https://www.metacritic.com/game/"][href*="critic-reviews"]'))
  2160. .filter(a => !a.href.includes(`platform=${currentPlatform}`)).map(a => a.href.toString())
  2161.  
  2162. document.querySelector('#mcdiv123').style.maxHeight = '230px'
  2163.  
  2164. tryToLoadMoreMetacriticDetails(null, frameElement, platforms)
  2165. }
  2166. }, 2000)
  2167.  
  2168. functions.parent()
  2169.  
  2170. const sub = $('<div></div>').appendTo(div)
  2171. $('<time style="color:#b6b6b6; font-size: 11px;" datetime="' + time + '" title="' + time.toLocaleTimeString() + ' ' + time.toLocaleDateString() + '">' + minutesSince(time) + '</time>').appendTo(sub)
  2172. $('<a style="color:#b6b6b6; font-size: 11px;" target="_blank" href="' + url + '" title="Open Metacritic">' + decodeURI(url.replace('https://www.', '@')) + '</a>').appendTo(sub)
  2173. $('<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 () {
  2174. const metaurl = $(this).data('url')
  2175. addToTemporaryBlacklist(metaurl)
  2176. document.body.removeChild(this.parentNode.parentNode)
  2177. })
  2178.  
  2179. $('<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 () {
  2180. const docurl = document.location.href
  2181. const metaurl = $(this).data('url')
  2182. addToMap(docurl, metaurl).then(function (r) {
  2183. balloonAlert('Thanks for your submission!\n\nSaved as a correct entry.\n\n' + r[0] + '\n' + r[1], 6000, 'Success')
  2184. })
  2185. })
  2186. $('<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 () {
  2187. if (!confirm('This is NOT the correct entry!\n\nAdd to blacklist?')) return
  2188. const docurl = document.location.href
  2189. const metaurl = $(this).data('url')
  2190. addToBlacklist(docurl, metaurl).then(function (r) {
  2191. balloonAlert('Thanks for your submission!\n\nSaved to blacklist.\n\n' + r[0] + '\n' + r[1], 6000, 'Success')
  2192. })
  2193.  
  2194. openSearchBox(true)
  2195. })
  2196.  
  2197. // Store response in cache:
  2198. if (!('cached' in response)) {
  2199. storeInHoverCache(current.metaurl, response, orgMetaUrl)
  2200. }
  2201. }
  2202.  
  2203. function metacriticGeneralProductSetup () {
  2204. current.broadenCounter = 0
  2205. }
  2206.  
  2207. const metacritic = {
  2208. mapped: function metacriticMapped (docurl, product, metaurl, type, searchTerm) {
  2209. // url was in the map/whitelist
  2210. current.data = searchTerm ? [searchTerm] : []
  2211. current.docurl = docurl
  2212. current.product = product
  2213. current.metaurl = metaurl
  2214. current.type = type
  2215. current.searchTerm = searchTerm || null
  2216. loadMetacriticUrl()
  2217. },
  2218. music: function metacriticMusic (docurl, product, artistname, albumname) {
  2219. current.data = [albumname.trim(), artistname.trim()]
  2220. artistname = name2metacritic(artistname)
  2221. albumname = albumname.replace('&', ' ')
  2222. albumname = name2metacritic(albumname)
  2223. current.docurl = docurl
  2224. current.product = product
  2225. current.metaurl = baseURLmusic + albumname + '/' + artistname
  2226. current.type = 'music'
  2227. current.searchTerm = albumname + '/' + artistname
  2228. loadMetacriticUrl()
  2229. },
  2230. movie: function metacriticMovie (docurl, product, moviename) {
  2231. current.data = [moviename.trim()]
  2232. moviename = name2metacritic(moviename)
  2233. current.docurl = docurl
  2234. current.product = product
  2235. current.metaurl = baseURLmovie + moviename
  2236. current.type = 'movie'
  2237. current.searchTerm = moviename
  2238. loadMetacriticUrl()
  2239. },
  2240. tv: function metacriticTv (docurl, product, seriesname) {
  2241. current.data = [seriesname.trim()]
  2242. seriesname = name2metacritic(seriesname)
  2243. current.docurl = docurl
  2244. current.product = product
  2245. current.metaurl = baseURLtv + seriesname
  2246. current.type = 'tv'
  2247. current.searchTerm = seriesname
  2248. loadMetacriticUrl()
  2249. },
  2250. pcgame: function metacriticPcgame (docurl, product, gamename) {
  2251. current.data = [gamename.trim()]
  2252. gamename = name2metacritic(gamename)
  2253. current.docurl = docurl
  2254. current.product = product
  2255. current.metaurl = baseURLpcgame + gamename
  2256. current.type = 'pcgame'
  2257. current.searchTerm = gamename
  2258. loadMetacriticUrl()
  2259. },
  2260. ps4game: function metacriticPs4game (docurl, product, gamename) {
  2261. current.data = [gamename.trim()]
  2262. gamename = name2metacritic(gamename)
  2263. current.docurl = docurl
  2264. current.product = product
  2265. current.metaurl = baseURLps4 + gamename
  2266. current.type = 'ps4game'
  2267. current.searchTerm = gamename
  2268. loadMetacriticUrl()
  2269. },
  2270. xonegame: function metacriticXonegame (docurl, product, gamename) {
  2271. current.data = [gamename.trim()]
  2272. gamename = name2metacritic(gamename)
  2273. current.docurl = docurl
  2274. current.product = product
  2275. current.metaurl = baseURLxone + gamename
  2276. current.type = 'xonegame'
  2277. current.searchTerm = gamename
  2278. loadMetacriticUrl()
  2279. }
  2280. }
  2281.  
  2282. const Always = () => true
  2283. const sites = {
  2284. bandcamp: {
  2285. host: ['bandcamp.com'],
  2286. condition: () => unsafeWindow && unsafeWindow.TralbumData && unsafeWindow.TralbumData.current,
  2287. products: [{
  2288. condition: Always,
  2289. type: 'music',
  2290. data: () => [unsafeWindow.TralbumData.artist, unsafeWindow.TralbumData.current.title]
  2291. }]
  2292. },
  2293. itunes: {
  2294. host: ['itunes.apple.com'],
  2295. condition: Always,
  2296. products: [{
  2297. condition: () => ~document.location.href.indexOf('/movie/'),
  2298. type: 'movie',
  2299. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  2300. },
  2301. {
  2302. condition: () => ~document.location.href.indexOf('/tv-season/'),
  2303. type: 'tv',
  2304. data: function () {
  2305. let name = parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  2306. if (~name.indexOf(', Season')) {
  2307. name = name.split(', Season')[0]
  2308. }
  2309. return name
  2310. }
  2311. },
  2312. {
  2313. condition: () => ~document.location.href.indexOf('/album/'),
  2314. type: 'music',
  2315. data: function () {
  2316. const ld = parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum'))
  2317. const album = ld[0]
  2318. const artist = 'name' in ld[1] ? ld[1].name : ld[1].map(x => x.name).join(' ')
  2319. return [artist, album]
  2320. }
  2321. }]
  2322. },
  2323. 'music.apple': {
  2324. host: ['music.apple.com'],
  2325. condition: Always,
  2326. products: [{
  2327. condition: () => ~document.location.href.indexOf('/album/') && parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum')).length > 1,
  2328. type: 'music',
  2329. data: function () {
  2330. const ld = parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum'))
  2331. const album = ld[0]
  2332. const artist = 'name' in ld[1] ? ld[1].name : ld[1].map(x => x.name).join(' ')
  2333. return [artist, album]
  2334. }
  2335. }]
  2336. },
  2337. googleplay: {
  2338. host: ['play.google.com'],
  2339. condition: Always,
  2340. products: [
  2341. {
  2342. condition: () => ~document.location.href.indexOf('/album/'),
  2343. type: 'music',
  2344. data: () => [document.querySelector('[itemprop="byArtist"] meta[itemprop="name"]').content, document.querySelector('[itemtype="https://schema.org/MusicAlbum"] meta[itemprop="name"]').content]
  2345. },
  2346. {
  2347. condition: () => ~document.location.href.indexOf('/movies/details/'),
  2348. type: 'movie',
  2349. data: () => document.querySelector('*[itemprop=name]').textContent
  2350. }
  2351. ]
  2352. },
  2353. imdb: {
  2354. host: ['imdb.com'],
  2355. condition: () => !~document.location.pathname.indexOf('/mediaviewer') && !~document.location.pathname.indexOf('/mediaindex') && !~document.location.pathname.indexOf('/videoplayer'),
  2356. products: [
  2357. {
  2358. condition: () => document.querySelector('a[href*="/criticreviews/"'),
  2359. type: 'mapped',
  2360. data: async function () {
  2361. // This is used if there is a metacritic link on the imdb page
  2362. const criticsUrl = document.querySelector('a[href*="/criticreviews/"').href.toString()
  2363. const response = await asyncRequest({ url: criticsUrl }).catch(function (response) {
  2364. console.warn('ShowMetacriticRatings: Error imdb01\nurl=' + criticsUrl + '\nstatus=' + response.status)
  2365. })
  2366. const m = response.responseText.match(/(https:\/\/www\.metacritic\.com\/(\w+)\/[^?&"']+)/)
  2367. console.debug('ShowMetacriticRatings: Metacritic link found on imdb:', m[2], m[1])
  2368. const query = document.querySelector('[data-testid="hero__pageTitle"]') ? document.querySelector('[data-testid="hero__pageTitle"]').textContent : null
  2369. return [m[1], m[2], query]
  2370. }
  2371. },
  2372. {
  2373. condition: function () {
  2374. const e = document.querySelector("meta[property='og:type']")
  2375. if (e && e.content === 'video.movie') {
  2376. return true
  2377. } else if (document.querySelector('[data-testid="hero__pageTitle"]') && !document.querySelector('[data-testid="hero-subnav-bar-left-block"] a[href*="episodes/"]')) {
  2378. return true
  2379. }
  2380. return false
  2381. },
  2382. type: 'movie',
  2383. data: async function () {
  2384. // If the page is not in English or the browser is not in English, request page in English.
  2385. // Then the title in <h1> will be the English title and Metacritic always uses the English title.
  2386. if (document.querySelector('[for="nav-language-selector"]').textContent.toLowerCase() !== 'en' || !navigator.language.startsWith('en')) {
  2387. const imdbID = document.location.pathname.match(/\/title\/(\w+)/)[1]
  2388. const homePageUrl = 'https://www.imdb.com/title/' + imdbID + '/?ref_=nv_sr_1'
  2389. // Set language cookie to English, request current page in English, then restore language cookie or expire it if it didn't exist before
  2390. const langM = document.cookie.match(/lc-main=([^;]+)/)
  2391. const langBefore = langM ? langM[0] : ';expires=Thu, 01 Jan 1970 00:00:01 GMT'
  2392. document.cookie = 'lc-main=en-US'
  2393. const response = await asyncRequest({
  2394. url: homePageUrl,
  2395. headers: {
  2396. 'Accept-Language': 'en-US,en'
  2397. }
  2398. }).catch(function (response) {
  2399. console.warn('ShowMetacriticRatings: Error imdb02\nurl=' + homePageUrl + '\nstatus=' + response.status)
  2400. })
  2401. document.cookie = 'lc-main=' + langBefore
  2402. // Extract <h1> title
  2403. const parts = response.responseText.split('</span></h1>')[0].split('>')
  2404. console.debug('ShowMetacriticRatings: Movie title from English page:', parts[parts.length - 1])
  2405. return parts[parts.length - 1]
  2406. } else if (document.querySelector('script[type="application/ld+json"]')) {
  2407. const ld = parseLDJSON(['name', 'alternateName'])
  2408. if (ld.length > 1 && ld[1]) {
  2409. console.debug('ShowMetacriticRatings: Movie ld+json alternateName', ld[1])
  2410. return ld[1]
  2411. }
  2412. console.debug('ShowMetacriticRatings: Movie ld+json name', ld[0])
  2413. return ld[0]
  2414. } else {
  2415. const m = document.title.match(/(.+?)\s+(\((\d+)\))? - /)
  2416. console.debug('ShowMetacriticRatings: Movie <title>', m[1])
  2417. return m[1]
  2418. }
  2419. }
  2420. },
  2421. {
  2422. condition: function () {
  2423. const e = document.querySelector("meta[property='og:type']")
  2424. if (e && e.content === 'video.tv_show') {
  2425. return true
  2426. } else if (document.querySelector('[data-testid="hero-subnav-bar-left-block"] a[href*="episodes/"]')) {
  2427. return true
  2428. }
  2429. return false
  2430. },
  2431. type: 'tv',
  2432. data: async function () {
  2433. if (document.querySelector('[for="nav-language-selector"]').textContent.toLowerCase() !== 'en' || !navigator.language.startsWith('en')) {
  2434. const imdbID = document.location.pathname.match(/\/title\/(\w+)/)[1]
  2435. const homePageUrl = 'https://www.imdb.com/title/' + imdbID + '/?ref_=nv_sr_1'
  2436. // Set language cookie to English, request current page in English, then restore language cookie or expire it if it didn't exist before
  2437. const langM = document.cookie.match(/lc-main=([^;]+)/)
  2438. const langBefore = langM ? langM[0] : ';expires=Thu, 01 Jan 1970 00:00:01 GMT'
  2439. document.cookie = 'lc-main=en-US'
  2440. const response = await asyncRequest({
  2441. url: homePageUrl,
  2442. headers: {
  2443. 'Accept-Language': 'en-US,en'
  2444. }
  2445. }).catch(function (response) {
  2446. console.warn('ShowMetacriticRatings: Error imdb03\nurl=' + homePageUrl + '\nstatus=' + response.status)
  2447. })
  2448. document.cookie = 'lc-main=' + langBefore
  2449. // Extract <h1> title
  2450. const parts = response.responseText.split('</span></h1>')[0].split('>')
  2451. console.debug('ShowMetacriticRatings: TV title from English page:', parts[parts.length - 1])
  2452. return parts[parts.length - 1]
  2453. } else if (document.querySelector('script[type="application/ld+json"]')) {
  2454. const ld = parseLDJSON(['name', 'alternateName'])
  2455. if (ld.length > 1 && ld[1]) {
  2456. console.debug('ShowMetacriticRatings: TV ld+json alternateName', ld[1])
  2457. return ld[1]
  2458. }
  2459. console.debug('ShowMetacriticRatings: TV ld+json name', ld[0])
  2460. return ld[0]
  2461. } else {
  2462. const m = document.title.match(/(.+?)\s+\(.+(\d{4}).+/)
  2463. console.debug('ShowMetacriticRatings: TV <title>', m[1])
  2464. return m[1]
  2465. }
  2466. }
  2467. }
  2468. ]
  2469. },
  2470. steam: {
  2471. host: ['store.steampowered.com'],
  2472. condition: () => document.querySelector('*[itemprop=name]'),
  2473. products: [{
  2474. condition: Always,
  2475. type: 'pcgame',
  2476. data: () => document.querySelector('*[itemprop=name]').textContent
  2477. }]
  2478. },
  2479. rottentomatoes: {
  2480. host: ['rottentomatoes.com'],
  2481. condition: Always,
  2482. products: [{
  2483. condition: () => document.location.pathname.startsWith('/m/'),
  2484. type: 'movie',
  2485. data: () => document.querySelector('h1').firstChild.textContent
  2486. },
  2487. {
  2488. condition: () => document.location.pathname.startsWith('/tv/'),
  2489. type: 'tv',
  2490. data: () => unsafeWindow.BK.TvSeriesTitle
  2491. }
  2492. ]
  2493. },
  2494. serienjunkies: {
  2495. host: ['www.serienjunkies.de'],
  2496. condition: Always,
  2497. products: [{
  2498. condition: () => document.getElementById('serienlinksbreit2aktuell'),
  2499. type: 'tv',
  2500. data: () => document.querySelector('h1').textContent.trim()
  2501. },
  2502. {
  2503. condition: () => document.location.pathname.search(/vod\/film\/.{3,}/) !== -1,
  2504. type: 'movie',
  2505. data: () => document.querySelector('h1').textContent.trim()
  2506. }]
  2507. },
  2508. gamespot: {
  2509. host: ['gamespot.com'],
  2510. condition: () => document.querySelector('[itemprop=device]') || document.location.pathname.startsWith('/reviews/'),
  2511. products: [
  2512. {
  2513. condition: () => ~$('[itemprop=device]').text().indexOf('PC'),
  2514. type: 'pcgame',
  2515. data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
  2516. },
  2517. {
  2518. condition: () => ~$('[itemprop=device]').text().indexOf('PS4'),
  2519. type: 'ps4game',
  2520. data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
  2521. },
  2522. {
  2523. condition: () => ~$('[itemprop=device]').text().indexOf('XONE'),
  2524. type: 'xonegame',
  2525. data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
  2526. },
  2527. {
  2528. condition: () => document.querySelector('.system.system--simple.system--pc'),
  2529. type: 'pcgame',
  2530. data: () => document.querySelector('h1').textContent.trim().split('Review')[0].trim()
  2531. },
  2532. {
  2533. condition: () => document.querySelector('.system.system--simple.system--ps5'),
  2534. type: 'ps4game',
  2535. data: () => document.querySelector('h1').textContent.trim().split('Review')[0].trim()
  2536. },
  2537. {
  2538. condition: () => document.querySelector('.system.system--simple.system--xbsx'),
  2539. type: 'xonegame',
  2540. data: () => document.querySelector('h1').textContent.trim().split('Review')[0].trim()
  2541. }
  2542. ]
  2543. },
  2544. amazon: {
  2545. host: ['amazon.'],
  2546. condition: Always,
  2547. products: [
  2548. {
  2549. condition: () => document.location.hostname === 'music.amazon.com' && document.location.pathname.startsWith('/albums/') && document.querySelector('.viewTitle'), // "Amazon Music Unlimited" page
  2550. type: 'music',
  2551. data: function () {
  2552. const artist = document.querySelector('.artistLink').textContent.trim()
  2553. let title = document.querySelector('.viewTitle').textContent.trim()
  2554. title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
  2555. if (artist && title) {
  2556. return [artist, title]
  2557. }
  2558. return false
  2559. }
  2560. },
  2561. {
  2562. condition: function () { // "Normal amazon" page
  2563. try {
  2564. if (document.querySelector('.nav-categ-image').alt.toLowerCase().indexOf('musi') !== -1) {
  2565. return true
  2566. }
  2567. } catch (e) {}
  2568. const music = ['Music', 'Musique', 'Musik', 'Música', 'Musica', '音楽']
  2569. return music.some(function (s) {
  2570. if (~document.title.indexOf(s)) {
  2571. return true
  2572. } else {
  2573. return false
  2574. }
  2575. })
  2576. },
  2577. type: 'music',
  2578. data: function () {
  2579. let artist = false
  2580. let title = false
  2581. if (document.querySelector('#ProductInfoArtistLink')) {
  2582. artist = document.querySelector('#ProductInfoArtistLink').textContent.trim()
  2583. } else if (document.querySelector('#bylineInfo .author>*')) {
  2584. artist = document.querySelector('#bylineInfo .author>*').textContent.trim()
  2585. }
  2586.  
  2587. if (document.querySelector('#dmusicProductTitle_feature_div')) {
  2588. title = document.querySelector('#dmusicProductTitle_feature_div').textContent.trim()
  2589. title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
  2590. } else if (document.querySelector('#productTitle')) {
  2591. title = document.querySelector('#productTitle').textContent.trim()
  2592. title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
  2593. }
  2594. return [artist, title]
  2595. }
  2596. },
  2597. {
  2598. condition: () => (document.querySelector('[data-automation-id=title]') && (
  2599. document.getElementsByClassName('av-season-single').length ||
  2600. document.querySelector('[data-automation-id="num-of-seasons-badge"]') ||
  2601. document.getElementById('tab-selector-episodes') ||
  2602. document.getElementById('av-droplist-av-atf-season-selector')
  2603. )),
  2604. type: 'tv',
  2605. data: () => document.querySelector('[data-automation-id=title]').textContent.trim()
  2606. },
  2607. {
  2608. condition: () => ((
  2609. document.getElementsByClassName('av-season-single').length ||
  2610. document.querySelector('[data-automation-id="num-of-seasons-badge"]') ||
  2611. document.getElementById('tab-selector-episodes') ||
  2612. document.getElementById('av-droplist-av-atf-season-selector')
  2613. ) && Array.from(document.querySelectorAll('script[type="text/template"]')).map(e => e.innerHTML.match(/parentTitle"\s*:\s*"(.+?)"/)).some((x) => x != null)),
  2614. type: 'tv',
  2615. data: () => Array.from(document.querySelectorAll('script[type="text/template"]')).map(e => e.innerHTML.match(/parentTitle"\s*:\s*"(.+?)"/)).filter((x) => x != null)[0][1]
  2616. },
  2617. {
  2618. condition: () => document.querySelector('[data-automation-id=title]'),
  2619. type: 'movie',
  2620. data: () => document.querySelector('[data-automation-id=title]').textContent.trim().replace(/\[.{1,8}\]/, '')
  2621. },
  2622. {
  2623. condition: () => document.querySelector('#watchNowContainer a[href*="/gp/video/"]'),
  2624. type: 'movie',
  2625. data: () => document.getElementById('productTitle').textContent.trim()
  2626. }
  2627. ]
  2628. },
  2629. BoxOfficeMojo: {
  2630. host: ['boxofficemojo.com'],
  2631. condition: () => Always,
  2632. products: [
  2633. {
  2634. condition: () => document.location.pathname.startsWith('/release/'),
  2635. type: 'movie',
  2636. data: () => document.querySelector('meta[name=title]').content
  2637. },
  2638. {
  2639. // Old page design
  2640. condition: () => ~document.location.search.indexOf('id=') && document.querySelector('#body table:nth-child(2) tr:first-child b'),
  2641. type: 'movie',
  2642. data: () => document.querySelector('#body table:nth-child(2) tr:first-child b').firstChild.textContent
  2643. }]
  2644. },
  2645. AllMovie: {
  2646. host: ['allmovie.com'],
  2647. condition: () => document.querySelector('h2.movie-title'),
  2648. products: [{
  2649. condition: () => document.querySelector('h2.movie-title'),
  2650. type: 'movie',
  2651. data: () => document.querySelector('h2.movie-title').firstChild.textContent.trim()
  2652. }]
  2653. },
  2654. 'en.wikipedia': {
  2655. host: ['en.wikipedia.org'],
  2656. condition: Always,
  2657. products: [{
  2658. condition: function () {
  2659. if (!document.querySelector('.infobox .summary')) {
  2660. return false
  2661. }
  2662. const r = /\d\d\d\d films/
  2663. return $('#catlinks a').filter((i, e) => e.firstChild.textContent.match(r)).length
  2664. },
  2665. type: 'movie',
  2666. data: () => document.querySelector('.infobox .summary').firstChild.textContent
  2667. },
  2668. {
  2669. condition: function () {
  2670. if (!document.querySelector('.infobox .summary')) {
  2671. return false
  2672. }
  2673. const r = /television series/
  2674. return $('#catlinks a').filter((i, e) => e.firstChild.textContent.match(r)).length
  2675. },
  2676. type: 'tv',
  2677. data: () => document.querySelector('.infobox .summary').firstChild.textContent
  2678. }]
  2679. },
  2680. fandango: {
  2681. host: ['fandango.com'],
  2682. condition: () => document.querySelector("meta[property='og:title']"),
  2683. products: [{
  2684. condition: Always,
  2685. type: 'movie',
  2686. data: () => document.querySelector("meta[property='og:title']").content.match(/(.+?)\s+\(\d{4}\)/)[1].trim()
  2687. }]
  2688. },
  2689. flixster: {
  2690. host: ['flixster.com'],
  2691. condition: () => Always,
  2692. products: [{
  2693. condition: () => parseLDJSON('@type') === 'Movie',
  2694. type: 'movie',
  2695. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  2696. }]
  2697. },
  2698. themoviedb: {
  2699. host: ['themoviedb.org'],
  2700. condition: () => document.querySelector("meta[property='og:type']"),
  2701. products: [{
  2702. condition: () => document.querySelector("meta[property='og:type']").content === 'movie' ||
  2703. document.querySelector("meta[property='og:type']").content === 'video.movie',
  2704. type: 'movie',
  2705. data: () => document.querySelector("meta[property='og:title']").content
  2706. },
  2707. {
  2708. condition: () => document.querySelector("meta[property='og:type']").content === 'tv' ||
  2709. document.querySelector("meta[property='og:type']").content === 'tv_series' ||
  2710. document.querySelector("meta[property='og:type']").content.indexOf('tv_show') !== -1,
  2711. type: 'tv',
  2712. data: () => document.querySelector("meta[property='og:title']").content
  2713. }]
  2714. },
  2715. letterboxd: {
  2716. host: ['letterboxd.com'],
  2717. condition: () => unsafeWindow.filmData && 'name' in unsafeWindow.filmData,
  2718. products: [{
  2719. condition: Always,
  2720. type: 'movie',
  2721. data: () => unsafeWindow.filmData.name
  2722. }]
  2723. },
  2724. TVmaze: {
  2725. host: ['tvmaze.com'],
  2726. condition: () => document.querySelector('h1'),
  2727. products: [{
  2728. condition: Always,
  2729. type: 'tv',
  2730. data: () => document.querySelector('h1').firstChild.textContent
  2731. }]
  2732. },
  2733. TVGuide: {
  2734. host: ['tvguide.com'],
  2735. condition: Always,
  2736. products: [{
  2737. condition: () => document.location.pathname.startsWith('/tvshows/'),
  2738. type: 'tv',
  2739. data: function () {
  2740. if (document.querySelector('meta[itemprop=name]')) {
  2741. return document.querySelector('meta[itemprop=name]').content
  2742. } else {
  2743. return document.querySelector("meta[property='og:title']").content.split('|')[0]
  2744. }
  2745. }
  2746. }]
  2747. },
  2748. followshows: {
  2749. host: ['followshows.com'],
  2750. condition: Always,
  2751. products: [{
  2752. condition: () => document.querySelector("meta[property='og:type']").content === 'video.tv_show',
  2753. type: 'tv',
  2754. data: () => document.querySelector("meta[property='og:title']").content.replace(/\(\d{4}\)$/, '')
  2755. }]
  2756. },
  2757. TheTVDB: {
  2758. host: ['thetvdb.com'],
  2759. condition: Always,
  2760. products: [{
  2761. condition: () => document.location.pathname.startsWith('/series/'),
  2762. type: 'tv',
  2763. data: () => document.getElementById('series_title').firstChild.textContent.trim()
  2764. },
  2765. {
  2766. condition: () => document.location.pathname.startsWith('/movies/'),
  2767. type: 'movie',
  2768. data: () => document.getElementById('series_title').firstChild.textContent.trim()
  2769. }]
  2770. },
  2771. ConsequenceOfSound: {
  2772. host: ['consequence.net', 'consequenceofsound.net'],
  2773. condition: () => document.querySelector('#main-content .review-summary'),
  2774. products: [
  2775. {
  2776. condition: () => document.querySelector('meta[name="cXenseParse:cns-artist-names"]') && document.querySelector('em'),
  2777. type: 'music',
  2778. data: function () {
  2779. window.setInterval(function () {
  2780. if (document.getElementById('ot-sdk-btn-floating')) {
  2781. document.getElementById('ot-sdk-btn-floating').remove()
  2782. }
  2783. }, 5000)
  2784. const artist = document.querySelector('meta[name="cXenseParse:cns-artist-names"]').content
  2785. const arr = Array.from(document.querySelectorAll('em')).map((em) => em.textContent.trim())
  2786. const counts = {}
  2787. for (const num of arr) {
  2788. counts[num] = counts[num] ? counts[num] + 1 : 1
  2789. }
  2790. const max = Math.max(...Object.values(counts))
  2791. const maxIndex = Object.values(counts).indexOf(max)
  2792. const title = Object.keys(counts)[maxIndex]
  2793. return [artist, title]
  2794. }
  2795. },
  2796. {
  2797. condition: () => document.title.match(/'(.*?)'\s*Album/i) && document.querySelector('meta[name="cXenseParse:cns-artist-names"]'),
  2798. type: 'music',
  2799. data: function () {
  2800. window.setInterval(function () {
  2801. if (document.getElementById('ot-sdk-btn-floating')) {
  2802. document.getElementById('ot-sdk-btn-floating').remove()
  2803. }
  2804. }, 5000)
  2805. const title = document.title.match(/'(.*?)'\s*Album/i)[1]
  2806. const artist = document.querySelector('meta[name="cXenseParse:cns-artist-names"]').content
  2807. return [artist, title]
  2808. }
  2809. },
  2810. {
  2811. condition: () => document.title.match(/(.+?)\s+\u2013\s+(.+?) \| Album Review/),
  2812. type: 'music',
  2813. data: function () {
  2814. window.setInterval(function () {
  2815. if (document.getElementById('ot-sdk-btn-floating')) {
  2816. document.getElementById('ot-sdk-btn-floating').remove()
  2817. }
  2818. }, 5000)
  2819. const m = document.title.match(/(.+?)\s+\u2013\s+(.+?) \| Album Review/)
  2820. return [m[1], m[2]]
  2821. }
  2822. },
  2823. {
  2824. condition: () => document.location.pathname.indexOf('/album-review') !== -1 && document.querySelector('a.tag[href*="/artist/"'),
  2825. type: 'music',
  2826. data: function () {
  2827. window.setInterval(function () {
  2828. if (document.getElementById('ot-sdk-btn-floating')) {
  2829. document.getElementById('ot-sdk-btn-floating').remove()
  2830. }
  2831. }, 5000)
  2832. const artistAndTitleWithDash = document.location.pathname.match(/album-review-([\w-]+)/)[1]
  2833. const artistWithDash = document.querySelector('a.tag[href*="/artist/"').pathname.match(/artist\/([\w-]+)/)[1]
  2834. const titleWithDash = artistAndTitleWithDash.replace(artistWithDash, '')
  2835. const title = titleWithDash.replace('-', ' ').trim()
  2836. const artist = artistWithDash.replace('-', ' ').trim()
  2837. return [artist, title]
  2838. }
  2839. }]
  2840. },
  2841. Pitchfork: {
  2842. host: ['pitchfork.com'],
  2843. condition: () => ~document.location.href.indexOf('/reviews/albums/'),
  2844. products: [{
  2845. condition: () => document.querySelector('.single-album-tombstone'),
  2846. type: 'music',
  2847. data: function () {
  2848. let artist
  2849. let album
  2850. if (document.querySelector('.single-album-tombstone .artists')) {
  2851. artist = document.querySelector('.single-album-tombstone .artists').innerText.trim()
  2852. } else if (document.querySelector('.single-album-tombstone .artist-list')) {
  2853. artist = document.querySelector('.single-album-tombstone .artist-list').innerText.trim()
  2854. }
  2855. if (document.querySelector('.single-album-tombstone h1.review-title')) {
  2856. album = document.querySelector('.single-album-tombstone h1.review-title').innerText.trim()
  2857. } else if (document.querySelector('.single-album-tombstone h1')) {
  2858. album = document.querySelector('.single-album-tombstone h1').innerText.trim()
  2859. }
  2860.  
  2861. return [artist, album]
  2862. }
  2863. }]
  2864. },
  2865. 'Last.fm': {
  2866. host: ['last.fm'],
  2867. condition: () => document.querySelector('*[data-page-resource-type]') && document.querySelector('*[data-page-resource-type]').dataset.pageResourceType === 'album',
  2868. products: [{
  2869. condition: () => document.querySelector('*[data-page-resource-type]').dataset.pageResourceName,
  2870. type: 'music',
  2871. data: function () {
  2872. const artist = document.querySelector('*[data-page-resource-type]').dataset.pageResourceArtistName
  2873. const album = document.querySelector('*[data-page-resource-type]').dataset.pageResourceName
  2874. return [artist, album]
  2875. }
  2876. }]
  2877. },
  2878. TVNfo: {
  2879. host: ['tvnfo.com'],
  2880. condition: () => document.querySelector('#title #name'),
  2881. products: [{
  2882. condition: Always,
  2883. type: 'tv',
  2884. data: function () {
  2885. const years = document.querySelector('#title #years').textContent.trim()
  2886. const title = document.querySelector('#title #name').textContent.replace(years, '').trim()
  2887. return title
  2888. }
  2889. }]
  2890. },
  2891. rateyourmusic: {
  2892. host: ['rateyourmusic.com'],
  2893. condition: () => document.querySelector("meta[property='og:type']"),
  2894. products: [{
  2895. condition: () => document.querySelector("meta[property='og:type']").content === 'music.album',
  2896. type: 'music',
  2897. data: function () {
  2898. const artist = document.querySelector('.section_main_info .artist').innerText.trim()
  2899. const album = document.querySelector('.section_main_info .album_title').innerText.trim()
  2900. return [artist, album]
  2901. }
  2902. }]
  2903. },
  2904. spotify: {
  2905. host: ['open.spotify.com'],
  2906. condition: Always,
  2907. products: [{
  2908. condition: () => document.location.pathname.startsWith('/album/') && document.querySelector('.Root__main-view h1'),
  2909. type: 'music',
  2910. data: function () {
  2911. const album = document.querySelector('.Root__main-view h1').textContent.trim()
  2912. let artist = []
  2913. document.querySelector('.Root__main-view h1').parentNode.parentNode.parentNode.querySelectorAll('a[href*="/artist/"]').forEach(function (a) {
  2914. artist.push(a.textContent.trim())
  2915. })
  2916. artist = artist.join(' ')
  2917. return [artist, album]
  2918. }
  2919. }]
  2920. },
  2921. nme: {
  2922. host: ['nme.com'],
  2923. condition: () => document.location.pathname.startsWith('/reviews/'),
  2924. products: [
  2925. {
  2926. condition: () => document.querySelector('.tdb-breadcrumbs a[href*="/reviews/film-reviews"]'),
  2927. type: 'movie',
  2928. data: function () {
  2929. try {
  2930. return document.title.match(/[‘'](.+?)[’']/)[1]
  2931. } catch (e) {
  2932. try {
  2933. return document.querySelector('h1.tdb-title-text').textContent.match(/[‘'](.+?)[’']/)[1]
  2934. } catch (e) {
  2935. return document.querySelector('h1').textContent.match(/:\s*(.+)/)[1].trim()
  2936. }
  2937. }
  2938. }
  2939. },
  2940. {
  2941. condition: () => document.querySelector('#nme-music-header'),
  2942. type: 'music',
  2943. data: () => document.querySelector('h1.tdb-title-text').textContent.match(/\s*(.+?)\s*.\s*[‘'](.+?)[’']/).slice(1)
  2944. },
  2945. {
  2946. condition: () => document.querySelector('.tdb-breadcrumbs a[href*="/reviews/tv-reviews"]'),
  2947. type: 'tv',
  2948. data: () => document.querySelector('h1.tdb-title-text').textContent.match(/[‘'](.+?)[’']/)[1]
  2949. }]
  2950. },
  2951. albumoftheyear: {
  2952. host: ['albumoftheyear.org'],
  2953. condition: Always,
  2954. products: [{
  2955. condition: () => document.location.pathname.startsWith('/album/'),
  2956. type: 'music',
  2957. data: function () {
  2958. const artist = document.querySelector('*[itemprop=byArtist] *[itemprop=name]').textContent
  2959. const album = document.querySelector('.albumTitle *[itemprop=name]').textContent
  2960. return [artist, album]
  2961. }
  2962. }]
  2963. },
  2964. epguides: {
  2965. host: ['epguides.com'],
  2966. condition: () => document.getElementById('eplist'),
  2967. products: [{
  2968. condition: () => document.getElementById('eplist') && document.querySelector('.center.titleblock h2'),
  2969. type: 'tv',
  2970. data: () => document.querySelector('.center.titleblock h2').textContent.trim()
  2971. }]
  2972. },
  2973. /*
  2974. netflix: {
  2975. host: ['netflix.com'],
  2976. condition: !(document.querySelector('.button-nfplayerPlay') || document.querySelector('.nf-big-play-pause') || document.querySelector('.AkiraPlayer video')),
  2977.  
  2978. // TODO
  2979. // https://www.netflix.com/de/title/70264888
  2980. // https://www.netflix.com/de/title/70178217
  2981. // https://www.netflix.com/de/title/70305892 ## Movie
  2982. // https://www.netflix.com/de-en/title/80108495 ## No meta
  2983.  
  2984. products: [{
  2985. condition: () => parseLDJSON('@type') === 'Movie',
  2986. type: 'movie',
  2987. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  2988. },
  2989. {
  2990. condition: () => parseLDJSON('@type') === 'TVSeries',
  2991. type: 'tv',
  2992. data: () => parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  2993. }]
  2994. },
  2995. */
  2996. ComedyCentral: {
  2997. host: ['cc.com'],
  2998. condition: () => document.location.pathname.startsWith('/shows/'),
  2999. products: [{
  3000. condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:title']"),
  3001. type: 'tv',
  3002. data: () => document.querySelector("meta[property='og:title']").content.replace('| Comedy Central', '').trim()
  3003. },
  3004. {
  3005. condition: () => document.location.pathname.split('/').length === 3 && document.title.match(/(.+?)\s+-\s+Series/),
  3006. type: 'tv',
  3007. data: () => document.title.match(/(.+?)\s+-\s+Series/)[1]
  3008. }]
  3009. },
  3010. AMC: {
  3011. host: ['amc.com'],
  3012. condition: () => document.location.pathname.startsWith('/shows/'),
  3013. products: [
  3014. {
  3015. condition: () => document.querySelector('.feeds[itemtype="http://schema.org/TVSeries"] h1'),
  3016. type: 'tv',
  3017. data: () => document.querySelector('.feeds[itemtype="http://schema.org/TVSeries"] h1').textContent
  3018. },
  3019. {
  3020. condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:type']") && document.querySelector("meta[property='og:type']").content.indexOf('tv_show') !== -1,
  3021. type: 'tv',
  3022. data: () => document.querySelector('.video-card-description h1').textContent.trim()
  3023. }]
  3024. },
  3025. AMCplus: {
  3026. host: ['amcplus.com'],
  3027. condition: () => Always,
  3028. products: [
  3029. {
  3030. condition: () => document.title.match(/Watch .+? |/),
  3031. type: 'tv',
  3032. data: () => document.title.match(/Watch (.+?) |/)[1].trim()
  3033. }]
  3034. },
  3035. RlsBB: {
  3036. host: ['rlsbb.ru'],
  3037. condition: () => document.querySelectorAll('.post').length === 1,
  3038. products: [
  3039. {
  3040. condition: () => document.querySelector('#post-wrapper .entry-meta a[href*="/category/movies/"]'),
  3041. type: 'movie',
  3042. data: () => document.querySelector('h1.entry-title').textContent.match(/(.+?)\s+\d{4}/)[1].trim()
  3043. },
  3044. {
  3045. condition: () => document.querySelector('#post-wrapper .entry-meta a[href*="/category/tv-shows/"]'),
  3046. type: 'tv',
  3047. data: () => document.querySelector('h1.entry-title').textContent.match(/(.+?)\s+S\d{2}/)[1].trim()
  3048. }]
  3049. },
  3050. newalbumreleases: {
  3051. host: ['newalbumreleases.net'],
  3052. condition: () => document.querySelectorAll('#content .single').length === 1,
  3053. products: [
  3054. {
  3055. condition: () => document.querySelector('#content .single .cover .entry'),
  3056. type: 'music',
  3057. data: function () {
  3058. const mArtist = document.querySelector('#content .single .cover .entry').textContent.match(/Artist.\s*(.+)\s+/i)
  3059. if (mArtist) {
  3060. const mAlbum = document.querySelector('#content .single .cover .entry').textContent.match(/Album.\s*(.+)\s+/i)
  3061. if (mAlbum) {
  3062. return [mArtist[1], mAlbum[1]]
  3063. }
  3064. }
  3065. }
  3066. }]
  3067. },
  3068. showtime: {
  3069. host: ['sho.com'],
  3070. condition: Always,
  3071. products: [
  3072. {
  3073. condition: () => parseLDJSON('@type') === 'Movie',
  3074. type: 'movie',
  3075. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  3076. },
  3077. {
  3078. condition: () => parseLDJSON('@type') === 'TVSeries',
  3079. type: 'tv',
  3080. data: () => parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  3081. }]
  3082. },
  3083. epicgames: {
  3084. host: ['www.epicgames.com', 'store.epicgames.com'],
  3085. condition: () => document.querySelector('.meta-schema'),
  3086. products: [{
  3087. condition: Always,
  3088. type: 'pcgame',
  3089. data: function () {
  3090. try {
  3091. return document.querySelector('.meta-schema').nextElementSibling.firstElementChild.lastElementChild.firstElementChild.firstElementChild.firstElementChild.textContent
  3092. } catch (e) {
  3093. return document.querySelector('h1').textContent
  3094. }
  3095. }
  3096. }]
  3097. },
  3098. gog: {
  3099. host: ['www.gog.com'],
  3100. condition: () => document.querySelector('.productcard-basics__title'),
  3101. products: [
  3102. {
  3103. condition: () => document.location.pathname.split('/').length > 2 && (
  3104. document.location.pathname.split('/')[1] === 'game' ||
  3105. document.location.pathname.split('/')[2] === 'game'),
  3106. type: 'pcgame',
  3107. data: () => document.querySelector('.productcard-basics__title').textContent
  3108. },
  3109. {
  3110. condition: () => document.location.pathname.split('/').length > 2 && (
  3111. document.location.pathname.split('/')[1] === 'movie' ||
  3112. document.location.pathname.split('/')[2] === 'movie'),
  3113. type: 'movie',
  3114. data: () => document.querySelector('.productcard-basics__title').textContent
  3115. }
  3116. ]
  3117. },
  3118. steamgifts: {
  3119. host: ['www.steamgifts.com'],
  3120. condition: () => document.querySelector('.featured__heading__medium'),
  3121. products: [{
  3122. condition: Always,
  3123. type: 'pcgame',
  3124. data: () => document.querySelector('.featured__heading__medium').innerText
  3125. }]
  3126. },
  3127. allmusic: {
  3128. host: ['allmusic.com'],
  3129. condition: Always,
  3130. products: [{
  3131. condition: () => document.location.pathname.indexOf('/album/') !== -1,
  3132. type: 'music',
  3133. data: function () {
  3134. const ld = parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum'))
  3135. const album = ld[0]
  3136. const artist = 'name' in ld[1] ? ld[1].name : ld[1].map(x => x.name).join(' ')
  3137. return [artist, album]
  3138. }
  3139. }]
  3140. },
  3141. psapm: {
  3142. host: ['psa.wf'],
  3143. condition: Always,
  3144. products: [
  3145. {
  3146. condition: () => document.location.pathname.startsWith('/movie/'),
  3147. type: 'movie',
  3148. data: function () {
  3149. const title = document.querySelector('h1').textContent.trim()
  3150. const m = title.match(/(.+)\((\d+)\)$/)
  3151. if (m) {
  3152. return m[1].trim()
  3153. } else {
  3154. return title
  3155. }
  3156. }
  3157. },
  3158. {
  3159. condition: () => document.location.pathname.startsWith('/tv-show/'),
  3160. type: 'tv',
  3161. data: () => document.querySelector('h1').textContent.trim()
  3162. }
  3163. ]
  3164. },
  3165. 'save.tv': {
  3166. host: ['save.tv'],
  3167. condition: () => document.location.pathname.startsWith('/STV/M/obj/archive/'),
  3168. products: [
  3169. {
  3170. condition: () => document.location.pathname.startsWith('/STV/M/obj/archive/'),
  3171. type: 'movie',
  3172. data: function () {
  3173. let title = null
  3174. if (document.querySelector("span[data-bind='text:OrigTitle']")) {
  3175. title = document.querySelector("span[data-bind='text:OrigTitle']").textContent
  3176. } else {
  3177. title = document.querySelector("h2[data-bind='text:Title']").textContent
  3178. }
  3179. let year = null
  3180. if (document.querySelector("span[data-bind='text:ProductionYear']")) {
  3181. year = parseInt(document.querySelector("span[data-bind='text:ProductionYear']").textContent)
  3182. }
  3183. return [title, year]
  3184. }
  3185. }
  3186. ]
  3187. },
  3188. wikiwand: {
  3189. host: ['www.wikiwand.com'],
  3190. condition: Always,
  3191. products: [{
  3192. condition: function () {
  3193. const title = document.querySelector('h1').textContent.toLowerCase()
  3194. const subtitle = document.querySelector('h2[class*="subtitle"]') ? document.querySelector('h2[class*="subtitle"]').textContent.toLowerCase() : ''
  3195. if (title.indexOf('film') === -1 && !subtitle) {
  3196. return false
  3197. }
  3198. return title.indexOf('film') !== -1 ||
  3199. subtitle.indexOf('film') !== -1 ||
  3200. subtitle.indexOf('movie') !== -1
  3201. },
  3202. type: 'movie',
  3203. data: () => document.querySelector('h1').textContent.replace(/\((\d{4} )?film\)/i, '').trim()
  3204. },
  3205. {
  3206. condition: function () {
  3207. const title = document.querySelector('h1').textContent.toLowerCase()
  3208. const subtitle = document.querySelector('h2[class*="subtitle"]') ? document.querySelector('h2[class*="subtitle"]').textContent.toLowerCase() : ''
  3209. if (title.indexOf('tv series') === -1 && !subtitle) {
  3210. return false
  3211. }
  3212. return title.indexOf('tv series') !== -1 ||
  3213. subtitle.indexOf('television') !== -1 ||
  3214. subtitle.indexOf('tv series') !== -1
  3215. },
  3216. type: 'tv',
  3217. data: () => document.querySelector('h1').textContent.replace(/\(tv series\)/i, '').trim()
  3218. }]
  3219. },
  3220. radarr: {
  3221. host: ['*'],
  3222. condition: () => document.location.pathname.startsWith('/movie/'),
  3223. products: [{
  3224. condition: () => document.querySelector('[class*="MovieDetails-title"] span'),
  3225. type: 'movie',
  3226. data: () => document.querySelector('[class*="MovieDetails-title"] span').textContent.trim()
  3227. }]
  3228. },
  3229. trakt: {
  3230. host: ['trakt.tv'],
  3231. condition: Always,
  3232. products: [
  3233. {
  3234. condition: () => document.location.pathname.startsWith('/movies/'),
  3235. type: 'movie',
  3236. data: () => Array.from(document.querySelector('.summary h1').childNodes).filter(node => node.nodeType === node.TEXT_NODE).map(node => node.textContent).join(' ').trim()
  3237. },
  3238. {
  3239. condition: () => document.location.pathname.startsWith('/shows/'),
  3240. type: 'tv',
  3241. data: () => Array.from(document.querySelector('.summary h1').childNodes).filter(node => node.nodeType === node.TEXT_NODE).map(node => node.textContent).join(' ').trim()
  3242. }
  3243. ]
  3244. }
  3245.  
  3246. }
  3247.  
  3248. async function main () {
  3249. let dataFound = false
  3250.  
  3251. let map = false
  3252.  
  3253. for (const name in sites) {
  3254. const site = sites[name]
  3255. if (site.host.some(function (e) { return ~this.indexOf(e) || e === '*' }, document.location.hostname) && site.condition()) {
  3256. for (let i = 0; i < site.products.length; i++) {
  3257. if (site.products[i].condition()) {
  3258. // Check map for a match
  3259. if (map === false) {
  3260. map = JSON.parse(await GM.getValue('map', '{}'))
  3261. }
  3262. const docurl = filterUniversalUrl(document.location.href)
  3263. if (docurl in map) {
  3264. // Found in map, show result
  3265. const metaurl = map[docurl]
  3266. metacriticGeneralProductSetup()
  3267. metacritic.mapped.apply(undefined, [docurl, site.products[i], absoluteMetaURL(metaurl), site.products[i].type])
  3268. dataFound = true
  3269. break
  3270. }
  3271. // Try to retrieve item name from page
  3272. let data
  3273. try {
  3274. data = await site.products[i].data()
  3275. } catch (e) {
  3276. data = false
  3277. console.error(`ShowMetacriticRatings: Error in data() of site='${name}', type='${site.products[i].type}'`)
  3278. console.error(e)
  3279. }
  3280. if (data) {
  3281. const params = [docurl, site.products[i]]
  3282. if (Array.isArray(data)) {
  3283. params.push(...data)
  3284. } else {
  3285. params.push(data)
  3286. }
  3287. metacriticGeneralProductSetup()
  3288. metacritic[site.products[i].type].apply(undefined, params)
  3289. dataFound = true
  3290. }
  3291. break
  3292. }
  3293. }
  3294. break
  3295. }
  3296. }
  3297. return dataFound
  3298. }
  3299.  
  3300. (async function () {
  3301. const gdpr = await acceptGDPR()
  3302. if (!gdpr) {
  3303. GM.registerMenuCommand('Show Metacritic.com ratings - Accept terms of service', () => acceptGDPR(true).then((yes) => yes && document.location.reload()))
  3304. return
  3305. }
  3306. await versionUpdate()
  3307.  
  3308. if (!document.getElementById('mcdiv123_box_css')) {
  3309. const style = document.createElement('style')
  3310. style.setAttribute('id', 'mcdiv123_box_css')
  3311. style.innerHTML = BOX_CSS
  3312. document.head.appendChild(style)
  3313. }
  3314.  
  3315. const firstRunResult = await main()
  3316.  
  3317. GM.registerMenuCommand('Show Metacritic.com ratings - Search now', () => openSearchBox())
  3318. GM.registerMenuCommand('Show Metacritic.com ratings - Change corner', () => changePosition())
  3319. GM.registerMenuCommand('Show Metacritic.com ratings - Enlarge', () => changeSizeEnlarge())
  3320. GM.registerMenuCommand('Show Metacritic.com ratings - Shrink', () => changeSizeShrink())
  3321.  
  3322. let lastLoc = document.location.href
  3323. let lastContent = document.body.innerText
  3324. let lastCounter = 0
  3325. async function newpage () {
  3326. if (lastContent === document.body.innerText && lastCounter < 15) {
  3327. window.setTimeout(newpage, 500)
  3328. lastCounter++
  3329. } else {
  3330. lastContent = document.body.innerText
  3331. lastCounter = 0
  3332. const re = await main()
  3333. if (!re) { // No page matched or no data found
  3334. window.setTimeout(newpage, 1000)
  3335. }
  3336. }
  3337. }
  3338. window.setInterval(function () {
  3339. if (document.location.href !== lastLoc) {
  3340. lastLoc = document.location.href
  3341. $('#mcdiv123').remove()
  3342.  
  3343. window.setTimeout(newpage, 1000)
  3344. }
  3345. }, 500)
  3346.  
  3347. if (!firstRunResult) {
  3348. // Initial run had no match, let's try again there may be new content
  3349. window.setTimeout(main, 2000)
  3350. }
  3351. })()