├── README.md ├── friends-average-histogram-preview.png ├── friends-average-histogram.user.js ├── letterboxd-lists-progress-preview.png ├── letterboxd-lists-progress.user.js ├── top-2000-flame.svg ├── top-2000-preview.png └── top-2000.user.js /README.md: -------------------------------------------------------------------------------- 1 | # Letterboxd userscripts 2 | 3 | ## Installation 4 | 5 | 1. Download a userscript manager such as [Tampermonkey](https://www.tampermonkey.net/) or [Greasemonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/). 6 | 2. Click on your desired script in the file list above, then click the "Raw" button. 7 | 8 | ## List of scripts 9 | 10 | ### Friends Average for Letterboxd (`friends-average-histogram.user.js`) 11 | 12 | Shows a histogram and ratings average for just the users you follow, in addition to the global one. Port of the ["Friends Average for Letterboxd"](https://chrome.google.com/webstore/detail/friends-average-for-lette/fffalfghjklopnhmkpdadlfopnnbnabg) Chrome extension by Klaspas. 13 | 14 |  15 | 16 | ### Letterboxd Lists Progress (`letterboxd-lists-progress.user.js`) 17 | 18 | Displays list progress underneath cover art. Port of the ["Letterboxd Lists Progress"](https://chrome.google.com/webstore/detail/letterboxd-lists-progress/cjpnlmdbmlefonmfkobjpfpmpbaijldn) Chrome extension by Lucas Franco. 19 | 20 |  21 | 22 | 23 | ### Letterboxd Top 2000 (`top-2000.user.js`) 24 | 25 | Displays relative ranking (crown) & popularity (flame) for the top 2000 films. Port of the ["Letterboxd Top 2000"](https://chrome.google.com/webstore/detail/letterboxd-top-2000/akajmboonlckanooginhfnpafjjnimko) Chrome extension by koenhagen. 26 | 27 |  -------------------------------------------------------------------------------- /friends-average-histogram-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frozenpandaman/letterboxd-userscripts/7d8a397cd845db64d9871a9950b3009194e5b68d/friends-average-histogram-preview.png -------------------------------------------------------------------------------- /friends-average-histogram.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | /* globals jQuery, $, waitForKeyElements */ 3 | // @name Friends Average for Letterboxd 4 | // @namespace https://github.com/frozenpandaman 5 | // @version 0.2 (0.9.1) 6 | // @description Shows a histogram and ratings average for just the users you follow, in addition to the global one. 7 | // @author eli / frozenpandaman 8 | // @match https://letterboxd.com/film/* 9 | // @icon https://letterboxd.com/favicon.ico 10 | // @grant none 11 | // ==/UserScript== 12 | 13 | // This userscript is a port of the "Friends Average for Letterboxd" Chrome extension by Klaspas 14 | 15 | 'use strict'; 16 | window.addEventListener('load', function (e) { 17 | var element = e.srcElement; 18 | let single = $(element).attr('id') 19 | let double = $(element).parent().attr('id') 20 | if (ids.includes(double) || ids.includes(single)) { 21 | var position; 22 | var arrow; 23 | var text; 24 | 25 | if (single == 'a11') { 26 | text = $(element).data('popup'); 27 | let li_nr = 11; 28 | // let width = $('#aad').width(); 29 | position = - (Number(widths[li_nr - 1]) * 3 / 4) + 190; 30 | arrow = "left: 145px" 31 | } 32 | 33 | else { 34 | text = $(element).text(); 35 | if (text == '') { 36 | text = $(element).parent().text(); 37 | } 38 | let li_nr = Number(single.replace('a', '')); 39 | if (isNaN(li_nr)) { 40 | li_nr = Number(double.replace('a', '')); 41 | } 42 | // let width = $('#aad').width(); 43 | position = - (Number(widths[li_nr - 1]) / 2) + (li_nr * 16) - 7.5; 44 | arrow = "left: 50%"; 45 | 46 | } 47 | 48 | $('#popup1').attr('style', 'display: block; top: -3px; left:' + position + 'px;') 49 | $('#popup2').attr('style', arrow) 50 | $('#aad').text(text); 51 | } 52 | else { 53 | $('#popup1').attr('style', 'display: none'); 54 | } 55 | }, false); 56 | 57 | let sleep = function (ms) { 58 | return new Promise(resolve => setTimeout(resolve, ms)); 59 | }; 60 | 61 | let getHTML = function (url) { 62 | return fetch(url).then(result => { return result.text() }) 63 | }; 64 | 65 | 66 | let getinfo = async () => { 67 | // Gets Username and movie from the current site 68 | var main_nav = $('.main-nav').html(); 69 | if (typeof main_nav == 'undefined') { 70 | await sleep(100); 71 | return getinfo(); 72 | } 73 | else { 74 | let movie_link = $('meta[property="og:url"]').attr('content'); 75 | let url_part = movie_link.split('film/')[1].split('/')[1]; 76 | let exclude = ['members', 'likes', 'reviews', 'ratings', 'fans', 'lists']; 77 | if (!exclude.includes(url_part)) { 78 | let movie = movie_link.match('(?<=film\/)(.*?)(?=\/)')[0]; 79 | let user_link = $('a:contains("Profile")').parent().html(); 80 | let user = $(user_link).attr('href'); 81 | if (typeof user !== 'undefined') { 82 | return [user, movie]; 83 | } 84 | } 85 | return null; 86 | } 87 | } 88 | 89 | let getContent = async (url, user_movie) => { 90 | var rating_list = []; 91 | var person_count = 0 92 | var like_count = 0; 93 | while (true) { 94 | if (url !== 'undefined') { 95 | let html = getHTML(url); 96 | let table = await html.then(function (html) { 97 | let tbody = $(html).find('tbody').html(); 98 | if (typeof tbody !== 'undefined') { 99 | let table = '
' + tbody + ''; 100 | $(table).find('tr').each(function () { 101 | let person = $(this).find(".name").attr('href'); 102 | if (person !== user_movie[0]) { 103 | let rating = $(this).find(".rating").attr('class') 104 | person_count += 1; 105 | let like = $(this).find('.icon-liked').html(); 106 | if (typeof like !== 'undefined') { 107 | like_count += 1; 108 | } 109 | if (typeof rating !== 'undefined') { 110 | rating = rating.split('-')[1]; 111 | rating_list.push(Number(rating)); 112 | } 113 | } 114 | }); 115 | 116 | } 117 | let next_page_loc = $(html).find('.next').parent().html(); 118 | let next_page = $(next_page_loc).attr('href'); 119 | return [next_page, rating_list, person_count, like_count]; 120 | 121 | }) 122 | if (typeof table[0] == 'undefined') { 123 | if (table[1].length == 0 & table[3] == 0) { 124 | break; 125 | } 126 | else { 127 | prepContent(table, user_movie); 128 | return true; 129 | } 130 | } 131 | else { 132 | url = 'https://letterboxd.com' + table[0]; 133 | } 134 | } 135 | } 136 | }; 137 | 138 | let prepContent = function (table, user_movie) { 139 | var rating_list = table[1]; 140 | var votes = rating_list.length; 141 | var avg; 142 | var avg_1; 143 | var avg_2; 144 | var rating; 145 | if (votes == 0) { 146 | avg_1 = '–.–'; 147 | avg_2 = '–.–'; 148 | } 149 | else { 150 | let sum = 0; 151 | for (var r of rating_list) { 152 | sum += r; 153 | } 154 | avg = sum / (votes * 2); 155 | avg_1 = avg.toFixed(1); 156 | avg_2 = avg.toFixed(2); 157 | } 158 | 159 | var href_head = user_movie[0] + 'friends/film/' + user_movie[1]; 160 | var href_likes = user_movie[0] + 'friends/film/' + user_movie[1] + '/likes/'; 161 | if (votes == 1) { 162 | rating = 'rating'; 163 | } 164 | else { 165 | rating = 'ratings'; 166 | } 167 | var data_popup = 'Average of ' + avg_2 + ' based on ' + votes + ' ' + rating; 168 | let rating_count = []; 169 | for (let i = 1; i < 11; i++) { 170 | let count = 0 171 | for (rating of rating_list) { 172 | if (rating == i) { 173 | count += 1; 174 | } 175 | } 176 | rating_count.push(count); 177 | } 178 | 179 | let max_rating = Math.max(...rating_count); 180 | let relative_rating = []; 181 | let percent_rating = []; 182 | 183 | for (rating of rating_count) { 184 | let hight = (rating / max_rating) * 44.0; 185 | if (hight < 1 || hight == Number.POSITIVE_INFINITY || isNaN(hight)) { 186 | hight = 1; 187 | } 188 | relative_rating.push(hight); 189 | let perc = Math.round((rating / votes) * 100); 190 | percent_rating.push(perc); 191 | } 192 | 193 | let rat = []; 194 | var stars = ['half-★', '★', '★½', '★★', '★★½', '★★★', '★★★½', '★★★★', '★★★★½', '★★★★★']; 195 | for (let i = 1; i < 11; i++) { 196 | if (rating_count[i - 1] == 1) { 197 | rating = 'rating'; 198 | } 199 | else { 200 | rating = 'ratings'; 201 | } 202 | let r_n = rating_count[i - 1] + ' ' + stars[i - 1] + ' ' + rating + ' (' + percent_rating[i - 1] + '%)'; 203 | rat.push(r_n); 204 | }; 205 | 206 | 207 | let str1 = ' '; 210 | let str = str1 + str2 + str3; 211 | 212 | var html = $.parseHTML(str) 213 | $(html).find('#aaa').attr('href', href_head); 214 | $(html).find('#aab').attr('href', href_likes); 215 | if (table[3] == 1) { 216 | $(html).find('#aab').text('1 like'); 217 | } 218 | else { 219 | $(html).find('#aab').text(table[3] + ' likes'); 220 | } 221 | $(html).find('#a11').attr('href', href_head); 222 | $(html).find('#a11').attr('data-popup', data_popup); 223 | $(html).find('#a11').text(avg_1); 224 | 225 | for (let i = 1; i < 11; i++) { 226 | let id = '#a' + i 227 | let i_str = '' 228 | $(html).find(id).attr('href', href_head+'/rated/'+ (0.5 * i).toString() +'/'); 229 | $(html).find(id).text(rat[i - 1]); 230 | $(html).find(id).append($.parseHTML(i_str)); 231 | } 232 | 233 | injectContent(html); 234 | return true; 235 | } 236 | 237 | let injectContent = function (html) { 238 | 239 | let path = $('.sidebar'); 240 | $(html).appendTo(path); 241 | return true; 242 | } 243 | 244 | let getWidths = async () => { 245 | var ids = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'a10']; 246 | var widths = [] 247 | $('#popup1').attr('style', 'display: block; top: -3px; left: -10px;') 248 | 249 | for (var a of ids) { 250 | let id = '#' + a; 251 | let text = $(id).text(); 252 | $('#aad').text(text); 253 | let width = $('#aad').width(); 254 | widths.push(width) 255 | } 256 | 257 | let text = $('#a11').data('popup') 258 | $('#aad').text(text); 259 | let width = $('#aad').width(); 260 | widths.push(width); 261 | 262 | $('#popup1').attr('style', 'display: none') 263 | return widths 264 | } 265 | 266 | let main = async () => { 267 | var user_movie = await getinfo(); 268 | if (user_movie !== null && typeof user_movie !== 'undefined') { 269 | var user = user_movie[0]; 270 | var movie = user_movie[1]; 271 | let newURL = 'https://letterboxd.com' + user + 'friends/film/' + movie; 272 | getContent(newURL, user_movie); 273 | widths = await getWidths(); 274 | return widths 275 | } 276 | } 277 | 278 | var widths = main(); 279 | var ids = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'a10', 'a11']; 280 | -------------------------------------------------------------------------------- /letterboxd-lists-progress-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frozenpandaman/letterboxd-userscripts/7d8a397cd845db64d9871a9950b3009194e5b68d/letterboxd-lists-progress-preview.png -------------------------------------------------------------------------------- /letterboxd-lists-progress.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | /* globals jQuery, $, waitForKeyElements */ 3 | // @name Letterboxd Lists Progress 4 | // @namespace https://github.com/frozenpandaman 5 | // @version 0.1 (0.2) 6 | // @description Displays list progress underneath cover art. 7 | // @author eli / frozenpandaman 8 | // @match https://letterboxd.com/* 9 | // @icon https://letterboxd.com/favicon.ico 10 | // @grant GM_addStyle 11 | // ==/UserScript== 12 | 13 | // This userscript is a port of the "Letterboxd Lists Progress" Chrome extension by Lucas Franco 14 | 15 | GM_addStyle ( ` 16 | .list-link { 17 | margin-bottom: 23px !important; 18 | } 19 | .wide-sidebar .list-link { 20 | margin-bottom: 31px !important; 21 | } 22 | .list-link.lf-no-margin, .wide-sidebar .list-link.lf-no-margin, .wide-sidebar .lf-progress-container, .lf-progress-container { 23 | margin-bottom: 0 !important; 24 | } 25 | .lf-progress-container { 26 | height: 20px; 27 | background: #14181c; 28 | border-top: 1px solid #456; 29 | position: relative !important; 30 | border: 1px solid #456 !important; 31 | border-radius: 3px; 32 | margin-top: 3px !important; 33 | padding: 4px !important; 34 | box-sizing: border-box; 35 | background-color: #303840; 36 | } 37 | .lf-progress-container:active:after, .lf-progress-container:hover:after { 38 | display: none !important; 39 | } 40 | .lf-progress-bar { 41 | position: absolute; 42 | left: 0; 43 | top: 0; 44 | height: 18px; 45 | background: #40bcf4; 46 | } 47 | .lf-description { 48 | position: relative; 49 | color: #fff; 50 | font-size: 11px; 51 | } 52 | .lf-prgress-counter { 53 | position: absolute; 54 | right: 0; 55 | top: 0; 56 | } 57 | ` ); 58 | 59 | 'use strict'; 60 | window.addEventListener('load', function (e) { 61 | if ($("#add-new-button").length > 0) { 62 | passByLists(); 63 | setInterval(function() { 64 | passByLists(); 65 | }, 500); 66 | } 67 | }, false); 68 | 69 | let passByLists = function () { 70 | $(".list-link:not(.lf-checked), .list-link-stacked:not(.lf-checked)").each(function() { 71 | var $self = $(this), 72 | link = $self.attr("href"), 73 | $where = $(""); 74 | 75 | $self.addClass("lf-checked"); 76 | $where.load(link + " .progress-panel", function() { 77 | if ($where.find(".progress-percentage").text().length < 1) { 78 | return false; 79 | } 80 | 81 | let $progress = $("