-
Notifications
You must be signed in to change notification settings - Fork 1
/
Letterboxd rating on IMDb.user.js
284 lines (261 loc) · 14.9 KB
/
Letterboxd rating on IMDb.user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
// ==UserScript==
// @name Letterboxd ratings on IMDb
// @version 1.0.4
// @namespace https://github.com/chrisjp
// @description Shows a film's Letterboxd rating on its IMDb page.
// @license MIT
// @updateURL https://raw.githubusercontent.com/chrisjp/LetterboxdOnIMDb/master/Letterboxd%20rating%20on%20IMDb.user.js
// @downloadURL https://raw.githubusercontent.com/chrisjp/LetterboxdOnIMDb/master/Letterboxd%20rating%20on%20IMDb.user.js
// @homepageURL https://github.com/chrisjp/LetterboxdOnIMDb
// @supportURL https://github.com/chrisjp/LetterboxdOnIMDb/issues
// @match https://*.imdb.com/title/tt*
// @icon https://www.google.com/s2/favicons?sz=64&domain=www.imdb.com
// @connect letterboxd.com
// @grant GM.xmlHttpRequest
// @grant GM.addStyle
// @run-at document-end
// @noframes
// ==/UserScript==
(function() {
'use strict';
// Perform some checks...
// 1. that this film has a rating (i.e. it's been released)
// 2. User rating button. This is useful in rare cases where a film is too obscure or new to have
// enough ratings (>=5) for IMDb to display an average
// 3. Popularity meter. If neither of the above are found it's likely an upcoming film that has trailers
// or other pre-release media available such that it's getting attention.
// Obviously no rating can be displayed for such films, but we can at least link to the Letterboxd
// page (if it exists) for convenience.
let filmHasAggRating = document.querySelector('[data-testid="hero-rating-bar__aggregate-rating__score"]');
let filmHasYourRating = document.querySelector('[data-testid="hero-rating-bar__user-rating"]');
let filmHasPopularity = document.querySelector('[data-testid="hero-rating-bar__popularity"]');
// Get IMDb ID from the URL
let imdbId = document.URL.match(/\/tt([0-9]+)\//)[1];
if (imdbId > 0 && (filmHasAggRating !== null || filmHasYourRating !== null || filmHasPopularity !== null)) {
//console.log("IMDb ID: " + imdbId);
getLetterboxdRating(imdbId);
}
else {
console.log("Letterboxd on IMDb user script: No rating bar found. Film has probably not yet been released, or we're on a subpage of this film.");
}
})();
function getLetterboxdRating(imdbId)
{
// Letterboxd can redirect to a film's page if provided the IMDb ID (minus 'tt' prefix).
// Formatted as follows: https://letterboxd.com/imdb/0000000
const letterboxd = "https://letterboxd.com";
const url = letterboxd + "/imdb/" + imdbId + "/";
GM.xmlHttpRequest({
method: "GET",
timeout: 10000,
url: url,
onload: function(response) {
if (response.finalUrl != url) {
// We were redirected, so the film exists on Letterboxd. Now we can get its ID,
// or in this case the URL slug identifying it. With that we can then generate
// a URL to its rating histogram, which we can then parse to obtain ratings data.
const letterboxdUrl = response.finalUrl;
const letterboxdId = letterboxdUrl.split(letterboxd)[1];
const letterboxdHistUrl = letterboxd + "/csi" + letterboxdId + "rating-histogram/";
console.log("Letterboxd histogram URL for this film: " + letterboxdHistUrl);
getLetterboxdHistogram(letterboxdUrl, letterboxdHistUrl);
}
else {
// We did not get redirected to the film's URL on Letterboxd, meaning it hasn't been added to their database
// or doesn't have an IMDb ID associated with it.
console.log("Film not found on Letterboxd.");
}
},
onerror: function() {
console.log("Letterboxd on IMDb user script: Request Error in getLetterboxdRating.");
},
onabort: function() {
console.log("Letterboxd on IMDb user script: Request is aborted in getLetterboxdRating");
},
ontimeout: function() {
console.log("Letterboxd on IMDb user script: Request timed out in getLetterboxdRating.");
}
});
}
function getLetterboxdHistogram(letterboxdUrl, letterboxdHistUrl)
{
// Scraping the rating histogram page is much more reliable than the film page
// especially for films with very few ratings
GM.xmlHttpRequest({
method: "GET",
timeout: 10000,
url: letterboxdHistUrl,
onload: function(response) {
const parser = new DOMParser();
const result = parser.parseFromString(response.responseText, "text/html");
// Parse the scraped HTML if we have a .display-rating element.
const letterboxdRatingA = result.getElementsByClassName("display-rating")[0];
if (letterboxdRatingA) {
const letterboxdRating = parseFloat(letterboxdRatingA.innerText);
const letterboxdTotalRatingsText = letterboxdRatingA.title;
const letterboxdTotalRatings = parseInt(letterboxdTotalRatingsText.match("based on \(.*\)ratings")[1].replaceAll(",",""));
addLetterboxdRatingToIMDb(letterboxdUrl, letterboxdRating, letterboxdTotalRatings);
}
else {
// If we reached this point it's almost certainly because the film does not yet have enough ratings for Letterboxd
// to calculate the weighted average. Check for "not enough ratings" text to confirm, then manually calculate.
let letterboxdTotalRatings = 0;
const notEnoughRatings = result.querySelector('[title="Not enough ratings to calculate average"]');
if (notEnoughRatings) {
// we can try to manually calculate the number of ratings
const regexCalc = /title="(\d) /gm;
const matches = response.responseText.matchAll(regexCalc);
for (let match of matches) {
letterboxdTotalRatings += parseInt(match[1]);
}
console.log("Manually counted " + letterboxdTotalRatings + " ratings on Letterboxd for this film.");
}
else {
// If the "not enough ratings" text can't be found that means there's no ratings at all.
// This will usually mean it's a currently unreleased film. We can still try to show
// a link to the Letterboxd page without any rating data.
console.log("Film exists on Letterboxd but has no ratings data.");
}
letterboxdTotalRatings = letterboxdTotalRatings > 0 ? letterboxdTotalRatings : "-";
addLetterboxdRatingToIMDb(letterboxdUrl, "-", letterboxdTotalRatings);
}
},
onerror: function() {
console.log("Letterboxd on IMDb user script: Request Error in getLetterboxdHistogram.");
},
onabort: function() {
console.log("Letterboxd on IMDb user script: Request is aborted in getLetterboxdHistogram.");
},
ontimeout: function() {
console.log("Letterboxd on IMDb user script: Request timed out in getLetterboxdHistogram.");
}
});
}
function addLetterboxdRatingToIMDb(letterboxdUrl, letterboxdRating, letterboxdTotalRatings)
{
// Since a lot of relevant class names are random on each page load... Basically we:
// 1. get the div.rating-bar__base-button elements
// 2. clone the first one (IMDb average user rating)
// 3. set its HTML to the information we just scraped from Letterboxd
// 4. add it to the DOM with the other div.rating-bar__base-button elements
// That way it keeps all IMDB's styling and looks like a normal part of the page
// Clone the node
let ratingBarBtns = document.querySelectorAll(".rating-bar__base-button");
let ratingBarBtnLetterboxd = ratingBarBtns[0].cloneNode(true);
// Add CSS (this forces it to the leftmost position in the ratings bar)
// Also adds CSS for Letterboxd button on films without a rating yet.
ratingBarBtnLetterboxd.classList.add('letterboxd-rating');
GM.addStyle(`
.letterboxd-rating { order: -1; }
.letterboxd-rating-bottom {
color: var(--ipt-on-baseAlt-textSecondary-color, rgba(255,255,255,0.7));
font-family: var(--ipt-font-family);
font-size: var(--ipt-type-bodySmall-size, .875rem);
font-weight: var(--ipt-type-bodySmall-weight, 400);
letter-spacing: var(--ipt-type-bodySmall-letterSpacing, .01786em);
line-height: var(--ipt-type-bodySmall-lineHeight, 1.25rem);
text-transform: var(--ipt-type-bodySmall-textTransform, none);
}
@media screen and (min-width: 1024px) {
.letterboxd-rating-bottom {
font-family: var(--ipt-font-family);
font-size: var(--ipt-type-copyright-size, .75rem);
font-weight: var(--ipt-type-copyright-weight, 400);
letter-spacing: var(--ipt-type-copyright-letterSpacing, .03333em);
line-height: var(--ipt-type-copyright-lineHeight, 1rem);
text-transform: var(--ipt-type-copyright-textTransform, none);
}
}
`);
// Set title
ratingBarBtnLetterboxd.children[0].innerHTML = "Letterboxd".toUpperCase();
// If the cloned node is the IMDb aggregate rating we can simply overwrite the child elements' innerHTML
// with data we've obtained from Letterboxd
if (ratingBarBtnLetterboxd.dataset.testid === "hero-rating-bar__aggregate-rating") {
console.log("We have a valid IMDb rating. Adding Letterboxd rating to DOM...");
// set a.href
let letterboxdElementA = ratingBarBtnLetterboxd.children[1];
letterboxdElementA.href = letterboxdUrl;
// edit all its child elements
let letterboxdElementADiv = letterboxdElementA.children[0].children[0];
// icon set to 24x24
letterboxdElementADiv.children[0].innerHTML = '<img src="https://www.google.com/s2/favicons?sz=64&domain=letterboxd.com" alt="" width="24" height="24">';
// ratings data
let letterboxdElementRatingDiv = letterboxdElementADiv.children[1];
// average rating
letterboxdElementRatingDiv.children[0].children[0].innerHTML = letterboxdRating;
letterboxdElementRatingDiv.children[0].children[1].innerHTML = "/5";
// total ratings
letterboxdElementRatingDiv.children[2].innerHTML = letterboxdTotalRatings != "-" ? numRound(letterboxdTotalRatings) : "-";
// data-testid
letterboxdElementRatingDiv.children[0].dataset.testid = "hero-rating-bar__letterboxd-rating";
}
// If the cloned node is NOT the IMDb aggregate rating (it doesn't have one) it'll be the button allowing us to rate it if logged in
// The child nodes of the <button> are very similar so we can still modify the HTML to show the Letterboxd rating, then add our own
// <div> to display the total number of ratings.
else if (ratingBarBtnLetterboxd.dataset.testid === "hero-rating-bar__user-rating") {
console.log("We don't have a valid IMDb rating. Adding Letterboxd link to DOM with manual rate count...");
let btnNode = ratingBarBtnLetterboxd.children[1];
let btnChildNode = ratingBarBtnLetterboxd.children[1].children[0];
// create <a> element
let letterboxdElementA = document.createElement("a");
letterboxdElementA.className = btnNode.classList.toString();
letterboxdElementA.href = letterboxdUrl;
// clone the <button>'s child node (should be a span) and append it to our <a>
letterboxdElementA.append(btnChildNode.cloneNode(true));
// edit all its child elements
let letterboxdElementADiv = letterboxdElementA.children[0].children[0];
// icon set to 24x24
letterboxdElementADiv.children[0].innerHTML = '<img src="https://www.google.com/s2/favicons?sz=64&domain=letterboxd.com" alt="" width="24" height="24">';
// ratings data container
let letterboxdElementRatingDiv = letterboxdElementADiv.children[1];
// average rating
letterboxdElementRatingDiv.children[0].children[0].innerHTML = letterboxdRating;
letterboxdElementRatingDiv.children[0].innerHTML = letterboxdElementRatingDiv.children[0].innerHTML.replace("/10", "/5");
// total ratings (need to make our own div for this)
let letterboxdTotalRatingsDiv = document.createElement("div");
letterboxdTotalRatingsDiv.className = "letterboxd-rating-bottom";
letterboxdTotalRatingsDiv.innerHTML = letterboxdTotalRatings;
letterboxdElementRatingDiv.append(letterboxdTotalRatingsDiv);
// replace the <button> with the <a> we created and modified above
btnNode.replaceWith(letterboxdElementA);
}
// If we get this far the film must be an upcoming one that's getting enough attention to trigger the Popularity Meter.
// We won't have any ratings to display here obviously, but we can at least link to the Letterboxd page for convenience.
else {
console.log("We don't have a valid IMDb rating. This is probably an unreleased film. Adding Letterboxd link to DOM...");
// set a.href
let letterboxdElementA = ratingBarBtnLetterboxd.children[1];
letterboxdElementA.href = letterboxdUrl;
// edit all its child elements
let letterboxdElementADiv = letterboxdElementA.children[0].children[0];
// icon set to 24x24
letterboxdElementADiv.children[0].innerHTML = '<img src="https://www.google.com/s2/favicons?domain=letterboxd.com&sz=64" alt="" width="24" height="24">';
// replace score and delta and change data-testid
letterboxdElementADiv.children[1].dataset.testid = "hero-rating-bar__letterboxd-link";
letterboxdElementADiv.children[1].children[0].dataset.testid = "";
letterboxdElementADiv.children[1].children[0].innerText = "View";
letterboxdElementADiv.children[1].children[1].remove();
}
// Add the finished element to the DOM
ratingBarBtnLetterboxd.dataset.testid = "hero-rating-bar__letterboxd-rating";
ratingBarBtns[0].parentNode.appendChild(ratingBarBtnLetterboxd);
}
function numRound(num)
{
// https://stackoverflow.com/a/68273755/403476
num = Math.abs(Number(num))
const billions = num/1.0e+9
const millions = num/1.0e+6
const thousands = num/1.0e+3
return num >= 1.0e+9 && billions >= 100 ? Math.round(billions) + "B"
: num >= 1.0e+9 && billions >= 10 ? billions.toFixed(1) + "B"
: num >= 1.0e+9 ? billions.toFixed(2) + "B"
: num >= 1.0e+6 && millions >= 100 ? Math.round(millions) + "M"
: num >= 1.0e+6 && millions >= 10 ? millions.toFixed(1) + "M"
: num >= 1.0e+6 ? millions.toFixed(2) + "M"
: num >= 1.0e+3 && thousands >= 100 ? Math.round(thousands) + "K"
: num >= 1.0e+3 && thousands >= 10 ? thousands.toFixed(1) + "K"
: num >= 1.0e+3 ? thousands.toFixed(2) + "K"
: num.toFixed()
}