// ==UserScript==
// @name Letterboxd to IMDb Links with Ratings and Additional Sites
// @namespace http://tampermonkey.net/
// @version 4.0
// @description Add IMDb, Rotten Tomatoes, Metacritic ratings, and links to Ekşi Sözlük, Trakt, and Sinefil for Letterboxd films using PythonAnywhere API, with console logs for debugging missing information issues
// @author sheeper
// @match https://letterboxd.com/*
// @grant GM_xmlhttpRequest
// @connect sheeper.pythonanywhere.com
// @require https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==
(function() {
'use strict';
const API_URL = 'https://sheeper.pythonanywhere.com/movie_by_letterboxd?link=';
function fetchPythonAnywhereData(filmLink, callback) {
console.log(`Fetching data for: ${filmLink}`);
GM_xmlhttpRequest({
method: 'GET',
url: API_URL + encodeURIComponent(filmLink),
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
console.log(`Data fetched for: ${filmLink}`, data);
callback(data);
} catch (error) {
console.error(`Error parsing response for: ${filmLink}`, error);
}
},
onerror: function(error) {
console.error(`Error fetching data from PythonAnywhere for: ${filmLink}`, error);
}
});
}
function addRatingsFromPythonAnywhereData(data, filmPoster) {
if (data && data.imdbID) {
const imdbId = data.imdbID;
const imdbRating = data.IMDbSc;
const rtRating = data['RT Score'];
const mcRating = data.MetaSc;
const letterboxdRating = data.Lbx;
const ratingsDiv = $('<div>')
.css('background-color', 'rgba(0, 0, 0, 0.7)')
.css('padding', '2px 3px') // Reduced padding
.css('border-radius', '5px')
.css('display', 'flex')
.css('align-items', 'center')
.css('margin-top', '2px') // Reduced margin
.css('font-size', '10px') // Reduced font size
.css('position', 'absolute')
.css('top', '0')
.css('left', '0')
.css('width', '100%')
.css('justify-content', 'space-around');
if (letterboxdRating && letterboxdRating !== 'N/A') {
const letterboxdLogo = $('<a>')
.attr('href', data['Letterboxd Link'])
.attr('target', '_blank')
.append(
$('<img>')
.attr('src', 'https://i.imgur.com/YSiskZp.png')
.attr('alt', 'Letterboxd Logo')
.css('width', '12.5px') // Increased size
.css('vertical-align', 'middle')
.css('margin-right', '1px')
);
const letterboxdRatingText = $('<span>')
.text(letterboxdRating)
.css('vertical-align', 'middle')
.css('color', '#fff')
.css('margin-right', '5px'); // Reduced margin
ratingsDiv.append(letterboxdLogo).append(letterboxdRatingText);
}
if (imdbRating && imdbRating !== 'N/A') {
const imdbLogo = $('<a>')
.attr('href', `https://www.imdb.com/title/${imdbId}`)
.attr('target', '_blank')
.append(
$('<img>')
.attr('src', 'https://upload.wikimedia.org/wikipedia/commons/6/69/IMDB_Logo_2016.svg')
.attr('alt', 'IMDb Logo')
.css('width', '12.5px') // Increased size
.css('vertical-align', 'middle')
.css('margin-right', '1px')
);
const imdbRatingText = $('<span>')
.text(imdbRating)
.css('vertical-align', 'middle')
.css('color', '#fff')
.css('margin-right', '5px'); // Reduced margin
ratingsDiv.append(imdbLogo).append(imdbRatingText);
}
if (rtRating && rtRating !== 'N/A') {
const rtLogo = $('<a>')
.attr('href', `https://www.rottentomatoes.com/search?search=${encodeURIComponent(data.Title)}`)
.attr('target', '_blank')
.append(
$('<img>')
.attr('src', 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/5b/Rotten_Tomatoes.svg/2019px-Rotten_Tomatoes.svg.png')
.attr('alt', 'Rotten Tomatoes Logo')
.css('width', '12.5px') // Increased size
.css('vertical-align', 'middle')
.css('margin-right', '1px')
);
const rtRatingText = $('<span>')
.text(rtRating)
.css('vertical-align', 'middle')
.css('color', '#fff')
.css('margin-right', '5px'); // Reduced margin
ratingsDiv.append(rtLogo).append(rtRatingText);
}
if (mcRating && mcRating !== 'N/A') {
const mcLogo = $('<a>')
.attr('href', `https://www.metacritic.com/search/all/${encodeURIComponent(data.Title)}/results`)
.attr('target', '_blank')
.append(
$('<img>')
.attr('src', 'https://upload.wikimedia.org/wikipedia/commons/2/20/Metacritic.svg')
.attr('alt', 'Metacritic Logo')
.css('width', '12.5px') // Increased size
.css('vertical-align', 'middle')
.css('margin-right', '1px')
);
const mcRatingText = $('<span>')
.text(mcRating)
.css('vertical-align', 'middle')
.css('color', '#fff')
.css('margin-right', '5px'); // Reduced margin
ratingsDiv.append(mcLogo).append(mcRatingText);
}
const ekşiLink = `https://eksisozluk.com/?q=${encodeURIComponent(data.Title)}`;
const traktLink = `https://trakt.tv/search/imdb?query=${encodeURIComponent(imdbId)}`;
const sinefilLink = `https://www.sinefil.com/ara/${encodeURIComponent(imdbId)}`;
filmPoster.css('position', 'relative').append(ratingsDiv);
// Links div
const linksDiv = $('<div>')
.css('background-color', 'rgba(0, 0, 0, 0.7)')
.css('padding', '1px 1px') // Reduced padding
.css('border-radius', '3px')
.css('display', 'flex')
.css('align-items', 'center')
.css('font-size', '10px') // Reduced font size
.css('position', 'absolute')
.css('bottom', '0')
.css('left', '0')
.css('width', '100%')
.css('justify-content', 'space-around');
const ekşiLogo = $('<a>')
.attr('href', ekşiLink)
.attr('target', '_blank')
.append(
$('<img>')
.attr('src', 'https://i.imgur.com/k5K7m9h.png')
.attr('alt', 'Ekşi Sözlük Logo')
.css('width', '10px') // Increased size
.css('vertical-align', 'middle')
.css('margin', '0 1px')
);
const traktLogo = $('<a>')
.attr('href', traktLink)
.attr('target', '_blank')
.append(
$('<img>')
.attr('src', 'https://i.imgur.com/adN3cCW.png')
.attr('alt', 'Trakt Logo')
.css('width', '12.5px') // Increased size
.css('vertical-align', 'middle')
.css('margin', '0 1px')
);
const sinefilLogo = $('<a>')
.attr('href', sinefilLink)
.attr('target', '_blank')
.append(
$('<img>')
.attr('src', 'https://i.imgur.com/Z8E36pP.png')
.attr('alt', 'Sinefil Logo')
.css('width', '12.5px') // Increased size
.css('vertical-align', 'middle')
.css('margin', '0 1px')
);
linksDiv.append(ekşiLogo).append(traktLogo).append(sinefilLogo);
filmPoster.append(linksDiv);
}
}
function processAllFilms() {
const filmLinks = new Set();
$('li.poster-container').each(function() {
const lazyLoadElement = $(this).find('.really-lazy-load, .react-component');
let filmLink = lazyLoadElement.attr('data-target-link');
if (!filmLink) {
filmLink = lazyLoadElement.attr('data-film-link');
}
if (!filmLink) {
filmLink = lazyLoadElement.find('a').attr('href');
}
if (filmLink) {
filmLinks.add(`https://letterboxd.com${filmLink}`);
} else {
console.warn('No film link found for poster container:', $(this).html());
}
});
filmLinks.forEach(filmLink => {
fetchPythonAnywhereData(filmLink, data => {
const filmPoster = $(`li.poster-container:has(.really-lazy-load[data-target-link="${filmLink.replace('https://letterboxd.com', '')}"]), li.poster-container:has(.really-lazy-load[data-film-link="${filmLink.replace('https://letterboxd.com', '')}"]), li.poster-container:has(.react-component[data-film-link="${filmLink.replace('https://letterboxd.com', '')}"])`);
if (filmPoster.length > 0) {
addRatingsFromPythonAnywhereData(data, filmPoster);
} else {
console.warn('No poster container found for film link:', filmLink);
}
});
});
}
function ensureAllPosterContainersLoaded(callback) {
const checkInterval = setInterval(() => {
const posterContainersLoaded = $('li.poster-container').length;
if (posterContainersLoaded > 0) {
clearInterval(checkInterval);
console.log('All poster containers loaded.');
callback();
}
}, 500);
}
function processNewFilms() {
new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
const addedNodes = Array.from(mutation.addedNodes);
addedNodes.filter(node => node.nodeType === Node.ELEMENT_NODE).forEach(node => {
if ($(node).find('.poster-container').length) {
ensureAllPosterContainersLoaded(processAllFilms);
}
});
}
}).observe(document.body, { childList: true, subtree: true });
}
$(document).ready(function() {
ensureAllPosterContainersLoaded(processAllFilms);
processNewFilms();
});
})();