// ==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.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 5 // @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 https://www.boxofficemojo.com/release/* // @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.rottentomatoes.com/m/* // @include https://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/* // @include https://www.tvhoard.com/* // @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("Letterboxd 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 || !result.data.length)) { alert("Letterboxd userscript\n\nNo results for "+current.query); } else if(result.result == false || !result.data || !result.data.length) { console.log("Letterboxd: 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; }); if(!forceList && result.data.length > 1 && result.data[0].matchQuality > 100 && result.data[1].matchQuality < result.data[0].matchQuality) { loadMovieRating(result.data[0]); } else { 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 = $('").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 = $('").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, data); } 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, data); }, onerror: function(response) { console.log("GM.xmlHttpRequest Error: "+response.status+"\nURL: "+requestURL+"\nResponse:\n"+response.responseText); }, }); } } function showMovieRating(response, letterboxdUrl, otherData) { // 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(); let identName = current.query let identYear = current.year?' ('+current.year+')':'' let identOriginalName = '' let identDirector = '' if(otherData) { if('name' in otherData && otherData.name) { identName = otherData.name } if('year' in otherData && otherData.year) { identYear = ' ('+otherData.year+')' } if('originalName' in otherData && otherData.originalName) { identOriginalName = ' "'+otherData.originalName+'"' } if('directors' in otherData) { identDirector = [] for(let i = 0; i < otherData.directors.length; i++) { if('name' in otherData.directors[i]) { identDirector.push(otherData.directors[i].name) } } if(identDirector) { identDirector = '