Show Metacritic.com ratings

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

当前为 2023-09-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Show Metacritic.com ratings
  3. // @description Show metacritic metascore and user ratings on: Bandcamp, Apple Itunes (Music), Amazon (Music,Movies,TV Shows), IMDb (Movies), Google Play (Music, Movies), Steam, Gamespot (PS4, XONE, PC), Rotten Tomatoes, Serienjunkies, BoxOfficeMojo, allmovie.com, fandango.com, Wikipedia (en), themoviedb.org, letterboxd, TVmaze, TVGuide, followshows.com, TheTVDB.com, ConsequenceOfSound, Pitchfork, Last.fm, TVnfo, rateyourmusic.com, GOG, Epic Games Store, save.tv, argenteam.net
  4. // @namespace cuzi
  5. // @icon https://www.metacritic.com/a/img/favicon.svg
  6. // @supportURL https://github.com/cvzi/Metacritic-userscript/issues
  7. // @contributionURL https://buymeacoff.ee/cuzi
  8. // @contributionURL https://ko-fi.com/cuzicvzi
  9. // @grant unsafeWindow
  10. // @grant GM.xmlHttpRequest
  11. // @grant GM.setValue
  12. // @grant GM.getValue
  13. // @grant GM.registerMenuCommand
  14. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js
  15. // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
  16. // @antifeature tracking When a metacritic rating is displayed, we may store the url of the current website and the metacritic url in our database. Log files are temporarily retained by our database hoster Cloudflare Workers® and contain your IP address and browser configuration.
  17. // @version 93
  18. // @connect metacritic.com
  19. // @connect met.acritic.workers.dev
  20. // @connect imdb.com
  21. // @match https://*.bandcamp.com/*
  22. // @match https://play.google.com/store/music/album/*
  23. // @match https://play.google.com/store/movies/details/*
  24. // @match https://music.amazon.com/*
  25. // @match https://www.amazon.ca/*
  26. // @match https://www.amazon.co.jp/*
  27. // @match https://www.amazon.co.uk/*
  28. // @match https://smile.amazon.co.uk/*
  29. // @match https://www.amazon.com.au/*
  30. // @match https://www.amazon.com.mx/*
  31. // @match https://www.amazon.com/*
  32. // @match https://smile.amazon.com/*
  33. // @match https://www.amazon.de/*
  34. // @match https://smile.amazon.de/*
  35. // @match https://www.amazon.es/*
  36. // @match https://www.amazon.fr/*
  37. // @match https://www.amazon.in/*
  38. // @match https://www.amazon.it/*
  39. // @match https://www.imdb.com/title/*
  40. // @match https://store.steampowered.com/app/*
  41. // @match https://www.gamespot.com/*
  42. // @match https://www.serienjunkies.de/*
  43. // @match https://www.rottentomatoes.com/m/*
  44. // @match https://rottentomatoes.com/m/*
  45. // @match https://www.rottentomatoes.com/tv/*
  46. // @match https://rottentomatoes.com/tv/*
  47. // @match https://www.rottentomatoes.com/tv/*
  48. // @match https://rottentomatoes.com/tv/*
  49. // @match https://www.boxofficemojo.com/movies/*
  50. // @match https://www.boxofficemojo.com/release/*
  51. // @match https://www.allmovie.com/movie/*
  52. // @match https://en.wikipedia.org/*
  53. // @match https://www.fandango.com/*
  54. // @match https://www.flixster.com/movie/*
  55. // @match https://www.themoviedb.org/movie/*
  56. // @match https://www.themoviedb.org/tv/*
  57. // @match https://letterboxd.com/film/*
  58. // @match https://www.tvmaze.com/shows/*
  59. // @match https://www.tvguide.com/tvshows/*
  60. // @match https://followshows.com/show/*
  61. // @match https://thetvdb.com/series/*
  62. // @match https://thetvdb.com/movies/*
  63. // @match https://consequenceofsound.net/*
  64. // @match https://consequence.net/*
  65. // @match https://pitchfork.com/*
  66. // @match https://www.last.fm/*
  67. // @match https://tvnfo.com/s/*
  68. // @match https://rateyourmusic.com/release/album/*
  69. // @match https://open.spotify.com/*
  70. // @match https://play.spotify.com/album/*
  71. // @match https://www.nme.com/reviews/*
  72. // @match https://www.albumoftheyear.org/album/*
  73. // @match https://itunes.apple.com/*
  74. // @match https://music.apple.com/*
  75. // @match https://epguides.com/*
  76. // @match https://www.epguides.com/*
  77. // @match https://sharetv.com/shows/*
  78. // @match https://www.netflix.com/*
  79. // @match https://www.cc.com/*
  80. // @match https://www.tvhoard.com/*
  81. // @match https://www.amc.com/*
  82. // @match https://www.amcplus.com/*
  83. // @match https://comment.rlsbb.ru/*/
  84. // @match https://newalbumreleases.net/*
  85. // @match https://www.sho.com/*
  86. // @match https://www.epicgames.com/store/*
  87. // @match https://www.gog.com/*
  88. // @match https://www.allmusic.com/album/*
  89. // @match https://store.epicgames.com/*
  90. // @match https://www.steamgifts.com/giveaway/*
  91. // @match https://psa.pm/*
  92. // @match https://www.save.tv/*
  93. // @match https://argenteam.net/*
  94. // ==/UserScript==
  95.  
  96. /* globals alert, confirm, GM, DOMParser, $, Image, unsafeWindow, parent, Blob, failedImages */
  97.  
  98. const baseURL = 'https://www.metacritic.com/'
  99.  
  100. const baseURLmusic = 'https://www.metacritic.com/music/'
  101. const baseURLmovie = 'https://www.metacritic.com/movie/'
  102. const baseURLpcgame = 'https://www.metacritic.com/game/pc/'
  103. const baseURLps4 = 'https://www.metacritic.com/game/playstation-4/'
  104. const baseURLxone = 'https://www.metacritic.com/game/xbox-one/'
  105. const baseURLtv = 'https://www.metacritic.com/tv/'
  106.  
  107. const baseURLsearch = 'https://www.metacritic.com/search/{query}?page=1&category={type}'
  108. const baseURLautosearch = 'https://www.metacritic.com/autosearch'
  109.  
  110. const baseURLdatabase = 'https://met.acritic.workers.dev/r.php'
  111. const baseURLwhitelist = 'https://met.acritic.workers.dev/whitelist.php'
  112. const baseURLblacklist = 'https://met.acritic.workers.dev/blacklist.php'
  113.  
  114. const TEMPORARY_BLACKLIST_TIMEOUT = 5 * 60
  115.  
  116. const windowPositions = [
  117. {
  118. bottom: 0,
  119. left: 0
  120. },
  121. {
  122. bottom: 0,
  123. right: 0
  124. },
  125. {
  126. top: 0,
  127. right: 0
  128. },
  129. {
  130. top: 0,
  131. left: 0
  132. }
  133. ]
  134.  
  135. // http://www.designcouch.com/home/why/2013/05/23/dead-simple-pure-css-loading-spinner/
  136. 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%}'
  137.  
  138. let myDOMParser = null
  139. function domParser () {
  140. if (myDOMParser === null) {
  141. myDOMParser = new DOMParser()
  142. }
  143. return myDOMParser
  144. }
  145.  
  146. async function versionUpdate () {
  147. const version = parseInt(await GM.getValue('version', 0))
  148. if (version <= 91) {
  149. // Reset database
  150. await GM.setValue('map', '{}')
  151. await GM.setValue('black', '[]')
  152. await GM.setValue('hovercache', '{}')
  153. await GM.setValue('requestcache', '{}')
  154. await GM.setValue('searchcache', '{}')
  155. await GM.setValue('autosearchcache', '{}')
  156. await GM.setValue('temporaryblack', '{}')
  157. }
  158. if (version < 92) {
  159. await GM.setValue('version', 92)
  160. }
  161. }
  162.  
  163. async function acceptGDPR (showDialog) {
  164. if (showDialog === true) {
  165. await GM.setValue('gdpr', null)
  166. return acceptGDPR()
  167. }
  168. return new Promise(function (resolve) {
  169. GM.getValue('gdpr', null).then(function (value) {
  170. if (value === true) {
  171. return resolve(true)
  172. }
  173. if (value === false) {
  174. return resolve(false)
  175. }
  176. const html = '<h1>Privacy Policy for &quot;Show Metacritic.com ratings&quot;</h1><h2>General Data Protection Regulation (GDPR)</h2><p>We are a Data Controller of your information.</p> <p>&quot;Show Metacritic.com ratings&quot; legal basis for collecting and using the personal information described in this Privacy Policy depends on the Personal Information we collect and the specific context in which we collect the information:</p><ul> <li>&quot;Show Metacritic.com ratings&quot; needs to perform a contract with you</li> <li>You have given &quot;Show Metacritic.com ratings&quot; permission to do so</li> <li>Processing your personal information is in &quot;Show Metacritic.com ratings&quot; legitimate interests</li> <li>&quot;Show Metacritic.com ratings&quot; needs to comply with the law</li></ul> <p>&quot;Show Metacritic.com ratings&quot; will retain your personal information only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use your information to the extent necessary to comply with our legal obligations, resolve disputes, and enforce our policies.</p> <p>If you are a resident of the European Economic Area (EEA), you have certain data protection rights. If you wish to be informed what Personal Information we hold about you and if you want it to be removed from our systems, please contact us. Our Privacy Policy was generated with the help of <a href="https://www.gdprprivacypolicy.net/">GDPR Privacy Policy Generator</a> and the <a href="https://www.app-privacy-policy.com">App Privacy Policy Generator</a>.</p><p>In certain circumstances, you have the following data protection rights:</p><ul> <li>The right to access, update or to delete the information we have on you.</li> <li>The right of rectification.</li> <li>The right to object.</li> <li>The right of restriction.</li> <li>The right to data portability</li> <li>The right to withdraw consent</li></ul><h2>Log Files</h2><p>&quot;Show Metacritic.com ratings&quot; follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services\' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users\' movement on the website, and gathering demographic information.</p><h2>Privacy Policies</h2><P>You may consult this list to find the Privacy Policy for each of the advertising partners of &quot;Show Metacritic.com ratings&quot;.</p><p>Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on &quot;Show Metacritic.com ratings&quot;, which are sent directly to users\' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.</p><p>Note that &quot;Show Metacritic.com ratings&quot; has no access to or control over these cookies that are used by third-party advertisers.</p><h2>Third Party Privacy Policies</h2><p>&quot;Show Metacritic.com ratings&quot;\'s Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options.List of these Privacy Policies and their links: <ul> <li>Cloudflare Workers®: <a href="https://www.cloudflare.com/privacypolicy/">https://www.cloudflare.com/privacypolicy/</a></li> <li>www.metacritic.com: <a href="https://privacy.cbs/">https://privacy.cbs/</a></li></ul></p><p>You can choose to disable cookies through your individual browser options.</p><h2>Children\'s Information</h2><p>Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, and/or monitor and guide their online activity.</p><p>&quot;Show Metacritic.com ratings&quot; does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.</p><h2>Online Privacy Policy Only</h2><p>Our Privacy Policy created at GDPRPrivacyPolicy.net) applies only to our online activities and is valid for users of our program with regards to the information that they shared and/or collect in &quot;Show Metacritic.com ratings&quot;. This policy is not applicable to any information collected offline or via channels other than this program. <a href="https://gdprprivacypolicy.net">Our GDPR Privacy Policy</a> was generated from the GDPR Privacy Policy Generator.</p><h2>Contact</h2><p>Contact us via github <a href="https://github.com/cvzi/Metacritic-userscript">https://github.com/cvzi/Metacritic-userscript</a> or email cuzi@openmail.cc</p><h2>Consent</h2><p>By using our program ("userscript"), you hereby consent to our Privacy Policy and agree to its terms.</p>'
  177. const div = document.body.appendChild(document.createElement('div'))
  178. div.innerHTML = html
  179. div.style = 'z-index:9999;position:absolute;min-height:100%;top:0px; left:0px; right:0px; padding:10px; background:white; color:black; font-family:serif; font-size:16px'
  180. div.appendChild(document.createElement('br'))
  181. const acceptButton = div.appendChild(document.createElement('button'))
  182. acceptButton.setAttribute('style', 'color:black;background:#e5e4e4;border:2px #bbb outset;margin:5px;padding:2px 10px;font-size:16px;font-family:sans-serif;cursor:pointer')
  183. acceptButton.appendChild(document.createTextNode('Accept'))
  184. acceptButton.addEventListener('click', function () {
  185. div.remove()
  186. resolve(true)
  187. GM.setValue('gdpr', true)
  188. })
  189. const declineButton = div.appendChild(document.createElement('button'))
  190. declineButton.setAttribute('style', 'color:black;background:#e5e4e4;border:2px #bbb outset;margin:5px;padding:2px 10px;font-size:16px;font-family:sans-serif;cursor:pointer')
  191. declineButton.appendChild(document.createTextNode('Decline'))
  192. declineButton.addEventListener('click', function () {
  193. alert('You may uninstall the userscript now.')
  194. div.remove()
  195. resolve(false)
  196. GM.setValue('gdpr', false)
  197. })
  198. const space = div.appendChild(document.createElement('div'))
  199. space.style = 'height:2000px;'
  200. div.scrollIntoView()
  201. window.setTimeout(function () {
  202. 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.')
  203. }, 20)
  204. })
  205. })
  206. }
  207.  
  208. function delay (ms) {
  209. return new Promise(function (resolve) {
  210. window.setTimeout(() => resolve(), ms)
  211. })
  212. }
  213.  
  214. function getHostname (url) {
  215. const a = document.createElement('a')
  216. a.href = url
  217. return a.hostname
  218. }
  219. function absoluteMetaURL (url) {
  220. if (url.startsWith('https://')) {
  221. return url
  222. }
  223. if (url.startsWith('http://')) {
  224. return 'https' + url.substr(4)
  225. }
  226. if (url.startsWith('//')) {
  227. return baseURL + url.substr(2)
  228. }
  229. if (url.startsWith('/')) {
  230. return baseURL + url.substr(1)
  231. }
  232. return baseURL + url
  233. }
  234.  
  235. const parseLDJSONCache = {}
  236. function parseLDJSON (keys, condition) {
  237. if (document.querySelector('script[type="application/ld+json"]')) {
  238. const xmlEntitiesElement = document.createElement('div')
  239. const xmlEntitiesPattern = /&(?:#x[a-f0-9]+|#[0-9]+|[a-z0-9]+);?/ig
  240. const xmlEntities = function (s) {
  241. s = s.replace(xmlEntitiesPattern, (m) => {
  242. xmlEntitiesElement.innerHTML = m
  243. return xmlEntitiesElement.textContent
  244. })
  245. return s
  246. }
  247. const decodeXmlEntities = function (jsonObj) {
  248. // Traverse through object, decoding all strings
  249. if (jsonObj !== null && typeof jsonObj === 'object') {
  250. Object.entries(jsonObj).forEach(([key, value]) => {
  251. // key is either an array index or object key
  252. jsonObj[key] = decodeXmlEntities(value)
  253. })
  254. } else if (typeof jsonObj === 'string') {
  255. return xmlEntities(jsonObj)
  256. }
  257. return jsonObj
  258. }
  259.  
  260. const data = []
  261. const scripts = document.querySelectorAll('script[type="application/ld+json"]')
  262. for (let i = 0; i < scripts.length; i++) {
  263. let jsonld
  264. if (scripts[i].innerText in parseLDJSONCache) {
  265. jsonld = parseLDJSONCache[scripts[i].innerText]
  266. } else {
  267. try {
  268. jsonld = JSON.parse(scripts[i].innerText)
  269. parseLDJSONCache[scripts[i].innerText] = jsonld
  270. } catch (e) {
  271. parseLDJSONCache[scripts[i].innerText] = null
  272. continue
  273. }
  274. }
  275. if (jsonld) {
  276. if (Array.isArray(jsonld)) {
  277. data.push(...jsonld)
  278. } else {
  279. data.push(jsonld)
  280. }
  281. }
  282. }
  283. for (let i = 0; i < data.length; i++) {
  284. try {
  285. if (data[i] && data[i] && (typeof condition !== 'function' || condition(data[i]))) {
  286. if (Array.isArray(keys)) {
  287. const r = []
  288. for (let j = 0; j < keys.length; j++) {
  289. r.push(data[i][keys[j]])
  290. }
  291. return decodeXmlEntities(r)
  292. } else if (keys) {
  293. return decodeXmlEntities(data[i][keys])
  294. } else if (typeof condition === 'function') {
  295. return decodeXmlEntities(data[i]) // Return whole object
  296. }
  297. }
  298. } catch (e) {
  299. continue
  300. }
  301. }
  302. return decodeXmlEntities(data)
  303. }
  304. return null
  305. }
  306.  
  307. function name2metacritic (s) {
  308. const mc = s.normalize('NFKD').replace(/\//g, '').replace(/[\u0300-\u036F]/g, '').replace(/&/g, 'and').replace(/\W+/g, ' ').toLowerCase().trim().replace(/\W+/g, '-')
  309. if (!mc) {
  310. throw new Error("name2metacritic converted '" + s + "' to empty string")
  311. }
  312. return mc
  313. }
  314. function minutesSince (time) {
  315. const seconds = ((new Date()).getTime() - time.getTime()) / 1000
  316. return seconds > 60 ? parseInt(seconds / 60) + ' min ago' : 'now'
  317. }
  318. function randomStringId () {
  319. const id10 = () => Math.floor((1 + Math.random()) * 0x10000000000).toString(16).substring(1)
  320. return id10() + id10() + id10() + id10() + id10() + id10()
  321. }
  322. function fixMetacriticURLs (html) {
  323. return html.replace(/<a /g, '<a target="_blank" ').replace(/href="\//g, 'href="' + baseURL).replace(/src="\//g, 'src="' + baseURL)
  324. }
  325. function searchType2metacritic (type) {
  326. return ({
  327. movie: '2',
  328. pcgame: '13',
  329. xonegame: '13',
  330. ps4game: '13',
  331. music: '4', // TODO this is probably wrong
  332. tv: '1'
  333. })[type]
  334. }
  335. function metacritic2searchType (type) {
  336. return ({
  337. Album: 'music',
  338. TV: 'tv',
  339. Movie: 'movie',
  340. 'PC Game': 'pcgame',
  341. 'PS4 Game': 'ps4game',
  342. 'XONE Game': 'onegame',
  343. 'WIIU Game': 'xxxxx',
  344. '3DS Game': 'xxxx'
  345. })[type]
  346. }
  347.  
  348. function replaceBrackets (str) {
  349. str = str.replace(/\([^(]*\)/g, '')
  350. str = str.replace(/\[[^\]]*\]/g, '')
  351. return str.trim()
  352. }
  353. function removeSymbols (str) {
  354. str = str.replace(/[^\s0-9A-Za-zÀ-ÖØ-öø-ÿ]*/gi, '').trim()
  355. return str.trim()
  356. }
  357. const dashRegExp = /[-\u2010\u2011\u2012\u2013\u2014\u2015\uFE58\uFE63\uFF0D]/
  358. function removeAnythingAfterDash (str) {
  359. str = str.split(dashRegExp)[0]
  360. return str.trim()
  361. }
  362.  
  363. function broadenSearch (data, step, type) {
  364. if (type === 'pcgame') {
  365. if (step > 0) {
  366. data[0] = replaceBrackets(data[0])
  367. } else if (step > 1) {
  368. data[0] = removeSymbols(data[0])
  369. } else if (step > 2) {
  370. data[0] = removeAnythingAfterDash(data[0])
  371. }
  372. } else {
  373. data = data.map(removeSymbols)
  374. }
  375. return data
  376. }
  377.  
  378. function balloonAlert (message, timeout, title, css, click) {
  379. let header
  380. if (title) {
  381. header = '<div style="background:rgb(220,230,150); padding: 2px 12px;">' + title + '</div>'
  382. } else if (title === false) {
  383. header = ''
  384. } else {
  385. header = '<div style="background:rgb(220,230,150); padding: 2px 12px;">Userscript alert</div>'
  386. }
  387. const div = $('<div>' + header + '<div style="padding:5px">' + message.split('\n').join('<br>') + '</div></div>')
  388. div.css({
  389. position: 'fixed',
  390. top: 10,
  391. left: 10,
  392. maxWidth: 200,
  393. zIndex: '2147483601',
  394. background: 'rgb(240,240,240)',
  395. border: '2px solid yellow',
  396. borderRadius: '6px',
  397. boxShadow: '0 0 3px 3px rgba(100, 100, 100, 0.2)',
  398. fontFamily: 'sans-serif',
  399. color: 'black'
  400. })
  401. if (css) {
  402. div.css(css)
  403. }
  404. div.appendTo(document.body)
  405.  
  406. if (click) {
  407. div.click(function (ev) {
  408. $(this).hide(500)
  409. click.call(this, ev)
  410. })
  411. }
  412.  
  413. if (!click) {
  414. const close = $('<div title="Close" style="cursor:pointer; position:absolute; top:0px; right:3px;">&#10062;</div>').appendTo(div)
  415. close.click(function () {
  416. $(this.parentNode).hide(1000)
  417. })
  418. }
  419.  
  420. if (timeout && timeout > 0) {
  421. window.setTimeout(function () {
  422. div.hide(3000)
  423. }, timeout)
  424. }
  425. return div
  426. }
  427.  
  428. function filterUniversalUrl (url) {
  429. try {
  430. url = url.match(/http.+/)[0]
  431. } catch (e) { }
  432.  
  433. try {
  434. url = url.replace(/https?:\/\/(www.)?/, '')
  435. } catch (e) { }
  436.  
  437. if (url.indexOf('#') !== -1) {
  438. url = url.split('#')[0]
  439. }
  440.  
  441. if (url.startsWith('imdb.com/') && url.match(/(imdb\.com\/\w+\/\w+\/)/)) {
  442. // Remove movie subpage from imdb url
  443. return url.match(/(imdb\.com\/\w+\/\w+\/)/)[1]
  444. } else if (url.startsWith('boxofficemojo.com/') && url.indexOf('id=') !== -1) {
  445. // Keep the important id= on
  446. try {
  447. const parts = url.split('?')
  448. const page = parts[0] + '?'
  449. const idparam = parts[1].match(/(id=.+?)(\.|&)/)[1]
  450. return page + idparam
  451. } catch (e) {
  452. return url
  453. }
  454. } else {
  455. // Default: Remove parameters
  456. return url.split('?')[0].split('&')[0]
  457. }
  458. }
  459.  
  460. async function addToMap (url, metaurl) {
  461. const data = JSON.parse(await GM.getValue('map', '{}'))
  462.  
  463. url = filterUniversalUrl(url)
  464. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  465.  
  466. data[url] = metaurl
  467.  
  468. await GM.setValue('map', JSON.stringify(data));
  469.  
  470. (new Image()).src = baseURLwhitelist + '?docurl=' + encodeURIComponent(url) + '&metaurl=' + encodeURIComponent(metaurl) + '&ref=' + encodeURIComponent(randomStringId())
  471. return [url, metaurl]
  472. }
  473.  
  474. async function removeFromMap (url) {
  475. const data = JSON.parse(await GM.getValue('map', '{}'))
  476.  
  477. url = filterUniversalUrl(url)
  478. if (url in data) {
  479. delete data[url]
  480. await GM.setValue('map', JSON.stringify(data))
  481. }
  482. }
  483.  
  484. async function addToTemporaryBlacklist (metaurl) {
  485. const data = JSON.parse(await GM.getValue('temporaryblack', '{}'))
  486.  
  487. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  488. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/')
  489. metaurl = metaurl.replace(/^\/+/, '')
  490.  
  491. data[metaurl] = (new Date()).toJSON()
  492.  
  493. // Remove old entries
  494. const now = (new Date()).getTime()
  495. const timeout = TEMPORARY_BLACKLIST_TIMEOUT * 1000
  496. for (const prop in data) {
  497. if (now - (new Date(data[prop].time)).getTime() > timeout) {
  498. delete data[prop]
  499. }
  500. }
  501.  
  502. await GM.setValue('temporaryblack', JSON.stringify(data))
  503.  
  504. return true
  505. }
  506.  
  507. async function isTemporaryBlacklisted (metaurl) {
  508. const data = JSON.parse(await GM.getValue('temporaryblack', '{}'))
  509.  
  510. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  511. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/')
  512. metaurl = metaurl.replace(/^\/+/, '')
  513.  
  514. if (metaurl in data) {
  515. const now = (new Date()).getTime()
  516. const timeout = TEMPORARY_BLACKLIST_TIMEOUT * 1000
  517. if (now - (new Date(data[metaurl])).getTime() < timeout) {
  518. return true
  519. }
  520. }
  521. return false
  522. }
  523.  
  524. async function addToBlacklist (url, metaurl) {
  525. const data = JSON.parse(await GM.getValue('black', '[]'))
  526.  
  527. url = filterUniversalUrl(url)
  528. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  529.  
  530. data.push([url, metaurl])
  531.  
  532. await GM.setValue('black', JSON.stringify(data));
  533.  
  534. (new Image()).src = baseURLblacklist + '?docurl=' + encodeURIComponent(url) + '&metaurl=' + encodeURIComponent(metaurl) + '&ref=' + encodeURIComponent(randomStringId())
  535. return [url, metaurl]
  536. }
  537.  
  538. async function removeFromBlacklist (docurl, metaurl) {
  539. docurl = filterUniversalUrl(docurl)
  540. docurl = docurl.replace(/https?:\/\/(www.)?/, '')
  541.  
  542. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  543. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/') // remove double slash
  544. metaurl = metaurl.replace(/^\/+/, '') // remove starting slash
  545.  
  546. const data = JSON.parse(await GM.getValue('black', '[]')) // [ [docurl0, metaurl0] , [docurl1, metaurl1] , ... ]
  547. const found = []
  548. for (let i = 0; i < data.length; i++) {
  549. if (data[i][0] === docurl && data[i][1] === metaurl) {
  550. found.push(i)
  551. }
  552. }
  553. for (let i = found.length - 1; i >= 0; i--) {
  554. data.pop(i)
  555. }
  556.  
  557. await GM.setValue('black', JSON.stringify(data))
  558. }
  559.  
  560. async function isBlacklistedUrl (docurl, metaurl) {
  561. docurl = filterUniversalUrl(docurl)
  562. docurl = docurl.replace(/https?:\/\/(www.)?/, '')
  563.  
  564. metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
  565. metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/') // remove double slash
  566. metaurl = metaurl.replace(/^\/+/, '') // remove starting slash
  567.  
  568. const data = JSON.parse(await GM.getValue('black', '[]')) // [ [docurl0, metaurl0] , [docurl1, metaurl1] , ... ]
  569. for (let i = 0; i < data.length; i++) {
  570. if (data[i][0] === docurl && data[i][1] === metaurl) {
  571. return true
  572. }
  573. }
  574. return false
  575. }
  576.  
  577. let listenForHotkeysActive = false
  578. function listenForHotkeys (code, cb) {
  579. // Call cb() as soon as the code sequence was typed
  580. if (listenForHotkeysActive) {
  581. return
  582. }
  583. listenForHotkeysActive = true
  584. let i = 0
  585. $(document).bind('keydown.listenForHotkeys', function (ev) {
  586. if (document.activeElement === document.body) {
  587. if (ev.key !== code[i]) {
  588. i = 0
  589. } else {
  590. i++
  591. if (i === code.length) {
  592. ev.preventDefault()
  593. $(document).unbind('keydown.listenForHotkeys')
  594. cb()
  595. }
  596. }
  597. }
  598. })
  599. }
  600.  
  601. function waitForHotkeysMETA () {
  602. listenForHotkeys('meta', (ev) => openSearchBox())
  603. }
  604.  
  605. async function handleJSONredirect (response) {
  606. let blacklistedredirect = false
  607. const j = JSON.parse(response.responseText)
  608.  
  609. // Blacklist items from database received?
  610. if ('blacklist' in j && j.blacklist && j.blacklist.length) {
  611. // Save new blacklist items
  612. const data = JSON.parse(await GM.getValue('black', '[]'))
  613. for (let i = 0; i < j.blacklist.length; i++) {
  614. const saveDocurl = j.blacklist[i].docurl
  615. const saveMetaurl = j.blacklist[i].metaurl
  616.  
  617. data.push([saveDocurl, saveMetaurl])
  618. if (j.jsonRedirect === '/' + saveMetaurl) {
  619. // Redirect is blacklisted!
  620. blacklistedredirect = true
  621. }
  622. }
  623. await GM.setValue('black', JSON.stringify(data))
  624. }
  625. if (blacklistedredirect) {
  626. // Redirect was blacklisted, show nothing
  627. console.debug('ShowMetacriticRatings: Redirect was blacklisted -> show nothing')
  628. return null
  629. } else {
  630. // Load redirect
  631. current.metaurl = absoluteMetaURL(j.jsonRedirect)
  632. response = await asyncRequest({
  633. url: current.metaurl
  634. }).catch(function (response) {
  635. console.error('ShowMetacriticRatings: Error 01')
  636. })
  637. return response
  638. }
  639. }
  640.  
  641. function extractHoverFromFullPage (response) {
  642. let html = 'ShowMetacriticRatings:<br>Error occured in extractHoverFromFullPage()'
  643. try {
  644. // Try parsing HTML
  645. const doc = domParser().parseFromString(response.responseText, 'text/html')
  646.  
  647. let content = null
  648. // Try to get the review containers from the bottom of the page below the actors
  649. const carouselItems = doc.querySelectorAll('.c-reviewsSection_carouselContainer .c-reviewsOverview_overviewDetails')
  650. if (carouselItems.length > 0) {
  651. content = Array.from(carouselItems).map(e => e.outerHTML).join('\n\n')
  652. } else {
  653. // Fallback: Try to get the review containers from the right side of the page next to the poster/screenshot
  654. content = doc.querySelector('.c-productHero_scoreInfo').innerHTML
  655. }
  656.  
  657. if (!content) {
  658. throw new Error('No content found')
  659. }
  660.  
  661. html = `
  662. <div id="hover_div_a20230915">
  663.  
  664. ${content}
  665.  
  666. </div>
  667. `
  668. } catch (e) {
  669. console.warn('ShowMetacriticRatings: Error parsing HTML: ' + e)
  670. // fallback to cutting out the relevant parts
  671. const parts = response.responseText.split('c-productHero_score-container')
  672.  
  673. html = '<div class="' + parts[1].split('c-ratingReviewWrapper')[0] + '"></div></div>'
  674. if (html.length > 5000) {
  675. // Probably something went wrong, let's cut the response to prevent too long content
  676. console.warn('ShowMetacriticRatings: Cutting response to 5000 chars')
  677. html = html.substring(0, 5000)
  678. }
  679. }
  680. return html
  681. }
  682.  
  683. function asyncRequest (data) {
  684. return new Promise(function (resolve, reject) {
  685. isInRequestCache(data).then(function (cachedValue) {
  686. if (cachedValue) {
  687. return window.setTimeout(() => resolve(cachedValue), 10)
  688. }
  689. const defaultHeaders = {
  690. Referer: data.url,
  691. // Host: getHostname(data.url),
  692. 'User-Agent': navigator.userAgent
  693. }
  694. const defaultData = {
  695. method: 'GET',
  696. onload: function (response) {
  697. storeInRequestCache(data, response)
  698. resolve(response)
  699. },
  700. onerror: (response) => reject(response)
  701. }
  702. if ('headers' in data) {
  703. data.headers = Object.assign(defaultHeaders, data.headers)
  704. } else {
  705. data.headers = defaultHeaders
  706. }
  707. data = Object.assign(defaultData, data)
  708. GM.xmlHttpRequest(data)
  709. })
  710. })
  711. }
  712.  
  713. async function storeInRequestCache (requestData, response) {
  714. const newkey = JSON.stringify({
  715. url: requestData.url,
  716. method: requestData.method || 'GET',
  717. data: requestData.data || null
  718. })
  719. const cache = JSON.parse(await GM.getValue('requestcache', '{}'))
  720. const now = (new Date()).getTime()
  721. const timeout = 15 * 60 * 1000
  722. for (const prop in cache) {
  723. // Delete cached values, that are older than 15 minutes
  724. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  725. delete cache[prop]
  726. }
  727. }
  728.  
  729. const newobj = {}
  730. for (const key in response) {
  731. newobj[key] = response[key]
  732. }
  733. newobj.responseText = '' + response.responseText
  734. newobj.cached = true
  735. if (!('time' in newobj)) {
  736. newobj.time = (new Date()).toJSON()
  737. }
  738.  
  739. cache[newkey] = newobj
  740.  
  741. await GM.setValue('requestcache', JSON.stringify(cache))
  742. }
  743.  
  744. async function isInRequestCache (requestData) {
  745. const key = JSON.stringify({
  746. url: requestData.url,
  747. method: requestData.method || 'GET',
  748. data: requestData.data || null
  749. })
  750.  
  751. const cache = JSON.parse(await GM.getValue('requestcache', '{}'))
  752. const now = (new Date()).getTime()
  753. const timeout = 15 * 60 * 1000
  754. for (const prop in cache) {
  755. // Delete cached values, that are older than 15 minutes
  756. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  757. delete cache[prop]
  758. }
  759. }
  760.  
  761. if (key in cache) {
  762. return cache[key]
  763. } else {
  764. return false
  765. }
  766. }
  767.  
  768. async function storeInHoverCache (metaurl, response, orgMetaUrl) {
  769. const cache = JSON.parse(await GM.getValue('hovercache', '{}'))
  770. const now = (new Date()).getTime()
  771. const timeout = 2 * 60 * 60 * 1000
  772. for (const prop in cache) {
  773. // Delete cached values, that are older than 2 hours
  774. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  775. delete cache[prop]
  776. }
  777. }
  778.  
  779. const newobj = {}
  780. for (const key in response) {
  781. newobj[key] = response[key]
  782. }
  783. newobj.responseText = '' + response.responseText
  784. newobj.cached = true
  785. if (!('time' in newobj)) {
  786. newobj.time = (new Date()).toJSON()
  787. }
  788.  
  789. cache[metaurl] = newobj
  790. if (orgMetaUrl && orgMetaUrl !== metaurl) { // Store redirect
  791. cache[orgMetaUrl] = { time: (new Date()).toJSON(), redirect: metaurl }
  792. }
  793.  
  794. await GM.setValue('hovercache', JSON.stringify(cache))
  795. }
  796.  
  797. async function isInHoverCache (metaurl) {
  798. const cache = JSON.parse(await GM.getValue('hovercache', '{}'))
  799. const now = (new Date()).getTime()
  800. const timeout = 2 * 60 * 60 * 1000
  801. for (const prop in cache) {
  802. // Delete cached values, that are older than 2 hours
  803. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  804. delete cache[prop]
  805. }
  806. }
  807.  
  808. function resolveRedirects (cacheEntry) {
  809. if (cacheEntry.redirect) {
  810. const newkey = cacheEntry.redirect
  811. if (newkey in cache) {
  812. const value = cache[newkey]
  813. delete cache[newkey]
  814. return resolveRedirects(value)
  815. }
  816. } else {
  817. return cacheEntry
  818. }
  819. return false
  820. }
  821.  
  822. if (metaurl in cache) {
  823. const value = cache[metaurl]
  824. delete cache[metaurl]
  825. return resolveRedirects(value)
  826. } else {
  827. return false
  828. }
  829. }
  830.  
  831. async function loadHoverInfo () {
  832. const cacheResponse = await isInHoverCache(current.metaurl)
  833. if (cacheResponse !== false) {
  834. console.debug(`ShowMetacriticRatings: loadHoverInfo () ${current.metaurl} found in hover cache`)
  835. if (cacheResponse.responseText.indexOf('"jsonRedirect"') !== -1) {
  836. return await handleJSONredirect(cacheResponse)
  837. }
  838. return cacheResponse
  839. }
  840. const requestURL = baseURLdatabase
  841. const requestParams = 'm=' + encodeURIComponent(current.docurl) + '&a=' + encodeURIComponent(current.metaurl)
  842.  
  843. let response = await asyncRequest({
  844. method: 'POST',
  845. url: requestURL,
  846. data: requestParams,
  847. headers: {
  848. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
  849. }
  850. }).catch(function (response) {
  851. console.warn('ShowMetacriticRatings: Error 02\nurl=' + requestURL + '\nparams=' + requestParams + '\nstatus=' + response.status)
  852. })
  853.  
  854. if (response.responseText && response.responseText.indexOf('"jsonRedirect"') !== -1) {
  855. response = await handleJSONredirect(response)
  856. }
  857.  
  858. if (response.status >= 500) {
  859. // Metacritic server error, try again after 2 seconds
  860. console.warn('ShowMetacriticRatings: Metacritic server error\nwait 2s for retry\nurl=' + current.metaurl + '\nstatus=' + response.status)
  861. await delay(2000)
  862. response = await asyncRequest({ url: current.metaurl }).catch(function (response) {
  863. console.warn('ShowMetacriticRatings: Error 06\nurl=' + current.metaurl + '\nstatus=' + response.status)
  864. })
  865. if (response.status > 300) {
  866. console.warn('ShowMetacriticRatings: Metacritic server error. Error 07. Retry failed as well.\nurl=' + current.metaurl + '\nstatus=' + response.status)
  867. } else {
  868. const newobj = {}
  869. for (const key in response) {
  870. newobj[key] = response[key]
  871. }
  872. newobj.responseText = extractHoverFromFullPage(response)
  873. response = newobj
  874. }
  875. }
  876.  
  877. // Extract relevant data from HTML
  878. if (!('time' in response)) {
  879. response.time = (new Date()).toJSON()
  880. }
  881. if (response.status === 200 && response.responseText) {
  882. const newobj = {}
  883. for (const key in response) {
  884. newobj[key] = response[key]
  885. }
  886. newobj.responseText = extractHoverFromFullPage(response)
  887. response = newobj
  888. return response
  889. } else {
  890. const error = new Error('ShowMetacriticRatings: loadHoverInfo()\nUrl: ' + response.finalUrl + '\nStatus: ' + response.status)
  891. error.status = response.status
  892. error.responseText = response.responseText
  893. throw error
  894. }
  895. }
  896.  
  897. function changePosition () {
  898. // Cycle through positions
  899. GM.getValue('position', JSON.stringify(windowPositions[0])).then(function (s) {
  900. let index
  901. for (index = 0; index < windowPositions.length; index++) {
  902. if (JSON.stringify(windowPositions[index]) === s) {
  903. break
  904. }
  905. }
  906. const nextIndex = (index + 1) % windowPositions.length
  907. GM.setValue('position', JSON.stringify(windowPositions[nextIndex])).then(function () {
  908. document.location.reload()
  909. })
  910. })
  911. }
  912.  
  913. const current = {
  914. metaurl: false,
  915. docurl: false,
  916. type: false,
  917. data: [], // Array of raw search keys
  918. searchTerm: false,
  919. product: null,
  920. broadenCounter: 0
  921. }
  922.  
  923. async function loadMetacriticUrl (fromSearch) {
  924. if (!current.metaurl) {
  925. alert('ShowMetacriticRatings: Error 04')
  926. return
  927. }
  928. const orgMetaUrl = current.metaurl
  929. if (await isBlacklistedUrl(document.location.href, current.metaurl)) {
  930. waitForHotkeysMETA()
  931. return
  932. }
  933.  
  934. if (await isTemporaryBlacklisted(current.metaurl)) {
  935. console.debug(`ShowMetacriticRatings: loadMetacriticUrl(fromSearch=${fromSearch}) ${current.metaurl} is temporary blacklisted`)
  936. waitForHotkeysMETA()
  937. return
  938. }
  939.  
  940. const response = await loadHoverInfo().catch(async function (response) {
  941. if (response instanceof Error || (response && response.stack && response.message)) {
  942. if (!fromSearch && ('status' in response && response.status === 404)) {
  943. console.debug('ShowMetacriticRatings: loadMetacriticUrl(): status=404', response)
  944. // No results
  945. let broadenFct = broadenSearch // global broadenSearch function is the default
  946. if ('broaden' in current.product) {
  947. // try product 'broaden'-function if it is defined
  948. broadenFct = current.product.broaden
  949. }
  950. const newData = await broadenFct(current.data.slice(0), ++current.broadenCounter, current.type)
  951. if (JSON.stringify(newData) !== JSON.stringify(current.data)) {
  952. current.data = newData
  953. metacritic[current.type](current.docurl, current.product, ...newData)
  954. } else if (JSON.stringify(newData) === JSON.stringify(current.data)) {
  955. // Same data as before, try once again to broaden
  956. const newData2 = await broadenFct(current.data.slice(0), ++current.broadenCounter, current.type)
  957. if (JSON.stringify(newData2) !== JSON.stringify(current.data)) {
  958. current.data = newData2
  959. metacritic[current.type](current.docurl, current.product, ...newData2)
  960. } else {
  961. console.debug('ShowMetacriticRatings: loadMetacriticUrl(): ' + ('broaden' in current.product ? 'product specific' : 'global') + " 'broaden search' did not change after " + current.broadenCounter + ' steps')
  962. }
  963. } else {
  964. console.debug("ShowMetacriticRatings: loadMetacriticUrl(): Unexpected result from 'broaden'-function: ", newData)
  965. }
  966. } else {
  967. console.error(`ShowMetacriticRatings: loadMetacriticUrl(fromSearch=${fromSearch}) current.metaurl = ${current.metaurl}. Error in loadHoverInfo():\n`, response)
  968. }
  969. }
  970.  
  971. if (fromSearch) {
  972. startSearch()
  973. }
  974. })
  975.  
  976. if (await isBlacklistedUrl(document.location.href, current.metaurl)) {
  977. waitForHotkeysMETA()
  978. return
  979. }
  980.  
  981. if (typeof response !== 'undefined') {
  982. showHoverInfo(response, orgMetaUrl)
  983. } else {
  984. waitForHotkeysMETA()
  985. }
  986. }
  987.  
  988. async function startSearch () {
  989. waitForHotkeysMETA()
  990.  
  991. const cache = JSON.parse(await GM.getValue('autosearchcache', '{}'))
  992. const now = (new Date()).getTime()
  993. const timeout = 2 * 60 * 60 * 1000
  994. for (const prop in cache) {
  995. // Delete cached values, that are older than 2 hours
  996. if (now - (new Date(cache[prop].time)).getTime() > timeout) {
  997. delete cache[prop]
  998. }
  999. }
  1000.  
  1001. if (current.type === 'music') {
  1002. current.searchTerm = current.data[0]
  1003. } else {
  1004. current.searchTerm = current.data.join(' ')
  1005. }
  1006. let response
  1007. if (current.searchTerm in cache) {
  1008. response = cache[current.searchTerm]
  1009. } else {
  1010. response = await asyncRequest({
  1011. method: 'POST',
  1012. url: baseURLautosearch,
  1013. data: 'search_term=' + encodeURIComponent(current.searchTerm) + '&image_size=98&search_each=1&sort_type=popular',
  1014. headers: {
  1015. Referer: current.metaurl,
  1016. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  1017. // Host: 'www.metacritic.com',
  1018. 'User-Agent': 'MetacriticUserscript Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0',
  1019. 'X-Requested-With': 'XMLHttpRequest'
  1020. }
  1021. })
  1022. response = {
  1023. time: (new Date()).toJSON(),
  1024. json: JSON.parse(response.responseText)
  1025. }
  1026. cache[current.searchTerm] = response
  1027. await GM.setValue('autosearchcache', JSON.stringify(cache))
  1028. }
  1029.  
  1030. if (!response || !('json' in response)) {
  1031. alert('ShowMetacriticRatings: Error 05')
  1032. }
  1033. const data = response.json
  1034. let multiple = false
  1035. if (data && data.autoComplete && data.autoComplete.results && data.autoComplete.results.length) {
  1036. // Remove data with wrong type
  1037. data.autoComplete = data.autoComplete.results
  1038.  
  1039. const newdata = []
  1040. data.autoComplete.forEach(function (result) {
  1041. if (metacritic2searchType(result.refType) === current.type) {
  1042. newdata.push(result)
  1043. }
  1044. })
  1045. data.autoComplete = newdata
  1046. if (data.autoComplete.length === 0) {
  1047. // No results
  1048. console.debug('ShowMetacriticRatings: No results (after filtering by type) for searchTerm=' + current.searchTerm)
  1049. } else if (data.autoComplete.length === 1) {
  1050. // One result, let's show it
  1051. if (!await isBlacklistedUrl(document.location.href, absoluteMetaURL(data.autoComplete[0].url))) {
  1052. current.metaurl = absoluteMetaURL(data.autoComplete[0].url)
  1053. loadMetacriticUrl(true)
  1054. return
  1055. }
  1056. } else {
  1057. // More than one result
  1058. multiple = true
  1059. console.debug('ShowMetacriticRatings: Multiple results for searchTerm=' + current.searchTerm)
  1060. const exactMatches = []
  1061. data.autoComplete.forEach(function (result, i) { // Try to find the correct result by matching the search term to exactly one movie title
  1062. if (current.searchTerm === result.name) {
  1063. exactMatches.push(result)
  1064. }
  1065. })
  1066. if (exactMatches.length === 1) {
  1067. // Only one exact match, let's show it
  1068. console.debug('ShowMetacriticRatings: Only one exact match for searchTerm=' + current.searchTerm)
  1069. if (!await isBlacklistedUrl(document.location.href, absoluteMetaURL(exactMatches[0].url))) {
  1070. current.metaurl = absoluteMetaURL(exactMatches[0].url)
  1071. loadMetacriticUrl(true)
  1072. return
  1073. }
  1074. }
  1075. }
  1076. } else {
  1077. console.debug('ShowMetacriticRatings: No results (at all) for searchTerm=' + current.searchTerm)
  1078. }
  1079. // HERE: multiple results or no result. The user may type "meta" now
  1080. if (multiple) {
  1081. balloonAlert('Multiple metacritic results. Type &#34;meta&#34; for manual search.', 10000, false, { bottom: 5, top: 'auto', maxWidth: 400, paddingRight: 5 }, () => openSearchBox(true))
  1082. }
  1083. }
  1084.  
  1085. function openSearchBox (search) {
  1086. let query
  1087. if (current.type === 'music') {
  1088. query = current.data[0]
  1089. } else {
  1090. query = current.data.join(' ')
  1091. }
  1092. $('#mcdiv123').remove()
  1093. const div = $('<div id="mcdiv123"></div>').appendTo(document.body)
  1094. div.css({
  1095. position: 'fixed',
  1096. bottom: 0,
  1097. left: 0,
  1098. minWidth: 300,
  1099. maxHeight: '80%',
  1100. maxWidth: 640,
  1101. overflow: 'auto',
  1102. backgroundColor: '#fff',
  1103. border: '2px solid #bbb',
  1104. borderRadius: ' 6px',
  1105. boxShadow: '0 0 3px 3px rgba(100, 100, 100, 0.2)',
  1106. color: '#000',
  1107. padding: ' 3px',
  1108. zIndex: '2147483601'
  1109. })
  1110.  
  1111. GM.getValue('position', false).then(function (s) {
  1112. if (s) {
  1113. div.css({
  1114. top: '',
  1115. left: '',
  1116. bottom: '',
  1117. right: ''
  1118. })
  1119. div.css(JSON.parse(s))
  1120. }
  1121. })
  1122.  
  1123. $('<input type="text" size="60" id="mcisearchquery" style="background:white;color:black;">').appendTo(div).focus().val(query).on('keypress', function (e) {
  1124. const code = e.keyCode || e.which
  1125. if (code === 13) { // Enter key
  1126. searchBoxSearch(e, $('#mcisearchquery').val())
  1127. }
  1128. })
  1129. $('<button id="mcisearchbutton" style="background:silver;color:black;">').text('Search').appendTo(div).click((ev) => searchBoxSearch(ev, $('#mcisearchquery').val()))
  1130. $('<div style="color:red; font-family:sans-serif; padding:5px;">Sorry, the search function is currently broken because of the new website design of metacritic.com 😭</div>').appendTo(div)
  1131. }
  1132. async function searchBoxSearch (ev, query) {
  1133. if (!query) { // Use values from search form
  1134. query = current.searchTerm
  1135. }
  1136.  
  1137. const type = searchType2metacritic(current.type)
  1138.  
  1139. const style = document.createElement('style')
  1140. style.type = 'text/css'
  1141. style.innerHTML = CSS
  1142. document.head.appendChild(style)
  1143.  
  1144. const div = $('#mcdiv123')
  1145. const loader = $('<div style="width:20px; height:20px;display:inline-block" class="grespinner"></div>').appendTo($('#mcisearchbutton'))
  1146.  
  1147. const url = baseURLsearch.replace('{type}', encodeURIComponent(type)).replace('{query}', encodeURIComponent(query))
  1148.  
  1149. const response = await asyncRequest({
  1150. url,
  1151. headers: {
  1152. 'User-Agent': 'MetacriticUserscript ' + navigator.userAgent
  1153. }
  1154. }).catch(function (response) {
  1155. alert('Search failed!\n' + response.finalUrl + '\nStatus: ' + response.status + '\n' + response.responseText ? response.responseText.substring(0, 500) : 'Empty response')
  1156. })
  1157.  
  1158. const results = []
  1159. if (!~response.responseText.indexOf('No search results found.')) {
  1160. const d = $('<html>').html(response.responseText)
  1161. d.find('.c-pageSiteSearch-results .g-grid-container').each(function () {
  1162. const item = this.querySelector('.c-pageSiteSearch-results-item')
  1163. if (item) {
  1164. item.querySelectorAll('.c-globalImagePlaceholder').forEach(e => e.remove())
  1165. item.querySelectorAll('picture img').forEach(e => (e.style.display = ''))
  1166. item.querySelectorAll('c-pageSiteSearch-results-item-image').forEach(e => (e.style.float = 'left'))
  1167.  
  1168. results.push(item.innerHTML)
  1169. }
  1170. })
  1171. }
  1172.  
  1173. if (results && results.length > 0) {
  1174. // Show results
  1175. loader.remove()
  1176.  
  1177. const accept = function (ev) {
  1178. const parentDiv = $(this.parentNode)
  1179. const a = parentDiv.find("a[href*='metacritic.com']")
  1180. const metaurl = a.attr('href')
  1181. const docurl = document.location.href
  1182.  
  1183. const resultDivParent = parentDiv.parent()
  1184. resultDivParent.html('')
  1185. resultDivParent.append(loader)
  1186.  
  1187. removeFromBlacklist(docurl, metaurl).then(function () {
  1188. addToMap(docurl, metaurl).then(function () {
  1189. current.metaurl = metaurl
  1190. loadMetacriticUrl().then(() => loader.remove())
  1191. })
  1192. })
  1193. }
  1194. const denyAll = function (ev) {
  1195. const docurl = document.location.href
  1196. $('#mcdiv123searchresults').find("div.result a[href*='metacritic.com']").each(function () {
  1197. addToBlacklist(docurl, this.href)
  1198. })
  1199. }
  1200.  
  1201. const resultdiv = $('#mcdiv123searchresults').length
  1202. ? $('#mcdiv123searchresults').html('')
  1203. : $('<div id="mcdiv123searchresults"></div>').css({
  1204. 'max-width': '95%',
  1205. '--grid-cols': '2',
  1206. display: 'grid',
  1207. 'grid-gap': '1rem',
  1208. gap: '1rem',
  1209. 'grid-template-columns': '1fr'
  1210. }).appendTo(div)
  1211. results.forEach(function (html) {
  1212. const singleresult = $('<div class="result"></div>').html(fixMetacriticURLs(html) + '<div style="clear:left"></div>').appendTo(resultdiv)
  1213. $('<span title="Assist us: This is the correct entry!" style="cursor:pointer; color:green; font-size: 13px;">&check;</span>').prependTo(singleresult).click(accept)
  1214. })
  1215. resultdiv.find('.metascore_w.album').removeClass('album') // Remove some classes
  1216. resultdiv.find('.must-see').remove() // Remove some elements
  1217.  
  1218. const sub = $('#mcdiv123 .sub').length ? $('#mcdiv123 .sub').html('') : $('<div class="sub"></div>').appendTo(div)
  1219. $('<a style="color:#b6b6b6; font-size: 11px;" target="_blank" href="' + url + '" title="Open Metacritic">' + decodeURI(url.replace('https://www.', '@')) + '</a>').appendTo(sub)
  1220. $('<span title="Hide me" style="cursor:pointer; float:right; color:#b6b6b6; font-size: 11px;">&#10062;</span>').appendTo(sub).click(function () {
  1221. document.body.removeChild(this.parentNode.parentNode)
  1222. })
  1223. $('<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() })
  1224. } else {
  1225. // No results
  1226. loader.remove()
  1227. const resultdiv = $('#mcdiv123searchresults').length ? $('#mcdiv123searchresults').html('') : $('<div id="mcdiv123searchresults"></div>').appendTo(div)
  1228. resultdiv.html('No search results.')
  1229.  
  1230. const sub = $('#mcdiv123 .sub').length ? $('#mcdiv123 .sub').html('') : $('<div class="sub"></div>').appendTo(div)
  1231. $('<a style="color:#b6b6b6; font-size: 11px;" target="_blank" href="' + url + '" title="Open Metacritic">' + decodeURI(url.replace('https://www.', '@')) + '</a>').appendTo(sub)
  1232. $('<span title="Hide me" style="cursor:pointer; float:right; color:#b6b6b6; font-size: 11px;">&#10062;</span>').appendTo(sub).click(function () {
  1233. document.body.removeChild(this.parentNode.parentNode)
  1234. })
  1235. }
  1236. }
  1237.  
  1238. function showHoverInfo (response, orgMetaUrl) {
  1239. const html = fixMetacriticURLs(response.responseText)
  1240. const time = new Date(response.time)
  1241. const url = response.finalUrl
  1242.  
  1243. $('#mcdiv123').remove()
  1244. const div = $('<div id="mcdiv123"></div>').appendTo(document.body)
  1245. div.css({
  1246. position: 'fixed',
  1247. bottom: 0,
  1248. left: 0,
  1249. minWidth: 150,
  1250. backgroundColor: '#fff',
  1251. border: '2px solid #bbb',
  1252. borderRadius: ' 6px',
  1253. boxShadow: '0 0 3px 3px rgba(100, 100, 100, 0.2)',
  1254. color: '#000',
  1255. padding: ' 3px',
  1256. zIndex: '2147483601'
  1257. })
  1258.  
  1259. GM.getValue('position', false).then(function (s) {
  1260. if (s) {
  1261. div.css({
  1262. top: '',
  1263. left: '',
  1264. bottom: '',
  1265. right: ''
  1266. })
  1267. div.css(JSON.parse(s))
  1268. }
  1269. })
  1270.  
  1271. // Functions for communication between page and iframe
  1272. // Mozilla can access parent.document
  1273. // Chrome can use postMessage()
  1274. let frameStatus = false // if this remains false, loading the frame content failed. A reason could be "Content Security Policy"
  1275. function tryToLoadMoreMetacriticDetails (myframe) {
  1276. console.log('ShowMetacriticRatings: tryToLoadMoreMetacriticDetails current', current)
  1277. if (!current.metaurl) {
  1278. return
  1279. }
  1280.  
  1281. let url = current.metaurl
  1282. if (url.endsWith('/')) {
  1283. url = url + 'details/'
  1284. } else {
  1285. url = url + '/details/'
  1286. }
  1287.  
  1288. GM.xmlHttpRequest({
  1289. url,
  1290. onload: function (response) {
  1291. const doc = domParser().parseFromString(response.responseText, 'text/html')
  1292.  
  1293. const titleA = doc.querySelector('.c-productSubpageHeader_back')
  1294. titleA.querySelectorAll('.c-productSubpageHeader_backIcon').forEach(e => e.remove())
  1295. const titleHTML = titleA.outerHTML
  1296.  
  1297. const image = doc.querySelector('picture img')
  1298. image.style.display = ''
  1299. const imageHTML = image.outerHTML
  1300.  
  1301. let detailsTable = Array.from(doc.querySelectorAll('.c-movieDetails_sectionContainer')).map(e => Array.from(e.children).map(e => e.textContent.trim()))
  1302.  
  1303. detailsTable = detailsTable.filter(columns => {
  1304. if (columns[0].search(/release date/i) !== -1) {
  1305. return true
  1306. }
  1307. if (columns[0].search(/genres/i) !== -1) {
  1308. return true
  1309. }
  1310. if (columns[0].search(/developer/i) !== -1) {
  1311. return true
  1312. }
  1313. if (columns[0].search(/publisher/i) !== -1) {
  1314. return true
  1315. }
  1316. return false
  1317. }).map(columns => columns.join(': '))
  1318.  
  1319. const html = imageHTML + '<br>' + titleHTML + '<br>' + detailsTable.join('<br>')
  1320.  
  1321. myframe.contentWindow.postMessage({
  1322. mcimessage_addhtml: true,
  1323. mcimessage_html: html
  1324. }, '*')
  1325. }
  1326. })
  1327. }
  1328. function loadExternalImage (url, myframe) {
  1329. // Load external image, bypass CSP
  1330. GM.xmlHttpRequest({
  1331. method: 'GET',
  1332. url,
  1333. responseType: 'arraybuffer',
  1334. onload: function (response) {
  1335. myframe.contentWindow.postMessage({
  1336. mcimessage_imgLoaded: true,
  1337. mcimessage_imgData: response.response,
  1338. mcimessage_imgOrgSrc: url
  1339. }, '*')
  1340. }
  1341. })
  1342. }
  1343. const functions = {
  1344. parent: function () {
  1345. const f = parent.document.getElementById('mciframe123')
  1346. let lastdiff = -200000
  1347. window.addEventListener('message', function (e) {
  1348. if (typeof e.data !== 'object') {
  1349. return
  1350. } else if ('mcimessage0' in e.data) {
  1351. frameStatus = true // Frame content was loaded successfully
  1352. tryToLoadMoreMetacriticDetails(f)
  1353. } else if ('mcimessage1' in e.data) {
  1354. f.style.width = parseInt(f.style.width) + 5 + 'px'
  1355. if (e.data.heightdiff === lastdiff) {
  1356. f.style.height = parseInt(f.style.height) + 10 + 'px'
  1357. }
  1358. lastdiff = e.data.heightdiff
  1359. } else if ('mcimessage2' in e.data) {
  1360. f.style.height = parseInt(f.style.height) + 10 + 'px'
  1361. } else if ('mcimessage_loadImg' in e.data) {
  1362. loadExternalImage(e.data.mcimessage_imgUrl, f)
  1363. } else {
  1364. return
  1365. }
  1366. if (f.contentWindow != null) {
  1367. f.contentWindow.postMessage({
  1368. mcimessage3: true,
  1369. mciframe123_clientHeight: f.clientHeight,
  1370. mciframe123_clientWidth: f.clientWidth
  1371. }, '*')
  1372. }
  1373. })
  1374. },
  1375. frame: function () {
  1376. parent.postMessage({ mcimessage0: true }, '*') // Loading frame content was successfull
  1377.  
  1378. let i = 0
  1379. window.addEventListener('message', function (e) {
  1380. if (typeof e.data === 'object' && 'mcimessage_imgLoaded' in e.data) {
  1381. // Load external image
  1382. const arrayBufferView = new Uint8Array(e.data.mcimessage_imgData)
  1383. const blob = new Blob([arrayBufferView], { type: 'image/jpeg' })
  1384. const urlCreator = window.URL || window.webkitURL
  1385. const imageUrl = urlCreator.createObjectURL(blob)
  1386. const img = failedImages[e.data.mcimessage_imgOrgSrc]
  1387. img.src = imageUrl
  1388. }
  1389. if (typeof e.data === 'object' && 'mcimessage_addhtml' in e.data) {
  1390. const div = document.body.appendChild(document.createElement('div'))
  1391. div.innerHTML = e.data.mcimessage_html
  1392. }
  1393.  
  1394. if (!('mcimessage3' in e.data)) return
  1395.  
  1396. if (e.data.mciframe123_clientHeight < document.body.scrollHeight && i < 100) {
  1397. parent.postMessage({ mcimessage2: 1 }, '*')
  1398. i++
  1399. }
  1400. if (i >= 100) {
  1401. parent.postMessage({ mcimessage1: 1, heightdiff: document.body.scrollHeight - e.data.mciframe123_clientHeight }, '*')
  1402. i = 0
  1403. }
  1404. })
  1405. parent.postMessage({ mcimessage1: 1, heightdiff: -100000 }, '*')
  1406. }
  1407.  
  1408. }
  1409.  
  1410. const css = `
  1411. #hover_div_a20230915{font-family:sans-serif;color:#262626;font-size:1rem;line-height:1.625rem}#hover_div_a20230915 a,#hover_div_a20230915 a:hover{text-decoration:none}#hover_div_a20230915 a:hover{color:#09f}#hover_div_a20230915 a{color:#000}#hover_div_a20230915 a:focus{color:grey}#hover_div_a20230915 .g-border-black,#hover_div_a20230915 .g-border-gray100{border-color:#000}#hover_div_a20230915 .g-color-black,#hover_div_a20230915 .g-color-gray100{color:#000}#hover_div_a20230915 .g-border-gray98{border-color:#191919}#hover_div_a20230915 .g-color-gray98{color:#191919}#hover_div_a20230915 .g-border-gray90{border-color:#262626}#hover_div_a20230915 .g-color-gray90{color:#262626}#hover_div_a20230915 .g-border-gray80{border-color:#404040}#hover_div_a20230915 .g-color-gray80{color:#404040}#hover_div_a20230915 .g-border-gray70{border-color:#666}#hover_div_a20230915 .g-color-gray70{color:#666}#hover_div_a20230915 .g-border-gray60{border-color:grey}#hover_div_a20230915 .g-color-gray60{color:grey}#hover_div_a20230915 .g-border-gray50{border-color:#999}#hover_div_a20230915 .g-color-gray50{color:#999}#hover_div_a20230915 .g-border-gray40{border-color:#bfbfbf}#hover_div_a20230915 .g-color-gray40{color:#bfbfbf}#hover_div_a20230915 .g-border-gray30{border-color:#d8d8d8}#hover_div_a20230915 .g-color-gray30{color:#d8d8d8}#hover_div_a20230915 .g-border-gray20{border-color:#e6e6e6}#hover_div_a20230915 .g-color-gray20{color:#e6e6e6}#hover_div_a20230915 .g-border-gray10{border-color:#f2f2f2}#hover_div_a20230915 .g-color-gray10{color:#f2f2f2}#hover_div_a20230915 .g-border-gray0,#hover_div_a20230915 .g-border-white{border-color:#fff}#hover_div_a20230915 .g-color-gray0,#hover_div_a20230915 .g-color-white{color:#fff}#hover_div_a20230915 .g-border-red{border-color:#eb0036}#hover_div_a20230915 .g-color-red{color:#eb0036}#hover_div_a20230915 .g-border-green{border-color:#01b44f}#hover_div_a20230915 .g-color-green{color:#01b44f}#hover_div_a20230915 .g-width-large{width:1.5rem}#hover_div_a20230915 .g-height-large{height:1.5rem}#hover_div_a20230915 .g-width-100{width:100%}#hover_div_a20230915 .g-height-100{height:100%}#hover_div_a20230915 .g-text-large{font-size:1.5rem;line-height:2rem}#hover_div_a20230915 .g-text-xxsmall{font-size:xx-small}#hover_div_a20230915 .g-text-bold{font-weight:700}#hover_div_a20230915 .g-text-link{text-decoration:underline}#hover_div_a20230915 .u-block{display:block}#hover_div_a20230915 .u-flexbox{display:flex}#hover_div_a20230915 .u-flexbox-column{display:flex;flex-direction:column}#hover_div_a20230915 .u-flexbox-justifyCenter{justify-content:center}#hover_div_a20230915 .u-flexbox-alignCenter{align-items:center}#hover_div_a20230915 .u-grid{display:grid;grid-gap:0;grid-gap:var(--grid-gap,0)}#hover_div_a20230915 .u-grid-2column{-ms-grid-columns:50% 50%;display:grid;grid-template:auto/repeat(2,1fr)}#hover_div_a20230915 .u-grid-3column{-ms-grid-columns:33.3% 33.3% 33.3%;display:grid;grid-template:auto/repeat(3,1fr)}#hover_div_a20230915 .u-grid-4column{-ms-grid-columns:25% 25% 25% 25%;display:grid;grid-template:auto/repeat(4,1fr)}#hover_div_a20230915 .u-grid-5column{-ms-grid-columns:20% 20% 20% 20% 20%;display:grid;grid-template:auto/repeat(5,1fr)}#hover_div_a20230915 .u-grid-7column{-ms-grid-columns:14.2857% 14.2857% 14.2857% 14.2857% 14.2857% 14.2857% 14.2857%;display:grid;grid-template:auto/repeat(7,1fr)}#hover_div_a20230915 .u-grid-column-span2{grid-column-end:span 2}#hover_div_a20230915 .u-grid-column-span3{grid-column-end:span 3}#hover_div_a20230915 .u-grid-column-span4{grid-column-end:span 4}#hover_div_a20230915 .u-text-center{text-align:center}#hover_div_a20230915 .c-siteReviewScore_large{border-radius:0.5rem;height:4rem;width:4rem;font-size:2rem}#hover_div_a20230915 .c-siteReviewScore_user{border-radius:50%}#hover_div_a20230915 .c-reviewsStats{padding:1rem 0;grid-template-columns:1fr 1fr 1fr;justify-content:space-evenly;font-size:0.75rem;line-height:1.25rem}#hover_div_a20230915 div[class^=c-reviewsStats_]:first-child,#hover_div_a20230915 div[class^=c-reviewsStats_]:nth-child(2){border-right:0.0625rem solid #d8d8d8}#hover_div_a20230915 .c-ScoreCardGraph{overflow:hidden;white-space:nowrap}#hover_div_a20230915 .c-ScoreCardGraph > div{margin-left:0.25rem;padding:0 0.25rem;text-align:right;height:0.5rem;min-width:2rem;line-height:1rem}#hover_div_a20230915 .c-ScoreCardGraph > div:first-child{margin-left:0}#hover_div_a20230915 .c-ScoreCardGraph_scoreTitle{letter-spacing:0.25rem}#hover_div_a20230915 .c-ScoreCardGraph_scoreSentiment{color:#00ce7a}#hover_div_a20230915 .c-ScoreCardGraph_scoreGraphPositive{background:#00ce7a;border-radius:0.25rem 0 0 0.25rem}#hover_div_a20230915 .c-ScoreCardGraph_scoreGraphNeutral{background:#ffbd3f}#hover_div_a20230915 .c-ScoreCardGraph_scoreGraphNegative{background:#ff6874;border-radius:0 0.25rem 0.25rem 0}#hover_div_a20230915 .gray{background:#bfbfbf;height:1rem;display:inline-block}#hover_div_a20230915 .c-ScoreCard_scoreContent{display:flex;align-content:flex-start;flex-wrap:nowrap;grid-gap:10px;gap:10px;width:100%;justify-content:space-between;align-items:stretch}#hover_div_a20230915 .c-ScoreCard_scoreContent_text{line-height:normal;display:flex;flex-direction:column;justify-content:space-between}#hover_div_a20230915 .c-ScoreCard_scoreContent_number > .c-siteReviewScore_background-critic_large,#hover_div_a20230915 .c-ScoreCard_scoreContent_number > .c-siteReviewScore_background-critic_large .c-siteReviewScore_large{width:4rem;height:4rem}#hover_div_a20230915 .c-ScoreCard_scoreSentiment{font-size:1rem;line-height:1.25rem;text-transform:capitalize}#hover_div_a20230915 .c-ScoreCard_scoreTitle{letter-spacing:0.25rem}#hover_div_a20230915 .c-reviewsOverview_overviewDetails{grid-template-columns:1fr 1fr;grid-gap:1.25rem;border-top:1px solid #262626;margin-top:auto;padding:2px}#hover_div_a20230915 .c-reviewsOverview_overviewDetails:first-child{border-top:0 solid #262626}#hover_div_a20230915 .c-siteReviewScore_green{background:#00ce7a}#hover_div_a20230915 .c-siteReviewScore_yellow{background:#ffbd3f}#hover_div_a20230915 .c-siteReviewScore_red{background:#ff6874}#hover_div_a20230915 .c-siteReviewScore_grey{background:#404040}#hover_div_a20230915 .c-siteReviewScore_tbdCritic,#hover_div_a20230915 .c-siteReviewScore_tbdUser{border-width:0.125rem;border-style:solid}#hover_div_a20230915 .o-inlineScore{border-radius:0.25rem;font-size:1.25rem;font-weight:700;color:#404040;width:2.5rem;height:2.5rem;display:inline-flex;justify-content:center;align-items:center;text-decoration:none!important}#hover_div_a20230915 .o-inlineScore-green{background:#00ce7a}#hover_div_a20230915 .o-inlineScore-yellow{background:#ffbd3f}#hover_div_a20230915 .o-inlineScore-red{background:#ff6874}#hover_div_a20230915 .o-inlineScore-tbd{border:1px solid grey}#hover_div_a20230915 .u-pointer{cursor:pointer}#hover_div_a20230915 .c-siteReviewScore_green{background:#00ce7a}#hover_div_a20230915 .c-siteReviewScore_yellow{background:#ffbd3f}#hover_div_a20230915 .c-siteReviewScore_red{background:#ff6874}#hover_div_a20230915 .c-siteReviewScore_grey{background:#404040}#hover_div_a20230915 .c-siteReviewScore_tbdCritic,#hover_div_a20230915 .c-siteReviewScore_tbdUser{border-width:0.125rem;border-style:solid}#hover_div_a20230915{max-width:440px}
  1412. `
  1413.  
  1414. let framesrc = 'data:text/html,'
  1415. framesrc += encodeURIComponent(`<!DOCTYPE html>
  1416. <html lang="en">
  1417. <head>
  1418. <meta charset="utf-8">
  1419. <title>Metacritic info</title>
  1420. <style>body { margin:0px; padding:0px; background:white; }${css}
  1421. </style>
  1422. <script>
  1423. const failedImages = {};
  1424. function detectCSP(img) {
  1425. if(img.complete && (!img.naturalWidth || !img.naturalHeight)) {
  1426. return true;
  1427. }
  1428. return false;
  1429. }
  1430. function findCSPerrors() {
  1431. const imgs = document.querySelectorAll("img");
  1432. for(let i = 0; i < imgs.length; i++) {
  1433. if(imgs[i].complete && detectCSP(imgs[i])) {
  1434. fixCSP(imgs[i]);
  1435. }
  1436. }
  1437. }
  1438. function fixCSP(img) {
  1439. console.debug("ShowMetacriticRatings(iFrame): Loading image failed. Bypassing CSP...");
  1440. failedImages[img.src] = img;
  1441. parent.postMessage({"mcimessage_loadImg":true, "mcimessage_imgUrl": img.src},"*");
  1442. }
  1443. function on_load() {
  1444. (${functions.frame.toString()})();
  1445. window.setTimeout(findCSPerrors, 500);
  1446. }
  1447. </script>
  1448. </head>
  1449. <body onload="on_load();">
  1450. <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">
  1451. <div class="hover_content">${html}</div>
  1452. </div>
  1453. </body>
  1454. </html>`)
  1455.  
  1456. const frame = $('<iframe></iframe>').appendTo(div)
  1457. frame.attr('id', 'mciframe123')
  1458. frame.attr('src', framesrc)
  1459. frame.attr('scrolling', 'auto')
  1460. frame.css({
  1461. width: 440,
  1462. height: 110,
  1463. border: 'none'
  1464. })
  1465.  
  1466. window.setTimeout(function () {
  1467. if (!frameStatus) { // Loading frame content failed.
  1468. // Directly inject the html without an iframe (this may break the site or the metacritic)
  1469. console.debug('ShowMetacriticRatings: Loading iframe content failed. Injecting directly.')
  1470. $('head').append(`<style>${css}</style>`)
  1471. 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">
  1472. <div class="hover_content">${html}</div>
  1473. </div>`)
  1474. frame.replaceWith(noframe)
  1475. }
  1476. }, 2000)
  1477.  
  1478. functions.parent()
  1479.  
  1480. const sub = $('<div></div>').appendTo(div)
  1481. $('<time style="color:#b6b6b6; font-size: 11px;" datetime="' + time + '" title="' + time.toLocaleTimeString() + ' ' + time.toLocaleDateString() + '">' + minutesSince(time) + '</time>').appendTo(sub)
  1482. $('<a style="color:#b6b6b6; font-size: 11px;" target="_blank" href="' + url + '" title="Open Metacritic">' + decodeURI(url.replace('https://www.', '@')) + '</a>').appendTo(sub)
  1483. $('<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 () {
  1484. const metaurl = $(this).data('url')
  1485. addToTemporaryBlacklist(metaurl)
  1486. document.body.removeChild(this.parentNode.parentNode)
  1487. })
  1488.  
  1489. $('<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 () {
  1490. const docurl = document.location.href
  1491. const metaurl = $(this).data('url')
  1492. addToMap(docurl, metaurl).then(function (r) {
  1493. balloonAlert('Thanks for your submission!\n\nSaved as a correct entry.\n\n' + r[0] + '\n' + r[1], 6000, 'Success')
  1494. })
  1495. })
  1496. $('<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 () {
  1497. if (!confirm('This is NOT the correct entry!\n\nAdd to blacklist?')) return
  1498. const docurl = document.location.href
  1499. const metaurl = $(this).data('url')
  1500. addToBlacklist(docurl, metaurl).then(function (r) {
  1501. balloonAlert('Thanks for your submission!\n\nSaved to blacklist.\n\n' + r[0] + '\n' + r[1], 6000, 'Success')
  1502. })
  1503.  
  1504. openSearchBox(true)
  1505. })
  1506.  
  1507. // Store response in cache:
  1508. if (!('cached' in response)) {
  1509. storeInHoverCache(current.metaurl, response, orgMetaUrl)
  1510. }
  1511. }
  1512.  
  1513. function metacriticGeneralProductSetup () {
  1514. current.broadenCounter = 0
  1515. }
  1516.  
  1517. const metacritic = {
  1518. mapped: function metacriticMapped (docurl, product, metaurl, type, searchTerm) {
  1519. // url was in the map/whitelist
  1520. current.data = searchTerm ? [searchTerm] : []
  1521. current.docurl = docurl
  1522. current.product = product
  1523. current.metaurl = metaurl
  1524. current.type = type
  1525. current.searchTerm = searchTerm || null
  1526. loadMetacriticUrl()
  1527. },
  1528. music: function metacriticMusic (docurl, product, artistname, albumname) {
  1529. current.data = [albumname.trim(), artistname.trim()]
  1530. artistname = name2metacritic(artistname)
  1531. albumname = albumname.replace('&', ' ')
  1532. albumname = name2metacritic(albumname)
  1533. current.docurl = docurl
  1534. current.product = product
  1535. current.metaurl = baseURLmusic + albumname + '/' + artistname
  1536. current.type = 'music'
  1537. current.searchTerm = albumname + '/' + artistname
  1538. loadMetacriticUrl()
  1539. },
  1540. movie: function metacriticMovie (docurl, product, moviename) {
  1541. current.data = [moviename.trim()]
  1542. moviename = name2metacritic(moviename)
  1543. current.docurl = docurl
  1544. current.product = product
  1545. current.metaurl = baseURLmovie + moviename
  1546. current.type = 'movie'
  1547. current.searchTerm = moviename
  1548. loadMetacriticUrl()
  1549. },
  1550. tv: function metacriticTv (docurl, product, seriesname) {
  1551. current.data = [seriesname.trim()]
  1552. seriesname = name2metacritic(seriesname)
  1553. current.docurl = docurl
  1554. current.product = product
  1555. current.metaurl = baseURLtv + seriesname
  1556. current.type = 'tv'
  1557. current.searchTerm = seriesname
  1558. loadMetacriticUrl()
  1559. },
  1560. pcgame: function metacriticPcgame (docurl, product, gamename) {
  1561. current.data = [gamename.trim()]
  1562. gamename = name2metacritic(gamename)
  1563. current.docurl = docurl
  1564. current.product = product
  1565. current.metaurl = baseURLpcgame + gamename
  1566. current.type = 'pcgame'
  1567. current.searchTerm = gamename
  1568. loadMetacriticUrl()
  1569. },
  1570. ps4game: function metacriticPs4game (docurl, product, gamename) {
  1571. current.data = [gamename.trim()]
  1572. gamename = name2metacritic(gamename)
  1573. current.docurl = docurl
  1574. current.product = product
  1575. current.metaurl = baseURLps4 + gamename
  1576. current.type = 'ps4game'
  1577. current.searchTerm = gamename
  1578. loadMetacriticUrl()
  1579. },
  1580. xonegame: function metacriticXonegame (docurl, product, gamename) {
  1581. current.data = [gamename.trim()]
  1582. gamename = name2metacritic(gamename)
  1583. current.docurl = docurl
  1584. current.product = product
  1585. current.metaurl = baseURLxone + gamename
  1586. current.type = 'xonegame'
  1587. current.searchTerm = gamename
  1588. loadMetacriticUrl()
  1589. }
  1590. }
  1591.  
  1592. const Always = () => true
  1593. const sites = {
  1594. bandcamp: {
  1595. host: ['bandcamp.com'],
  1596. condition: () => unsafeWindow && unsafeWindow.TralbumData && unsafeWindow.TralbumData.current,
  1597. products: [{
  1598. condition: Always,
  1599. type: 'music',
  1600. data: () => [unsafeWindow.TralbumData.artist, unsafeWindow.TralbumData.current.title]
  1601. }]
  1602. },
  1603. itunes: {
  1604. host: ['itunes.apple.com'],
  1605. condition: Always,
  1606. products: [{
  1607. condition: () => ~document.location.href.indexOf('/movie/'),
  1608. type: 'movie',
  1609. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  1610. },
  1611. {
  1612. condition: () => ~document.location.href.indexOf('/tv-season/'),
  1613. type: 'tv',
  1614. data: function () {
  1615. let name = parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  1616. if (~name.indexOf(', Season')) {
  1617. name = name.split(', Season')[0]
  1618. }
  1619. return name
  1620. }
  1621. },
  1622. {
  1623. condition: () => ~document.location.href.indexOf('/album/'),
  1624. type: 'music',
  1625. data: function () {
  1626. const ld = parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum'))
  1627. const album = ld[0]
  1628. const artist = 'name' in ld[1] ? ld[1].name : ld[1].map(x => x.name).join(' ')
  1629. return [artist, album]
  1630. }
  1631. }]
  1632. },
  1633. 'music.apple': {
  1634. host: ['music.apple.com'],
  1635. condition: Always,
  1636. products: [{
  1637. condition: () => ~document.location.href.indexOf('/album/') && parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum')).length > 1,
  1638. type: 'music',
  1639. data: function () {
  1640. const ld = parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum'))
  1641. const album = ld[0]
  1642. const artist = 'name' in ld[1] ? ld[1].name : ld[1].map(x => x.name).join(' ')
  1643. return [artist, album]
  1644. }
  1645. }]
  1646. },
  1647. googleplay: {
  1648. host: ['play.google.com'],
  1649. condition: Always,
  1650. products: [
  1651. {
  1652. condition: () => ~document.location.href.indexOf('/album/'),
  1653. type: 'music',
  1654. data: () => [document.querySelector('[itemprop="byArtist"] meta[itemprop="name"]').content, document.querySelector('[itemtype="https://schema.org/MusicAlbum"] meta[itemprop="name"]').content]
  1655. },
  1656. {
  1657. condition: () => ~document.location.href.indexOf('/movies/details/'),
  1658. type: 'movie',
  1659. data: () => document.querySelector('*[itemprop=name]').textContent
  1660. }
  1661. ]
  1662. },
  1663. imdb: {
  1664. host: ['imdb.com'],
  1665. condition: () => !~document.location.pathname.indexOf('/mediaviewer') && !~document.location.pathname.indexOf('/mediaindex') && !~document.location.pathname.indexOf('/videoplayer'),
  1666. products: [
  1667. {
  1668. condition: () => document.querySelector('a[href*="/criticreviews/"'),
  1669. type: 'mapped',
  1670. data: async function () {
  1671. // This is used if there is a metacritic link on the imdb page
  1672. const criticsUrl = document.querySelector('a[href*="/criticreviews/"').href.toString()
  1673. const response = await asyncRequest({ url: criticsUrl }).catch(function (response) {
  1674. console.warn('ShowMetacriticRatings: Error imdb01\nurl=' + criticsUrl + '\nstatus=' + response.status)
  1675. })
  1676. const m = response.responseText.match(/(https:\/\/www\.metacritic\.com\/(\w+)\/[^?&"']+)/)
  1677. console.debug('ShowMetacriticRatings: Metacritic link found on imdb:', m[2], m[1])
  1678. const query = document.querySelector('[data-testid="hero__pageTitle"]') ? document.querySelector('[data-testid="hero__pageTitle"]').textContent : null
  1679. return [m[1], m[2], query]
  1680. }
  1681. },
  1682. {
  1683. condition: function () {
  1684. const e = document.querySelector("meta[property='og:type']")
  1685. if (e && e.content === 'video.movie') {
  1686. return true
  1687. } else if (document.querySelector('[data-testid="hero__pageTitle"]') && !document.querySelector('[data-testid="hero-subnav-bar-left-block"] a[href*="episodes/"]')) {
  1688. return true
  1689. }
  1690. return false
  1691. },
  1692. type: 'movie',
  1693. data: async function () {
  1694. // If the page is not in English or the browser is not in English, request page in English.
  1695. // Then the title in <h1> will be the English title and Metacritic always uses the English title.
  1696. if (document.querySelector('[for="nav-language-selector"]').textContent.toLowerCase() !== 'en' || !navigator.language.startsWith('en')) {
  1697. // Set language cookie to English, request current page in English, then restore language cookie or expire it if it didn't exist before
  1698. const langM = document.cookie.match(/lc-main=([^;]+)/)
  1699. const langBefore = langM ? langM[0] : ';expires=Thu, 01 Jan 1970 00:00:01 GMT'
  1700. document.cookie = 'lc-main=en-US'
  1701. const response = await asyncRequest({
  1702. url: document.location.href,
  1703. headers: {
  1704. 'Accept-Language': 'en-US,en'
  1705. }
  1706. }).catch(function (response) {
  1707. console.warn('ShowMetacriticRatings: Error imdb02\nurl=' + document.location.href + '\nstatus=' + response.status)
  1708. })
  1709. document.cookie = 'lc-main=' + langBefore
  1710. // Extract <h1> title
  1711. const parts = response.responseText.split('</span></h1>')[0].split('>')
  1712. console.debug('ShowMetacriticRatings: Movie title from English page:', parts[parts.length - 1])
  1713. return parts[parts.length - 1]
  1714. } else if (document.querySelector('script[type="application/ld+json"]')) {
  1715. const ld = parseLDJSON(['name', 'alternateName'])
  1716. if (ld.length > 1 && ld[1]) {
  1717. console.debug('ShowMetacriticRatings: Movie ld+json alternateName', ld[1])
  1718. return ld[1]
  1719. }
  1720. console.debug('ShowMetacriticRatings: Movie ld+json name', ld[0])
  1721. return ld[0]
  1722. } else {
  1723. console.debug('ShowMetacriticRatings: Movie <title>', document.title.match(/(.+?)\s+(\(\d+\))? - IMDb/)[1])
  1724. return document.title.match(/(.+?)\s+(\(\d+\))? - IMDb/)[1]
  1725. }
  1726. }
  1727. },
  1728. {
  1729. condition: function () {
  1730. const e = document.querySelector("meta[property='og:type']")
  1731. if (e && e.content === 'video.tv_show') {
  1732. return true
  1733. } else if (document.querySelector('[data-testid="hero-subnav-bar-left-block"] a[href*="episodes/"]')) {
  1734. return true
  1735. }
  1736. return false
  1737. },
  1738. type: 'tv',
  1739. data: async function () {
  1740. if (document.querySelector('[for="nav-language-selector"]').textContent.toLowerCase() !== 'en' || !navigator.language.startsWith('en')) {
  1741. // Set language cookie to English, request current page in English, then restore language cookie or expire it if it didn't exist before
  1742. const langM = document.cookie.match(/lc-main=([^;]+)/)
  1743. const langBefore = langM ? langM[0] : ';expires=Thu, 01 Jan 1970 00:00:01 GMT'
  1744. document.cookie = 'lc-main=en-US'
  1745. const response = await asyncRequest({
  1746. url: document.location.href,
  1747. headers: {
  1748. 'Accept-Language': 'en-US,en'
  1749. }
  1750. }).catch(function (response) {
  1751. console.warn('ShowMetacriticRatings: Error imdb03\nurl=' + document.location.href + '\nstatus=' + response.status)
  1752. })
  1753. document.cookie = 'lc-main=' + langBefore
  1754. // Extract <h1> title
  1755. const parts = response.responseText.split('</span></h1>')[0].split('>')
  1756. console.debug('ShowMetacriticRatings: TV title from English page:', parts[parts.length - 1])
  1757. return parts[parts.length - 1]
  1758. } else if (document.querySelector('script[type="application/ld+json"]')) {
  1759. const ld = parseLDJSON(['name', 'alternateName'])
  1760. if (ld.length > 1 && ld[1]) {
  1761. console.debug('ShowMetacriticRatings: TV ld+json alternateName', ld[1])
  1762. return ld[1]
  1763. }
  1764. console.debug('ShowMetacriticRatings: TV ld+json name', ld[0])
  1765. return ld[0]
  1766. } else {
  1767. console.debug('ShowMetacriticRatings: TV <title>', document.title.match(/(.+?)\s+(\(\d+\))? - IMDb/)[1])
  1768. return document.title.match(/(.+?)\s+\(TV/)[1]
  1769. }
  1770. }
  1771. }
  1772. ]
  1773. },
  1774. steam: {
  1775. host: ['store.steampowered.com'],
  1776. condition: () => document.querySelector('*[itemprop=name]'),
  1777. products: [{
  1778. condition: Always,
  1779. type: 'pcgame',
  1780. data: () => document.querySelector('*[itemprop=name]').textContent
  1781. }]
  1782. },
  1783. rottentomatoes: {
  1784. host: ['rottentomatoes.com'],
  1785. condition: Always,
  1786. products: [{
  1787. condition: () => document.location.pathname.startsWith('/m/'),
  1788. type: 'movie',
  1789. data: () => document.querySelector('h1').firstChild.textContent
  1790. },
  1791. {
  1792. condition: () => document.location.pathname.startsWith('/tv/'),
  1793. type: 'tv',
  1794. data: () => unsafeWindow.BK.TvSeriesTitle
  1795. }
  1796. ]
  1797. },
  1798. serienjunkies: {
  1799. host: ['www.serienjunkies.de'],
  1800. condition: Always,
  1801. products: [{
  1802. condition: () => Always,
  1803. type: 'tv',
  1804. data: () => parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  1805. }]
  1806. },
  1807. gamespot: {
  1808. host: ['gamespot.com'],
  1809. condition: () => document.querySelector('[itemprop=device]'),
  1810. products: [
  1811. {
  1812. condition: () => ~$('[itemprop=device]').text().indexOf('PC'),
  1813. type: 'pcgame',
  1814. data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
  1815. },
  1816. {
  1817. condition: () => ~$('[itemprop=device]').text().indexOf('PS4'),
  1818. type: 'ps4game',
  1819. data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
  1820. },
  1821. {
  1822. condition: () => ~$('[itemprop=device]').text().indexOf('XONE'),
  1823. type: 'xonegame',
  1824. data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
  1825. }
  1826. ]
  1827. },
  1828. amazon: {
  1829. host: ['amazon.'],
  1830. condition: Always,
  1831. products: [
  1832. {
  1833. condition: () => document.location.hostname === 'music.amazon.com' && document.location.pathname.startsWith('/albums/') && document.querySelector('.viewTitle'), // "Amazon Music Unlimited" page
  1834. type: 'music',
  1835. data: function () {
  1836. const artist = document.querySelector('.artistLink').textContent.trim()
  1837. let title = document.querySelector('.viewTitle').textContent.trim()
  1838. title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
  1839. if (artist && title) {
  1840. return [artist, title]
  1841. }
  1842. return false
  1843. }
  1844. },
  1845. {
  1846. condition: function () { // "Normal amazon" page
  1847. try {
  1848. if (document.querySelector('.nav-categ-image').alt.toLowerCase().indexOf('musi') !== -1) {
  1849. return true
  1850. }
  1851. } catch (e) {}
  1852. const music = ['Music', 'Musique', 'Musik', 'Música', 'Musica', '音楽']
  1853. return music.some(function (s) {
  1854. if (~document.title.indexOf(s)) {
  1855. return true
  1856. } else {
  1857. return false
  1858. }
  1859. })
  1860. },
  1861. type: 'music',
  1862. data: function () {
  1863. let artist = false
  1864. let title = false
  1865. if (document.querySelector('#ProductInfoArtistLink')) {
  1866. artist = document.querySelector('#ProductInfoArtistLink').textContent.trim()
  1867. } else if (document.querySelector('#bylineInfo .author>*')) {
  1868. artist = document.querySelector('#bylineInfo .author>*').textContent.trim()
  1869. }
  1870.  
  1871. if (document.querySelector('#dmusicProductTitle_feature_div')) {
  1872. title = document.querySelector('#dmusicProductTitle_feature_div').textContent.trim()
  1873. title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
  1874. } else if (document.querySelector('#productTitle')) {
  1875. title = document.querySelector('#productTitle').textContent.trim()
  1876. title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
  1877. }
  1878. return [artist, title]
  1879. }
  1880. },
  1881. {
  1882. condition: () => (document.querySelector('[data-automation-id=title]') && (
  1883. document.getElementsByClassName('av-season-single').length ||
  1884. document.querySelector('[data-automation-id="num-of-seasons-badge"]') ||
  1885. document.getElementById('tab-selector-episodes') ||
  1886. document.getElementById('av-droplist-av-atf-season-selector')
  1887. )),
  1888. type: 'tv',
  1889. data: () => document.querySelector('[data-automation-id=title]').textContent.trim()
  1890. },
  1891. {
  1892. condition: () => ((
  1893. document.getElementsByClassName('av-season-single').length ||
  1894. document.querySelector('[data-automation-id="num-of-seasons-badge"]') ||
  1895. document.getElementById('tab-selector-episodes') ||
  1896. document.getElementById('av-droplist-av-atf-season-selector')
  1897. ) && Array.from(document.querySelectorAll('script[type="text/template"]')).map(e => e.innerHTML.match(/parentTitle"\s*:\s*"(.+?)"/)).some((x) => x != null)),
  1898. type: 'tv',
  1899. data: () => Array.from(document.querySelectorAll('script[type="text/template"]')).map(e => e.innerHTML.match(/parentTitle"\s*:\s*"(.+?)"/)).filter((x) => x != null)[0][1]
  1900. },
  1901. {
  1902. condition: () => document.querySelector('[data-automation-id=title]'),
  1903. type: 'movie',
  1904. data: () => document.querySelector('[data-automation-id=title]').textContent.trim().replace(/\[.{1,8}\]/, '')
  1905. }
  1906. ]
  1907. },
  1908. BoxOfficeMojo: {
  1909. host: ['boxofficemojo.com'],
  1910. condition: () => Always,
  1911. products: [
  1912. {
  1913. condition: () => document.location.pathname.startsWith('/release/'),
  1914. type: 'movie',
  1915. data: () => document.querySelector('meta[name=title]').content
  1916. },
  1917. {
  1918. // Old page design
  1919. condition: () => ~document.location.search.indexOf('id=') && document.querySelector('#body table:nth-child(2) tr:first-child b'),
  1920. type: 'movie',
  1921. data: () => document.querySelector('#body table:nth-child(2) tr:first-child b').firstChild.textContent
  1922. }]
  1923. },
  1924. AllMovie: {
  1925. host: ['allmovie.com'],
  1926. condition: () => document.querySelector('h2.movie-title'),
  1927. products: [{
  1928. condition: () => document.querySelector('h2.movie-title'),
  1929. type: 'movie',
  1930. data: () => document.querySelector('h2.movie-title').firstChild.textContent.trim()
  1931. }]
  1932. },
  1933. 'en.wikipedia': {
  1934. host: ['en.wikipedia.org'],
  1935. condition: Always,
  1936. products: [{
  1937. condition: function () {
  1938. if (!document.querySelector('.infobox .summary')) {
  1939. return false
  1940. }
  1941. const r = /\d\d\d\d films/
  1942. return $('#catlinks a').filter((i, e) => e.firstChild.textContent.match(r)).length
  1943. },
  1944. type: 'movie',
  1945. data: () => document.querySelector('.infobox .summary').firstChild.textContent
  1946. },
  1947. {
  1948. condition: function () {
  1949. if (!document.querySelector('.infobox .summary')) {
  1950. return false
  1951. }
  1952. const r = /television series/
  1953. return $('#catlinks a').filter((i, e) => e.firstChild.textContent.match(r)).length
  1954. },
  1955. type: 'tv',
  1956. data: () => document.querySelector('.infobox .summary').firstChild.textContent
  1957. }]
  1958. },
  1959. fandango: {
  1960. host: ['fandango.com'],
  1961. condition: () => document.querySelector("meta[property='og:title']"),
  1962. products: [{
  1963. condition: Always,
  1964. type: 'movie',
  1965. data: () => document.querySelector("meta[property='og:title']").content.match(/(.+?)\s+\(\d{4}\)/)[1].trim()
  1966. }]
  1967. },
  1968. flixster: {
  1969. host: ['www.flixster.com'],
  1970. condition: () => Always,
  1971. products: [{
  1972. condition: () => parseLDJSON('@type') === 'Movie',
  1973. type: 'movie',
  1974. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  1975. }]
  1976. },
  1977. themoviedb: {
  1978. host: ['themoviedb.org'],
  1979. condition: () => document.querySelector("meta[property='og:type']"),
  1980. products: [{
  1981. condition: () => document.querySelector("meta[property='og:type']").content === 'movie',
  1982. type: 'movie',
  1983. data: () => document.querySelector("meta[property='og:title']").content
  1984. },
  1985. {
  1986. condition: () => document.querySelector("meta[property='og:type']").content === 'tv' || document.querySelector("meta[property='og:type']").content === 'tv_series',
  1987. type: 'tv',
  1988. data: () => document.querySelector("meta[property='og:title']").content
  1989. }]
  1990. },
  1991. letterboxd: {
  1992. host: ['letterboxd.com'],
  1993. condition: () => unsafeWindow.filmData && 'name' in unsafeWindow.filmData,
  1994. products: [{
  1995. condition: Always,
  1996. type: 'movie',
  1997. data: () => unsafeWindow.filmData.name
  1998. }]
  1999. },
  2000. TVmaze: {
  2001. host: ['tvmaze.com'],
  2002. condition: () => document.querySelector('h1'),
  2003. products: [{
  2004. condition: Always,
  2005. type: 'tv',
  2006. data: () => document.querySelector('h1').firstChild.textContent
  2007. }]
  2008. },
  2009. TVGuide: {
  2010. host: ['tvguide.com'],
  2011. condition: Always,
  2012. products: [{
  2013. condition: () => document.location.pathname.startsWith('/tvshows/'),
  2014. type: 'tv',
  2015. data: function () {
  2016. if (document.querySelector('meta[itemprop=name]')) {
  2017. return document.querySelector('meta[itemprop=name]').content
  2018. } else {
  2019. return document.querySelector("meta[property='og:title']").content.split('|')[0]
  2020. }
  2021. }
  2022. }]
  2023. },
  2024. followshows: {
  2025. host: ['followshows.com'],
  2026. condition: Always,
  2027. products: [{
  2028. condition: () => document.querySelector("meta[property='og:type']").content === 'video.tv_show',
  2029. type: 'tv',
  2030. data: () => document.querySelector("meta[property='og:title']").content
  2031. }]
  2032. },
  2033. TheTVDB: {
  2034. host: ['thetvdb.com'],
  2035. condition: Always,
  2036. products: [{
  2037. condition: () => document.location.pathname.startsWith('/series/'),
  2038. type: 'tv',
  2039. data: () => document.getElementById('series_title').firstChild.textContent.trim()
  2040. },
  2041. {
  2042. condition: () => document.location.pathname.startsWith('/movies/'),
  2043. type: 'movie',
  2044. data: () => document.getElementById('series_title').firstChild.textContent.trim()
  2045. }]
  2046. },
  2047. ConsequenceOfSound: {
  2048. host: ['consequence.net', 'consequenceofsound.net'],
  2049. condition: () => document.querySelector('#main-content .review-summary'),
  2050. products: [
  2051. {
  2052. condition: () => document.querySelector('meta[name="cXenseParse:cns-artist-names"]') && document.querySelector('em'),
  2053. type: 'music',
  2054. data: function () {
  2055. window.setInterval(function () {
  2056. if (document.getElementById('ot-sdk-btn-floating')) {
  2057. document.getElementById('ot-sdk-btn-floating').remove()
  2058. }
  2059. }, 5000)
  2060. const artist = document.querySelector('meta[name="cXenseParse:cns-artist-names"]').content
  2061. const arr = Array.from(document.querySelectorAll('em')).map((em) => em.textContent.trim())
  2062. const counts = {}
  2063. for (const num of arr) {
  2064. counts[num] = counts[num] ? counts[num] + 1 : 1
  2065. }
  2066. const max = Math.max(...Object.values(counts))
  2067. const maxIndex = Object.values(counts).indexOf(max)
  2068. const title = Object.keys(counts)[maxIndex]
  2069. return [artist, title]
  2070. }
  2071. },
  2072. {
  2073. condition: () => document.title.match(/'(.*?)'\s*Album/i) && document.querySelector('meta[name="cXenseParse:cns-artist-names"]'),
  2074. type: 'music',
  2075. data: function () {
  2076. window.setInterval(function () {
  2077. if (document.getElementById('ot-sdk-btn-floating')) {
  2078. document.getElementById('ot-sdk-btn-floating').remove()
  2079. }
  2080. }, 5000)
  2081. const title = document.title.match(/'(.*?)'\s*Album/i)[1]
  2082. const artist = document.querySelector('meta[name="cXenseParse:cns-artist-names"]').content
  2083. return [artist, title]
  2084. }
  2085. },
  2086. {
  2087. condition: () => document.title.match(/(.+?)\s+\u2013\s+(.+?) \| Album Review/),
  2088. type: 'music',
  2089. data: function () {
  2090. window.setInterval(function () {
  2091. if (document.getElementById('ot-sdk-btn-floating')) {
  2092. document.getElementById('ot-sdk-btn-floating').remove()
  2093. }
  2094. }, 5000)
  2095. const m = document.title.match(/(.+?)\s+\u2013\s+(.+?) \| Album Review/)
  2096. return [m[1], m[2]]
  2097. }
  2098. },
  2099. {
  2100. condition: () => document.location.pathname.indexOf('/album-review') !== -1 && document.querySelector('a.tag[href*="/artist/"'),
  2101. type: 'music',
  2102. data: function () {
  2103. window.setInterval(function () {
  2104. if (document.getElementById('ot-sdk-btn-floating')) {
  2105. document.getElementById('ot-sdk-btn-floating').remove()
  2106. }
  2107. }, 5000)
  2108. const artistAndTitleWithDash = document.location.pathname.match(/album-review-([\w-]+)/)[1]
  2109. const artistWithDash = document.querySelector('a.tag[href*="/artist/"').pathname.match(/artist\/([\w-]+)/)[1]
  2110. const titleWithDash = artistAndTitleWithDash.replace(artistWithDash, '')
  2111. const title = titleWithDash.replace('-', ' ').trim()
  2112. const artist = artistWithDash.replace('-', ' ').trim()
  2113. return [artist, title]
  2114. }
  2115. }]
  2116. },
  2117. Pitchfork: {
  2118. host: ['pitchfork.com'],
  2119. condition: () => ~document.location.href.indexOf('/reviews/albums/'),
  2120. products: [{
  2121. condition: () => document.querySelector('.single-album-tombstone'),
  2122. type: 'music',
  2123. data: function () {
  2124. let artist
  2125. let album
  2126. if (document.querySelector('.single-album-tombstone .artists')) {
  2127. artist = document.querySelector('.single-album-tombstone .artists').innerText.trim()
  2128. } else if (document.querySelector('.single-album-tombstone .artist-list')) {
  2129. artist = document.querySelector('.single-album-tombstone .artist-list').innerText.trim()
  2130. }
  2131. if (document.querySelector('.single-album-tombstone h1.review-title')) {
  2132. album = document.querySelector('.single-album-tombstone h1.review-title').innerText.trim()
  2133. } else if (document.querySelector('.single-album-tombstone h1')) {
  2134. album = document.querySelector('.single-album-tombstone h1').innerText.trim()
  2135. }
  2136.  
  2137. return [artist, album]
  2138. }
  2139. }]
  2140. },
  2141. 'Last.fm': {
  2142. host: ['last.fm'],
  2143. condition: () => document.querySelector('*[data-page-resource-type]') && document.querySelector('*[data-page-resource-type]').dataset.pageResourceType === 'album',
  2144. products: [{
  2145. condition: () => document.querySelector('*[data-page-resource-type]').dataset.pageResourceName,
  2146. type: 'music',
  2147. data: function () {
  2148. const artist = document.querySelector('*[data-page-resource-type]').dataset.pageResourceArtistName
  2149. const album = document.querySelector('*[data-page-resource-type]').dataset.pageResourceName
  2150. return [artist, album]
  2151. }
  2152. }]
  2153. },
  2154. TVNfo: {
  2155. host: ['tvnfo.com'],
  2156. condition: () => document.querySelector('.ui.breadcrumb a[href*="/series"]'),
  2157. products: [{
  2158. condition: Always,
  2159. type: 'tv',
  2160. data: function () {
  2161. const years = document.querySelector('#title h1 .years').textContent.trim()
  2162. const title = document.querySelector('#title h1').textContent.replace(years, '').trim()
  2163. return title
  2164. }
  2165. }]
  2166. },
  2167. rateyourmusic: {
  2168. host: ['rateyourmusic.com'],
  2169. condition: () => document.querySelector("meta[property='og:type']"),
  2170. products: [{
  2171. condition: () => document.querySelector("meta[property='og:type']").content === 'music.album',
  2172. type: 'music',
  2173. data: function () {
  2174. const artist = document.querySelector('.section_main_info .artist').innerText.trim()
  2175. const album = document.querySelector('.section_main_info .album_title').innerText.trim()
  2176. return [artist, album]
  2177. }
  2178. }]
  2179. },
  2180. spotify: {
  2181. host: ['open.spotify.com'],
  2182. condition: Always,
  2183. products: [{
  2184. condition: () => document.location.pathname.startsWith('/album/') && document.querySelector('.Root__main-view h1'),
  2185. type: 'music',
  2186. data: function () {
  2187. const album = document.querySelector('.Root__main-view h1').textContent.trim()
  2188. let artist = []
  2189. document.querySelector('.Root__main-view h1').parentNode.parentNode.parentNode.querySelectorAll('a[href*="/artist/"]').forEach(function (a) {
  2190. artist.push(a.textContent.trim())
  2191. })
  2192. artist = artist.join(' ')
  2193. return [artist, album]
  2194. }
  2195. }]
  2196. },
  2197. nme: {
  2198. host: ['nme.com'],
  2199. condition: () => document.location.pathname.startsWith('/reviews/'),
  2200. products: [
  2201. {
  2202. condition: () => document.querySelector('.tdb-breadcrumbs a[href*="/reviews/film-reviews"]'),
  2203. type: 'movie',
  2204. data: function () {
  2205. try {
  2206. return document.title.match(/[‘'](.+?)[’']/)[1]
  2207. } catch (e) {
  2208. try {
  2209. return document.querySelector('h1.tdb-title-text').textContent.match(/[‘'](.+?)[’']/)[1]
  2210. } catch (e) {
  2211. return document.querySelector('h1').textContent.match(/:\s*(.+)/)[1].trim()
  2212. }
  2213. }
  2214. }
  2215. },
  2216. {
  2217. condition: () => document.querySelector('#nme-music-header'),
  2218. type: 'music',
  2219. data: () => document.querySelector('h1.tdb-title-text').textContent.match(/\s*(.+?)\s*.\s*[‘'](.+?)[’']/).slice(1)
  2220. },
  2221. {
  2222. condition: () => document.querySelector('.tdb-breadcrumbs a[href*="/reviews/tv-reviews"]'),
  2223. type: 'tv',
  2224. data: () => document.querySelector('h1.tdb-title-text').textContent.match(/[‘'](.+?)[’']/)[1]
  2225. }]
  2226. },
  2227. albumoftheyear: {
  2228. host: ['albumoftheyear.org'],
  2229. condition: Always,
  2230. products: [{
  2231. condition: () => document.location.pathname.startsWith('/album/'),
  2232. type: 'music',
  2233. data: function () {
  2234. const artist = document.querySelector('*[itemprop=byArtist] *[itemprop=name]').textContent
  2235. const album = document.querySelector('.albumTitle *[itemprop=name]').textContent
  2236. return [artist, album]
  2237. }
  2238. }]
  2239. },
  2240. epguides: {
  2241. host: ['epguides.com'],
  2242. condition: () => document.getElementById('eplist'),
  2243. products: [{
  2244. condition: () => document.getElementById('eplist') && document.querySelector('.center.titleblock h2'),
  2245. type: 'tv',
  2246. data: () => document.querySelector('.center.titleblock h2').textContent.trim()
  2247. }]
  2248. },
  2249. ShareTV: {
  2250. host: ['sharetv.com'],
  2251. condition: () => document.location.pathname.startsWith('/shows/'),
  2252. products: [{
  2253. condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:title']"),
  2254. type: 'tv',
  2255. data: () => document.querySelector("meta[property='og:title']").content
  2256. }]
  2257. },
  2258. /*
  2259. netflix: {
  2260. host: ['netflix.com'],
  2261. condition: !(document.querySelector('.button-nfplayerPlay') || document.querySelector('.nf-big-play-pause') || document.querySelector('.AkiraPlayer video')),
  2262.  
  2263. // TODO
  2264. // https://www.netflix.com/de/title/70264888
  2265. // https://www.netflix.com/de/title/70178217
  2266. // https://www.netflix.com/de/title/70305892 ## Movie
  2267. // https://www.netflix.com/de-en/title/80108495 ## No meta
  2268.  
  2269. products: [{
  2270. condition: () => parseLDJSON('@type') === 'Movie',
  2271. type: 'movie',
  2272. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  2273. },
  2274. {
  2275. condition: () => parseLDJSON('@type') === 'TVSeries',
  2276. type: 'tv',
  2277. data: () => parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  2278. }]
  2279. },
  2280. */
  2281. ComedyCentral: {
  2282. host: ['cc.com'],
  2283. condition: () => document.location.pathname.startsWith('/shows/'),
  2284. products: [{
  2285. condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:title']"),
  2286. type: 'tv',
  2287. data: () => document.querySelector("meta[property='og:title']").content.replace('| Comedy Central', '').trim()
  2288. },
  2289. {
  2290. condition: () => document.location.pathname.split('/').length === 3 && document.title.match(/(.+?)\s+-\s+Series/),
  2291. type: 'tv',
  2292. data: () => document.title.match(/(.+?)\s+-\s+Series/)[1]
  2293. }]
  2294. },
  2295. TVHoard: {
  2296. host: ['tvhoard.com'],
  2297. condition: Always,
  2298. products: [{
  2299. 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'),
  2300. type: 'movie',
  2301. data: () => document.querySelector('app-root title-page-container h1.title a').textContent.trim()
  2302. },
  2303. {
  2304. 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'),
  2305. type: 'tv',
  2306. data: () => document.querySelector('app-root title-page-container h1.title a').textContent.trim()
  2307. }]
  2308. },
  2309. AMC: {
  2310. host: ['amc.com'],
  2311. condition: () => document.location.pathname.startsWith('/shows/'),
  2312. products: [
  2313. {
  2314. condition: () => document.querySelector('.feeds[itemtype="http://schema.org/TVSeries"] h1'),
  2315. type: 'tv',
  2316. data: () => document.querySelector('.feeds[itemtype="http://schema.org/TVSeries"] h1').textContent
  2317. },
  2318. {
  2319. condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:type']") && document.querySelector("meta[property='og:type']").content.indexOf('tv_show') !== -1,
  2320. type: 'tv',
  2321. data: () => document.querySelector('.video-card-description h1').textContent.trim()
  2322. }]
  2323. },
  2324. AMCplus: {
  2325. host: ['amcplus.com'],
  2326. condition: () => Always,
  2327. products: [
  2328. {
  2329. condition: () => document.title.match(/Watch .+? |/),
  2330. type: 'tv',
  2331. data: () => document.title.match(/Watch (.+?) |/)[1].trim()
  2332. }]
  2333. },
  2334. RlsBB: {
  2335. host: ['comment.rlsbb.ru'],
  2336. condition: () => document.querySelectorAll('.post').length === 1,
  2337. products: [
  2338. {
  2339. condition: () => document.querySelector('#post-wrapper .entry-meta a[href*="/category/movies/"]'),
  2340. type: 'movie',
  2341. data: () => document.querySelector('h1.entry-title').textContent.match(/(.+?)\s+\d{4}/)[1].trim()
  2342. },
  2343. {
  2344. condition: () => document.querySelector('#post-wrapper .entry-meta a[href*="/category/tv-shows/"]'),
  2345. type: 'tv',
  2346. data: () => document.querySelector('h1.entry-title').textContent.match(/(.+?)\s+S\d{2}/)[1].trim()
  2347. }]
  2348. },
  2349. newalbumreleases: {
  2350. host: ['newalbumreleases.net'],
  2351. condition: () => document.querySelectorAll('#content .single').length === 1,
  2352. products: [
  2353. {
  2354. condition: () => document.querySelector('#content .single .cover .entry'),
  2355. type: 'music',
  2356. data: function () {
  2357. const mArtist = document.querySelector('#content .single .cover .entry').textContent.match(/Artist.\s*(.+)\s+/i)
  2358. if (mArtist) {
  2359. const mAlbum = document.querySelector('#content .single .cover .entry').textContent.match(/Album.\s*(.+)\s+/i)
  2360. if (mAlbum) {
  2361. return [mArtist[1], mAlbum[1]]
  2362. }
  2363. }
  2364. }
  2365. }]
  2366. },
  2367. showtime: {
  2368. host: ['sho.com'],
  2369. condition: Always,
  2370. products: [
  2371. {
  2372. condition: () => parseLDJSON('@type') === 'Movie',
  2373. type: 'movie',
  2374. data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
  2375. },
  2376. {
  2377. condition: () => parseLDJSON('@type') === 'TVSeries',
  2378. type: 'tv',
  2379. data: () => parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
  2380. }]
  2381. },
  2382. epicgames: {
  2383. host: ['www.epicgames.com', 'store.epicgames.com'],
  2384. condition: () => document.querySelector('.meta-schema'),
  2385. products: [{
  2386. condition: Always,
  2387. type: 'pcgame',
  2388. data: () => document.querySelector('.meta-schema').nextElementSibling.firstElementChild.lastElementChild.firstElementChild.firstElementChild.firstElementChild.textContent
  2389. }]
  2390. },
  2391. gog: {
  2392. host: ['www.gog.com'],
  2393. condition: () => document.querySelector('.productcard-basics__title'),
  2394. products: [
  2395. {
  2396. condition: () => document.location.pathname.slice(1, 5) === 'game',
  2397. type: 'pcgame',
  2398. data: () => document.querySelector('.productcard-basics__title').textContent
  2399. },
  2400. {
  2401. condition: () => document.location.pathname.slice(1, 6) === 'movie',
  2402. type: 'movie',
  2403. data: () => document.querySelector('.productcard-basics__title').textContent
  2404. }
  2405. ]
  2406. },
  2407. steamgifts: {
  2408. host: ['www.steamgifts.com'],
  2409. condition: () => document.querySelector('.featured__heading__medium'),
  2410. products: [{
  2411. condition: Always,
  2412. type: 'pcgame',
  2413. data: () => document.querySelector('.featured__heading__medium').innerText
  2414. }]
  2415. },
  2416. allmusic: {
  2417. host: ['allmusic.com'],
  2418. condition: Always,
  2419. products: [{
  2420. condition: () => document.location.pathname.indexOf('/album/') !== -1,
  2421. type: 'music',
  2422. data: function () {
  2423. const ld = parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum'))
  2424. const album = ld[0]
  2425. const artist = 'name' in ld[1] ? ld[1].name : ld[1].map(x => x.name).join(' ')
  2426. return [artist, album]
  2427. }
  2428. }]
  2429. },
  2430. psapm: {
  2431. host: ['psa.pm'],
  2432. condition: Always,
  2433. products: [
  2434. {
  2435. condition: () => document.location.pathname.startsWith('/movie/'),
  2436. type: 'movie',
  2437. data: function () {
  2438. const title = document.querySelector('h1').textContent.trim()
  2439. const m = title.match(/(.+)\((\d+)\)$/)
  2440. if (m) {
  2441. return m[1].trim()
  2442. } else {
  2443. return title
  2444. }
  2445. }
  2446. },
  2447. {
  2448. condition: () => document.location.pathname.startsWith('/tv-show/'),
  2449. type: 'tv',
  2450. data: () => document.querySelector('h1').textContent.trim()
  2451. }
  2452. ]
  2453. },
  2454. 'save.tv': {
  2455. host: ['save.tv'],
  2456. condition: () => document.location.pathname.startsWith('/STV/M/obj/archive/'),
  2457. products: [
  2458. {
  2459. condition: () => document.location.pathname.startsWith('/STV/M/obj/archive/'),
  2460. type: 'movie',
  2461. data: function () {
  2462. let title = null
  2463. if (document.querySelector("span[data-bind='text:OrigTitle']")) {
  2464. title = document.querySelector("span[data-bind='text:OrigTitle']").textContent
  2465. } else {
  2466. title = document.querySelector("h2[data-bind='text:Title']").textContent
  2467. }
  2468. let year = null
  2469. if (document.querySelector("span[data-bind='text:ProductionYear']")) {
  2470. year = parseInt(document.querySelector("span[data-bind='text:ProductionYear']").textContent)
  2471. }
  2472. return [title, year]
  2473. }
  2474. }
  2475. ]
  2476. },
  2477. aRGENTeaM: {
  2478. host: ['argenteam.net'],
  2479. condition: Always,
  2480. products: [
  2481. {
  2482. condition: () => document.location.pathname.startsWith('/movie/'),
  2483. type: 'movie',
  2484. data: function () {
  2485. const partes = document.title.split('•')
  2486. const SinArgenteam = partes[1].trim()
  2487. const SoloTitulo = SinArgenteam.split('(')[0].trim()
  2488. const Year = SinArgenteam.split('(')[1].split(')')[0]
  2489. return [SoloTitulo, Year]
  2490. }
  2491. },
  2492. {
  2493. condition: () => document.location.pathname.startsWith('/episode/'),
  2494. type: 'tv',
  2495. data: () => document.querySelector('.pserie h1').firstChild.textContent.trim()
  2496. }
  2497. ]
  2498. }
  2499.  
  2500. }
  2501.  
  2502. async function main () {
  2503. let dataFound = false
  2504.  
  2505. let map = false
  2506.  
  2507. for (const name in sites) {
  2508. const site = sites[name]
  2509. if (site.host.some(function (e) { return ~this.indexOf(e) }, document.location.hostname) && site.condition()) {
  2510. for (let i = 0; i < site.products.length; i++) {
  2511. if (site.products[i].condition()) {
  2512. // Check map for a match
  2513. if (map === false) {
  2514. map = JSON.parse(await GM.getValue('map', '{}'))
  2515. }
  2516. const docurl = filterUniversalUrl(document.location.href)
  2517. if (docurl in map) {
  2518. // Found in map, show result
  2519. const metaurl = map[docurl]
  2520. metacriticGeneralProductSetup()
  2521. metacritic.mapped.apply(undefined, [docurl, site.products[i], absoluteMetaURL(metaurl), site.products[i].type])
  2522. dataFound = true
  2523. break
  2524. }
  2525. // Try to retrieve item name from page
  2526. let data
  2527. try {
  2528. data = await site.products[i].data()
  2529. } catch (e) {
  2530. data = false
  2531. console.error(`ShowMetacriticRatings: Error in data() of site='${name}', type='${site.products[i].type}'`)
  2532. console.error(e)
  2533. }
  2534. if (data) {
  2535. const params = [docurl, site.products[i]]
  2536. if (Array.isArray(data)) {
  2537. params.push(...data)
  2538. } else {
  2539. params.push(data)
  2540. }
  2541. metacriticGeneralProductSetup()
  2542. metacritic[site.products[i].type].apply(undefined, params)
  2543. dataFound = true
  2544. }
  2545. break
  2546. }
  2547. }
  2548. break
  2549. }
  2550. }
  2551. return dataFound
  2552. }
  2553.  
  2554. (async function () {
  2555. const gdpr = await acceptGDPR()
  2556. if (!gdpr) {
  2557. GM.registerMenuCommand('Show Metacritic.com ratings - Accept terms of service', () => acceptGDPR(true).then((yes) => yes && document.location.reload()))
  2558. return
  2559. }
  2560. await versionUpdate()
  2561. const firstRunResult = await main()
  2562.  
  2563. GM.registerMenuCommand('Show Metacritic.com ratings - Search now', () => openSearchBox())
  2564. GM.registerMenuCommand('Show Metacritic.com ratings - Change corner', () => changePosition())
  2565.  
  2566. let lastLoc = document.location.href
  2567. let lastContent = document.body.innerText
  2568. let lastCounter = 0
  2569. async function newpage () {
  2570. if (lastContent === document.body.innerText && lastCounter < 15) {
  2571. window.setTimeout(newpage, 500)
  2572. lastCounter++
  2573. } else {
  2574. lastContent = document.body.innerText
  2575. lastCounter = 0
  2576. const re = await main()
  2577. if (!re) { // No page matched or no data found
  2578. window.setTimeout(newpage, 1000)
  2579. }
  2580. }
  2581. }
  2582. window.setInterval(function () {
  2583. if (document.location.href !== lastLoc) {
  2584. lastLoc = document.location.href
  2585. $('#mcdiv123').remove()
  2586.  
  2587. window.setTimeout(newpage, 1000)
  2588. }
  2589. }, 500)
  2590.  
  2591. if (!firstRunResult) {
  2592. // Initial run had no match, let's try again there may be new content
  2593. window.setTimeout(main, 2000)
  2594. }
  2595. })()