// ==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), TV.com, Steam, Gamespot (PS4, XONE, PC), Rotten Tomatoes, Serienjunkies, BoxOfficeMojo, allmovie.com, movie.com, Wikipedia (en), themoviedb.org, letterboxd, TVmaze, TVGuide, followshows.com, TheTVDB.com, ConsequenceOfSound, Pitchfork, Last.fm, TVnfo, rateyourmusic.com // @namespace cuzi // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @grant GM.xmlHttpRequest // @grant GM.setValue // @grant GM.getValue // @require http://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt // @version 48 // @connect metacritic.com // @connect php-cuzi.herokuapp.com // @include https://*.bandcamp.com/* // @include https://itunes.apple.com/*/album/* // @include https://play.google.com/store/music/album/* // @include https://play.google.com/store/movies/details/* // @include https://music.amazon.com/* // @include http://www.amazon.com/* // @include https://www.amazon.com/* // @include http://www.amazon.co.uk/* // @include https://www.amazon.co.uk/* // @include http://www.amazon.fr/* // @include https://www.amazon.fr/* // @include http://www.amazon.de/* // @include https://www.amazon.de/* // @include http://www.amazon.es/* // @include https://www.amazon.es/* // @include http://www.amazon.ca/* // @include https://www.amazon.ca/* // @include http://www.amazon.in/* // @include https://www.amazon.in/* // @include http://www.amazon.it/* // @include https://www.amazon.it/* // @include http://www.amazon.co.jp/* // @include https://www.amazon.co.jp/* // @include http://www.amazon.com.mx/* // @include https://www.amazon.com.mx/* // @include http://www.amazon.com.au/* // @include https://www.amazon.com.au/* // @include http://www.imdb.com/title/* // @include https://www.imdb.com/title/* // @include http://store.steampowered.com/app/* // @include https://store.steampowered.com/app/* // @include http://www.gamespot.com/* // @include https://www.gamespot.com/* // @include http://www.serienjunkies.de/* // @include https://www.serienjunkies.de/* // @include http://www.tv.com/shows/* // @include http://www.rottentomatoes.com/m/* // @include https://www.rottentomatoes.com/m/* // @include http://www.rottentomatoes.com/tv/* // @include https://www.rottentomatoes.com/tv/* // @include http://www.rottentomatoes.com/tv/*/s*/ // @include https://www.rottentomatoes.com/tv/*/s*/ // @include http://www.boxofficemojo.com/movies/* // @include https://www.boxofficemojo.com/movies/* // @include http://www.allmovie.com/movie/* // @include https://www.allmovie.com/movie/* // @include https://en.wikipedia.org/* // @include http://www.movies.com/*/m* // @include https://www.themoviedb.org/movie/* // @include https://www.themoviedb.org/tv/* // @include http://letterboxd.com/film/* // @include https://letterboxd.com/film/* // @include http://www.tvmaze.com/shows/* // @include https://www.tvmaze.com/shows/* // @include http://www.tvguide.com/tvshows/* // @include https://www.tvguide.com/tvshows/* // @include http://followshows.com/show/* // @include https://followshows.com/show/* // @include http://thetvdb.com/*tab=series* // @include https://thetvdb.com/*tab=series* // @include http://www.thetvdb.com/*tab=series* // @include https://www.thetvdb.com/*tab=series* // @include https://www.thetvdb.com/series/* // @include http://consequenceofsound.net/* // @include https://consequenceofsound.net/* // @include http://pitchfork.com/* // @include https://pitchfork.com/* // @include http://www.last.fm/* // @include https://www.last.fm/* // @include http://tvnfo.com/s/* // @include https://tvnfo.com/s/* // @include http://rateyourmusic.com/release/album/* // @include https://rateyourmusic.com/release/album/* // @include https://open.spotify.com/* // @include https://play.spotify.com/album/* // @include https://www.nme.com/reviews/* // @include https://www.albumoftheyear.org/album/* // @include https://itunes.apple.com/*/movie/* // @include https://itunes.apple.com/*/album/* // @include https://itunes.apple.com/*/tv-season/* // @include http://epguides.com/* // @include http://www.epguides.com/* // @include https://sharetv.com/shows/* // @downloadURL none // ==/UserScript== var baseURL = "https://www.metacritic.com/"; var baseURL_music = "https://www.metacritic.com/music/"; var baseURL_movie = "https://www.metacritic.com/movie/"; var baseURL_pcgame = "https://www.metacritic.com/game/pc/"; var baseURL_ps4 = "https://www.metacritic.com/game/playstation-4/"; var baseURL_xone = "https://www.metacritic.com/game/xbox-one/"; var baseURL_tv = "https://www.metacritic.com/tv/"; var baseURL_search = "https://www.metacritic.com/search/{type}/{query}/results"; var baseURL_autosearch = "https://www.metacritic.com/autosearch"; var baseURL_database = "https://php-cuzi.herokuapp.com/r.php"; var baseURL_whitelist = "https://php-cuzi.herokuapp.com/whitelist.php"; var baseURL_blacklist = "https://php-cuzi.herokuapp.com/blacklist.php"; // http://www.designcouch.com/home/why/2013/05/23/dead-simple-pure-css-loading-spinner/ var CSS = "#mcdiv123 .grespinner{height:16px;width:16px;margin:0 auto;position:relative;-webkit-animation:rotation .6s infinite linear;-moz-animation:rotation .6s infinite linear;-o-animation:rotation .6s infinite linear;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%}@-webkit-keyframes rotation{from{-webkit-transform:rotate(0)}to{-webkit-transform:rotate(359deg)}}@-moz-keyframes rotation{from{-moz-transform:rotate(0)}to{-moz-transform:rotate(359deg)}}@-o-keyframes rotation{from{-o-transform:rotate(0)}to{-o-transform:rotate(359deg)}}@keyframes rotation{from{transform:rotate(0)}to{transform:rotate(359deg)}}#mcdiv123searchresults .result{font:12px arial,helvetica,serif;border-top-width:1px;border-top-color:#ccc;border-top-style:solid;padding:5px}#mcdiv123searchresults .result .result_type{display:inline}#mcdiv123searchresults .result .result_wrap{float:left;width:100%}#mcdiv123searchresults .result .has_score{padding-left:42px}#mcdiv123searchresults .result .basic_stats{height:1%;overflow:hidden}#mcdiv123searchresults .result h3{font-size:14px;font-weight:700}#mcdiv123searchresults .result a{color:#09f;font-weight:700;text-decoration:none}#mcdiv123searchresults .metascore_w.game.seventyfive,#mcdiv123searchresults .metascore_w.positive,#mcdiv123searchresults .metascore_w.score_favorable,#mcdiv123searchresults .metascore_w.score_outstanding,#mcdiv123searchresults .metascore_w.sixtyone{background-color:#6c3}#mcdiv123searchresults .metascore_w.forty,#mcdiv123searchresults .metascore_w.game.fifty,#mcdiv123searchresults .metascore_w.mixed,#mcdiv123searchresults .metascore_w.score_mixed{background-color:#fc3}#mcdiv123searchresults .metascore_w.negative,#mcdiv123searchresults .metascore_w.score_terrible,#mcdiv123searchresults .metascore_w.score_unfavorable{background-color:red}#mcdiv123searchresults a.metascore_w,#mcdiv123searchresults span.metascore_w{display:inline-block}#mcdiv123searchresults .result .metascore_w{color:#fff!important;font-family:Arial,Helvetica,sans-serif;font-size:17px;font-style:normal!important;font-weight:700!important;height:2em;line-height:2em;text-align:center;vertical-align:middle;width:2em;float:left;margin:0 0 0 -42px}#mcdiv123searchresults .result .more_stats{font-size:10px;color:#444}#mcdiv123searchresults .result .release_date .data{font-weight:700;color:#000}#mcdiv123searchresults ol,#mcdiv123searchresults ul{list-style:none}#mcdiv123searchresults .result li.stat{background:0 0;display:inline;float:left;margin:0;padding:0 6px 0 0;white-space:nowrap}#mcdiv123searchresults .result .deck{margin:3px 0 0}#mcdiv123searchresults .result .basic_stat{display:inline;float:right;overflow:hidden;width:100%}"; var myDOMParser = null; function domParser() { if(myDOMParser===null) { myDOMParser = new DOMParser() } return myDOMParser; } async function versionUpdate() { let version = parseInt(await GM.getValue("version", 0)); if(version <= 46) { // Reset database await GM.setValue("map", "{}"); await GM.setValue("black", "{}"); await GM.setValue("hovercache", "{}"); await GM.setValue("searchcache", "{}"); await GM.setValue("autosearchcache", "{}"); } if(version < 48) { await GM.setValue("version", 48); } } function getHostname(url) { with(document.createElement("a")) { href = url; return 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; } function parseLDJSON(keys, condition) { if(document.querySelector('script[type="application/ld+json"]')) { var data = []; var scripts = document.querySelectorAll('script[type="application/ld+json"]'); for(let i = 0; i < scripts.length; i++) { try { var jsonld = JSON.parse(scripts[i].innerText); } catch(e) { 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)) { let 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) { return s.normalize('NFKD').replace(/\//g,"").replace(/[\u0300-\u036F]/g, '').replace(/&/g,"and").replace(/\W+/g, " ").toLowerCase().trim().replace(/\W+/g,"-"); } function minutesSince(time) { var seconds = ((new Date()).getTime() - time.getTime()) / 1000; return seconds>60?parseInt(seconds/60)+" min ago":"now"; } function randomStringId() { var id10 = () => Math.floor((1 + Math.random()) * 0x10000000000).toString(16).substring(1); return id10()+id10()+id10()+id10()+id10()+id10(); } function fixMetacriticURLs(html) { return html.replace(/= 75) { fg = "white"; bg = "#6c3"; t = parseInt(score); } else if(score < 40) { fg = "white"; bg = "#f00"; t = parseInt(score); } else { fg = "white"; bg = "#fc3"; t = parseInt(score); } return ''+t+''; } function balloonAlert(message, timeout, title, css, click) { var header; if(title) { header = '
' + title +"
"; } else if(title === false) { header = ''; } else { header = '
Userscript alert
'; } var div = $("
" + header + '
' + message.split("\n").join("
") + "
"); div.css({ position : "fixed", top : 10, left : 10, maxWidth: 200, zIndex : "5010002", 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) { var 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.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("thetvdb.com/")) { // Do nothing with thetvdb.com urls return url; } else if(url.startsWith("boxofficemojo.com/")) { // Keep the important id= on try { var parts = url.split("?"); var page = url[0] + "?"; var idparam = url[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) { var data = JSON.parse(await GM.getValue("map","{}")); var url = filterUniversalUrl(url); var metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//,""); data[url] = metaurl; await GM.setValue("map", JSON.stringify(data)); (new Image()).src = baseURL_whitelist + "?docurl="+encodeURIComponent(url)+"&metaurl="+encodeURIComponent(metaurl)+"&ref="+encodeURIComponent(randomStringId()); return [url, metaurl]; } async function addToBlacklist(url, metaurl) { var data = JSON.parse(await GM.getValue("black","[]")); var url = filterUniversalUrl(url); var metaurl = metaurl.replace(/^https?:\/\/(www.)?metacritic\.com\//,""); data.push([url,metaurl]); await GM.setValue("black", JSON.stringify(data)); (new Image()).src = baseURL_blacklist + "?docurl="+encodeURIComponent(url)+"&metaurl="+encodeURIComponent(metaurl)+"&ref="+encodeURIComponent(randomStringId()); return [url, metaurl]; } 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 var data = JSON.parse(await GM.getValue("black","[]")); // [ [docurl0, metaurl0] , [docurl1, metaurl1] , ... ] for(var i = 0; i < data.length; i++) { if(data[i][0] == docurl && data[i][1] == metaurl) { return true; } } return false; } async function isBlacklisted(metaurl) { return await isBlacklistedUrl("" + document.location.host.replace(/^www\./,"") + document.location.pathname + document.location.search, metaurl); } function listenForHotkeys(code, cb) { // Call cb() as soon as the code sequence was typed var 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(); } } } }); } async function metacritic_hoverInfo(url, docurl, cb, errorcb, nocache) { // Get the metacritic hover info. Requests are cached. var handleresponse = function(response, cached) { if(response.status == 200 && response.responseText && cb) { if(~response.responseText.indexOf('"jsonRedirect"')) { // {"viewer":{},"mixpanelToken":"6e219fd....","mixpanelDistinctId":"255.255.255.255","omnitureDebug":0,"jsonRedirect":"\/movie\/national-lampoons-vacation"} var blacklistedredirect = false; var j = JSON.parse(response.responseText); current.url = absoluteMetaURL(j["jsonRedirect"]); delete cache[url]; // Delete original url from cache. The redirect URL will then be saved in metacritic_hoverInfo(...) // Blacklist items from database received? if("blacklist" in j && j.blacklist && j.blacklist.length) { // Save new blacklist items GM.getValue("black","[]").then(function(json_data) { var data = JSON.parse(json_data); for(var i = 0; i < j.blacklist.length; i++) { var save_docurl = j.blacklist[i].docurl; var save_metaurl = j.blacklist[i].metaurl; data.push([save_docurl,save_metaurl]); if(j["jsonRedirect"] == "/"+save_metaurl) { // Redirect is blacklisted! blacklistedredirect = true; } } GM.setValue("black", JSON.stringify(data)); }); } if(blacklistedredirect && errorcb) { // Redirect was blacklisted, show nothing errorcb(response.responseText, new Date(response.time)); } else { // Load redirect metacritic_hoverInfo(absoluteMetaURL(j["jsonRedirect"]), false, cb, errorcb, true); } } else { // Show cb(response.responseText, new Date(response.time)); } } else if(response.status != 200 && errorcb) { errorcb(response.responseText, new Date(response.time)); if(!cached) console.log("Show metacritic ratings: Error 01:"+response.status+"\n"+url); } else if(!response.responseText) { errorcb("", new Date(response.time)); if(!cached) console.log("Show metacritic ratings: Error 02: response empty. Status:"+response.status+"\n"+url); } }; var cache = JSON.parse(await GM.getValue("hovercache","{}")); for(var prop in cache) { // Delete cached values, that are older than 2 hours if((new Date()).getTime() - (new Date(cache[prop].time)).getTime() > 2*60*60*1000) { delete cache[prop]; } } if(!nocache && url in cache) { handleresponse(cache[url], true); } else { var requestURL = url; var requestParams = "hoverinfo=1"; if(docurl && docurl.indexOf("metacritic.com") == -1 && docurl.indexOf(baseURL_database) == -1) { // Ask database for correct metacritic entry: requestURL = baseURL_database; requestParams = "m=" + encodeURIComponent(docurl) + "&a=" + encodeURIComponent(url); } GM.xmlHttpRequest({ method: "POST", url: requestURL, data: requestParams, headers: { "Referer" : url, "Content-Type" : "application/x-www-form-urlencoded; charset=UTF-8", "Host" : getHostname(requestURL), // This is important, otherwise Metacritic refuses to answer! //"User-Agent" : "MetacriticUserscript "+navigator.userAgent, "User-Agent" : navigator.userAgent, "X-Requested-With" : "XMLHttpRequest" }, onload: async function(response) { response.time = (new Date()).toJSON(); if(!response.responseText) { // Temporary fix: // Hover info seems to be available only for movies. requestURL = url; if(requestURL.indexOf('/critic-reviews') !== -1) { requestURL = url.split('/critic-reviews')[0] } GM.xmlHttpRequest({ method: "GET", url: requestURL, headers: { "Referer" : url, "Host" : getHostname(requestURL), // This is important, otherwise Metacritic refuses to answer! //"User-Agent" : "MetacriticUserscript "+navigator.userAgent, "User-Agent" : navigator.userAgent }, onload: async function(response) { response.time = (new Date()).toJSON(); let html; try { // Try parsing HTML let doc = domParser().parseFromString(response.responseText, 'text/html'); doc.querySelector(".product_page_title h1") doc.querySelector(".summary_img") doc.querySelectorAll(".details_section") doc.querySelectorAll("#nav_to_metascore .distribution") let page_url = img_src = img_alt = title = publisher = release_date = starring = critics_score = critics_class = critics_number = critics_charts = user_score = user_class = user_number = user_charts = '' page_url = requestURL + (requestURL.endsWith("/") ? "" : "/") img_src = doc.querySelector(".summary_img").src img_alt = doc.querySelector(".summary_img").alt title = doc.querySelector(".product_page_title h1").textContent if(doc.querySelector(".details_section .distributor a")) publisher = doc.querySelector(".details_section .distributor a").textContent if(doc.querySelector(".details_section .release_date span:nth-child(2)")) { let date = doc.querySelector(".details_section .release_date span:nth-child(2)").textContent release_date = `
Release Date: ${date}
` } if(doc.querySelector(".details_section.summary_cast span:nth-child(2)")) { let stars = doc.querySelector(".details_section.summary_cast span:nth-child(2)").innerHTML starring = `
Starring: ${stars}
` } critics_class = "metascore_w medium tbd" critics_score = "tbd" user_class = "metascore_w medium user tbd" user_score = "tbd" if(doc.querySelector(".score_details .based_on")) { critics_number = doc.querySelector(".score_details .based_on").textContent.match(/\d+/) } else { critics_number = "By" } if(doc.querySelector(".user_score_summary .based_on")) { user_number = doc.querySelector(".user_score_summary .based_on").textContent.match(/\d+/) } else { user_number = "User" } // Remove text from distribution charts: let label = doc.querySelector("#nav_to_metascore .charts .label.fl") while(label) { label.parentNode.title = label.textContent.trim() + " " + label.parentNode.querySelector(".count").textContent.trim() label.remove() label = doc.querySelector("#nav_to_metascore .charts .label.fl") } let scores = doc.querySelectorAll("#nav_to_metascore .distribution .metascore_w") if(scores.length == 2) { critics_score = scores[0].innerText critics_class = scores[0].className.replace("larger", "medium") scores[0].parentNode.parentNode.querySelector(".charts").style.width = '40px' critics_charts = '' + scores[0].parentNode.parentNode.querySelector(".charts").outerHTML + "" user_score = scores[1].innerText user_class = scores[1].className.replace("larger", "medium") scores[1].parentNode.parentNode.querySelector(".charts").style.width = '40px' user_charts = '' + scores[1].parentNode.parentNode.querySelector(".charts").outerHTML + "" } else if(scores.length == 1) { if(scores[0].className.indexOf("user") === -1) { critics_score = scores[0].innerText critics_class = scores[0].className.replace("larger", "medium") scores[0].parentNode.parentNode.querySelector(".charts").style.width = '40px' critics_charts = '' + scores[0].parentNode.parentNode.querySelector(".charts").outerHTML + "" } else { user_score = scores[0].innerText user_class = scores[0].className.replace("larger", "medium") scores[0].parentNode.parentNode.querySelector(".charts").style.width = '40px' user_charts = '' + scores[0].parentNode.parentNode.querySelector(".charts").outerHTML + "" } } html = `

