Show Metacritic.com ratings

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

当前为 2020-12-06 提交的版本,查看 最新版本

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