NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Add rotten tomatoes Ratings to Netflix // @namespace lumpSoftware // @description Adds one or more Rotten Tomatoes ratings under each netflix title. If there are multiple exact matches (different years, tv or movie), it adds multiple lines (netflix doesn't contain the year until you hover over title, so I don't know the year of the item being displayed). The script maintains a cache of search titles for up to a week. Link added to top of page so cache may be copied to the clipboard in csv format for searching. // @include https://*.netflix.com/* // @content www.rottentomatoes.com // @version 0.0.1.0 // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_setClipboard // @license MIT // ==/UserScript== // based on: https://greasyfork.org/en/scripts/2271-netflix-ratings document.netflixRatingsObject = { debug: true, clearCacheInd: false, //by default the clear cache option is not displayed httpRequestCount: 0, typeRottenTomatoes: 'RottenTomatoes', classLoading: 'nr-loading', classLoaded: 'nr-loaded', netflixPageTitleCard: "title-card-container", netflixRatingClassName : "nr-ratings-container", fetchCacheExpiry: 3, cacheExpiryInMinutes: 1440, load: function() { this.addMenu(); this.httpRequestCount = 0; try { const netFlixItems = document.getElementsByClassName("fallback-text"); if (netFlixItems) { for (let i = 0; i < netFlixItems.length; i++) { // at th moment, the nextflix page consists of a bunc of title cards const titleCard = document.getElementsByClassName(this.netflixPageTitleCard)[i]; if (!titleCard) continue; const title = this.getTitle(titleCard); if (!title) continue; const searchURL = this.getRottenTomatoesUrl(title); // if we already have a link element then we we're already done (previous iteration) const previousLinkElement = titleCard.parentNode.getElementsByClassName(this.netflixRatingClassName); if ( previousLinkElement && previousLinkElement.length == 0) this.fetchRating(searchURL, title, "20??", titleCard, "Search", "??"); if (this.httpRequestCount > 15) return; } } } catch (error) { console.log("Error in load \n" + error + "\n" + title); } }, // this adds links to the top of the netflix page addMenu(){ let m = document.getElementById("lummpyCopyCache"); if (m) return; let x =document.getElementsByClassName("tabbed-primary-navigation") if (this.clearCacheInd) { m = document.createElement("li"); m.id="lummpyClearCache" m.innerHTML="<a onclick='document.netflixRatingsObject.clearCache()' >Clear Cache</a>" m.className = "navigation-tab"; x[0].appendChild(m); } let m2 = document.createElement("li"); m2.id="lummpyCopyCache" m2.innerHTML="<a onclick='document.netflixRatingsObject.copyCacheToClipboard()' >Copy Cache</a>" m2.className = "navigation-tab"; x[0].appendChild(m2); this.cleanCache(); }, // on initial start up remove expired or ...fetching... (rotten tomato lookups that never returned) cache items cleanCache() { let cache = GM_listValues(); const now = new Date(); cache.forEach((c) =>{ let v = GM_getValue(c); let t = GM_getValue(c + "_time"); if (v == "...fetching..." || new Date(t) < now) { GM_deleteValue(c); GM_deleteValue(c + "_url"); GM_deleteValue(c + "_time"); GM_deleteValue(c + "_critic"); GM_deleteValue(c + "_audience"); GM_deleteValue(c + "_movieOrTV"); } }); }, clearCache() { alert("Clearing Ratings Cache"); let cache = GM_listValues(); alert("Clearing Ratings Cache of " + cache.length + " items"); cache.forEach((c) => GM_deleteValue(c)); }, copyCacheToClipboard() { let cache = GM_listValues(); alert("Copying Ratings Cache of " + cache.length + " items to clipboard"); let clipBrdText = ""; cache.forEach((c) =>{ let v = GM_getValue(c); let u = GM_getValue(c + "_url"); let t = GM_getValue(c + "_time"); let critic = GM_getValue(c + "_critic"); let audience = GM_getValue(c + "_audience"); let movieOrTV = GM_getValue(c + "_movieOrTV"); let clipBrdItem = ""; if (v && u && t ) { clipBrdItem += '"'; clipBrdItem += c; clipBrdItem +='", "'; v = v.replace(/\n/g,"").replace(/ /g,"").replace("liked it", "").replace("%","").replace("%",""); clipBrdItem += v; clipBrdItem +='", "'; clipBrdItem += u clipBrdItem +='", "'; clipBrdItem += t; clipBrdItem +='", "'; clipBrdItem += critic || ""; clipBrdItem +='", "'; clipBrdItem += audience || ""; clipBrdItem +='", "'; clipBrdItem += movieOrTV || ""; clipBrdItem +='"\n'; } clipBrdText += clipBrdItem; }); GM_setClipboard(clipBrdText); }, getTitle(titleCard) { const title = (titleCard.getElementsByClassName("fallback-text") || [{innerText:"error"}])[0].innerText; return title; }, getRottenTomatoesUrl: function(title) { return "http://www.rottentomatoes.com/search/?search=" + this.encodeTitle(title); }, encodeTitle: function(title) { return title.replace(/-/g, " ").replace(/ /g, "+"); }, fetchRating: function(url, title, year, titleCard, detailOrSearch, movieOrTV) { // Check for a cached rating let cachedRating = this.getCachedRating(year, title); if (cachedRating) { if (cachedRating[0] == "...fetching..." || cachedRating[0] == "...about to parse...") return; //waiting for search page or for it to be parsed so don't make another call, it's time will come if (cachedRating[0] == "...parsed...") { //search page have previously been returned, so do details from cache if (cachedRating[2].length > 0) cachedRating[2].forEach((searchObj) => { this.fetchRating(searchObj.url, title, searchObj.year, titleCard, "Detail", searchObj.movieOrTV); }); if (cachedRating[2].length == 0) this.addNewLinkElement(titleCard, title, "no matches" , cachedRating[1], this.classLoaded); return; } this.addNewLinkElement(titleCard, title, cachedRating[0], cachedRating[1], this.classLoaded); return; } else this.saveCachedRating(year, title, "...fetching...", url); // Make a cross-domain request for this url console.log("\nSearching RT with:\t\t" + url + "\n"); this.httpRequestCount += 1; GM_xmlhttpRequest({ method: 'get', url: url, onload: function(response) { //GM_setClipboard(response.responseText,"text"); if (detailOrSearch == "Search") document.netflixRatingsObject.parseRottenTomatoesSearchResponse(response.responseText, url, title, year, titleCard); else document.netflixRatingsObject.parseRottenTomatoesDetailResponse(response.responseText, url, title, year, titleCard, movieOrTV); }, onerror: function(response) { console.log('failed to get a response from rotten tomatoes for ' + url); } }); }, // ----------------------------------- // Caching functions // ----------------------------------- getCachedRating: function(year, title) { try { const valueKey = year + "_" + this.encodeTitle(title); const urlKey = valueKey + '_url'; const timeKey = valueKey + "_time"; const searchKeys = valueKey + "_searchKeys"; const criticsKeys = valueKey + "_criticsKeys"; const audienceKeys = valueKey + "_audienceKeys"; const movieTVKeys = valueKey + "_movieOrTV"; const displayRatingValue = GM_getValue(valueKey); if (!displayRatingValue) { return null; } const url = GM_getValue(urlKey); if (!url) { return null; } const time = new Date(GM_getValue(timeKey)); if (!time) { return null; } const now = new Date(); if (this.debug) console.log('time ' + time.toString() + ' < now ' + now.toString()); if (time < now) { // cache is stale, clear it GM_setValue(valueKey,null); GM_setValue(urlKey,null); GM_setValue(timeKey,null); GM_setValue(searchKeys,null); GM_setValue(criticsKeys,null); GM_setValue(audienceKeys,null); return null; } if (this.debug) console.log('found cached rating [' + valueKey + ', ' + time + ']: ' + displayRatingValue + ', ' + url); const searchURL = GM_getValue(searchKeys) || []; return [displayRatingValue, url, searchURL, GM_getValue(criticsKeys), GM_getValue(audienceKeys), GM_getValue(movieTVKeys) ]; } catch (error) { console.log("Error in getCachedRating\n" + error + " " + title); } }, saveCachedRating: function(year, title, displayRatingsValue, url, criticsRating, audienceRating, movieOrTV) { try { const valueKey = year + '_' + this.encodeTitle(title); const urlKey = valueKey + '_url'; const timeKey = valueKey + '_time'; const criticsKeys = valueKey + "_criticsKeys"; const audienceKeys = valueKey + "_audienceKeys"; const movieOrTVKeys = valueKey + "_movieOrTV"; GM_setValue(valueKey, displayRatingsValue); GM_setValue(urlKey, url); let expireDate = new Date(); let waitTime = this.cacheExpiryInMinutes; if (displayRatingsValue == "...fetching...") waitTime = this.fetchCacheExpiry; expireDate.setMinutes(expireDate.getMinutes() + waitTime); if (this.debug) console.log('caching ' + valueKey + ' until ' + expireDate.toString()); GM_setValue(timeKey, expireDate); if (criticsRating) GM_setValue(criticsKeys, criticsRating); if (audienceRating) GM_setValue(audienceKeys, audienceRating); if (movieOrTV) GM_setValue(movieOrTVKeys, movieOrTV); } catch (error) { console.log("Error in saveCachedRating\n" + error + " " + title); } }, addSearchURLToCache: function(year, title, searchURL, movieOrTV) { try { const valueKey = '20??_' + this.encodeTitle(title); const searchKeys = valueKey + "_searchKeys"; let cachedSearchObj = GM_getValue(searchKeys) || []; if (cachedSearchObj.map((i) => i.url == searchObj.url).includes(true)) return; const searchObj = {"year": year, "movieOrTV":movieOrTV, "url":searchURL}; cachedSearchObj.push(searchObj); GM_setValue(searchKeys, cachedSearchObj); } catch (error) { console.log("Error in addSearchURLToCache\n" + error + " " + title); } }, // ----------------------------------- // Parsing functions // ----------------------------------- parseRottenTomatoesSearchResponse: function(responseText, searchPageURL, title, year, titleCard) { try { this.saveCachedRating(year, title, "...about to parse...", searchPageURL); // Check for no results if (responseText.match(/Sorry, no results found for/)) { this.addNewLinkElement(titleCard, title, "not found", "", this.classLoaded); this.saveCachedRating(year, title, "not found" , searchPageURL); return; } // Look for movie in search page let result1 = this.tryToFindMovie(responseText, title, year, titleCard); // Look for TV Series let result2 = this.tryToFindTVSeries(responseText, title, year, titleCard); if (result1 <= 0 && result2 <= 0) this.addNewLinkElement(titleCard, title, "no matches", "", this.classLoaded); this.saveCachedRating(year, title, "...parsed...", searchPageURL); } catch (error) { console.log("Error in parseRottenTomatoesSearchResponse\n" + error + " " + title); } }, // this adds a new link item underneth the netflix title. addNewLinkElement: function( titleCard, title, displayText, url, className){ if (title !== this.getTitle(titleCard)) return; const id = "nrRottenTomatoesContainer"; const ratingSite = "Rotten Tomatoes.."; let containerDiv = document.createElement("div"); containerDiv.className = this.netflixRatingClassName; let descriptionElement = document.createElement("span"); descriptionElement.innerHTML = "Rot: "; let linkElement = document.createElement("a"); linkElement.id = id; linkElement.innerHTML = displayText; linkElement.href = url; this.addClass(linkElement, className); containerDiv.appendChild(descriptionElement); containerDiv.appendChild(linkElement); this.insertAfter(titleCard, containerDiv); return linkElement; }, tryToFindMovie: function(responseText, title, year, titleCard) { const movieRegex = /{.*"movies":(\[.*\]),"tvCount".*}/; const match = movieRegex.exec(responseText); if (match) { const movieItems = JSON.parse(match[1]); if (movieItems.length > 0) { // Look for a full match for (let i = 0; i < movieItems.length; i++) { let parsedTitle = movieItems[i].name; let parsedYear = movieItems[i].year; let parsedDetMovieUrl = "http://www.rottentomatoes.com" + movieItems[i].url; // Check the title // and year if (parsedTitle.toLowerCase().replace(/[^a-z0-9]/g, "") == title.toLowerCase().replace(/[^a-z0-9]/g, "")) // && parsedYear == year { this.addSearchURLToCache(parsedYear, parsedTitle, parsedDetMovieUrl, "M"); // found movie in search result so now get detail page (might be wrong year, but oh well this is as good as it gets - for now) document.netflixRatingsObject.fetchRating( parsedDetMovieUrl, title, parsedYear, titleCard, "Detail", "M"); this.saveCachedRating(year, title, "...fetching...", parsedDetMovieUrl); } } return movieItems.length; } // Did not find movie with exact name in search result (sure hope there is a tv series with that name) return 0; } return -1; }, tryToFindTVSeries: function(responseText, title, year, titleCard) { const tvRegex = /{.*,"tvCount":.*,.*"tvSeries":(\[.*\]).*}/; const match = tvRegex.exec(responseText); if (match) { const tvItems = JSON.parse(match[1]); if (tvItems.length > 0) { for (let i = 0; i< tvItems.length; i++){ let parsedTitle = tvItems[i].title; let parsedDetTVUrl = "http://www.rottentomatoes.com" + tvItems[i].url; let parsedYear = tvItems[i].startYear; // Check the title if (parsedTitle.toLowerCase().replace(/[^a-z0-9]/g, "") == title.toLowerCase().replace(/[^a-z0-9]/g, "")) { this.addSearchURLToCache(parsedYear, parsedTitle, parsedDetTVUrl, "T"); // found tv series in search result so now get detail page (might be wrong year, but oh well this is as good as it gets - for now) document.netflixRatingsObject.fetchRating(parsedDetTVUrl, title, parsedYear, titleCard, "Details", "T"); this.saveCachedRating(year, title, "...fetching...", parsedDetTVUrl); } } return tvItems.length; } // Did not find any matches return 0; } return -1; }, parseRottenTomatoesDetailResponse: function(responseText, detailPageURL, title, year, titleCard, movieOrTV) { let dataElement = this.addNewLinkElement(titleCard, title, "about to parse " + movieOrTV, detailPageURL, this.classLoading); try { // Set the link dataElement.href = detailPageURL; // Check for no results if (responseText.match(/Sorry, no results found for/)) { dataElement.innerHTML = "no details??"; this.addClass(dataElement, this.classLoaded); this.saveCachedRating(year, title, "not found", detailPageURL); return; } let responseDoc = this.getDom(responseText); let text = (movieOrTV == "T" ? "(" : "(") + year + ") "; // Set the loaded flag this.addClass(dataElement, this.classLoaded); let critText, audText; // Get the critics rating if (movieOrTV == "T"){ const criticsRating = responseDoc.getElementsByClassName("critic-score"); const allCriticsRating = criticsRating.length > 0 ? criticsRating[0].getElementsByClassName("meter-value") : criticsRating; critText = allCriticsRating.length > 0 ? allCriticsRating[0].textContent : "#" const audienceRating = responseDoc.getElementsByClassName("audience-score"); const audienceRating2 = audienceRating.length > 0 ? audienceRating[0].getElementsByClassName("meter-value") : audienceRating; audText = audienceRating2.length > 0 ? audienceRating2[0].textContent : " # " } if (movieOrTV == "M") { const criticsRating = responseDoc.querySelector('#tomato_meter_link > span.mop-ratings-wrap__percentage'); critText = (criticsRating)? criticsRating.innerText : "#" // Get the users rating let audienceRating2 = responseDoc.getElementsByClassName('mop-ratings-wrap__percentage mop-ratings-wrap__percentage--audience'); audText = audienceRating2.length > 0 ? audienceRating2[0].textContent : " # " } critText = critText.replace(/\n/g,"").replace(/ /g,"").replace("liked it", ""); audText = audText.replace(/\n/g,"").replace(/ /g,"").replace("liked it", ""); text += critText + " | " + audText + " " + movieOrTV.substring(0,1); // Assign to element dataElement.innerHTML = text + " "; // Save in cache this.saveCachedRating(year, title, text, detailPageURL, critText, audText, movieOrTV); } catch (error) { console.log("Error in parseRottenTomatoesDetailResponse\n" + error + " " + title); } }, // ----------------------------------- // Helper functions // ----------------------------------- hasClass: function(element, cls) { return (' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1; }, addClass: function(element, cls) { if (!this.hasClass(element, cls)) { element.className += ' ' + cls; } }, insertAfter: function(referenceNode, newNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); }, getDom: function(responseText) { let parser = new DOMParser(); return parser.parseFromString(responseText, 'text/html'); } }; // Inject CSS var styleElement = document.createElement('style'); styleElement.type = 'text/css'; styleElement.innerHTML = '.nr-ratings-container { margin-top: 8px; color: #666666; }'; document.head.appendChild(styleElement); // Load ratings let count = 0; function doRatings() { //debugger; count = count+1; document.netflixRatingsObject.load(); if (count < 500) setTimeout( function() {doRatings()}, 2000); } setTimeout( function() {doRatings()}, 5000);