${title}

${publisher}  |  
${release_date}
${starring}
 
${critics_charts} ${user_charts}
${critics_score}
Metascore
${user_score}
User Score
` } catch(e) { console.log("Show metacritic ratings: Error parsing HTML: "+e); // fallback to cutting out the relevant parts let parts = response.responseText.split('class="score_details') let text_part = '
') + '
' parts = response.responseText.split('id="nav_to_metascore"') let meta_score_part = '
' meta_score_part = meta_score_part.split('href="">').join('href="' + requestURL + '">') meta_score_part = meta_score_part.split('section_title bold">').join('section_title bold">' + title_text) html = meta_score_part.split('
').join(text_part + '
') 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 overly big content console.log("Show metacritic ratings: Cutting response to 5000 chars") html = html.substr(0, 5000) } } // Chrome fix: Otherwise JSON.stringify(cache) omits responseText var newobj = {}; for(var key in response) { newobj[key] = response[key]; } newobj.responseText = html; //alert(requestURL+'\n'+requestParams+'\nResult:\n'+response.responseText) cache[url] = newobj; await GM.setValue("hovercache",JSON.stringify(cache)); handleresponse(newobj, false); }, onerror: function(response) { console.log("Show metacritic ratings: Hover info error 03: "+response.status+"\nURL: "+requestURL+"\nResponse:\n"+response.responseText); }, }); } else { // Chrome fix: Otherwise JSON.stringify(cache) omits responseText var newobj = {}; for(var key in response) { newobj[key] = response[key]; } newobj.responseText = response.responseText; cache[url] = newobj; await GM.setValue("hovercache",JSON.stringify(cache)); handleresponse(response, false); } }, onerror: function(response) { console.log("Show metacritic ratings: Hover info error 03: "+response.status+"\nURL: "+requestURL+"\nResponse:\n"+response.responseText); }, }); } } async function metacritic_searchResults(url, cb, errorcb) { // Get metacritic search results. Requests are cached. var handleresponse = function(response, cached) { if(response.results.length && cb) { cb(response.results, new Date(response.time)); } else if(response.results.length == 0 && errorcb) { errorcb(response.results, new Date(response.time)); } }; var cache = JSON.parse(await GM.getValue("searchcache","{}")); for(var prop in cache) { // Delete cached values, that are older than 2 hours if((new Date()).getTime() - (new Date(cache[prop].time)).getTime() > 2*60*60*1000) { delete cache[prop]; } } if(url in cache) { handleresponse(cache[url], true); } else { GM.xmlHttpRequest({ method: "GET", url: url, headers: { "Referer" : url, "Content-Type" : "application/x-www-form-urlencoded; charset=UTF-8", "Host" : "www.metacritic.com", "User-Agent" : "MetacriticUserscript "+navigator.userAgent, }, onload: async function(response) { var results = []; if(!~response.responseText.indexOf("No search results found.")) { var d = $('').html(response.responseText); d.find("ul.search_results.module .result").each(function() { results.push(this.innerHTML); }); } response = { time : (new Date()).toJSON(), results : results, }; cache[url] = response; await GM.setValue("searchcache",JSON.stringify(cache)); handleresponse(response, false); }, onerror: function(response) { console.log("Show metacritic ratings: Search error 04: "+response.status+"\n"+url); handleresponse({ time : (new Date()).toJSON(), results : [], }, false); } }); } } function metacritic_showHoverInfo(url, docurl) { if(!url) { return; } metacritic_hoverInfo(url, docurl?docurl:false, // On Success function(html, time) { $("#mcdiv123").remove(); var div = $('
').appendTo(document.body); div.css({ position:"fixed", bottom :0, left: 0, minWidth: 300, backgroundColor: "#fff", border: "2px solid #bbb", borderRadius:" 6px", boxShadow: "0 0 3px 3px rgba(100, 100, 100, 0.2)", color: "#000", padding:" 3px", zIndex: "5010001", }); // Functions for communication between page and iframe // Mozilla can access parent.document // Chrome can use postMessage() var frame_status = false; // if this remains false, loading the frame content failed. A reason could be "Content Security Policy" function loadExternalImage(url, myframe) { // Load external image, bypass CSP GM.xmlHttpRequest({ method: "GET", url: url, responseType : "arraybuffer", onload: function(response) { myframe.contentWindow.postMessage({ "mcimessage_imgLoaded" : true, "mcimessage_imgData" : response.response, "mcimessage_imgOrgSrc" : url },'*'); } }); } var functions = { "parent" : function() { var f = parent.document.getElementById('mciframe123'); var lastdiff = -200000; window.addEventListener("message", function(e){ if(typeof e.data != "object") { return; } else if("mcimessage0" in e.data) { frame_status = true; // Frame content was loaded successfully } else if("mcimessage1" in e.data) { f.style.width = parseInt(f.style.width)+10+"px"; if(e.data.heightdiff == lastdiff) { f.style.height = parseInt(f.style.height)+5+"px"; } lastdiff = e.data.heightdiff; } else if("mcimessage2" in e.data) { f.style.height = parseInt(f.style.height)+15+"px"; f.style.width = "400px"; } else if("mcimessage_loadImg" in e.data) { loadExternalImage(e.data.mcimessage_imgUrl, f); } else { return; } f.contentWindow.postMessage({ "mcimessage3" : true, "mciframe123_clientHeight" : f.clientHeight, "mciframe123_clientWidth" : f.clientWidth, },'*'); }); }, "frame" : function() { parent.postMessage({"mcimessage0":true},'*'); // Loading frame content was successfull var i = 0; window.addEventListener("message", function(e){ if(typeof e.data == "object" && "mcimessage_imgLoaded" in e.data) { // Load external image var arrayBufferView = new Uint8Array( e.data["mcimessage_imgData"] ); var blob = new Blob( [ arrayBufferView ], { type: "image/jpeg" } ); var urlCreator = window.URL || window.webkitURL; var imageUrl = urlCreator.createObjectURL( blob ); var img = failedImages[e.data["mcimessage_imgOrgSrc"]]; img.src = imageUrl; } if(!("mcimessage3" in e.data)) return; if(e.data.mciframe123_clientHeight < document.body.scrollHeight && i < 100) { parent.postMessage({"mcimessage1":1, "heightdiff":document.body.scrollHeight - e.data.mciframe123_clientHeight},'*'); i++; } if(i >= 100) { parent.postMessage({"mcimessage2":1},'*') i = 0; } }); parent.postMessage({"mcimessage1":1,"heightdiff":-100000},'*'); } }; var css = "#hover_div .clr { clear: both} #hover_div .fl{float: left} #hover_div { background-color: #fff; color: #666; font-family:Arial,Helvetica,sans-serif; font-size:12px; font-weight:400; font-style:normal;} #hover_div .hoverinfo .hover_left { float: left} #hover_div .hoverinfo .product_image_wrapper { color: #999; font-size: 6px; font-weight: normal; min-height: 98px; min-width: 98px;} #hover_div .hoverinfo .product_image_wrapper a { color: #999; font-size: 6px; font-weight: normal;} #hover_div a * { cursor: pointer} #hover_div a { color: #09f; font-weight: bold;} #hover_div a:link, #hover_div a:visited { text-decoration: none;} #hover_div a:hover { text-decoration: underline;} #hover_div .hoverinfo .hover_right { float: left; margin-left: 15px; max-width: 395px;} #hover_div .hoverinfo .product_title { color: #333; font-family: georgia,serif; font-size: 24px; line-height: 26px; margin-bottom: 10px;} #hover_div .hoverinfo .product_title a { color:#333; font-family: georgia,serif; font-size: 24px;} #hover_div .hoverinfo .summary_detail.publisher, .hoverinfo .summary_detail.release_data { float: left} #hover_div .hoverinfo .summary_detail { font-size: 11px; margin-bottom: 10px;} #hover_div .hoverinfo .summary_detail.product_credits a { color: #999; font-weight: normal; } #hover_div .hoverinfo .hr { background-color: #ccc; height: 2px; margin: 15px 0 10px;} #hover_div .hoverinfo .hover_scores { width: 100%; border-collapse: collapse; border-spacing: 0;} #hover_div .hoverinfo .hover_scores td.num { width: 39px} #hover_div .hoverinfo .hover_scores td { vertical-align: middle} #hover_div caption, #hover_div th, #hover_div td { font-weight: normal; text-align: left;} #hover_div .metascore_anchor, #hover_div a.metascore_w { text-decoration: none !important} #hover_div span.metascore_w, #hover_div a.metascore_w { display: inline-block; padding:0px;}.metascore_w { background-color: transparent; color: #fff !important; font-family: Arial,Helvetica,sans-serif; font-size: 17px; font-style: normal !important; font-weight: bold !important; height: 2em; line-height: 2em; text-align: center; vertical-align: middle; width: 2em;} #hover_div .metascore, #hover_div .metascore a, #hover_div .avguserscore, #hover_div .avguserscore a { color: #fff} #hover_div .critscore, #hover_div .critscore a, #hover_div .userscore, #hover_div .userscore a { color: #333}.score_tbd { background: #eaeaea; color: #333; font-size: 14px;} #hover_div .score_tbd a { color: #333}.negative, .score_terrible, .score_unfavorable, .carousel_set a.product_terrible:hover, .carousel_set a.product_unfavorable:hover { background-color: #f00}.mixed, .neutral, .score_mixed, .carousel_set a.product_mixed:hover { background-color: #fc3; color: #333;} #hover_div .score_mixed a { color: #333}.positive, .score_favorable, .score_outstanding, .carousel_set a.product_favorable:hover, .carousel_set a.product_outstanding:hover { background-color: #6c3}.critscore_terrible, .critscore_unfavorable { border-color: #f00}.critscore_mixed { border-color: #fc3}.critscore_favorable, .critscore_outstanding { border-color: #6c3}.metascore .score_total, .userscore .score_total { display: none; visibility: hidden;}.hoverinfo .metascore_label, .hoverinfo .userscore_label { font-size: 12px; font-weight: bold; line-height: 16px; margin-top: 2%;}.hoverinfo .metascore_review_count, .hoverinfo .userscore_review_count { font-size: 11px}.hoverinfo .hover_scores td { vertical-align: middle}.hoverinfo .hover_scores td.num { width: 39px}.hoverinfo .hover_scores td.usr.num { padding-left: 20px}.metascore_anchor, a.metascore_w { text-decoration: none !important} .metascore_w.album { padding-top:0px; !important} .metascore_w.user { border-radius: 55%; color: #fff;}.metascore_anchor, .metascore_w.album { padding: 0px;!important, padding-top: 0px;!important} a.metascore_w { text-decoration: none!important}.metascore_anchor:hover { text-decoration: none!important}.metascore_w:hover { text-decoration: none!important}span.metascore_w, a.metascore_w { display: inline-block}.metascore_w.xlarge, .metascore_w.xl { font-size: 42px}.metascore_w.large, .metascore_w.lrg { font-size: 25px}.m .metascore_w.medium, .m .metascore_w.med { font-size: 19px}.metascore_w.med_small { font-size: 14px}.metascore_w.small, .metascore_w.sm { font-size: 12px}.metascore_w.tiny { height: 1.9em; font-size: 11px; line-height: 1.9em;}.metascore_w.user { border-radius: 55%; color: #fff;}.metascore_w.user.small, .metascore_w.user.sm { font-size: 11px}.metascore_w.tbd, .metascore_w.score_tbd { color: #000!important; background-color: #ccc;}.metascore_w.tbd.hide_tbd, .metascore_w.score_tbd.hide_tbd { visibility: hidden}.metascore_w.tbd.no_tbd, .metascore_w.score_tbd.no_tbd { display: none}.metascore_w.noscore::before, .metascore_w.score_noscore::before { content: '\2022\2022\2022'}.metascore_w.noscore, .metascore_w.score_noscore { color: #fff!important; background-color: #ccc;}.metascore_w.rip, .metascore_w.score_rip { border-radius: 4px; color: #fff!important; background-color: #999;}.metascore_w.negative, .metascore_w.score_terrible, .metascore_w.score_unfavorable { background-color: #f00}.metascore_w.mixed, .metascore_w.forty, .metascore_w.game.fifty, .metascore_w.score_mixed { background-color: #fc3}.metascore_w.positive, .metascore_w.sixtyone, .metascore_w.game.seventyfive, .metascore_w.score_favorable, .metascore_w.score_outstanding { background-color: #6c3}.metascore_w.indiv { height: 1.9em; width: 1.9em; font-size: 15px; line-height: 1.9em;}.metascore_w.indiv.large, .metascore_w.indiv.lrg { font-size: 24px}.m .metascore_w.indiv.medium, .m .metascore_w.indiv.med { font-size: 16px}.metascore_w.indiv.small, .metascore_w.indiv.sm { font-size: 11px}.metascore_w.indiv.perfect { padding-right: 1px}.promo_amazon .esite_btn { margin: 3px 0 0 7px;}.esite_amazon { background-color: #fdc354; border: 1px solid #aaa;}.esite_label_wrapper { display:none;}.esite_btn { border-radius: 4px; color: #222; font-size: 12px; height: 40px; line-height: 40px; width: 120px;} .chart{background-color:inherit!important;margin-top:-3px} .chart_bg{width:100%;border-top:3px solid rgba(150,150,150,0.3)} .chart .bar{width:100%;height:3px} .chart .count{font-size:10px}"; var framesrc = 'data:text/html,'; framesrc += encodeURIComponent('\ \ \ \ Metacritic info\ \ \ \ \
\
'+fixMetacriticURLs(html)+'
\
\ \ '); var frame = $("").appendTo(div); frame.attr("id","mciframe123"); frame.attr("src",framesrc); frame.attr("scrolling","auto"); frame.css({ width: 400, height: 170, border: "none" }); window.setTimeout(function() { if(!frame_status) { // Loading frame content failed. // Directly inject the html without an iframe (this may break the site or the metacritic) console.log("Loading iframe content failed. Injecting directly."); $("head").append(""); var noframe = $('
\
'+fixMetacriticURLs(html)+'
\
'); frame.replaceWith(noframe); } },2000); functions.parent(); var sub = $("
").appendTo(div); $('').appendTo(sub); $(''+decodeURI(url.replace("https://www.","@"))+'').appendTo(sub); $('').appendTo(sub).click(function() { document.body.removeChild(this.parentNode.parentNode); }); $('').data("url", url).appendTo(sub).click(function() { var docurl = document.location.href; var 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", url).appendTo(sub).click(function() { if(!confirm("This is NOT the correct entry!\n\nAdd to blacklist?")) return; var docurl = document.location.href; var 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"); }); // Open search metacritic_searchcontainer(null, current.searchTerm); metacritic_search(null, current.searchTerm); }); }, // On error i.e. no result on metacritic.com async function(html, time) { // Make search available metacritic_waitForHotkeys(); var handleresponse = await async function(response, fromcache) { var data; var multiple = false; try { data = JSON.parse(response.responseText); } catch(e) { console.log("Error in JSON 05: search_term="+current.searchTerm); console.log(e); } if(data && data.autoComplete && data.autoComplete.results && data.autoComplete.results.length) { // Remove data with wrong type data.autoComplete = data.autoComplete.results; var 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.log("No results (after filtering by type) for search_term="+current.searchTerm); } else if(data.autoComplete.length == 1) { // One result, let's show it if(! await isBlacklisted(absoluteMetaURL(data.autoComplete[0].url))) { metacritic_showHoverInfo(absoluteMetaURL(data.autoComplete[0].url)); return; } } else { // More than one result multiple = true; console.log("Multiple results for search_term="+current.searchTerm); var 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.log("Only one exact match for search_term="+current.searchTerm); if(! await isBlacklisted(absoluteMetaURL(exactMatches[0].url))) { if( url != absoluteMetaURL(exactMatches[0].url)) { metacritic_showHoverInfo(absoluteMetaURL(exactMatches[0].url)); } else { console.log("Loop detected for: "+url); } return; } } } } else { console.log("No results (at all) for search_term="+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}, metacritic_searchcontainer); } }; var cache = JSON.parse(await GM.getValue("autosearchcache","{}")); for(var prop in cache) { // Delete cached values, that are older than 2 hours if((new Date()).getTime() - (new Date(cache[prop].time)).getTime() > 2*60*60*1000) { delete cache[prop]; } } if(current.type == "music") { current.searchTerm = current.data[0]; } else { current.searchTerm = current.data.join(" "); } if(current.searchTerm in cache) { handleresponse(cache[current.searchTerm], true); } else { GM.xmlHttpRequest({ method: "POST", url: baseURL_autosearch, data: "search_term="+encodeURIComponent(current.searchTerm)+"&image_size=98&search_each=1&sort_type=popular", headers: { "Referer" : url, "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" }, onload: async function(response) { response = { time : (new Date()).toJSON(), responseText : response.responseText, }; cache[current.searchTerm] = response; await GM.setValue("autosearchcache",JSON.stringify(cache)); handleresponse(response, false); } }); } }); } function metacritic_waitForHotkeys() { listenForHotkeys("meta",metacritic_searchcontainer); } function metacritic_searchcontainer(ev, query) { if(!query) { if(current.type == "music") { query = current.data[0]; } else { query = current.data.join(" "); } } $("#mcdiv123").remove(); var 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: "5010001", }); var query = $('').appendTo(div).focus().val(query).on('keypress', function(e) { var code = e.keyCode || e.which; if(code == 13) { // Enter key metacritic_search.call(this,e); } }); $('