// ==UserScript==
// @name RottenTomatoes Utility Library
// @namespace driver8.net
// @version 0.1.1
// @description Utility library for grabbing info from rottentomatoes.com
// @author driver8
// @grant GM_xmlhttpRequest
// @connect rottentomatoes.com
// ==/UserScript==
console.log('hi rt');
var MAX_YEAR_DIFF = 2;
function _parse(query, regex, doc) {
doc = doc || document;
try {
let text = doc.querySelector(query).textContent.trim();
if (regex) {
text = text.match(regex)[1];
}
return text.trim();
} catch (e) {
console.log('error', e);
return '';
}
};
function getRtIdFromTitle(title, tv, year) {
tv = tv || false;
year = parseInt(year) || 1800;
return new Promise(function(resolve) {
GM_xmlhttpRequest({
method: 'GET',
responseType: 'json',
url: 'https://www.rottentomatoes.com/api/private/v2.0/search/?limit=10&q=' + title,
onload: (resp) => {
let movies = tv ? resp.response.tvSeries : resp.response.movies;
let sorted = movies.concat();
if (year) {
sorted.sort((a, b) => {
if (Math.abs(a.year - year) !== Math.abs(b.year - year)) {
// Prefer closest year to the given one
return Math.abs(a.year - year) - Math.abs(b.year - year);
} else {
return b.year - a.year; // In a tie, later year should come first
}
});
}
// Search for matches with exact title in order of proximity by year
let bestMatch;
for (let m of sorted) {
m.title = m.title || m.name;
if (m.title.toLowerCase() === title.toLowerCase()) {
bestMatch = m;
break;
}
}
//console.log('sorted', sorted, 'bestMatch', bestMatch, movies);
// Fall back on closest year match if with 2 years, or whatever the first result was.
bestMatch = bestMatch || (sorted && Math.abs(sorted[0].year - year) <= MAX_YEAR_DIFF && sorted[0]) || (movies && movies[0]);
let id = bestMatch.url.replace(/\/s\d{2}\/?$/, ''); // remove season suffix from tv matches
console.log('found id', id);
resolve(id);
}
});
});
}
function getRtInfoFromId(id) {
return new Promise(function(resolve) {
let url = 'https://www.rottentomatoes.com' + id + (id.startsWith('/tv/') ? '/s01' : ''); // Look up season 1 for TV shows
GM_xmlhttpRequest({
method: 'GET',
responseType: 'document',
url: url,
onload: (resp) => {
let text = resp.responseText;
// Create DOM from responseText
const doc = document.implementation.createHTMLDocument().documentElement;
doc.innerHTML = text;
let year = parseInt(_parse('.h3.year, .movie_title .h3.subtle, .meta-row .meta-value time', /(\d{4})/, doc));
// Find the javascript snippet storing the tomatometer/score info.
// Everything is named different for TV shows for some stupid reason.
let m = text.match(/root\.RottenTomatoes\.context\.scoreInfo = ({.+});/);
m = m || text.match(/root\.RottenTomatoes\.context\.scoreboardCriticInfo = ({.+});/);
let dataString = m[1];
let scoreInfo = JSON.parse(dataString);
let all = scoreInfo.tomatometerAllCritics || scoreInfo.all || {},
top = scoreInfo.tomatometerTopCritics || scoreInfo.top || {};
//console.log('scoreInfo', scoreInfo);
// TV consensus is stored in a totally different object :/
m = text.match(/root\.RottenTomatoes\.context\.result = ({.+});/);
let contextResult = m && JSON.parse(m[1]);
// Try field names used for movie data, then TV show data.
const data = {
id: id,
score: all.score || all.tomatometer,
rating: all.avgScore || all.averageRating,
votes: all.numberOfReviews || all.totalCount,
consensus: all.consensus || (contextResult && contextResult.seasonData && contextResult.seasonData.tomatometer.consensus), // TV consensus is stored in a totally different object :/
state: all.tomatometerState || all.state,
topScore: top.score || top.tomatometer,
topRating: top.avgScore || top.averageRating,
topVotes: top.numberOfReviews || top.totalCount,
year: year,
fetched: new Date()
};
console.log('found data', data);
resolve(data);
}
});
});
}
function getRtInfoFromTitle(title, tv, year) {
return getRtIdFromTitle(title, tv, year).then((id) => {
return getRtInfoFromId(id);
})
}