// ==UserScript==
// @name Show Metacritic.com ratings
// @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
// @namespace cuzi
// @icon https://www.metacritic.com/a/img/favicon.svg
// @supportURL https://github.com/cvzi/Metacritic-userscript/issues
// @contributionURL https://buymeacoff.ee/cuzi
// @contributionURL https://ko-fi.com/cuzicvzi
// @grant unsafeWindow
// @grant GM.xmlHttpRequest
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.registerMenuCommand
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// @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.
// @version 103
// @connect metacritic.com
// @connect backend.metacritic.com
// @connect met.acritic.workers.dev
// @connect imdb.com
// @match https://*.bandcamp.com/*
// @match https://play.google.com/store/music/album/*
// @match https://play.google.com/store/movies/details/*
// @match https://music.amazon.com/*
// @match https://www.amazon.ca/*
// @match https://www.amazon.co.jp/*
// @match https://www.amazon.co.uk/*
// @match https://smile.amazon.co.uk/*
// @match https://www.amazon.com.au/*
// @match https://www.amazon.com.mx/*
// @match https://www.amazon.com/*
// @match https://smile.amazon.com/*
// @match https://www.amazon.de/*
// @match https://smile.amazon.de/*
// @match https://www.amazon.es/*
// @match https://www.amazon.fr/*
// @match https://www.amazon.in/*
// @match https://www.amazon.it/*
// @match https://www.imdb.com/title/*
// @match https://store.steampowered.com/app/*
// @match https://www.gamespot.com/*
// @match http://www.serienjunkies.de/*
// @match https://www.serienjunkies.de/*
// @match https://www.rottentomatoes.com/m/*
// @match https://rottentomatoes.com/m/*
// @match https://www.rottentomatoes.com/tv/*
// @match https://rottentomatoes.com/tv/*
// @match https://www.rottentomatoes.com/tv/*
// @match https://rottentomatoes.com/tv/*
// @match https://www.boxofficemojo.com/movies/*
// @match https://www.boxofficemojo.com/release/*
// @match https://www.allmovie.com/movie/*
// @match https://en.wikipedia.org/*
// @match https://www.fandango.com/*
// @match https://flixster.com/movie/*
// @match https://www.themoviedb.org/movie/*
// @match https://www.themoviedb.org/tv/*
// @match https://letterboxd.com/film/*
// @match https://www.tvmaze.com/shows/*
// @match https://www.tvguide.com/tvshows/*
// @match https://followshows.com/show/*
// @match https://thetvdb.com/series/*
// @match https://thetvdb.com/movies/*
// @match https://consequenceofsound.net/*
// @match https://consequence.net/*
// @match https://pitchfork.com/*
// @match https://www.last.fm/*
// @match https://tvnfo.com/tv/*
// @match https://rateyourmusic.com/release/album/*
// @match https://open.spotify.com/*
// @match https://play.spotify.com/album/*
// @match https://www.nme.com/reviews/*
// @match https://www.albumoftheyear.org/album/*
// @match https://itunes.apple.com/*
// @match https://music.apple.com/*
// @match https://epguides.com/*
// @match https://www.epguides.com/*
// @match https://www.netflix.com/*
// @match https://www.cc.com/*
// @match https://www.amc.com/*
// @match https://www.amcplus.com/*
// @match https://rlsbb.ru/*/
// @match https://newalbumreleases.net/*
// @match https://www.sho.com/*
// @match https://www.epicgames.com/store/*
// @match https://store.epicgames.com/*
// @match https://www.gog.com/*
// @match https://www.allmusic.com/album/*
// @match https://www.steamgifts.com/giveaway/*
// @match https://psa.wf/*
// @match https://www.save.tv/*
// @match https://www.wikiwand.com/*
// @match https://trakt.tv/*
// @match http://localhost:7878/*
// @downloadURL none
// ==/UserScript==
/* globals alert, confirm, GM, DOMParser, $, Image, unsafeWindow, parent, Blob, failedImages */
/* jshint asi: true, esversion: 8 */
const scriptName = 'Show Metacritic.com ratings'
const baseURL = 'https://www.metacritic.com/'
const baseURLmusic = 'https://www.metacritic.com/music/'
const baseURLmovie = 'https://www.metacritic.com/movie/'
const baseURLpcgame = 'https://www.metacritic.com/game/pc/'
const baseURLps4 = 'https://www.metacritic.com/game/playstation-4/'
const baseURLxone = 'https://www.metacritic.com/game/xbox-one/'
const baseURLtv = 'https://www.metacritic.com/tv/'
const baseURLsearch = 'https://backend.metacritic.com/finder/metacritic/search/{query}/web?apiKey={apiKey}&componentName=search-tabs&componentDisplayName=Search+Page+Tab+Filters&componentType=FilterConfig&mcoTypeId={type}&offset=0&limit=30'
const baseURLdatabase = 'https://met.acritic.workers.dev/r.php'
const baseURLwhitelist = 'https://met.acritic.workers.dev/whitelist.php'
const baseURLblacklist = 'https://met.acritic.workers.dev/blacklist.php'
const TEMPORARY_BLACKLIST_TIMEOUT = 5 * 60
const windowPositions = [
{
bottom: 0,
left: 0
},
{
bottom: 0,
right: 0
},
{
top: 0,
right: 0
},
{
top: 0,
left: 0
}
]
// Detect dark theme of darkreader.org extension
const darkTheme = 'darkreaderScheme' in document.documentElement.dataset && document.documentElement.dataset.darkreaderScheme
let myDOMParser = null
function domParser () {
if (myDOMParser === null) {
myDOMParser = new DOMParser()
}
return myDOMParser
}
async function versionUpdate () {
const version = parseInt(await GM.getValue('version', 0))
if (version <= 93) {
// Reset database
await GM.setValue('map', '{}')
await GM.setValue('black', '[]')
await GM.setValue('hovercache', '{}')
await GM.setValue('requestcache', '{}')
await GM.setValue('temporaryblack', '{}')
await GM.setValue('searchcache', false) // Unused
await GM.setValue('autosearchcache', false) // Unused
}
if (version < 94) {
await GM.setValue('version', 94)
}
}
const BOX_CSS_DARK_THEME = `
#mcdiv123 {
position: fixed;
background-color: #262626;
border: 2px solid #313131;
color: white;
}
#mcisearchquery {
background: #262626;
color: white;
}
#mcisearchbutton {
background: rgb(56, 56, 56);
color: white;
border: 2px solid white;
}
#mcdiv123 .grespinner {
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);
}
#mcdiv123searchresults .result {
border-top-color: #525252;
}
#mcdiv123searchresults .result .mcdiv123_score_badge {
color: white;
}
#mcdiv123searchresults .result .mcdiv_release_date {
color: silver
}
.mcdiv123_image_placeholder {
background: rgb(64, 64, 64);
}
#mcdiv123searchresults .result a {
color: #09f;
}
#mcdiv123searchresults .mcdiv_desc {
scrollbar-color: #003c09 #00ce7a;
}
#mcdiv123searchresults .mcdiv_desc::-webkit-scrollbar-thumb {
background-color: #003c09;
}
`
const BOX_CSS = `
#mcdiv123 {
position: fixed;
background-color: #fff;
border: 2px solid #bbb;
border-radius: 6px;
box-shadow: 0 0 3px 3px rgba(100, 100, 100, 0.2);
color: #000;
min-width: 150;
max-height: 80%;
max-width: 640;
overflow: auto;
padding: 3px;
z-index: 2147483601;
}
#mcisearchquery {
background: white;
color: black;
width: 450px;
display: inline;
}
#mcisearchbutton {
background: silver;
color: black;
border: 2px solid black;
padding: 3px;
display: inline;
margin: 0px 5px;
cursor: pointer;
}
/* http://www.designcouch.com/home/why/2013/05/23/dead-simple-pure-css-loading-spinner/ */
#mcdiv123 .grespinner {
display: inline-block;
height: 20px;
width: 20px;
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 {
font-size: 12px;
max-width: 95%
}
.mcdiv123_correct_entry {
cursor: pointer;
color: green;
font-size: 25px;
margin-top: 10px;
}
.mcdiv123_correct_entry:hover {
color: #41fd41;
}
.mcdiv123_incorrect {
cursor: pointer;
float: right;
color: crimson;
font-size: 11px;
}
.mcdiv123_incorrect {
cursor: pointer;
float: right;
color: crimson;
font-size: 15px;
margin-right: 10px;
}
.mcdiv123_incorrect:hover {
cursor: pointer;
float: right;
color: crimson;
font-size: 15px;
margin-right: 10px;
border:2px solid white;
}
.mcdiv123_incorrect:hover {
border-color: crimson;
}
#mcdiv123searchresults .result {
font: 12px arial,helvetica,serif;
border-top-width: 1px;
border-top-color: #ccc;
border-top-style: solid;
padding: 5px
}
.mcdiv123_cover {
max-width: 200px;
max-height: 140px;
}
#mcdiv123searchresults .result .mcdiv123_score_badge {
display: inline-block;
margin: 3px;
font-weight: 600;
border-radius: 6px;
color: black;
padding: 5px;
}
#mcdiv123searchresults .result .floatleft {
float: left;
}
#mcdiv123searchresults .result .clearleft {
clear: left;
}
#mcdiv123searchresults .result .resultcontent {
max-width: 360px;
margin-left: 10px;
}
#mcdiv123searchresults .result .mcdiv_release_date {
color: silver
}
.mcdiv123_image_placeholder {
width: 82px;
height: 82px;
background: rgb(64, 64, 64);
border-radius: 8px;
}
#mcdiv123searchresults .result a {
color: #09f;
font-weight: 700;
text-decoration: none
}
#mcdiv123searchresults .mcdiv_desc {
max-height:120px;
overflow-y: auto;
scrollbar-color: #d9d9d9 #eee;
scrollbar-width: thin;
}
@media (prefers-color-scheme: dark) {
${BOX_CSS_DARK_THEME}
}
${
darkTheme ? BOX_CSS_DARK_THEME : ''
}
`
async function acceptGDPR (showDialog) {
if (showDialog === true) {
await GM.setValue('gdpr', null)
return acceptGDPR()
}
return new Promise(function (resolve) {
GM.getValue('gdpr', null).then(function (value) {
if (value === true) {
return resolve(true)
}
if (value === false) {
return resolve(false)
}
const html = '
Privacy Policy for "Show Metacritic.com ratings" General Data Protection Regulation (GDPR) We are a Data Controller of your information.
"Show Metacritic.com ratings" 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:
"Show Metacritic.com ratings" needs to perform a contract with you You have given "Show Metacritic.com ratings" permission to do so Processing your personal information is in "Show Metacritic.com ratings" legitimate interests "Show Metacritic.com ratings" needs to comply with the law "Show Metacritic.com ratings" 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.
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 GDPR Privacy Policy Generator and the App Privacy Policy Generator .
In certain circumstances, you have the following data protection rights:
The right to access, update or to delete the information we have on you. The right of rectification. The right to object. The right of restriction. The right to data portability The right to withdraw consent Log Files "Show Metacritic.com ratings" 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.
Privacy Policies You may consult this list to find the Privacy Policy for each of the advertising partners of "Show Metacritic.com ratings".
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 "Show Metacritic.com ratings", 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.
Note that "Show Metacritic.com ratings" has no access to or control over these cookies that are used by third-party advertisers.
Third Party Privacy Policies "Show Metacritic.com ratings"\'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:
You can choose to disable cookies through your individual browser options.
Children\'s Information 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.
"Show Metacritic.com ratings" 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.
Online Privacy Policy Only 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 "Show Metacritic.com ratings". This policy is not applicable to any information collected offline or via channels other than this program. Our GDPR Privacy Policy was generated from the GDPR Privacy Policy Generator.
Contact Contact us via github https://github.com/cvzi/Metacritic-userscript or email cuzi@openmail.cc
Consent By using our program ("userscript"), you hereby consent to our Privacy Policy and agree to its terms.
'
const div = document.body.appendChild(document.createElement('div'))
div.innerHTML = html
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'
div.appendChild(document.createElement('br'))
const acceptButton = div.appendChild(document.createElement('button'))
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')
acceptButton.appendChild(document.createTextNode('Accept'))
acceptButton.addEventListener('click', function () {
div.remove()
resolve(true)
GM.setValue('gdpr', true)
})
const declineButton = div.appendChild(document.createElement('button'))
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')
declineButton.appendChild(document.createTextNode('Decline'))
declineButton.addEventListener('click', function () {
alert('You may uninstall the userscript now.')
div.remove()
resolve(false)
GM.setValue('gdpr', false)
})
const space = div.appendChild(document.createElement('div'))
space.style = 'height:2000px;'
div.scrollIntoView()
window.setTimeout(function () {
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.')
}, 20)
})
})
}
function delay (ms) {
return new Promise(function (resolve) {
window.setTimeout(() => resolve(), ms)
})
}
function absoluteMetaURL (url) {
if (url.startsWith('https://')) {
return url
}
if (url.startsWith('http://')) {
return 'https' + url.substr(4)
}
if (url.startsWith('//')) {
return baseURL + url.substr(2)
}
if (url.startsWith('/')) {
return baseURL + url.substr(1)
}
return baseURL + url
}
const parseLDJSONCache = {}
function parseLDJSON (keys, condition) {
if (document.querySelector('script[type="application/ld+json"]')) {
const xmlEntitiesElement = document.createElement('div')
const xmlEntitiesPattern = /&(?:#x[a-f0-9]+|#[0-9]+|[a-z0-9]+);?/ig
const xmlEntities = function (s) {
s = s.replace(xmlEntitiesPattern, (m) => {
xmlEntitiesElement.innerHTML = m
return xmlEntitiesElement.textContent
})
return s
}
const decodeXmlEntities = function (jsonObj) {
// Traverse through object, decoding all strings
if (jsonObj !== null && typeof jsonObj === 'object') {
Object.entries(jsonObj).forEach(([key, value]) => {
// key is either an array index or object key
jsonObj[key] = decodeXmlEntities(value)
})
} else if (typeof jsonObj === 'string') {
return xmlEntities(jsonObj)
}
return jsonObj
}
const data = []
const scripts = document.querySelectorAll('script[type="application/ld+json"]')
for (let i = 0; i < scripts.length; i++) {
let jsonld
if (scripts[i].innerText in parseLDJSONCache) {
jsonld = parseLDJSONCache[scripts[i].innerText]
} else {
try {
jsonld = JSON.parse(scripts[i].innerText)
parseLDJSONCache[scripts[i].innerText] = jsonld
} catch (e) {
parseLDJSONCache[scripts[i].innerText] = null
continue
}
}
if (jsonld) {
if (Array.isArray(jsonld)) {
data.push(...jsonld)
} else {
data.push(jsonld)
}
}
}
for (let i = 0; i < data.length; i++) {
try {
if (data[i] && data[i] && (typeof condition !== 'function' || condition(data[i]))) {
if (Array.isArray(keys)) {
const r = []
for (let j = 0; j < keys.length; j++) {
r.push(data[i][keys[j]])
}
return decodeXmlEntities(r)
} else if (keys) {
return decodeXmlEntities(data[i][keys])
} else if (typeof condition === 'function') {
return decodeXmlEntities(data[i]) // Return whole object
}
}
} catch (e) {
continue
}
}
return decodeXmlEntities(data)
}
return null
}
function name2metacritic (s) {
const mc = s.normalize('NFKD').replace(/\//g, '').replace(/[\u0300-\u036F]/g, '').replace(/&/g, 'and').replace(/\W+/g, ' ').toLowerCase().trim().replace(/\W+/g, '-')
if (!mc) {
throw new Error("name2metacritic converted '" + s + "' to empty string")
}
return mc
}
function minutesSince (time) {
const seconds = ((new Date()).getTime() - time.getTime()) / 1000
return seconds > 60 ? parseInt(seconds / 60) + ' min ago' : 'now'
}
function randomStringId () {
const id10 = () => Math.floor((1 + Math.random()) * 0x10000000000).toString(16).substring(1)
return id10() + id10() + id10() + id10() + id10() + id10()
}
function fixMetacriticURLs (html) {
return html.replace(/ 89) {
return colors.universalAcclaim
}
if (score > 74) {
return colors.generallyFavorable
}
if (score > 49) {
return colors.mixedOrAverage
}
if (score > 19) {
return colors.generallyUnfavorable
}
if (score > 0) {
return colors.overwhelmingDislike
}
return colors.tbd
} else {
if (score > 80) {
return colors.universalAcclaim
}
if (score > 60) {
return colors.generallyFavorable
}
if (score > 39) {
return colors.mixedOrAverage
}
if (score > 19) {
return colors.generallyUnfavorable
}
if (score > 0) {
return colors.overwhelmingDislike
}
return colors.tbd
}
}
function replaceBrackets (str) {
str = str.replace(/\([^(]*\)/g, '')
str = str.replace(/\[[^\]]*\]/g, '')
return str.trim()
}
function removeSymbols (str) {
str = str.replace(/[^\s0-9A-Za-zÀ-ÖØ-öø-ÿ]*/gi, '').trim()
return str.trim()
}
const dashRegExp = /[-\u2010\u2011\u2012\u2013\u2014\u2015\uFE58\uFE63\uFF0D]/
function removeAnythingAfterDash (str) {
str = str.split(dashRegExp)[0]
return str.trim()
}
function broadenSearch (data, step, type) {
if (type === 'pcgame') {
if (step > 0) {
data[0] = replaceBrackets(data[0])
} else if (step > 1) {
data[0] = removeSymbols(data[0])
} else if (step > 2) {
data[0] = removeAnythingAfterDash(data[0])
}
} else {
data = data.map(removeSymbols)
}
return data
}
function balloonAlert (message, timeout, title, css, click) {
let header
if (title) {
header = '' + title + '
'
} else if (title === false) {
header = ''
} else {
header = 'Userscript alert
'
}
const div = $('' + header + '
' + message.split('\n').join(' ') + '
')
div.css({
position: 'fixed',
top: 10,
left: 10,
maxWidth: 200,
zIndex: '2147483601',
background: 'rgb(240,240,240)',
border: '2px solid yellow',
borderRadius: '6px',
boxShadow: '0 0 3px 3px rgba(100, 100, 100, 0.2)',
fontFamily: 'sans-serif',
color: 'black'
})
if (css) {
div.css(css)
}
div.appendTo(document.body)
if (click) {
div.click(function (ev) {
$(this).hide(500)
click.call(this, ev)
})
}
if (!click) {
const close = $('❎
').appendTo(div)
close.click(function () {
$(this.parentNode).hide(1000)
})
}
if (timeout && timeout > 0) {
window.setTimeout(function () {
div.hide(3000)
}, timeout)
}
return div
}
function filterUniversalUrl (url) {
try {
url = url.match(/http.+/)[0]
} catch (e) { }
try {
url = url.replace(/https?:\/\/(www.)?/, '')
} catch (e) { }
if (url.indexOf('#') !== -1) {
url = url.split('#')[0]
}
if (url.startsWith('imdb.com/') && url.match(/(imdb\.com\/\w+\/\w+\/)/)) {
// Remove movie subpage from imdb url
return url.match(/(imdb\.com\/\w+\/\w+\/)/)[1]
} else if (url.startsWith('boxofficemojo.com/') && url.indexOf('id=') !== -1) {
// Keep the important id= on
try {
const parts = url.split('?')
const page = parts[0] + '?'
const idparam = parts[1].match(/(id=.+?)(\.|&)/)[1]
return page + idparam
} catch (e) {
return url
}
} else {
// Default: Remove parameters
return url.split('?')[0].split('&')[0]
}
}
async function addToMap (url, metaurl) {
const data = JSON.parse(await GM.getValue('map', '{}'))
url = filterUniversalUrl(url)
metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
data[url] = metaurl
await GM.setValue('map', JSON.stringify(data));
(new Image()).src = baseURLwhitelist + '?docurl=' + encodeURIComponent(url) + '&metaurl=' + encodeURIComponent(metaurl) + '&ref=' + encodeURIComponent(randomStringId())
return [url, metaurl]
}
async function addToTemporaryBlacklist (metaurl) {
const data = JSON.parse(await GM.getValue('temporaryblack', '{}'))
metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/')
metaurl = metaurl.replace(/^\/+/, '')
data[metaurl] = (new Date()).toJSON()
// Remove old entries
const now = (new Date()).getTime()
const timeout = TEMPORARY_BLACKLIST_TIMEOUT * 1000
for (const prop in data) {
if (now - (new Date(data[prop].time)).getTime() > timeout) {
delete data[prop]
}
}
await GM.setValue('temporaryblack', JSON.stringify(data))
return true
}
async function removeFromTemporaryBlacklist (metaurl) {
const data = JSON.parse(await GM.getValue('temporaryblack', '{}'))
metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/')
metaurl = metaurl.replace(/^\/+/, '')
if (metaurl in data) {
delete data[metaurl]
await GM.setValue('temporaryblack', JSON.stringify(data))
}
}
async function isTemporaryBlacklisted (metaurl) {
const data = JSON.parse(await GM.getValue('temporaryblack', '{}'))
metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/')
metaurl = metaurl.replace(/^\/+/, '')
if (metaurl in data) {
const now = (new Date()).getTime()
const timeout = TEMPORARY_BLACKLIST_TIMEOUT * 1000
if (now - (new Date(data[metaurl])).getTime() < timeout) {
return true
}
}
return false
}
async function addToBlacklist (url, metaurl) {
const data = JSON.parse(await GM.getValue('black', '[]'))
url = filterUniversalUrl(url)
metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
data.push([url, metaurl])
await GM.setValue('black', JSON.stringify(data));
(new Image()).src = baseURLblacklist + '?docurl=' + encodeURIComponent(url) + '&metaurl=' + encodeURIComponent(metaurl) + '&ref=' + encodeURIComponent(randomStringId())
return [url, metaurl]
}
async function removeFromBlacklist (docurl, metaurl) {
docurl = filterUniversalUrl(docurl)
docurl = docurl.replace(/https?:\/\/(www.)?/, '')
metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/') // remove double slash
metaurl = metaurl.replace(/^\/+/, '') // remove starting slash
const data = JSON.parse(await GM.getValue('black', '[]')) // [ [docurl0, metaurl0] , [docurl1, metaurl1] , ... ]
const found = []
for (let i = 0; i < data.length; i++) {
if (data[i][0] === docurl && data[i][1] === metaurl) {
found.push(i)
}
}
for (let i = found.length - 1; i >= 0; i--) {
data.pop(i)
}
await GM.setValue('black', JSON.stringify(data))
}
async function isBlacklistedUrl (docurl, metaurl) {
docurl = filterUniversalUrl(docurl)
docurl = docurl.replace(/https?:\/\/(www.)?/, '')
metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//, '')
metaurl = metaurl.replace(/\/\//g, '/').replace(/\/\//g, '/') // remove double slash
metaurl = metaurl.replace(/^\/+/, '') // remove starting slash
const data = JSON.parse(await GM.getValue('black', '[]')) // [ [docurl0, metaurl0] , [docurl1, metaurl1] , ... ]
for (let i = 0; i < data.length; i++) {
if (data[i][0] === docurl && data[i][1] === metaurl) {
return true
}
}
return false
}
let listenForHotkeysActive = false
function listenForHotkeys (code, cb) {
// Call cb() as soon as the code sequence was typed
if (listenForHotkeysActive) {
return
}
listenForHotkeysActive = true
let i = 0
$(document).bind('keydown.listenForHotkeys', function (ev) {
if (document.activeElement === document.body) {
if (ev.key !== code[i]) {
i = 0
} else {
i++
if (i === code.length) {
ev.preventDefault()
$(document).unbind('keydown.listenForHotkeys')
cb()
}
}
}
})
}
function waitForHotkeysMETA () {
listenForHotkeys('meta', (ev) => openSearchBox())
}
async function handleJSONredirect (response) {
let blacklistedredirect = false
const j = JSON.parse(response.responseText)
// Blacklist items from database received?
if ('blacklist' in j && j.blacklist && j.blacklist.length) {
// Save new blacklist items
const data = JSON.parse(await GM.getValue('black', '[]'))
for (let i = 0; i < j.blacklist.length; i++) {
const saveDocurl = j.blacklist[i].docurl
const saveMetaurl = j.blacklist[i].metaurl
data.push([saveDocurl, saveMetaurl])
if (j.jsonRedirect === '/' + saveMetaurl) {
// Redirect is blacklisted!
blacklistedredirect = true
}
}
await GM.setValue('black', JSON.stringify(data))
}
if (blacklistedredirect) {
// Redirect was blacklisted, show nothing
console.debug('ShowMetacriticRatings: Redirect was blacklisted -> show nothing')
return null
} else {
// Load redirect
current.metaurl = absoluteMetaURL(j.jsonRedirect)
response = await asyncRequest({
url: current.metaurl
}).catch(function (response) {
console.error('ShowMetacriticRatings: Error 01')
})
return response
}
}
function extractHoverFromFullPage (response) {
let html = 'ShowMetacriticRatings: Error occured in extractHoverFromFullPage()'
try {
// Try parsing HTML
const doc = domParser().parseFromString(response.responseText, 'text/html')
let content = null
// Try to get the review containers from the bottom of the page below the actors
const carouselItems = doc.querySelectorAll('.c-reviewsSection_carouselContainer .c-reviewsOverview_overviewDetails')
if (carouselItems.length > 0) {
content = Array.from(carouselItems).map(e => e.outerHTML).join('\n\n')
} else {
// Fallback: Try to get the review containers from the right side of the page next to the poster/screenshot
content = doc.querySelector('.c-productHero_scoreInfo').innerHTML
}
if (!content) {
throw new Error('No content found')
}
html = `
${content}
`
} catch (e) {
console.warn('ShowMetacriticRatings: Error parsing HTML: ' + e)
// fallback to cutting out the relevant parts
const parts = response.responseText.split('c-productHero_score-container')
html = '
'
if (html.length > 5000) {
// Probably something went wrong, let's cut the response to prevent too long content
console.warn('ShowMetacriticRatings: Cutting response to 5000 chars')
html = html.substring(0, 5000)
}
}
return html
}
function asyncRequest (data) {
return new Promise(function (resolve, reject) {
isInRequestCache(data).then(function (cachedValue) {
if (cachedValue) {
console.debug(`${scriptName}: asyncRequest() Cache hit for`, data)
return window.setTimeout(() => resolve(cachedValue), 10)
}
const defaultHeaders = {
Referer: data.url,
'User-Agent': navigator.userAgent
}
const defaultData = {
method: 'GET',
onload: function (response) {
storeInRequestCache(data, response)
resolve(response)
},
onerror: (response) => reject(response)
}
if ('headers' in data) {
data.headers = Object.assign(defaultHeaders, data.headers)
} else {
data.headers = defaultHeaders
}
data = Object.assign(defaultData, data)
console.debug(`${scriptName}: asyncRequest() GM.xmlHttpRequest`, data)
GM.xmlHttpRequest(data)
})
})
}
async function storeInRequestCache (requestData, response) {
const newkey = JSON.stringify({
url: requestData.url,
method: requestData.method || 'GET',
data: requestData.data || null
})
const cache = JSON.parse(await GM.getValue('requestcache', '{}'))
const now = (new Date()).getTime()
const timeout = 15 * 60 * 1000
for (const prop in cache) {
// Delete cached values, that are older than 15 minutes
if (now - (new Date(cache[prop].time)).getTime() > timeout) {
delete cache[prop]
}
}
const newobj = {}
for (const key in response) {
newobj[key] = response[key]
}
newobj.responseText = '' + response.responseText
newobj.cached = true
if (!('time' in newobj)) {
newobj.time = (new Date()).toJSON()
}
cache[newkey] = newobj
await GM.setValue('requestcache', JSON.stringify(cache))
}
async function isInRequestCache (requestData) {
const key = JSON.stringify({
url: requestData.url,
method: requestData.method || 'GET',
data: requestData.data || null
})
const cache = JSON.parse(await GM.getValue('requestcache', '{}'))
const now = (new Date()).getTime()
const timeout = 15 * 60 * 1000
for (const prop in cache) {
// Delete cached values, that are older than 15 minutes
if (now - (new Date(cache[prop].time)).getTime() > timeout) {
delete cache[prop]
}
}
if (key in cache) {
return cache[key]
} else {
return false
}
}
async function storeInHoverCache (metaurl, response, orgMetaUrl) {
const cache = JSON.parse(await GM.getValue('hovercache', '{}'))
const now = (new Date()).getTime()
const timeout = 2 * 60 * 60 * 1000
for (const prop in cache) {
// Delete cached values, that are older than 2 hours
if (now - (new Date(cache[prop].time)).getTime() > timeout) {
delete cache[prop]
}
}
const newobj = {}
for (const key in response) {
newobj[key] = response[key]
}
newobj.responseText = '' + response.responseText
newobj.cached = true
if (!('time' in newobj)) {
newobj.time = (new Date()).toJSON()
}
cache[metaurl] = newobj
if (orgMetaUrl && orgMetaUrl !== metaurl) { // Store redirect
cache[orgMetaUrl] = { time: (new Date()).toJSON(), redirect: metaurl }
}
await GM.setValue('hovercache', JSON.stringify(cache))
}
async function isInHoverCache (metaurl) {
const cache = JSON.parse(await GM.getValue('hovercache', '{}'))
const now = (new Date()).getTime()
const timeout = 2 * 60 * 60 * 1000
for (const prop in cache) {
// Delete cached values, that are older than 2 hours
if (now - (new Date(cache[prop].time)).getTime() > timeout) {
delete cache[prop]
}
}
function resolveRedirects (cacheEntry) {
if (cacheEntry.redirect) {
const newkey = cacheEntry.redirect
if (newkey in cache) {
const value = cache[newkey]
delete cache[newkey]
return resolveRedirects(value)
}
} else {
return cacheEntry
}
return false
}
if (metaurl in cache) {
const value = cache[metaurl]
delete cache[metaurl]
return resolveRedirects(value)
} else {
return false
}
}
async function loadHoverInfo () {
const cacheResponse = await isInHoverCache(current.metaurl)
if (cacheResponse !== false) {
console.debug(`ShowMetacriticRatings: loadHoverInfo () ${current.metaurl} found in hover cache`)
if (cacheResponse.responseText.indexOf('"jsonRedirect"') !== -1) {
return await handleJSONredirect(cacheResponse)
}
return cacheResponse
}
const requestURL = baseURLdatabase
const requestParams = 'm=' + encodeURIComponent(current.docurl) + '&a=' + encodeURIComponent(current.metaurl)
let response = await asyncRequest({
method: 'POST',
url: requestURL,
data: requestParams,
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
}).catch(function (response) {
console.warn('ShowMetacriticRatings: Error 02\nurl=' + requestURL + '\nparams=' + requestParams + '\nstatus=' + response.status)
})
if (response.responseText && response.responseText.indexOf('"jsonRedirect"') !== -1) {
response = await handleJSONredirect(response)
}
if (response.status >= 500) {
// Metacritic server error, try again after 2 seconds
console.warn('ShowMetacriticRatings: Metacritic server error\nwait 2s for retry\nurl=' + current.metaurl + '\nstatus=' + response.status)
await delay(2000)
response = await asyncRequest({ url: current.metaurl }).catch(function (response) {
console.warn('ShowMetacriticRatings: Error 06\nurl=' + current.metaurl + '\nstatus=' + response.status)
})
if (response.status > 300) {
console.warn('ShowMetacriticRatings: Metacritic server error. Error 07. Retry failed as well.\nurl=' + current.metaurl + '\nstatus=' + response.status)
} else {
const newobj = {}
for (const key in response) {
newobj[key] = response[key]
}
newobj.responseText = extractHoverFromFullPage(response)
response = newobj
}
}
// Extract relevant data from HTML
if (!('time' in response)) {
response.time = (new Date()).toJSON()
}
if (response.status === 200 && response.responseText) {
const newobj = {}
for (const key in response) {
newobj[key] = response[key]
}
newobj.responseText = extractHoverFromFullPage(response)
response = newobj
return response
} else {
const error = new Error('ShowMetacriticRatings: loadHoverInfo()\nUrl: ' + response.finalUrl + '\nStatus: ' + response.status)
error.status = response.status
error.responseText = response.responseText
throw error
}
}
function changePosition () {
// Cycle through positions
GM.getValue('position', JSON.stringify(windowPositions[0])).then(function (s) {
let index
for (index = 0; index < windowPositions.length; index++) {
if (JSON.stringify(windowPositions[index]) === s) {
break
}
}
const nextIndex = (index + 1) % windowPositions.length
GM.setValue('position', JSON.stringify(windowPositions[nextIndex])).then(function () {
document.location.reload()
})
})
}
function onSizeChanged () {
GM.getValue('size', 100).then(function (size) {
if (size && size !== 100) {
size = parseInt(size)
$('#mcdiv123').css('transform', `scale(${size}%)`)
}
})
}
function changeSizeEnlarge () {
GM.getValue('size', 100).then((size) => {
GM.setValue('size', parseInt(size) + 5).then(onSizeChanged)
})
}
function changeSizeShrink () {
GM.getValue('size', 100).then((size) => {
GM.setValue('size', parseInt(size) - 5).then(onSizeChanged)
})
}
const current = {
metaurl: false,
docurl: false,
type: false,
data: [], // Array of raw search keys
searchTerm: false,
product: null,
broadenCounter: 0
}
async function onBlacklistedPage () {
GM.registerMenuCommand('Show Metacritic.com ratings - Remove from Blacklist', () => removeFromBlacklistAndReload())
}
async function removeFromBlacklistAndReload () {
await removeFromBlacklist(current.docurl, current.metaurl)
await removeFromTemporaryBlacklist(current.metaurl)
main()
}
async function loadMetacriticUrl (fromSearch) {
if (!current.metaurl) {
alert('ShowMetacriticRatings: Error 04')
return
}
const orgMetaUrl = current.metaurl
if (await isBlacklistedUrl(document.location.href, current.metaurl)) {
waitForHotkeysMETA()
onBlacklistedPage()
return
}
if (await isTemporaryBlacklisted(current.metaurl)) {
console.debug(`ShowMetacriticRatings: loadMetacriticUrl(fromSearch=${fromSearch}) ${current.metaurl} is temporary blacklisted`)
waitForHotkeysMETA()
onBlacklistedPage()
return
}
const response = await loadHoverInfo().catch(async function (response) {
if (response instanceof Error || (response && response.stack && response.message)) {
if (!fromSearch && ('status' in response && response.status === 404)) {
console.debug('ShowMetacriticRatings: loadMetacriticUrl(): status=404', response)
// No results
let broadenFct = broadenSearch // global broadenSearch function is the default
if ('broaden' in current.product) {
// try product 'broaden'-function if it is defined
broadenFct = current.product.broaden
}
const newData = await broadenFct(current.data.slice(0), ++current.broadenCounter, current.type)
if (JSON.stringify(newData) !== JSON.stringify(current.data)) {
current.data = newData
metacritic[current.type](current.docurl, current.product, ...newData)
} else if (JSON.stringify(newData) === JSON.stringify(current.data)) {
// Same data as before, try once again to broaden
const newData2 = await broadenFct(current.data.slice(0), ++current.broadenCounter, current.type)
if (JSON.stringify(newData2) !== JSON.stringify(current.data)) {
current.data = newData2
metacritic[current.type](current.docurl, current.product, ...newData2)
} else {
console.debug('ShowMetacriticRatings: loadMetacriticUrl(): ' + ('broaden' in current.product ? 'product specific' : 'global') + " 'broaden search' did not change after " + current.broadenCounter + ' steps')
}
} else {
console.debug("ShowMetacriticRatings: loadMetacriticUrl(): Unexpected result from 'broaden'-function: ", newData)
}
} else {
console.error(`ShowMetacriticRatings: loadMetacriticUrl(fromSearch=${fromSearch}) current.metaurl = ${current.metaurl}. Error in loadHoverInfo():\n`, response)
}
}
if (!fromSearch) {
startSearch()
}
})
if (await isBlacklistedUrl(document.location.href, current.metaurl)) {
waitForHotkeysMETA()
onBlacklistedPage()
return
}
if (typeof response !== 'undefined') {
showHoverInfo(response, orgMetaUrl)
} else {
waitForHotkeysMETA()
}
}
async function startSearch () {
waitForHotkeysMETA()
if (current.type === 'music') {
current.searchTerm = current.data[0]
} else {
current.searchTerm = current.data.join(' ')
}
const items = await fandomProdApigeeSearch(current.searchTerm, current.type)
if (!items) {
alert('ShowMetacriticRatings: Error 05 item=', items)
}
let multiple = false
if (items.length === 0) {
// No results
console.debug('ShowMetacriticRatings: No results for searchTerm=' + current.searchTerm)
} else if (items.length === 1) {
// One result, let's show it
const itemURL = absoluteMetaURL(items[0].metacriticUrl)
if (!await isBlacklistedUrl(document.location.href, itemURL)) {
current.metaurl = itemURL
loadMetacriticUrl(true)
return
} else {
onBlacklistedPage()
return
}
} else {
// More than one result
multiple = true
console.debug('ShowMetacriticRatings: Multiple results for searchTerm=' + current.searchTerm)
const exactMatches = []
items.forEach(function (result, i) { // Try to find the correct result by matching the search term to exactly one movie title
if (current.searchTerm.toLowerCase() === result.title.toLowerCase()) {
exactMatches.push(result)
}
})
if (exactMatches.length === 0) {
// Try to be a bit more fuzzy
items.forEach(function (result, i) {
if (removeSymbols(current.searchTerm.toLowerCase()) === removeSymbols(result.title.toLowerCase())) {
exactMatches.push(result)
}
})
}
if (exactMatches.length === 1) {
// Only one exact match, let's show it
console.debug('ShowMetacriticRatings: Only one exact match for searchTerm=' + current.searchTerm)
const itemURL = absoluteMetaURL(exactMatches[0].metacriticUrl)
if (!await isBlacklistedUrl(document.location.href, itemURL)) {
current.metaurl = itemURL
loadMetacriticUrl(true)
return
} else {
onBlacklistedPage()
return
}
}
}
// HERE: multiple results or no result. The user may type "meta" now
if (multiple) {
balloonAlert('Multiple metacritic results. Type "meta" for manual search.', 10000, false, { bottom: 5, top: 'auto', maxWidth: 400, paddingRight: 5, cursor: 'pointer' }, () => openSearchBox(true))
}
}
function openSearchBox (search) {
let query
if (current.type === 'music') {
query = current.data[0]
} else {
query = current.data.join(' ')
}
$('#mcdiv123').remove()
const div = $('
').appendTo(document.body)
div.css({
minWidth: 300,
bottom: 0,
left: 0
})
GM.getValue('position', false).then(function (s) {
if (s) {
div.css({
top: '',
left: '',
bottom: '',
right: ''
})
div.css(JSON.parse(s))
}
})
$(' ').appendTo(div).focus().val(query).on('keypress', function (e) {
const code = e.keyCode || e.which
if (code === 13) { // Enter key
searchBoxSearch(e, $('#mcisearchquery').val())
}
})
$('').text('Search').appendTo(div).click((ev) => searchBoxSearch(ev, $('#mcisearchquery').val()))
}
async function getFandomProdApigeeApiKey () {
let apiKey = await GM.getValue('fandomProdApigeeKey', false)
if (!apiKey) {
apiKey = await findFandomProdApigeeApiKey()
}
const lastUpdate = await GM.getValue('fandomProdApigeeTime', false)
if (!lastUpdate || (new Date()).getTime() - (new Date(lastUpdate)).getTime() > 7 * 24 * 60 * 60 * 1000) {
// Update api key once a week
const newApiKey = await findFandomProdApigeeApiKey()
if (newApiKey) {
apiKey = newApiKey
}
}
if (!apiKey) {
console.debug('ShowMetacriticRatings: Fallback to hard-coded api key')
apiKey = '1MOZgmNFxvmljaQR1X9KAij9Mo4xAY3u'
}
return apiKey
}
async function findFandomProdApigeeApiKey () {
// Get a new Api key from the metacritic website search results page
const url = 'https://www.metacritic.com/search/Fly/'
try {
const response = await asyncRequest({ url })
const m = response.responseText.match(/\?apiKey=(\w{20,})/)
if (m) {
const apiKey = m[1]
console.debug('ShowMetacriticRatings: Api key updated', apiKey)
await GM.setValue('fandomProdApigeeKey', apiKey)
await GM.setValue('fandomProdApigeeTime', (new Date()).toJSON())
return apiKey
}
} catch (e) {
console.error('ShowMetacriticRatings: findFandomProdApigeeApiKey() Error:', e)
}
console.error('ShowMetacriticRatings: Could not find fandomProdApigee api key')
return false
}
async function fandomProdApigeeSearch (query, searchType) {
const apiKey = await getFandomProdApigeeApiKey()
const type = searchType2fandomProdApigee(searchType)
const url = baseURLsearch.replace('{type}', encodeURIComponent(type)).replace('{query}', encodeURIComponent(query)).replace('{apiKey}', encodeURIComponent(apiKey))
const response = await asyncRequest({ url })
if (response.status !== 200) {
console.error('ShowMetacriticRatings: fandomProdApigeeSearch() response != 200: ', response)
}
const obj = JSON.parse(response.responseText)
return obj.data.items.map(item => {
// Improve results by adding the metacritic url
let itemUrl = 'criticScoreSummary' in item && 'url' in item.criticScoreSummary ? item.criticScoreSummary.url : null
if (!itemUrl) {
itemUrl = `${baseURL}${fandomProdApigee2metacriticUrl(item.typeId)}/${item.slug}/`
}
item.metacriticUrl = itemUrl.replace('/critic-reviews/', '/')
return item
})
}
async function searchBoxSearch (ev, query) {
if (!query) { // Use values from search form
query = current.searchTerm
}
const div = $('#mcdiv123')
div.css({
minWidth: '550px'
})
const loader = $('
').appendTo($('#mcisearchbutton'))
const resultItems = await fandomProdApigeeSearch(query, current.type).catch(function (response) {
alert('Search failed!\n' + response.finalUrl + '\nStatus: ' + response.status + '\n' + response.responseText ? response.responseText.substring(0, 500) : 'Empty response')
})
const results = []
resultItems.forEach(item => {
let img = `
`
if (item.images.length > 0) {
img = ` `
}
let score = ''
if ('criticScoreSummary' in item && 'score' in item.criticScoreSummary && item.criticScoreSummary.score > 0) {
const bgColor = badgeColor(item.criticScoreSummary.score, item.type)
score = `${item.criticScoreSummary.score}
`
}
results.push(`
${img}
${item.title}
${item.premiereYear ? item.premiereYear : (item.releaseDate ? item.releaseDate.substring(0, 4) : '')}
${score}
${item.genres.map(g => g.name).join(' • ')}
${item.releaseDate ? item.releaseDate : ''}
${item.description}
✓
`
)
})
const websiteSearchUrl = `${baseURL}search/${encodeURIComponent(query)}/`
if (results && results.length > 0) {
// Show results
loader.remove()
const accept = function (ev) {
const parentDiv = $(this).closest('.result')
const a = parentDiv.find("a[href*='metacritic.com']")
const metaurl = a.attr('href')
const docurl = document.location.href
const resultDivParent = parentDiv.parent()
resultDivParent.html('')
resultDivParent.append(loader)
removeFromBlacklist(docurl, metaurl).then(function () {
addToMap(docurl, metaurl).then(function () {
current.metaurl = metaurl
loadMetacriticUrl().then(() => loader.remove())
})
})
}
const denyAll = function (ev) {
const docurl = document.location.href
$('#mcdiv123searchresults').find("div.result a[href*='metacritic.com']").each(function () {
addToBlacklist(docurl, this.href)
})
}
const resultdiv = $('#mcdiv123searchresults').length
? $('#mcdiv123searchresults').html('')
: $('
').appendTo(div)
results.forEach(function (html) {
const singleresult = $('
').html(fixMetacriticURLs(html) + '
').appendTo(resultdiv)
singleresult.find('.mcdiv123_correct_entry').click(accept)
})
resultdiv.find('.metascore_w.album').removeClass('album') // Remove some classes
resultdiv.find('.must-see').remove() // Remove some elements
const sub = $('#mcdiv123 .sub').length ? $('#mcdiv123 .sub').html('') : $('
').appendTo(div)
$('' + decodeURI(websiteSearchUrl.replace('https://www.', '')) + ' ').appendTo(sub)
$('❎ ').appendTo(sub).click(function () {
document.body.removeChild(this.parentNode.parentNode)
})
$('✗ ').appendTo(sub).click(function () { if (confirm('None of the above is the correct item\nConfirm?')) denyAll() })
} else {
// No results
loader.remove()
const resultdiv = $('#mcdiv123searchresults').length ? $('#mcdiv123searchresults').html('') : $('
').appendTo(div)
resultdiv.html('No search results.')
const sub = $('#mcdiv123 .sub').length ? $('#mcdiv123 .sub').html('') : $('
').appendTo(div)
$('' + decodeURI(websiteSearchUrl.replace('https://www.', '')) + ' ').appendTo(sub)
$('❎ ').appendTo(sub).click(function () {
document.body.removeChild(this.parentNode.parentNode)
})
}
}
function showHoverInfo (response, orgMetaUrl) {
const html = fixMetacriticURLs(response.responseText)
const time = new Date(response.time)
const url = response.finalUrl
$('#mcdiv123').remove()
const div = $('
').appendTo(document.body)
div.css({
bottom: 0,
left: 0
})
div.css('transform-origin', 'bottom left')
GM.getValue('position', false).then(function (s) {
if (s) {
div.css({
top: '',
left: '',
bottom: '',
right: ''
})
const parsedPosition = JSON.parse(s)
div.css(parsedPosition)
div.css('transform-origin', Object.keys(parsedPosition).join(' '))
}
})
onSizeChanged()
// Functions for communication between page and iframe
// Mozilla can access parent.document
// Chrome can use postMessage()
let frameStatus = false // if this remains false, loading the frame content failed. A reason could be "Content Security Policy"
async function tryToLoadMoreMetacriticDetails (myframe) {
console.log('ShowMetacriticRatings: tryToLoadMoreMetacriticDetails current', current)
if (!current.metaurl) {
return
}
let url = current.metaurl
if (url.endsWith('/')) {
url = url + 'details/'
} else {
url = url + '/details/'
}
const response = await asyncRequest({ url })
const doc = domParser().parseFromString(response.responseText, 'text/html')
const titleA = doc.querySelector('.c-productSubpageHeader_back')
titleA.querySelectorAll('.c-productSubpageHeader_backIcon').forEach(e => e.remove())
const titleHTML = titleA.outerHTML
const image = doc.querySelector('picture img')
image.style.display = ''
const imageHTML = image.outerHTML
let detailsTable = Array.from(doc.querySelectorAll('.c-movieDetails_sectionContainer')).map(e => Array.from(e.children).map(e => e.textContent.trim()))
detailsTable = detailsTable.filter(columns => {
if (columns[0].search(/release date/i) !== -1) {
return true
}
if (columns[0].search(/genres/i) !== -1) {
return true
}
if (columns[0].search(/developer/i) !== -1) {
return true
}
if (columns[0].search(/publisher/i) !== -1) {
return true
}
return false
}).map(columns => columns.join(': '))
const html = imageHTML + ' ' + titleHTML + ' ' + detailsTable.join(' ')
myframe.contentWindow.postMessage({
mcimessage_addhtml: true,
mcimessage_element_id: 'metacritic_extra_data',
mcimessage_element_style: 'display:none;',
mcimessage_html: html
}, '*')
// Wait to show the extra data to avoid making the frame to big
window.setTimeout(function () {
myframe.contentWindow.postMessage({
mcimessage_showelement: true,
mcimessage_selector: '#metacritic_extra_data'
}, '*')
}, 1000)
}
function loadExternalImage (url, myframe) {
// Load external image, bypass CSP
GM.xmlHttpRequest({
method: 'GET',
url,
responseType: 'arraybuffer',
onload: function (response) {
myframe.contentWindow.postMessage({
mcimessage_imgLoaded: true,
mcimessage_imgData: response.response,
mcimessage_imgOrgSrc: url
}, '*')
}
})
}
const functions = {
parent: function () {
const f = parent.document.getElementById('mciframe123')
let lastdiff = -200000
window.addEventListener('message', function (e) {
if (typeof e.data !== 'object') {
return
} else if ('mcimessage0' in e.data) {
frameStatus = true // Frame content was loaded successfully
tryToLoadMoreMetacriticDetails(f)
} else if ('mcimessage1' in e.data) {
f.style.width = parseInt(f.style.width) + 5 + 'px'
if (e.data.heightdiff === lastdiff) {
f.style.height = parseInt(f.style.height) + 10 + 'px'
}
lastdiff = e.data.heightdiff
} else if ('mcimessage2' in e.data) {
f.style.height = parseInt(f.style.height) + 10 + 'px'
} else if ('mcimessage_loadImg' in e.data) {
loadExternalImage(e.data.mcimessage_imgUrl, f)
} else {
return
}
if (f.contentWindow != null) {
f.contentWindow.postMessage({
mcimessage3: true,
mciframe123_clientHeight: f.clientHeight,
mciframe123_clientWidth: f.clientWidth
}, '*')
}
})
},
frame: function () {
parent.postMessage({ mcimessage0: true }, '*') // Loading frame content was successfull
let i = 0
window.addEventListener('message', function (e) {
if (typeof e.data === 'object' && 'mcimessage_imgLoaded' in e.data) {
// Load external image
const arrayBufferView = new Uint8Array(e.data.mcimessage_imgData)
const blob = new Blob([arrayBufferView], { type: 'image/jpeg' })
const urlCreator = window.URL || window.webkitURL
const imageUrl = urlCreator.createObjectURL(blob)
const img = failedImages[e.data.mcimessage_imgOrgSrc]
img.src = imageUrl
}
if (typeof e.data === 'object' && 'mcimessage_addhtml' in e.data) {
const div = document.body.appendChild(document.createElement('div'))
div.setAttribute('id', e.data.mcimessage_element_id)
div.setAttribute('style', e.data.mcimessage_element_style)
div.innerHTML = e.data.mcimessage_html
}
if (typeof e.data === 'object' && 'mcimessage_showelement' in e.data) {
document.querySelector(e.data.mcimessage_selector).style.display = ''
}
if (!('mcimessage3' in e.data)) return
if (e.data.mciframe123_clientHeight < document.body.scrollHeight && i < 100) {
parent.postMessage({ mcimessage2: 1 }, '*')
i++
}
if (i >= 100) {
parent.postMessage({ mcimessage1: 1, heightdiff: document.body.scrollHeight - e.data.mciframe123_clientHeight }, '*')
i = 0
}
})
parent.postMessage({ mcimessage1: 1, heightdiff: -100000 }, '*')
}
}
const css = `
#hover_div_a20230915{font-family:sans-serif;color:#262626;font-size:1rem;line-height:1.625rem}#hover_div_a202309:hover15 a,#hover_div_a20230915 a:hover{text-decoration:none}#hover_div_a20230915 a:hover{color:#09f}#hover_div_a20230915 a{color:#000;text-decoration:none;}#hover_div_a20230915 a:focus{color:grey}#hover_div_a20230915 .g-border-black,#hover_div_a20230915 .g-border-gray100{border-color:#000}#hover_div_a20230915 .g-color-black,#hover_div_a20230915 .g-color-gray100{color:#000}#hover_div_a20230915 .g-border-gray98{border-color:#191919}#hover_div_a20230915 .g-color-gray98{color:#191919}#hover_div_a20230915 .g-border-gray90{border-color:#262626}#hover_div_a20230915 .g-color-gray90{color:#262626}#hover_div_a20230915 .g-border-gray80{border-color:#404040}#hover_div_a20230915 .g-color-gray80{color:#404040}#hover_div_a20230915 .g-border-gray70{border-color:#666}#hover_div_a20230915 .g-color-gray70{color:#666}#hover_div_a20230915 .g-border-gray60{border-color:grey}#hover_div_a20230915 .g-color-gray60{color:grey}#hover_div_a20230915 .g-border-gray50{border-color:#999}#hover_div_a20230915 .g-color-gray50{color:#999}#hover_div_a20230915 .g-border-gray40{border-color:#bfbfbf}#hover_div_a20230915 .g-color-gray40{color:#bfbfbf}#hover_div_a20230915 .g-border-gray30{border-color:#d8d8d8}#hover_div_a20230915 .g-color-gray30{color:#d8d8d8}#hover_div_a20230915 .g-border-gray20{border-color:#e6e6e6}#hover_div_a20230915 .g-color-gray20{color:#e6e6e6}#hover_div_a20230915 .g-border-gray10{border-color:#f2f2f2}#hover_div_a20230915 .g-color-gray10{color:#f2f2f2}#hover_div_a20230915 .g-border-gray0,#hover_div_a20230915 .g-border-white{border-color:#fff}#hover_div_a20230915 .g-color-gray0,#hover_div_a20230915 .g-color-white{color:#fff}#hover_div_a20230915 .g-border-red{border-color:#eb0036}#hover_div_a20230915 .g-color-red{color:#eb0036}#hover_div_a20230915 .g-border-green{border-color:#01b44f}#hover_div_a20230915 .g-color-green{color:#01b44f}#hover_div_a20230915 .g-width-large{width:1.5rem}#hover_div_a20230915 .g-height-large{height:1.5rem}#hover_div_a20230915 .g-width-100{width:100%}#hover_div_a20230915 .g-height-100{height:100%}#hover_div_a20230915 .g-text-large{font-size:1.5rem;line-height:2rem}#hover_div_a20230915 .g-text-xxsmall{font-size:xx-small}#hover_div_a20230915 .g-text-bold{font-weight:700}#hover_div_a20230915 .g-text-link{text-decoration:underline}#hover_div_a20230915 .u-block{display:block}#hover_div_a20230915 .u-flexbox{display:flex}#hover_div_a20230915 .u-flexbox-column{display:flex;flex-direction:column}#hover_div_a20230915 .u-flexbox-justifyCenter{justify-content:center}#hover_div_a20230915 .u-flexbox-alignCenter{align-items:center}#hover_div_a20230915 .u-grid{display:grid;grid-gap:0;grid-gap:var(--grid-gap,0)}#hover_div_a20230915 .u-grid-2column{-ms-grid-columns:50% 50%;display:grid;grid-template:auto/repeat(2,1fr)}#hover_div_a20230915 .u-grid-3column{-ms-grid-columns:33.3% 33.3% 33.3%;display:grid;grid-template:auto/repeat(3,1fr)}#hover_div_a20230915 .u-grid-4column{-ms-grid-columns:25% 25% 25% 25%;display:grid;grid-template:auto/repeat(4,1fr)}#hover_div_a20230915 .u-grid-5column{-ms-grid-columns:20% 20% 20% 20% 20%;display:grid;grid-template:auto/repeat(5,1fr)}#hover_div_a20230915 .u-grid-7column{-ms-grid-columns:14.2857% 14.2857% 14.2857% 14.2857% 14.2857% 14.2857% 14.2857%;display:grid;grid-template:auto/repeat(7,1fr)}#hover_div_a20230915 .u-grid-column-span2{grid-column-end:span 2}#hover_div_a20230915 .u-grid-column-span3{grid-column-end:span 3}#hover_div_a20230915 .u-grid-column-span4{grid-column-end:span 4}#hover_div_a20230915 .u-text-center{text-align:center}#hover_div_a20230915 .c-siteReviewScore_large{border-radius:0.5rem;height:4rem;width:4rem;font-size:2rem}#hover_div_a20230915 .c-siteReviewScore_user{border-radius:50%}#hover_div_a20230915 .c-reviewsStats{padding:1rem 0;grid-template-columns:1fr 1fr 1fr;justify-content:space-evenly;font-size:0.75rem;line-height:1.25rem}#hover_div_a20230915 div[class^=c-reviewsStats_]:first-child,#hover_div_a20230915 div[class^=c-reviewsStats_]:nth-child(2){border-right:0.0625rem solid #d8d8d8}#hover_div_a20230915 .c-ScoreCardGraph{overflow:hidden;white-space:nowrap}#hover_div_a20230915 .c-ScoreCardGraph > div{margin-left:0.25rem;padding:0 0.25rem;text-align:right;height:0.5rem;min-width:2rem;line-height:1rem}#hover_div_a20230915 .c-ScoreCardGraph > div:first-child{margin-left:0}#hover_div_a20230915 .c-ScoreCardGraph_scoreTitle{letter-spacing:0.25rem}#hover_div_a20230915 .c-ScoreCardGraph_scoreSentiment{color:#00ce7a}#hover_div_a20230915 .c-ScoreCardGraph_scoreGraphPositive{background:#00ce7a;border-radius:0.25rem 0 0 0.25rem}#hover_div_a20230915 .c-ScoreCardGraph_scoreGraphNeutral{background:#ffbd3f}#hover_div_a20230915 .c-ScoreCardGraph_scoreGraphNegative{background:#ff6874;border-radius:0 0.25rem 0.25rem 0}#hover_div_a20230915 .gray{background:#bfbfbf;height:1rem;display:inline-block}#hover_div_a20230915 .c-ScoreCard_scoreContent{display:flex;align-content:flex-start;flex-wrap:nowrap;grid-gap:10px;gap:10px;width:100%;justify-content:space-between;align-items:stretch}#hover_div_a20230915 .c-ScoreCard_scoreContent_text{line-height:normal;display:flex;flex-direction:column;justify-content:space-between}#hover_div_a20230915 .c-ScoreCard_scoreContent_number > .c-siteReviewScore_background-critic_large,#hover_div_a20230915 .c-ScoreCard_scoreContent_number > .c-siteReviewScore_background-critic_large .c-siteReviewScore_large{width:4rem;height:4rem}#hover_div_a20230915 .c-ScoreCard_scoreSentiment{font-size:1rem;line-height:1.25rem;text-transform:capitalize}#hover_div_a20230915 .c-ScoreCard_scoreTitle{letter-spacing:0.25rem}#hover_div_a20230915 .c-reviewsOverview_overviewDetails{grid-template-columns:1fr 1fr;grid-gap:1.25rem;border-top:1px solid #262626;margin-top:auto;padding:2px}#hover_div_a20230915 .c-reviewsOverview_overviewDetails:first-child{border-top:0 solid #262626}#hover_div_a20230915 .c-siteReviewScore_green{background:#00ce7a}#hover_div_a20230915 .c-siteReviewScore_yellow{background:#ffbd3f}#hover_div_a20230915 .c-siteReviewScore_red{background:#ff6874}#hover_div_a20230915 .c-siteReviewScore_grey{background:#404040}#hover_div_a20230915 .c-siteReviewScore_tbdCritic,#hover_div_a20230915 .c-siteReviewScore_tbdUser{border-width:0.125rem;border-style:solid}#hover_div_a20230915 .o-inlineScore{border-radius:0.25rem;font-size:1.25rem;font-weight:700;color:#404040;width:2.5rem;height:2.5rem;display:inline-flex;justify-content:center;align-items:center;text-decoration:none!important}#hover_div_a20230915 .o-inlineScore-green{background:#00ce7a}#hover_div_a20230915 .o-inlineScore-yellow{background:#ffbd3f}#hover_div_a20230915 .o-inlineScore-red{background:#ff6874}#hover_div_a20230915 .o-inlineScore-tbd{border:1px solid grey}#hover_div_a20230915 .u-pointer{cursor:pointer}#hover_div_a20230915 .c-siteReviewScore_green{background:#00ce7a}#hover_div_a20230915 .c-siteReviewScore_yellow{background:#ffbd3f}#hover_div_a20230915 .c-siteReviewScore_red{background:#ff6874}#hover_div_a20230915 .c-siteReviewScore_grey{background:#404040}#hover_div_a20230915 .c-siteReviewScore_tbdCritic,#hover_div_a20230915 .c-siteReviewScore_tbdUser{border-width:0.125rem;border-style:solid}#hover_div_a20230915{max-width:440px}
`
const cssDark = `
html {
scrollbar-color: #003c09 #00ce7a;
}
*::-webkit-scrollbar-thumb {
background-color: #003c09;
}
body {
background:#262626;
color:white;
}
#metacritic_extra_data {
color:white;
}
#metacritic_extra_data a:hover {
color: white;
}
#metacritic_extra_data a {
color: #5799ef;
text-decoration: none;
}
#hover_div_a20230915 {
color: #d1d1d1;
}
#hover_div_a20230915 a:hover {
color: #09f;
}
#hover_div_a20230915 a {
color: #ffffff;
text-decoration: none;
}
#hover_div_a20230915 a:focus {
color: rgb(184, 184, 184);
}
#hover_div_a20230915 .g-border-black,
#hover_div_a20230915 .g-border-gray100 {
border-color: #ffffff;
}
#hover_div_a20230915 .g-color-black,
#hover_div_a20230915 .g-color-gray100 {
color: #ffffff;
}
#hover_div_a20230915 .g-border-gray98 {
border-color: #d6d6d6;
}
#hover_div_a20230915 .g-color-gray98 {
color: #d6d6d6;
}
#hover_div_a20230915 .g-border-gray90 {
border-color: #d3d3d3;
}
#hover_div_a20230915 .g-color-gray90 {
color: #d3d3d3;
}
#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 .c-reviewsOverview_overviewDetails {
border-top: 1px solid #d1d1d1;
}
#hover_div_a20230915 .c-reviewsOverview_overviewDetails:first-child {
border-top: 0 solid #d1d1d1;
}
#hover_div_a20230915 .c-siteReviewScore_grey {
background: #404040;
}
#hover_div_a20230915 .o-inlineScore {
color: #404040;
}
#hover_div_a20230915 .o-inlineScore-tbd {
border: 1px solid grey;
}
#hover_div_a20230915 .u-pointer {
cursor: pointer;
}
#hover_div_a20230915 .c-siteReviewScore_grey {
background: #404040;
}
`
let framesrc = 'data:text/html,'
framesrc += encodeURIComponent(`
Metacritic info
`)
const frame = $('')
frame.attr('id', 'mciframe123')
frame.attr('src', framesrc)
frame.attr('scrolling', 'auto')
frame.css({
width: 440,
height: 110,
border: 'none',
opacity: '0.1',
transition: 'opacity 1s'
})
frame.appendTo(div)
window.setTimeout(function () {
frame.css('opacity', '1.0')
}, 1000)
window.setTimeout(function () {
if (!frameStatus) { // Loading frame content failed.
// Directly inject the html without an iframe (this may break the site or the metacritic)
console.debug('ShowMetacriticRatings: Loading iframe content failed. Injecting directly.')
$('head').append(``)
const noframe = $(``)
frame.replaceWith(noframe)
}
}, 2000)
functions.parent()
const sub = $('
').appendTo(div)
$('' + minutesSince(time) + ' ').appendTo(sub)
$('' + decodeURI(url.replace('https://www.', '@')) + ' ').appendTo(sub)
$('❎ ').data('url', current.metaurl).appendTo(sub).click(function () {
const metaurl = $(this).data('url')
addToTemporaryBlacklist(metaurl)
document.body.removeChild(this.parentNode.parentNode)
})
$('✓ ').data('url', current.metaurl).appendTo(sub).click(function () {
const docurl = document.location.href
const metaurl = $(this).data('url')
addToMap(docurl, metaurl).then(function (r) {
balloonAlert('Thanks for your submission!\n\nSaved as a correct entry.\n\n' + r[0] + '\n' + r[1], 6000, 'Success')
})
})
$('✗ ').data('url', current.metaurl).appendTo(sub).click(function () {
if (!confirm('This is NOT the correct entry!\n\nAdd to blacklist?')) return
const docurl = document.location.href
const metaurl = $(this).data('url')
addToBlacklist(docurl, metaurl).then(function (r) {
balloonAlert('Thanks for your submission!\n\nSaved to blacklist.\n\n' + r[0] + '\n' + r[1], 6000, 'Success')
})
openSearchBox(true)
})
// Store response in cache:
if (!('cached' in response)) {
storeInHoverCache(current.metaurl, response, orgMetaUrl)
}
}
function metacriticGeneralProductSetup () {
current.broadenCounter = 0
}
const metacritic = {
mapped: function metacriticMapped (docurl, product, metaurl, type, searchTerm) {
// url was in the map/whitelist
current.data = searchTerm ? [searchTerm] : []
current.docurl = docurl
current.product = product
current.metaurl = metaurl
current.type = type
current.searchTerm = searchTerm || null
loadMetacriticUrl()
},
music: function metacriticMusic (docurl, product, artistname, albumname) {
current.data = [albumname.trim(), artistname.trim()]
artistname = name2metacritic(artistname)
albumname = albumname.replace('&', ' ')
albumname = name2metacritic(albumname)
current.docurl = docurl
current.product = product
current.metaurl = baseURLmusic + albumname + '/' + artistname
current.type = 'music'
current.searchTerm = albumname + '/' + artistname
loadMetacriticUrl()
},
movie: function metacriticMovie (docurl, product, moviename) {
current.data = [moviename.trim()]
moviename = name2metacritic(moviename)
current.docurl = docurl
current.product = product
current.metaurl = baseURLmovie + moviename
current.type = 'movie'
current.searchTerm = moviename
loadMetacriticUrl()
},
tv: function metacriticTv (docurl, product, seriesname) {
current.data = [seriesname.trim()]
seriesname = name2metacritic(seriesname)
current.docurl = docurl
current.product = product
current.metaurl = baseURLtv + seriesname
current.type = 'tv'
current.searchTerm = seriesname
loadMetacriticUrl()
},
pcgame: function metacriticPcgame (docurl, product, gamename) {
current.data = [gamename.trim()]
gamename = name2metacritic(gamename)
current.docurl = docurl
current.product = product
current.metaurl = baseURLpcgame + gamename
current.type = 'pcgame'
current.searchTerm = gamename
loadMetacriticUrl()
},
ps4game: function metacriticPs4game (docurl, product, gamename) {
current.data = [gamename.trim()]
gamename = name2metacritic(gamename)
current.docurl = docurl
current.product = product
current.metaurl = baseURLps4 + gamename
current.type = 'ps4game'
current.searchTerm = gamename
loadMetacriticUrl()
},
xonegame: function metacriticXonegame (docurl, product, gamename) {
current.data = [gamename.trim()]
gamename = name2metacritic(gamename)
current.docurl = docurl
current.product = product
current.metaurl = baseURLxone + gamename
current.type = 'xonegame'
current.searchTerm = gamename
loadMetacriticUrl()
}
}
const Always = () => true
const sites = {
bandcamp: {
host: ['bandcamp.com'],
condition: () => unsafeWindow && unsafeWindow.TralbumData && unsafeWindow.TralbumData.current,
products: [{
condition: Always,
type: 'music',
data: () => [unsafeWindow.TralbumData.artist, unsafeWindow.TralbumData.current.title]
}]
},
itunes: {
host: ['itunes.apple.com'],
condition: Always,
products: [{
condition: () => ~document.location.href.indexOf('/movie/'),
type: 'movie',
data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
},
{
condition: () => ~document.location.href.indexOf('/tv-season/'),
type: 'tv',
data: function () {
let name = parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
if (~name.indexOf(', Season')) {
name = name.split(', Season')[0]
}
return name
}
},
{
condition: () => ~document.location.href.indexOf('/album/'),
type: 'music',
data: function () {
const ld = parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum'))
const album = ld[0]
const artist = 'name' in ld[1] ? ld[1].name : ld[1].map(x => x.name).join(' ')
return [artist, album]
}
}]
},
'music.apple': {
host: ['music.apple.com'],
condition: Always,
products: [{
condition: () => ~document.location.href.indexOf('/album/') && parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum')).length > 1,
type: 'music',
data: function () {
const ld = parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum'))
const album = ld[0]
const artist = 'name' in ld[1] ? ld[1].name : ld[1].map(x => x.name).join(' ')
return [artist, album]
}
}]
},
googleplay: {
host: ['play.google.com'],
condition: Always,
products: [
{
condition: () => ~document.location.href.indexOf('/album/'),
type: 'music',
data: () => [document.querySelector('[itemprop="byArtist"] meta[itemprop="name"]').content, document.querySelector('[itemtype="https://schema.org/MusicAlbum"] meta[itemprop="name"]').content]
},
{
condition: () => ~document.location.href.indexOf('/movies/details/'),
type: 'movie',
data: () => document.querySelector('*[itemprop=name]').textContent
}
]
},
imdb: {
host: ['imdb.com'],
condition: () => !~document.location.pathname.indexOf('/mediaviewer') && !~document.location.pathname.indexOf('/mediaindex') && !~document.location.pathname.indexOf('/videoplayer'),
products: [
{
condition: () => document.querySelector('a[href*="/criticreviews/"'),
type: 'mapped',
data: async function () {
// This is used if there is a metacritic link on the imdb page
const criticsUrl = document.querySelector('a[href*="/criticreviews/"').href.toString()
const response = await asyncRequest({ url: criticsUrl }).catch(function (response) {
console.warn('ShowMetacriticRatings: Error imdb01\nurl=' + criticsUrl + '\nstatus=' + response.status)
})
const m = response.responseText.match(/(https:\/\/www\.metacritic\.com\/(\w+)\/[^?&"']+)/)
console.debug('ShowMetacriticRatings: Metacritic link found on imdb:', m[2], m[1])
const query = document.querySelector('[data-testid="hero__pageTitle"]') ? document.querySelector('[data-testid="hero__pageTitle"]').textContent : null
return [m[1], m[2], query]
}
},
{
condition: function () {
const e = document.querySelector("meta[property='og:type']")
if (e && e.content === 'video.movie') {
return true
} else if (document.querySelector('[data-testid="hero__pageTitle"]') && !document.querySelector('[data-testid="hero-subnav-bar-left-block"] a[href*="episodes/"]')) {
return true
}
return false
},
type: 'movie',
data: async function () {
// If the page is not in English or the browser is not in English, request page in English.
// Then the title in will be the English title and Metacritic always uses the English title.
if (document.querySelector('[for="nav-language-selector"]').textContent.toLowerCase() !== 'en' || !navigator.language.startsWith('en')) {
const imdbID = document.location.pathname.match(/\/title\/(\w+)/)[1]
const homePageUrl = 'https://www.imdb.com/title/' + imdbID + '/?ref_=nv_sr_1'
// Set language cookie to English, request current page in English, then restore language cookie or expire it if it didn't exist before
const langM = document.cookie.match(/lc-main=([^;]+)/)
const langBefore = langM ? langM[0] : ';expires=Thu, 01 Jan 1970 00:00:01 GMT'
document.cookie = 'lc-main=en-US'
const response = await asyncRequest({
url: homePageUrl,
headers: {
'Accept-Language': 'en-US,en'
}
}).catch(function (response) {
console.warn('ShowMetacriticRatings: Error imdb02\nurl=' + homePageUrl + '\nstatus=' + response.status)
})
document.cookie = 'lc-main=' + langBefore
// Extract title
const parts = response.responseText.split(' ')[0].split('>')
console.debug('ShowMetacriticRatings: Movie title from English page:', parts[parts.length - 1])
return parts[parts.length - 1]
} else if (document.querySelector('script[type="application/ld+json"]')) {
const ld = parseLDJSON(['name', 'alternateName'])
if (ld.length > 1 && ld[1]) {
console.debug('ShowMetacriticRatings: Movie ld+json alternateName', ld[1])
return ld[1]
}
console.debug('ShowMetacriticRatings: Movie ld+json name', ld[0])
return ld[0]
} else {
const m = document.title.match(/(.+?)\s+(\((\d+)\))? - /)
console.debug('ShowMetacriticRatings: Movie ', m[1])
return m[1]
}
}
},
{
condition: function () {
const e = document.querySelector("meta[property='og:type']")
if (e && e.content === 'video.tv_show') {
return true
} else if (document.querySelector('[data-testid="hero-subnav-bar-left-block"] a[href*="episodes/"]')) {
return true
}
return false
},
type: 'tv',
data: async function () {
if (document.querySelector('[for="nav-language-selector"]').textContent.toLowerCase() !== 'en' || !navigator.language.startsWith('en')) {
const imdbID = document.location.pathname.match(/\/title\/(\w+)/)[1]
const homePageUrl = 'https://www.imdb.com/title/' + imdbID + '/?ref_=nv_sr_1'
// Set language cookie to English, request current page in English, then restore language cookie or expire it if it didn't exist before
const langM = document.cookie.match(/lc-main=([^;]+)/)
const langBefore = langM ? langM[0] : ';expires=Thu, 01 Jan 1970 00:00:01 GMT'
document.cookie = 'lc-main=en-US'
const response = await asyncRequest({
url: homePageUrl,
headers: {
'Accept-Language': 'en-US,en'
}
}).catch(function (response) {
console.warn('ShowMetacriticRatings: Error imdb03\nurl=' + homePageUrl + '\nstatus=' + response.status)
})
document.cookie = 'lc-main=' + langBefore
// Extract title
const parts = response.responseText.split(' ')[0].split('>')
console.debug('ShowMetacriticRatings: TV title from English page:', parts[parts.length - 1])
return parts[parts.length - 1]
} else if (document.querySelector('script[type="application/ld+json"]')) {
const ld = parseLDJSON(['name', 'alternateName'])
if (ld.length > 1 && ld[1]) {
console.debug('ShowMetacriticRatings: TV ld+json alternateName', ld[1])
return ld[1]
}
console.debug('ShowMetacriticRatings: TV ld+json name', ld[0])
return ld[0]
} else {
const m = document.title.match(/(.+?)\s+\(.+(\d{4}).+/)
console.debug('ShowMetacriticRatings: TV ', m[1])
return m[1]
}
}
}
]
},
steam: {
host: ['store.steampowered.com'],
condition: () => document.querySelector('*[itemprop=name]'),
products: [{
condition: Always,
type: 'pcgame',
data: () => document.querySelector('*[itemprop=name]').textContent
}]
},
rottentomatoes: {
host: ['rottentomatoes.com'],
condition: Always,
products: [{
condition: () => document.location.pathname.startsWith('/m/'),
type: 'movie',
data: () => document.querySelector('h1').firstChild.textContent
},
{
condition: () => document.location.pathname.startsWith('/tv/'),
type: 'tv',
data: () => unsafeWindow.BK.TvSeriesTitle
}
]
},
serienjunkies: {
host: ['www.serienjunkies.de'],
condition: Always,
products: [{
condition: () => document.getElementById('serienlinksbreit2aktuell'),
type: 'tv',
data: () => document.querySelector('h1').textContent.trim()
},
{
condition: () => document.location.pathname.search(/vod\/film\/.{3,}/) !== -1,
type: 'movie',
data: () => document.querySelector('h1').textContent.trim()
}]
},
gamespot: {
host: ['gamespot.com'],
condition: () => document.querySelector('[itemprop=device]') || document.location.pathname.startsWith('/reviews/'),
products: [
{
condition: () => ~$('[itemprop=device]').text().indexOf('PC'),
type: 'pcgame',
data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
},
{
condition: () => ~$('[itemprop=device]').text().indexOf('PS4'),
type: 'ps4game',
data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
},
{
condition: () => ~$('[itemprop=device]').text().indexOf('XONE'),
type: 'xonegame',
data: () => parseLDJSON('name', (j) => (j['@type'] === 'VideoGame'))
},
{
condition: () => document.querySelector('.system.system--simple.system--pc'),
type: 'pcgame',
data: () => document.querySelector('h1').textContent.trim().split('Review')[0].trim()
},
{
condition: () => document.querySelector('.system.system--simple.system--ps5'),
type: 'ps4game',
data: () => document.querySelector('h1').textContent.trim().split('Review')[0].trim()
},
{
condition: () => document.querySelector('.system.system--simple.system--xbsx'),
type: 'xonegame',
data: () => document.querySelector('h1').textContent.trim().split('Review')[0].trim()
}
]
},
amazon: {
host: ['amazon.'],
condition: Always,
products: [
{
condition: () => document.location.hostname === 'music.amazon.com' && document.location.pathname.startsWith('/albums/') && document.querySelector('.viewTitle'), // "Amazon Music Unlimited" page
type: 'music',
data: function () {
const artist = document.querySelector('.artistLink').textContent.trim()
let title = document.querySelector('.viewTitle').textContent.trim()
title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
if (artist && title) {
return [artist, title]
}
return false
}
},
{
condition: function () { // "Normal amazon" page
try {
if (document.querySelector('.nav-categ-image').alt.toLowerCase().indexOf('musi') !== -1) {
return true
}
} catch (e) {}
const music = ['Music', 'Musique', 'Musik', 'Música', 'Musica', '音楽']
return music.some(function (s) {
if (~document.title.indexOf(s)) {
return true
} else {
return false
}
})
},
type: 'music',
data: function () {
let artist = false
let title = false
if (document.querySelector('#ProductInfoArtistLink')) {
artist = document.querySelector('#ProductInfoArtistLink').textContent.trim()
} else if (document.querySelector('#bylineInfo .author>*')) {
artist = document.querySelector('#bylineInfo .author>*').textContent.trim()
}
if (document.querySelector('#dmusicProductTitle_feature_div')) {
title = document.querySelector('#dmusicProductTitle_feature_div').textContent.trim()
title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
} else if (document.querySelector('#productTitle')) {
title = document.querySelector('#productTitle').textContent.trim()
title = title.replace(/\[([^\]]*)\]/g, '').trim() // Remove [brackets] and their content
}
return [artist, title]
}
},
{
condition: () => (document.querySelector('[data-automation-id=title]') && (
document.getElementsByClassName('av-season-single').length ||
document.querySelector('[data-automation-id="num-of-seasons-badge"]') ||
document.getElementById('tab-selector-episodes') ||
document.getElementById('av-droplist-av-atf-season-selector')
)),
type: 'tv',
data: () => document.querySelector('[data-automation-id=title]').textContent.trim()
},
{
condition: () => ((
document.getElementsByClassName('av-season-single').length ||
document.querySelector('[data-automation-id="num-of-seasons-badge"]') ||
document.getElementById('tab-selector-episodes') ||
document.getElementById('av-droplist-av-atf-season-selector')
) && Array.from(document.querySelectorAll('script[type="text/template"]')).map(e => e.innerHTML.match(/parentTitle"\s*:\s*"(.+?)"/)).some((x) => x != null)),
type: 'tv',
data: () => Array.from(document.querySelectorAll('script[type="text/template"]')).map(e => e.innerHTML.match(/parentTitle"\s*:\s*"(.+?)"/)).filter((x) => x != null)[0][1]
},
{
condition: () => document.querySelector('[data-automation-id=title]'),
type: 'movie',
data: () => document.querySelector('[data-automation-id=title]').textContent.trim().replace(/\[.{1,8}\]/, '')
},
{
condition: () => document.querySelector('#watchNowContainer a[href*="/gp/video/"]'),
type: 'movie',
data: () => document.getElementById('productTitle').textContent.trim()
}
]
},
BoxOfficeMojo: {
host: ['boxofficemojo.com'],
condition: () => Always,
products: [
{
condition: () => document.location.pathname.startsWith('/release/'),
type: 'movie',
data: () => document.querySelector('meta[name=title]').content
},
{
// Old page design
condition: () => ~document.location.search.indexOf('id=') && document.querySelector('#body table:nth-child(2) tr:first-child b'),
type: 'movie',
data: () => document.querySelector('#body table:nth-child(2) tr:first-child b').firstChild.textContent
}]
},
AllMovie: {
host: ['allmovie.com'],
condition: () => document.querySelector('h2.movie-title'),
products: [{
condition: () => document.querySelector('h2.movie-title'),
type: 'movie',
data: () => document.querySelector('h2.movie-title').firstChild.textContent.trim()
}]
},
'en.wikipedia': {
host: ['en.wikipedia.org'],
condition: Always,
products: [{
condition: function () {
if (!document.querySelector('.infobox .summary')) {
return false
}
const r = /\d\d\d\d films/
return $('#catlinks a').filter((i, e) => e.firstChild.textContent.match(r)).length
},
type: 'movie',
data: () => document.querySelector('.infobox .summary').firstChild.textContent
},
{
condition: function () {
if (!document.querySelector('.infobox .summary')) {
return false
}
const r = /television series/
return $('#catlinks a').filter((i, e) => e.firstChild.textContent.match(r)).length
},
type: 'tv',
data: () => document.querySelector('.infobox .summary').firstChild.textContent
}]
},
fandango: {
host: ['fandango.com'],
condition: () => document.querySelector("meta[property='og:title']"),
products: [{
condition: Always,
type: 'movie',
data: () => document.querySelector("meta[property='og:title']").content.match(/(.+?)\s+\(\d{4}\)/)[1].trim()
}]
},
flixster: {
host: ['flixster.com'],
condition: () => Always,
products: [{
condition: () => parseLDJSON('@type') === 'Movie',
type: 'movie',
data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
}]
},
themoviedb: {
host: ['themoviedb.org'],
condition: () => document.querySelector("meta[property='og:type']"),
products: [{
condition: () => document.querySelector("meta[property='og:type']").content === 'movie' ||
document.querySelector("meta[property='og:type']").content === 'video.movie',
type: 'movie',
data: () => document.querySelector("meta[property='og:title']").content
},
{
condition: () => document.querySelector("meta[property='og:type']").content === 'tv' ||
document.querySelector("meta[property='og:type']").content === 'tv_series' ||
document.querySelector("meta[property='og:type']").content.indexOf('tv_show') !== -1,
type: 'tv',
data: () => document.querySelector("meta[property='og:title']").content
}]
},
letterboxd: {
host: ['letterboxd.com'],
condition: () => unsafeWindow.filmData && 'name' in unsafeWindow.filmData,
products: [{
condition: Always,
type: 'movie',
data: () => unsafeWindow.filmData.name
}]
},
TVmaze: {
host: ['tvmaze.com'],
condition: () => document.querySelector('h1'),
products: [{
condition: Always,
type: 'tv',
data: () => document.querySelector('h1').firstChild.textContent
}]
},
TVGuide: {
host: ['tvguide.com'],
condition: Always,
products: [{
condition: () => document.location.pathname.startsWith('/tvshows/'),
type: 'tv',
data: function () {
if (document.querySelector('meta[itemprop=name]')) {
return document.querySelector('meta[itemprop=name]').content
} else {
return document.querySelector("meta[property='og:title']").content.split('|')[0]
}
}
}]
},
followshows: {
host: ['followshows.com'],
condition: Always,
products: [{
condition: () => document.querySelector("meta[property='og:type']").content === 'video.tv_show',
type: 'tv',
data: () => document.querySelector("meta[property='og:title']").content.replace(/\(\d{4}\)$/, '')
}]
},
TheTVDB: {
host: ['thetvdb.com'],
condition: Always,
products: [{
condition: () => document.location.pathname.startsWith('/series/'),
type: 'tv',
data: () => document.getElementById('series_title').firstChild.textContent.trim()
},
{
condition: () => document.location.pathname.startsWith('/movies/'),
type: 'movie',
data: () => document.getElementById('series_title').firstChild.textContent.trim()
}]
},
ConsequenceOfSound: {
host: ['consequence.net', 'consequenceofsound.net'],
condition: () => document.querySelector('#main-content .review-summary'),
products: [
{
condition: () => document.querySelector('meta[name="cXenseParse:cns-artist-names"]') && document.querySelector('em'),
type: 'music',
data: function () {
window.setInterval(function () {
if (document.getElementById('ot-sdk-btn-floating')) {
document.getElementById('ot-sdk-btn-floating').remove()
}
}, 5000)
const artist = document.querySelector('meta[name="cXenseParse:cns-artist-names"]').content
const arr = Array.from(document.querySelectorAll('em')).map((em) => em.textContent.trim())
const counts = {}
for (const num of arr) {
counts[num] = counts[num] ? counts[num] + 1 : 1
}
const max = Math.max(...Object.values(counts))
const maxIndex = Object.values(counts).indexOf(max)
const title = Object.keys(counts)[maxIndex]
return [artist, title]
}
},
{
condition: () => document.title.match(/'(.*?)'\s*Album/i) && document.querySelector('meta[name="cXenseParse:cns-artist-names"]'),
type: 'music',
data: function () {
window.setInterval(function () {
if (document.getElementById('ot-sdk-btn-floating')) {
document.getElementById('ot-sdk-btn-floating').remove()
}
}, 5000)
const title = document.title.match(/'(.*?)'\s*Album/i)[1]
const artist = document.querySelector('meta[name="cXenseParse:cns-artist-names"]').content
return [artist, title]
}
},
{
condition: () => document.title.match(/(.+?)\s+\u2013\s+(.+?) \| Album Review/),
type: 'music',
data: function () {
window.setInterval(function () {
if (document.getElementById('ot-sdk-btn-floating')) {
document.getElementById('ot-sdk-btn-floating').remove()
}
}, 5000)
const m = document.title.match(/(.+?)\s+\u2013\s+(.+?) \| Album Review/)
return [m[1], m[2]]
}
},
{
condition: () => document.location.pathname.indexOf('/album-review') !== -1 && document.querySelector('a.tag[href*="/artist/"'),
type: 'music',
data: function () {
window.setInterval(function () {
if (document.getElementById('ot-sdk-btn-floating')) {
document.getElementById('ot-sdk-btn-floating').remove()
}
}, 5000)
const artistAndTitleWithDash = document.location.pathname.match(/album-review-([\w-]+)/)[1]
const artistWithDash = document.querySelector('a.tag[href*="/artist/"').pathname.match(/artist\/([\w-]+)/)[1]
const titleWithDash = artistAndTitleWithDash.replace(artistWithDash, '')
const title = titleWithDash.replace('-', ' ').trim()
const artist = artistWithDash.replace('-', ' ').trim()
return [artist, title]
}
}]
},
Pitchfork: {
host: ['pitchfork.com'],
condition: () => ~document.location.href.indexOf('/reviews/albums/'),
products: [{
condition: () => document.querySelector('.single-album-tombstone'),
type: 'music',
data: function () {
let artist
let album
if (document.querySelector('.single-album-tombstone .artists')) {
artist = document.querySelector('.single-album-tombstone .artists').innerText.trim()
} else if (document.querySelector('.single-album-tombstone .artist-list')) {
artist = document.querySelector('.single-album-tombstone .artist-list').innerText.trim()
}
if (document.querySelector('.single-album-tombstone h1.review-title')) {
album = document.querySelector('.single-album-tombstone h1.review-title').innerText.trim()
} else if (document.querySelector('.single-album-tombstone h1')) {
album = document.querySelector('.single-album-tombstone h1').innerText.trim()
}
return [artist, album]
}
}]
},
'Last.fm': {
host: ['last.fm'],
condition: () => document.querySelector('*[data-page-resource-type]') && document.querySelector('*[data-page-resource-type]').dataset.pageResourceType === 'album',
products: [{
condition: () => document.querySelector('*[data-page-resource-type]').dataset.pageResourceName,
type: 'music',
data: function () {
const artist = document.querySelector('*[data-page-resource-type]').dataset.pageResourceArtistName
const album = document.querySelector('*[data-page-resource-type]').dataset.pageResourceName
return [artist, album]
}
}]
},
TVNfo: {
host: ['tvnfo.com'],
condition: () => document.querySelector('#title #name'),
products: [{
condition: Always,
type: 'tv',
data: function () {
const years = document.querySelector('#title #years').textContent.trim()
const title = document.querySelector('#title #name').textContent.replace(years, '').trim()
return title
}
}]
},
rateyourmusic: {
host: ['rateyourmusic.com'],
condition: () => document.querySelector("meta[property='og:type']"),
products: [{
condition: () => document.querySelector("meta[property='og:type']").content === 'music.album',
type: 'music',
data: function () {
const artist = document.querySelector('.section_main_info .artist').innerText.trim()
const album = document.querySelector('.section_main_info .album_title').innerText.trim()
return [artist, album]
}
}]
},
spotify: {
host: ['open.spotify.com'],
condition: Always,
products: [{
condition: () => document.location.pathname.startsWith('/album/') && document.querySelector('.Root__main-view h1'),
type: 'music',
data: function () {
const album = document.querySelector('.Root__main-view h1').textContent.trim()
let artist = []
document.querySelector('.Root__main-view h1').parentNode.parentNode.parentNode.querySelectorAll('a[href*="/artist/"]').forEach(function (a) {
artist.push(a.textContent.trim())
})
artist = artist.join(' ')
return [artist, album]
}
}]
},
nme: {
host: ['nme.com'],
condition: () => document.location.pathname.startsWith('/reviews/'),
products: [
{
condition: () => document.querySelector('.tdb-breadcrumbs a[href*="/reviews/film-reviews"]'),
type: 'movie',
data: function () {
try {
return document.title.match(/[‘'](.+?)[’']/)[1]
} catch (e) {
try {
return document.querySelector('h1.tdb-title-text').textContent.match(/[‘'](.+?)[’']/)[1]
} catch (e) {
return document.querySelector('h1').textContent.match(/:\s*(.+)/)[1].trim()
}
}
}
},
{
condition: () => document.querySelector('#nme-music-header'),
type: 'music',
data: () => document.querySelector('h1.tdb-title-text').textContent.match(/\s*(.+?)\s*.\s*[‘'](.+?)[’']/).slice(1)
},
{
condition: () => document.querySelector('.tdb-breadcrumbs a[href*="/reviews/tv-reviews"]'),
type: 'tv',
data: () => document.querySelector('h1.tdb-title-text').textContent.match(/[‘'](.+?)[’']/)[1]
}]
},
albumoftheyear: {
host: ['albumoftheyear.org'],
condition: Always,
products: [{
condition: () => document.location.pathname.startsWith('/album/'),
type: 'music',
data: function () {
const artist = document.querySelector('*[itemprop=byArtist] *[itemprop=name]').textContent
const album = document.querySelector('.albumTitle *[itemprop=name]').textContent
return [artist, album]
}
}]
},
epguides: {
host: ['epguides.com'],
condition: () => document.getElementById('eplist'),
products: [{
condition: () => document.getElementById('eplist') && document.querySelector('.center.titleblock h2'),
type: 'tv',
data: () => document.querySelector('.center.titleblock h2').textContent.trim()
}]
},
/*
netflix: {
host: ['netflix.com'],
condition: !(document.querySelector('.button-nfplayerPlay') || document.querySelector('.nf-big-play-pause') || document.querySelector('.AkiraPlayer video')),
// TODO
// https://www.netflix.com/de/title/70264888
// https://www.netflix.com/de/title/70178217
// https://www.netflix.com/de/title/70305892 ## Movie
// https://www.netflix.com/de-en/title/80108495 ## No meta
products: [{
condition: () => parseLDJSON('@type') === 'Movie',
type: 'movie',
data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
},
{
condition: () => parseLDJSON('@type') === 'TVSeries',
type: 'tv',
data: () => parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
}]
},
*/
ComedyCentral: {
host: ['cc.com'],
condition: () => document.location.pathname.startsWith('/shows/'),
products: [{
condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:title']"),
type: 'tv',
data: () => document.querySelector("meta[property='og:title']").content.replace('| Comedy Central', '').trim()
},
{
condition: () => document.location.pathname.split('/').length === 3 && document.title.match(/(.+?)\s+-\s+Series/),
type: 'tv',
data: () => document.title.match(/(.+?)\s+-\s+Series/)[1]
}]
},
AMC: {
host: ['amc.com'],
condition: () => document.location.pathname.startsWith('/shows/'),
products: [
{
condition: () => document.querySelector('.feeds[itemtype="http://schema.org/TVSeries"] h1'),
type: 'tv',
data: () => document.querySelector('.feeds[itemtype="http://schema.org/TVSeries"] h1').textContent
},
{
condition: () => document.location.pathname.split('/').length === 3 && document.querySelector("meta[property='og:type']") && document.querySelector("meta[property='og:type']").content.indexOf('tv_show') !== -1,
type: 'tv',
data: () => document.querySelector('.video-card-description h1').textContent.trim()
}]
},
AMCplus: {
host: ['amcplus.com'],
condition: () => Always,
products: [
{
condition: () => document.title.match(/Watch .+? |/),
type: 'tv',
data: () => document.title.match(/Watch (.+?) |/)[1].trim()
}]
},
RlsBB: {
host: ['rlsbb.ru'],
condition: () => document.querySelectorAll('.post').length === 1,
products: [
{
condition: () => document.querySelector('#post-wrapper .entry-meta a[href*="/category/movies/"]'),
type: 'movie',
data: () => document.querySelector('h1.entry-title').textContent.match(/(.+?)\s+\d{4}/)[1].trim()
},
{
condition: () => document.querySelector('#post-wrapper .entry-meta a[href*="/category/tv-shows/"]'),
type: 'tv',
data: () => document.querySelector('h1.entry-title').textContent.match(/(.+?)\s+S\d{2}/)[1].trim()
}]
},
newalbumreleases: {
host: ['newalbumreleases.net'],
condition: () => document.querySelectorAll('#content .single').length === 1,
products: [
{
condition: () => document.querySelector('#content .single .cover .entry'),
type: 'music',
data: function () {
const mArtist = document.querySelector('#content .single .cover .entry').textContent.match(/Artist.\s*(.+)\s+/i)
if (mArtist) {
const mAlbum = document.querySelector('#content .single .cover .entry').textContent.match(/Album.\s*(.+)\s+/i)
if (mAlbum) {
return [mArtist[1], mAlbum[1]]
}
}
}
}]
},
showtime: {
host: ['sho.com'],
condition: Always,
products: [
{
condition: () => parseLDJSON('@type') === 'Movie',
type: 'movie',
data: () => parseLDJSON('name', (j) => (j['@type'] === 'Movie'))
},
{
condition: () => parseLDJSON('@type') === 'TVSeries',
type: 'tv',
data: () => parseLDJSON('name', (j) => (j['@type'] === 'TVSeries'))
}]
},
epicgames: {
host: ['www.epicgames.com', 'store.epicgames.com'],
condition: () => document.querySelector('.meta-schema'),
products: [{
condition: Always,
type: 'pcgame',
data: function () {
try {
return document.querySelector('.meta-schema').nextElementSibling.firstElementChild.lastElementChild.firstElementChild.firstElementChild.firstElementChild.textContent
} catch (e) {
return document.querySelector('h1').textContent
}
}
}]
},
gog: {
host: ['www.gog.com'],
condition: () => document.querySelector('.productcard-basics__title'),
products: [
{
condition: () => document.location.pathname.split('/').length > 2 && (
document.location.pathname.split('/')[1] === 'game' ||
document.location.pathname.split('/')[2] === 'game'),
type: 'pcgame',
data: () => document.querySelector('.productcard-basics__title').textContent
},
{
condition: () => document.location.pathname.split('/').length > 2 && (
document.location.pathname.split('/')[1] === 'movie' ||
document.location.pathname.split('/')[2] === 'movie'),
type: 'movie',
data: () => document.querySelector('.productcard-basics__title').textContent
}
]
},
steamgifts: {
host: ['www.steamgifts.com'],
condition: () => document.querySelector('.featured__heading__medium'),
products: [{
condition: Always,
type: 'pcgame',
data: () => document.querySelector('.featured__heading__medium').innerText
}]
},
allmusic: {
host: ['allmusic.com'],
condition: Always,
products: [{
condition: () => document.location.pathname.indexOf('/album/') !== -1,
type: 'music',
data: function () {
const ld = parseLDJSON(['name', 'byArtist'], (j) => (j['@type'] === 'MusicAlbum'))
const album = ld[0]
const artist = 'name' in ld[1] ? ld[1].name : ld[1].map(x => x.name).join(' ')
return [artist, album]
}
}]
},
psapm: {
host: ['psa.wf'],
condition: Always,
products: [
{
condition: () => document.location.pathname.startsWith('/movie/'),
type: 'movie',
data: function () {
const title = document.querySelector('h1').textContent.trim()
const m = title.match(/(.+)\((\d+)\)$/)
if (m) {
return m[1].trim()
} else {
return title
}
}
},
{
condition: () => document.location.pathname.startsWith('/tv-show/'),
type: 'tv',
data: () => document.querySelector('h1').textContent.trim()
}
]
},
'save.tv': {
host: ['save.tv'],
condition: () => document.location.pathname.startsWith('/STV/M/obj/archive/'),
products: [
{
condition: () => document.location.pathname.startsWith('/STV/M/obj/archive/'),
type: 'movie',
data: function () {
let title = null
if (document.querySelector("span[data-bind='text:OrigTitle']")) {
title = document.querySelector("span[data-bind='text:OrigTitle']").textContent
} else {
title = document.querySelector("h2[data-bind='text:Title']").textContent
}
let year = null
if (document.querySelector("span[data-bind='text:ProductionYear']")) {
year = parseInt(document.querySelector("span[data-bind='text:ProductionYear']").textContent)
}
return [title, year]
}
}
]
},
wikiwand: {
host: ['www.wikiwand.com'],
condition: Always,
products: [{
condition: function () {
const title = document.querySelector('h1').textContent.toLowerCase()
const subtitle = document.querySelector('h2[class*="subtitle"]') ? document.querySelector('h2[class*="subtitle"]').textContent.toLowerCase() : ''
if (title.indexOf('film') === -1 && !subtitle) {
return false
}
return title.indexOf('film') !== -1 ||
subtitle.indexOf('film') !== -1 ||
subtitle.indexOf('movie') !== -1
},
type: 'movie',
data: () => document.querySelector('h1').textContent.replace(/\((\d{4} )?film\)/i, '').trim()
},
{
condition: function () {
const title = document.querySelector('h1').textContent.toLowerCase()
const subtitle = document.querySelector('h2[class*="subtitle"]') ? document.querySelector('h2[class*="subtitle"]').textContent.toLowerCase() : ''
if (title.indexOf('tv series') === -1 && !subtitle) {
return false
}
return title.indexOf('tv series') !== -1 ||
subtitle.indexOf('television') !== -1 ||
subtitle.indexOf('tv series') !== -1
},
type: 'tv',
data: () => document.querySelector('h1').textContent.replace(/\(tv series\)/i, '').trim()
}]
},
radarr: {
host: ['*'],
condition: () => document.location.pathname.startsWith('/movie/'),
products: [{
condition: () => document.querySelector('[class*="MovieDetails-title"] span'),
type: 'movie',
data: () => document.querySelector('[class*="MovieDetails-title"] span').textContent.trim()
}]
},
trakt: {
host: ['trakt.tv'],
condition: Always,
products: [
{
condition: () => document.location.pathname.startsWith('/movies/'),
type: 'movie',
data: () => Array.from(document.querySelector('.summary h1').childNodes).filter(node => node.nodeType === node.TEXT_NODE).map(node => node.textContent).join(' ').trim()
},
{
condition: () => document.location.pathname.startsWith('/shows/'),
type: 'tv',
data: () => Array.from(document.querySelector('.summary h1').childNodes).filter(node => node.nodeType === node.TEXT_NODE).map(node => node.textContent).join(' ').trim()
}
]
}
}
async function main () {
let dataFound = false
let map = false
for (const name in sites) {
const site = sites[name]
if (site.host.some(function (e) { return ~this.indexOf(e) || e === '*' }, document.location.hostname) && site.condition()) {
for (let i = 0; i < site.products.length; i++) {
if (site.products[i].condition()) {
// Check map for a match
if (map === false) {
map = JSON.parse(await GM.getValue('map', '{}'))
}
const docurl = filterUniversalUrl(document.location.href)
if (docurl in map) {
// Found in map, show result
const metaurl = map[docurl]
metacriticGeneralProductSetup()
metacritic.mapped.apply(undefined, [docurl, site.products[i], absoluteMetaURL(metaurl), site.products[i].type])
dataFound = true
break
}
// Try to retrieve item name from page
let data
try {
data = await site.products[i].data()
} catch (e) {
data = false
console.error(`ShowMetacriticRatings: Error in data() of site='${name}', type='${site.products[i].type}'`)
console.error(e)
}
if (data) {
const params = [docurl, site.products[i]]
if (Array.isArray(data)) {
params.push(...data)
} else {
params.push(data)
}
metacriticGeneralProductSetup()
metacritic[site.products[i].type].apply(undefined, params)
dataFound = true
}
break
}
}
break
}
}
return dataFound
}
(async function () {
const gdpr = await acceptGDPR()
if (!gdpr) {
GM.registerMenuCommand('Show Metacritic.com ratings - Accept terms of service', () => acceptGDPR(true).then((yes) => yes && document.location.reload()))
return
}
await versionUpdate()
if (!document.getElementById('mcdiv123_box_css')) {
const style = document.createElement('style')
style.setAttribute('id', 'mcdiv123_box_css')
style.innerHTML = BOX_CSS
document.head.appendChild(style)
}
const firstRunResult = await main()
GM.registerMenuCommand('Show Metacritic.com ratings - Search now', () => openSearchBox())
GM.registerMenuCommand('Show Metacritic.com ratings - Change corner', () => changePosition())
GM.registerMenuCommand('Show Metacritic.com ratings - Enlarge', () => changeSizeEnlarge())
GM.registerMenuCommand('Show Metacritic.com ratings - Shrink', () => changeSizeShrink())
let lastLoc = document.location.href
let lastContent = document.body.innerText
let lastCounter = 0
async function newpage () {
if (lastContent === document.body.innerText && lastCounter < 15) {
window.setTimeout(newpage, 500)
lastCounter++
} else {
lastContent = document.body.innerText
lastCounter = 0
const re = await main()
if (!re) { // No page matched or no data found
window.setTimeout(newpage, 1000)
}
}
}
window.setInterval(function () {
if (document.location.href !== lastLoc) {
lastLoc = document.location.href
$('#mcdiv123').remove()
window.setTimeout(newpage, 1000)
}
}, 500)
if (!firstRunResult) {
// Initial run had no match, let's try again there may be new content
window.setTimeout(main, 2000)
}
})()