// ==UserScript==
// @name Show Rottentomatoes meter
// @description Show Rotten Tomatoes score on imdb.com, metacritic.com, letterboxd.com, BoxOfficeMojo, serienjunkies.de, Amazon, tv.com, Google Play, allmovie.com, Wikipedia, themoviedb.org, movies.com, tvmaze.com, tvguide.com, followshows.com, thetvdb.com, tvnfo.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.3.1/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 6
// @connect www.rottentomatoes.com
// @include https://play.google.com/store/movies/details/*
// @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://www.serienjunkies.de/*
// @include https://www.serienjunkies.de/*
// @include http://www.tv.com/shows/*
// @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 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://tvnfo.com/s/*
// @include https://tvnfo.com/s/*
// @include http://www.metacritic.com/movie/*
// @include https://www.metacritic.com/movie/*
// @include http://www.metacritic.com/tv/*
// @include https://www.metacritic.com/tv/*
// @downloadURL none
// ==/UserScript==
var baseURL = "https://www.rottentomatoes.com"
var baseURL_search = baseURL + "/api/private/v2.0/search/?limit=20&q={query}";
var baseURL_openTab = baseURL + "/search/?search={query}";
const cacheExpireAfterHours = 4;
const emoji_tomato = 0x1F345;
const emoji_green_apple = 0x1F34F;
const emoji_strawberry = 0x1F353;
function minutesSince(time) {
let seconds = ((new Date()).getTime() - time.getTime()) / 1000;
return seconds>60?parseInt(seconds/60)+" min ago":"now";
}
function parseLDJSON(condition, keys) {
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] && 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 {
return data[i][keys];
}
}
} catch(e) {
continue;
}
}
}
return null;
}
function meterBar(data) {
// Create the "progress" bar with the meter score
let barColor = "grey";
let bgColor = "#ECE4B5";
let color = "black";
let width = 0;
let textInside = "";
let textAfter = "";
if (data.meterClass == "certified_fresh") {
barColor = "#C91B22";
color = "yellow";
textInside = String.fromCodePoint(emoji_strawberry) + " " + data.meterScore + "%"
width = data.meterScore;
} else if (data.meterClass == "fresh") {
barColor = "#C91B22";
color = "white";
textInside = String.fromCodePoint(emoji_tomato) + " " + data.meterScore + "%"
width = data.meterScore;
} else if(data.meterClass == "rotten") {
color = "gray";
barColor = "#94B13C";
if(data.meterScore > 30) {
textAfter = data.meterScore + "% ";
textInside = '' + String.fromCodePoint(emoji_green_apple) + " ";
} else {
textAfter = data.meterScore + '% ' + String.fromCodePoint(emoji_green_apple) + " ";
}
width = data.meterScore;
} else {
bgColor = barColor = "#787878";
color = "silver";
textInside = "N/A";
width = 100
}
return '
' +
'
' + textInside + '
' + textAfter +'
';
}
var current = {
type : null,
query : null,
year : null
};
async function loadMeter(query, type, year) {
// Load data from rotten tomatoes search API or from cache
current.type = type;
current.query = query;
current.year = year;
let url = baseURL_search.replace("{query}", encodeURIComponent(query));
let cache = JSON.parse(await GM.getValue("cache","{}"));
// Delete cached values, that are expired
for(var prop in cache) {
if((new Date()).getTime() - (new Date(cache[prop].time)).getTime() > cacheExpireAfterHours*60*60*1000) {
delete cache[prop];
}
}
// Check cache or request new content
if(url in cache) {
// Use cached response
handleResponse(cache[url]);
} else {
GM.xmlHttpRequest({
method: "GET",
url: url,
onload: function(response) {
// Save to chache
response.time = (new Date()).toJSON();
cache[url] = response;
GM.setValue("cache",JSON.stringify(cache));
handleResponse(response);
},
onerror: function(response) {
console.log("GM.xmlHttpRequest Error: "+response.status+"\nURL: "+requestURL+"\nResponse:\n"+response.responseText);
},
});
}
}
function handleResponse(response) {
// Handle GM.xmlHttpRequest response
let data = JSON.parse(response.responseText);
// Adapt type name from original metacritic type to rotten tomatoes type
let prop;
if(current.type == "movie") {
prop = "movies";
} else {
prop = "tvSeries";
// Align series info with movie info
for(let i = 0; i < data[prop].length; i++) {
data[prop][i]["name"] = data[prop][i]["title"];
data[prop][i]["year"] = data[prop][i]["startYear"];
}
}
if(data[prop]) {
// Sort results by closest match
function matchQuality(title, year) {
if(title == current.query && year == current.year) {
return 102 + year;
}
if(title == current.query && current.year) {
return 101 - Math.abs(year - current.year);
}
if(title.replace(/\(.+\)/, "").trim() == current.query && current.year) {
return 100 - Math.abs(year - current.year);
}
if(title == current.query) {
return 7;
}
if(title.replace(/\(.+\)/, "").trim() == current.query) {
return 6;
}
if(title.startsWith(current.query)) {
return 5;
}
if(current.query.indexOf(title) != -1) {
return 4;
}
if(title.indexOf(current.query) != -1) {
return 3;
}
if(current.query.toLowerCase().indexOf(title.toLowerCase()) != -1) {
return 2;
}
if(title.toLowerCase().indexOf(current.query.toLowerCase()) != -1) {
return 1;
}
return 0;
}
data[prop].sort(function(a,b) {
a.matchQuality = matchQuality(a.name, a.year);
b.matchQuality = matchQuality(b.name, b.year);
return b.matchQuality - a.matchQuality;
});
showMeter(data[prop], new Date(response.time));
} else {
console.log("No results for "+current.query);
}
}
function showMeter(arr, time) {
// Show a small box in the right lower corner
$("#mcdiv321rotten").remove();
let main,div;
div = main = $('
').appendTo(document.body);
div.css({
position:"fixed",
bottom :0,
right: 0,
minWidth: 100,
maxHeight: "95%",
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",
fontFamily : "Helvetica,Arial,sans-serif"
});
// First result
$('').appendTo(main);
// Shall the following results be collapsed by default?
if((arr.length > 1 && arr[0].matchQuality > 10) || arr.length > 10) {
let a = $('More results... ').appendTo(main).click(function() { more.css("display", "block"); this.parentNode.removeChild(this); });
let more = div = $("
").appendTo(main);
}
// More results
for(let i = 1; i < arr.length; i++) {
$('').appendTo(div);
}
// Footer
let sub = $("
").appendTo(main);
$(''+minutesSince(time)+' ').appendTo(sub);
$('@rottentomatoes.com ').appendTo(sub);
$('❎ ').appendTo(sub).click(function() {
document.body.removeChild(this.parentNode.parentNode);
});
}
var Always = () => true;
var sites = {
'googleplay' : {
host : ["play.google.com"],
condition : Always,
products : [
{
condition : () => ~document.location.href.indexOf("/movies/details/"),
type : "movie",
data : () => document.querySelector("*[itemprop=name]").textContent
}
]
},
'imdb' : {
host : ["imdb.com"],
condition : Always,
products : [
{
condition : function() {
let e = document.querySelector("meta[property='og:type']");
if(e) {
return e.content == "video.movie"
}
return false;
},
type : "movie",
data : function() {
var year = null;
if(document.querySelector("#titleYear")) {
year = parseInt(document.querySelector("#titleYear a").firstChild.textContent);
}
if(document.querySelector('script[type="application/ld+json"]')) {
var jsonld = JSON.parse(document.querySelector('script[type="application/ld+json"]').innerText);
try {
if(year == null) {
year = parseInt(jsonld["datePublished"].match(/\d{4}/)[0]);
}
} catch(e) {}
return [jsonld["name"], year]
} else if(document.querySelector("h1[itemprop=name]")) { // Movie homepage (New design 2015-12)
return [document.querySelector("h1[itemprop=name]").firstChild.textContent.trim(), year];
} else if(document.querySelector("*[itemprop=name] a") && document.querySelector("*[itemprop=name] a").firstChild.data) { // Subpage of a move
return [document.querySelector("*[itemprop=name] a").firstChild.data.trim(), year];
} else if(document.querySelector(".title-extra[itemprop=name]")) { // Movie homepage: sub-/alternative-/original title
return [document.querySelector(".title-extra[itemprop=name]").firstChild.textContent.replace(/\"/g,"").trim(), year];
} else { // Movie homepage (old design)
return document.querySelector("*[itemprop=name]").firstChild.textContent.trim();
}
}
},
{
condition : function() {
var e = document.querySelector("meta[property='og:type']");
if(e) {
return e.content == "video.tv_show"
}
return false;
},
type : "tv",
data : function() {
var year = null;
if(document.querySelector("*[itemprop=name]")) {
var m = document.title.match(/\s(\d{4})(\S\d{4}?)?/);
if(m) {
year = parseInt(m[1]);
}
return [document.querySelector("*[itemprop=name]").textContent ,year]
} else {
var jsonld = JSON.parse(document.querySelector('script[type="application/ld+json"]').innerText);
try {
year = parseInt(jsonld["datePublished"].match(/\d{4}/)[0]);
} catch(e) {}
return [jsonld["name"], year]
}
}
}
]
},
'tv.com' : {
host : ["www.tv.com"],
condition : () => document.querySelector("meta[property='og:type']"),
products : [{
condition : () => document.querySelector("meta[property='og:type']").content == "tv_show" && document.querySelector("h1[data-name]"),
type : "tv",
data : () => document.querySelector("h1[data-name]").dataset.name
}]
},
'metacritic' : {
host : ["www.metacritic.com"],
condition : () => document.querySelector("meta[property='og:type']"),
products : [{
condition : () => document.querySelector("meta[property='og:type']").content == "video.movie",
type : "movie",
data : function() {
var year = null;
if(document.querySelector(".release_year")) {
year = parseInt(document.querySelector(".release_year").firstChild.textContent);
} else if(document.querySelector(".release_data .data")) {
year = document.querySelector(".release_data .data").textContent.match(/(\d{4})/)[1]
}
return [document.querySelector("meta[property='og:title']").content, year]
}
},
{
condition : () => document.querySelector("meta[property='og:type']").content == "video.tv_show",
type : "tv",
data : function() {
var title = document.querySelector("meta[property='og:title']").content
var year = null;
if(title.match(/\s\(\d{4}\)$/)) {
year = parseInt(title.match(/\s\((\d{4})\)$/)[1]);
title = title.replace(/\s\(\d{4}\)$/,""); // Remove year
} else if(document.querySelector(".release_data .data")) {
year = document.querySelector(".release_data .data").textContent.match(/(\d{4})/)[1]
}
return [title, year];
}
}
]
},
'serienjunkies' : {
host : ["www.serienjunkies.de"],
condition : Always,
products : [{
condition : () => Always,
type : "tv",
data : () => parseLDJSON((j) => (j["@type"] == "TVSeries"), "name")
}]
},
'amazon' : {
host : ["amazon."],
condition : Always,
products : [
{
condition : () => (document.getElementById("aiv-content-title") && document.getElementsByClassName("season-single-dark").length),
type : "tv",
data : () => document.getElementById("aiv-content-title").firstChild.data.trim()
},
{
condition : () => document.getElementById("aiv-content-title"),
type : "movie",
data : () => document.getElementById("aiv-content-title").firstChild.data.trim()
}
]
},
'BoxOfficeMojo' : {
host : ["boxofficemojo.com"],
condition : () => ~document.location.search.indexOf("id="),
products : [{
condition : () => document.querySelector("#body table:nth-child(2) tr:first-child b"),
type : "movie",
data : function() {
var year = null;
try {
var tds = document.querySelectorAll("#body table:nth-child(2) tr:first-child table table table td");
for(var i = 0; i< tds.length; i++) {
if(~tds[i].innerText.indexOf("Release Date")) {
year = parseInt(tds[i].innerText.match(/\d{4}/)[0]);
break;
}
}
} catch(e) { }
return [document.querySelector("#body table:nth-child(2) tr:first-child b").firstChild.data, year];
}
}]
},
'AllMovie' : {
host : ["allmovie.com"],
condition : () => document.querySelector("h2[itemprop=name].movie-title"),
products : [{
condition : () => document.querySelector("h2[itemprop=name].movie-title"),
type : "movie",
data : () => document.querySelector("h2[itemprop=name].movie-title").firstChild.data.trim()
}]
},
'en.wikipedia' : {
host : ["en.wikipedia.org"],
condition : Always,
products : [{
condition : function() {
if(!document.querySelector(".infobox .summary")) {
return false;
}
var r = /\d\d\d\d films/;
return $("#catlinks a").filter((i,e) => e.firstChild.data.match(r)).length;
},
type : "movie",
data : () => document.querySelector(".infobox .summary").firstChild.data
},
{
condition : function() {
if(!document.querySelector(".infobox .summary")) {
return false;
}
var r = /television series/;
return $("#catlinks a").filter((i,e) => e.firstChild.data.match(r)).length;
},
type : "tv",
data : () => document.querySelector(".infobox .summary").firstChild.data
}]
},
'movies.com' : {
host : ["movies.com"],
condition : () => document.querySelector("meta[property='og:title']"),
products : [{
condition : Always,
type : "movie",
data : () => document.querySelector("meta[property='og:title']").content
}]
},
'themoviedb' : {
host : ["themoviedb.org"],
condition : () => document.querySelector("meta[property='og:type']"),
products : [{
condition : () => document.querySelector("meta[property='og:type']").content == "movie",
type : "movie",
data : function() {
var year = null;
try {
year = parseInt(document.querySelector(".release_date").innerText.match(/\d{4}/)[0]);
} catch(e) {}
return [document.querySelector("meta[property='og:title']").content, year]
}
},
{
condition : () => document.querySelector("meta[property='og:type']").content == "tv_series",
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, unsafeWindow.filmData.releaseYear]
}]
},
'TVmaze' : {
host : ["tvmaze.com"],
condition : () => document.querySelector("h1"),
products : [{
condition : Always,
type : "tv",
data : () => document.querySelector("h1").firstChild.data
}]
},
'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
}]
},
'TheTVDB' : {
host : ["thetvdb.com"],
condition : Always,
products : [{
condition : () => document.location.pathname.startsWith("/series/") || ~document.location.search.indexOf("tab=series"),
type : "tv",
data : () => document.getElementById("series_title").firstChild.data.trim()
}]
},
'TVNfo' : {
host : ["tvnfo.com"],
condition : () => document.querySelector("#tvsign"),
products : [{
condition : Always,
type : "tv",
data : () => document.querySelector(".heading h1").textContent.trim()
}]
}
};
function main() {
for(var name in sites) {
var site = sites[name];
if(site.host.some(function(e) {return ~this.indexOf(e)}, document.location.hostname))
if(site.host.some(function(e) {return ~this.indexOf(e)}, document.location.hostname) && site.condition()) {
for(var i = 0; i < site.products.length; i++) {
if(site.products[i].condition()) {
// Try to retrieve item name from page
var data;
try {
data = site.products[i].data();
} catch(e) {
data = false;
console.log(e);
}
if(data) {
if(Array.isArray(data) && data[1]) {
loadMeter(data[0].trim(), site.products[i].type, parseInt(data[1]));
} else {
loadMeter(data.trim(), site.products[i].type);
}
}
break;
}
}
break;
}
}
}
(function() {
main();
var lastLoc = document.location.href;
var lastContent = document.body.innerText;
var lastCounter = 0;
function newpage() {
if(lastContent == document.body.innerText && lastCounter < 15) {
window.setTimeout(newpage, 500);
lastCounter++;
} else {
lastCounter = 0;
main();
}
}
window.setInterval(function() {
if(document.location.href != lastLoc) {
lastLoc = document.location.href;
$("#mcdiv321rotten").remove();
window.setTimeout(newpage,1000);
}
},500);
})();