// ==UserScript== // @name Show Letterboxd rating // @description Show Letterboxd rating on imdb.com, metacritic.com, rottentomatoes.com, BoxOfficeMojo, Amazon, Google Play, allmovie.com, Wikipedia, themoviedb.org, movies.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 1 // @connect letterboxd.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.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 http://www.rottentomatoes.com/m/* // @include https://www.rottentomatoes.com/m/* // @include http://www.metacritic.com/movie/* // @include https://www.metacritic.com/movie/* // @include https://www.nme.com/reviews/movie/* // @include https://itunes.apple.com/*/movie/* // @downloadURL none // ==/UserScript== var baseURL = "https://letterboxd.com" var baseURL_search = baseURL + "/s/autocompletefilm?q={query}&limit=20×tamp={timestamp}" var baseURL_openTab = baseURL + "/search/{query}/"; var baseURL_ratingHistogram = baseURL + "/csi{url}rating-histogram/"; const cacheExpireAfterHours = 4; function minutesSince(time) { let seconds = ((new Date()).getTime() - time.getTime()) / 1000; return seconds>60?parseInt(seconds/60)+" min ago":"now"; } function fixLetterboxdURLs(html) { return html.replace(/ cacheExpireAfterHours*60*60*1000) { delete cache[prop]; } } // Check cache or request new content if(url in cache) { // Use cached response handleSearchResponse(cache[url], forceList); } else { GM.xmlHttpRequest({ method: "GET", url: url, onload: function(response) { // Save to chache response.time = (new Date()).toJSON(); // 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; GM.setValue("cache",JSON.stringify(cache)); handleSearchResponse(response, forceList); }, onerror: function(response) { console.log("GM.xmlHttpRequest Error: "+response.status+"\nURL: "+requestURL+"\nResponse:\n"+response.responseText); }, }); } } function handleSearchResponse(response, forceList) { // Handle GM.xmlHttpRequest response let result = JSON.parse(response.responseText); if(forceList && (result.result == false || !result.data)) { alert("No results for "+current.query); } else if(result.result == false || !result.data) { console.log("No results for "+current.query); } else if(!forceList && result.data.length == 1) { loadMovieRating(result.data[0]); } else { // Sort results by closest match function matchQuality(title, year, originalTitle) { if(title == current.query && year == current.year) { return 105 + year; } if(originalTitle && originalTitle == current.query && year == current.year) { return 104 + year; } if(title == current.query && current.year) { return 103 - Math.abs(year - current.year); } if(originalTitle && originalTitle == current.query && current.year) { return 102 - Math.abs(year - current.year); } if(title.replace(/\(.+\)/, "").trim() == current.query && current.year) { return 101 - Math.abs(year - current.year); } if(originalTitle && originalTitle.replace(/\(.+\)/, "").trim() == current.query && current.year) { return 100 - Math.abs(year - current.year); } if(title == current.query) { return 12; } if(originalTitle && originalTitle == current.query) { return 11; } if(title.replace(/\(.+\)/, "").trim() == current.query) { return 10; } if(originalTitle && originalTitle.replace(/\(.+\)/, "").trim() == current.query) { return 9; } if(title.startsWith(current.query)) { return 8; } if(originalTitle && originalTitle.startsWith(current.query)) { return 7; } if(current.query.indexOf(title) != -1) { return 6; } if(originalTitle && current.query.indexOf(originalTitle) != -1) { return 5; } if(title.indexOf(current.query) != -1) { return 4; } if(originalTitle && originalTitle.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; } result.data.sort(function(a,b) { if(!a.hasOwnProperty('matchQuality')) { a.matchQuality = matchQuality(a.name, a.releaseYear, a.originalName); } if(!b.hasOwnProperty('matchQuality')) { b.matchQuality = matchQuality(b.name, b.releaseYear, b.originalName); } return b.matchQuality - a.matchQuality; }); showMovieList(result.data, new Date(response.time)); } } function showMovieList(arr, time) { // Show a small box in the right lower corner $("#mcdiv321letterboxd").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" }); var imgFrame = function imgFrameFct(image125, scale) { if(!image125) { return } let html = ' ' html += '
' return html } // First result let first = $('
' + imgFrame(arr[0].image125, 0.75) + arr[0].name + (arr[0].originalTitle?' ['+arr[0].originalTitle+']':'') + " (" + arr[0].releaseYear + ")
").click(selectMovie).appendTo(main); first[0].dataset["movie"] = JSON.stringify(arr[0]) // 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++) { let entry = $('
' + imgFrame(arr[i].image125, 0.5) + arr[i].name + (arr[i].originalTitle?' ['+arr[i].originalTitle+']':'') + " (" + arr[i].releaseYear + ")
").click(selectMovie).appendTo(div); entry[0].dataset["movie"] = JSON.stringify(arr[i]) } // Footer let sub = $("
").appendTo(main); $('').appendTo(sub); $('@letterboxd.com').appendTo(sub); $('').appendTo(sub).click(function() { document.body.removeChild(this.parentNode.parentNode); }); } function selectMovie(ev) { ev.preventDefault() $("#mcdiv321letterboxd").html("Loading...") const data = JSON.parse(this.dataset.movie) loadMovieRating(data) addToWhiteList(data.url) } async function loadMovieRating(data) { // Load page from letterboxd if("name" in data) { current.query = data.name; } if("releaseYear" in data) { current.year = data.releaseYear; } const url = baseURL_ratingHistogram.replace("{url}", data.url) 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 showMovieRating(cache[url], data.url); } else { GM.xmlHttpRequest({ method: "GET", url: url, onload: function(response) { // Save to chache response.time = (new Date()).toJSON(); // 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; GM.setValue("cache",JSON.stringify(cache)); showMovieRating(newobj, data.url); }, onerror: function(response) { console.log("GM.xmlHttpRequest Error: "+response.status+"\nURL: "+requestURL+"\nResponse:\n"+response.responseText); }, }); } } function showMovieRating(response, letterboxdUrl) { // Show a small box in the right lower corner const time = new Date(response.time) $("#mcdiv321letterboxd").remove(); let main,div; div = main = $('
').appendTo(document.body); div.css({ position:"fixed", bottom :0, right: 0, width: 230, minHeight: 44, color: "#789", padding:" 3px", zIndex: "5010001", fontFamily : "Helvetica,Arial,sans-serif" }); const CSS = `` $(CSS).appendTo(main); let section = $(fixLetterboxdURLs(response.responseText)).appendTo(main) section.find("h2").remove(); // Footer let sub = $('').appendTo(main); $('' + current.query + (current.year?' ('+current.year+')':'') + '').appendTo(sub) $('
').appendTo(sub) $('').appendTo(sub); $('@letterboxd.com').appendTo(sub); $('').appendTo(sub).click(function() { document.getElementById('mcdiv321letterboxd').remove() }); $('🙅').appendTo(sub).click(function() { removeFromWhiteList() searchMovie(current.query, current.type, current.year, true); }); $('').appendTo(sub) } 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 : () => !~document.location.pathname.indexOf("/mediaviewer") && !~document.location.pathname.indexOf("/mediaindex") && !~document.location.pathname.indexOf("/videoplayer"), 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; var name = null; var jsonld = null; if(document.querySelector("#titleYear")) { year = parseInt(document.querySelector("#titleYear a").firstChild.textContent); } if(document.querySelector('script[type="application/ld+json"]')) { jsonld = parseLDJSON(["name", "datePublished"]); name = jsonld[0]; year = parseInt(jsonld[1].match(/\d{4}/)[0]); } if(document.querySelector(".originalTitle") && document.querySelector(".title_wrapper h1")) { return [document.querySelector(".title_wrapper h1").firstChild.data.trim(), year] // Use English title } else if(jsonld) { return [name, year]; // Use original title } 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(); } } } ] }, '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] } }] }, 'amazon' : { host : ["amazon."], condition : Always, products : [{ condition : () => document.querySelector('[data-automation-id=title]'), type : "movie", data : () => document.querySelector('[data-automation-id=title]').textContent.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 }] }, '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] } }] }, 'rottentomatoes' : { host : ["www.rottentomatoes.com"], condition : Always, products : [{ condition : () => document.location.pathname.startsWith("/m/"), type : "movie", data : () => document.querySelector("h1").firstChild.textContent } ] }, 'nme' : { host : ["nme.com"], condition : () => document.location.pathname.startsWith("/reviews/"), products : [ { condition : () => document.location.pathname.startsWith("/reviews/movie/"), type : "movie", data : function() { var year = null; try { year = parseInt(document.querySelector("*[itemprop=datePublished]").content.match(/\d{4}/)[0]) } catch(e) {} try { return [ document.querySelector(".title-primary").textContent.match(/‘(.+?)’/)[1] , year ]; } catch(e) { return [ document.querySelector("h1").textContent.match(/:\s*(.+)/)[1].trim() , year ]; } } }] }, 'itunes' : { host : ["itunes.apple.com"], condition : Always, products : [{ condition : () => ~document.location.href.indexOf("/movie/"), type : "movie", data : () => parseLDJSON("name", (j) => (j["@type"] == "Movie")) }] } }; function main() { for(var name in sites) { var site = sites[name]; 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]) { searchMovie(data[0].trim(), site.products[i].type, parseInt(data[1])); } else { searchMovie(data.trim(), site.products[i].type); } } break; } } break; } } } async function adaptForRottentomatoesScript() { if(!document.getElementById('mcdiv321rotten') || !document.getElementById('mcdiv321letterboxd')) { return } const h = parseInt(document.getElementById('mcdiv321rotten').clientHeight) + 5 if(document.getElementById('mcdiv321letterboxd').dataset.adapted && parseInt(document.getElementById('mcdiv321letterboxd').dataset.adapted) == h) { return } const letterboxd = document.getElementById('mcdiv321letterboxd') letterboxd.style.bottom = h + 'px' document.getElementById('mcdiv321letterboxd').dataset.adapted = h } (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() { adaptForRottentomatoesScript() if(document.location.href != lastLoc) { lastLoc = document.location.href; $("#mcdiv321letterboxd").remove(); window.setTimeout(newpage,1000); } },500); })();