"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.
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:13px'
div.appendChild(document.createElement('br'))
const acceptButton = div.appendChild(document.createElement('button'))
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.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;'
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.')
})
})
}
function delay (ms) {
return new Promise(function (resolve) {
window.setTimeout(() => resolve(), ms)
})
}
function getHostname (url) {
const a = document.createElement('a')
a.href = url
return a.hostname
}
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 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 r
} else if (keys) {
return data[i][keys]
} else if (typeof condition === 'function') {
return data[i] // Return whole object
}
}
} catch (e) {
continue
}
}
return 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(/ 0) {
data[0] = replaceBrackets(data[0])
} else if (step > 1) {
data[0] = removeSymbols(data[0])
} else if (step > 2) {
data[0] = removeAnythingAfterDash(data[0])
}
}
return data
}
function balloonAlert (message, timeout, title, css, click) {
let header
if (title) {
header = '
`
} catch (e) {
console.warn('ShowMetacriticRatings: Error parsing HTML: ' + e)
// fallback to cutting out the relevant parts
let parts = response.responseText.split('class="score_details')
const textPart = '
')[0]
titleText = titleText.split('
').join('
') + '
'
parts = response.responseText.split('id="nav_to_metascore"')
let metaScorePart = ''
metaScorePart = metaScorePart.split('href="">').join('href="' + response.finalUrl + '">')
metaScorePart = metaScorePart.split('section_title bold">').join('section_title bold">' + titleText)
html = metaScorePart.split('
').join(textPart + '
')
if (html.indexOf('products_module') !== -1) {
// Critic reviews are not available for this Series yet -> Cut the preview for other series
html = html.split('products_module')[0] + '">
'
}
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.substr(0, 5000)
}
}
return html
}
async function storeInRequestCache (requestData, response) {
if ('onload' in requestData) {
delete requestData.onload
}
if ('onerror' in requestData) {
delete requestData.onerror
}
const newkey = JSON.stringify(requestData)
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) {
if ('onload' in requestData) {
delete requestData.onload
}
if ('onerror' in requestData) {
delete requestData.onerror
}
const key = JSON.stringify(requestData)
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
}
}
if (response.responseText && response.responseText.indexOf('500 Page') !== -1) {
// Hover info not available for this url, try again with GET
response = await asyncRequest({ url: current.metaurl }).catch(function (response) {
console.warn('ShowMetacriticRatings: Error 03\nurl=' + current.metaurl + '\nstatus=' + response.status)
})
const newobj = {}
for (const key in response) {
newobj[key] = response[key]
}
newobj.responseText = extractHoverFromFullPage(response)
response = newobj
}
if (!('time' in response)) {
response.time = (new Date()).toJSON()
}
if (response.status === 200 && response.responseText) {
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
}
}
const current = {
metaurl: false,
docurl: false,
type: false,
data: [], // Array of raw search keys
searchTerm: false,
product: null,
broadenCounter: 0
}
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()
return
}
if (await isTemporaryBlacklisted(current.metaurl)) {
console.debug(`ShowMetacriticRatings: loadMetacriticUrl(fromSearch=${fromSearch}) ${current.metaurl} is temporary blacklisted`)
waitForHotkeysMETA()
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)) {
// 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 (newData !== current.data) {
current.data = newData
metacritic[current.type](current.docurl, current.product, ...newData)
} else if (newData === current.data) {
// Same data as before, try once again to broaden
const newData2 = await broadenFct(current.data.slice(0), ++current.broadenCounter, current.type)
if (newData2 !== 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()
return
}
if (typeof response !== 'undefined') {
showHoverInfo(response, orgMetaUrl)
} else {
waitForHotkeysMETA()
}
}
async function startSearch () {
waitForHotkeysMETA()
const cache = JSON.parse(await GM.getValue('autosearchcache', '{}'))
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]
}
}
if (current.type === 'music') {
current.searchTerm = current.data[0]
} else {
current.searchTerm = current.data.join(' ')
}
let response
if (current.searchTerm in cache) {
response = cache[current.searchTerm]
} else {
response = await asyncRequest({
method: 'POST',
url: baseURLautosearch,
data: 'search_term=' + encodeURIComponent(current.searchTerm) + '&image_size=98&search_each=1&sort_type=popular',
headers: {
Referer: current.metaurl,
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
// Host: 'www.metacritic.com',
'User-Agent': 'MetacriticUserscript Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0',
'X-Requested-With': 'XMLHttpRequest'
}
})
response = {
time: (new Date()).toJSON(),
json: JSON.parse(response.responseText)
}
cache[current.searchTerm] = response
await GM.setValue('autosearchcache', JSON.stringify(cache))
}
if (!response || !('json' in response)) {
alert('ShowMetacriticRatings: Error 05')
}
const data = response.json
let multiple = false
if (data && data.autoComplete && data.autoComplete.results && data.autoComplete.results.length) {
// Remove data with wrong type
data.autoComplete = data.autoComplete.results
const newdata = []
data.autoComplete.forEach(function (result) {
if (metacritic2searchType(result.refType) === current.type) {
newdata.push(result)
}
})
data.autoComplete = newdata
if (data.autoComplete.length === 0) {
// No results
console.debug('ShowMetacriticRatings: No results (after filtering by type) for searchTerm=' + current.searchTerm)
} else if (data.autoComplete.length === 1) {
// One result, let's show it
if (!await isBlacklistedUrl(document.location.href, absoluteMetaURL(data.autoComplete[0].url))) {
current.metaurl = absoluteMetaURL(data.autoComplete[0].url)
loadMetacriticUrl(true)
return
}
} else {
// More than one result
multiple = true
console.debug('ShowMetacriticRatings: Multiple results for searchTerm=' + current.searchTerm)
const exactMatches = []
data.autoComplete.forEach(function (result, i) { // Try to find the correct result by matching the search term to exactly one movie title
if (current.searchTerm === result.name) {
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)
if (!await isBlacklistedUrl(document.location.href, absoluteMetaURL(exactMatches[0].url))) {
current.metaurl = absoluteMetaURL(exactMatches[0].url)
loadMetacriticUrl(true)
return
}
}
}
} else {
console.debug('ShowMetacriticRatings: No results (at all) for searchTerm=' + current.searchTerm)
}
// 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 }, () => 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({
position: 'fixed',
bottom: 0,
left: 0,
minWidth: 300,
maxHeight: '80%',
maxWidth: 640,
overflow: 'auto',
backgroundColor: '#fff',
border: '2px solid #bbb',
borderRadius: ' 6px',
boxShadow: '0 0 3px 3px rgba(100, 100, 100, 0.2)',
color: '#000',
padding: ' 3px',
zIndex: '2147483601'
})
$('').appendTo(div).focus().val(query).on('keypress', function (e) {
const code = e.keyCode || e.which
if (code === 13) { // Enter key
searchBoxSearch(e, $('#mcisearchquery').val())
}
})
$('