// ==UserScript== // @name Plex Letterboxd links // @namespace http://tampermonkey.net/ // @description Add Letterboxd link to its corresponding Plex film's page // @author CarnivalHipster // @match https://app.plex.tv/* // @icon https://www.google.com/s2/favicons?sz=64&domain=plex.tv // @license MIT // @grant GM_xmlhttpRequest // @connect letterboxd.com // @version 1.2 // @downloadURL none // ==/UserScript== (function() { 'use strict'; const letterboxdImg = 'https://www.google.com/s2/favicons?sz=64&domain=letterboxd.com'; var lastTitle = undefined; var lastYear = undefined; //Function Definitions function extractData() { const titleElement = document.querySelector('h1[data-testid="metadata-title"]'); const yearElement = document.querySelector('span[data-testid="metadata-line1"]'); if (titleElement) { const title = titleElement.textContent.trim() || titleElement.innerText.trim(); if (title !== lastTitle) { lastTitle = title; console.log('The title is:', lastTitle); } } else { lastTitle = ''; // Reset if no title is found } if (yearElement) { const text = yearElement.textContent.trim() || yearElement.innerText.trim(); const match = text.match(/\b\d{4}\b/); if (match && match[0] !== lastYear) { lastYear = match[0]; console.log('The year is:', lastYear); } } else { lastYear = ''; // Reset if no year is found } } function checkLink(url) { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'HEAD', url: url, onload: function(response) { if (response.status >= 200 && response.status < 300) { resolve({url: url, status: response.status, accessible: true}); } else { resolve({url: url, status: response.status, accessible: false}); } }, onerror: function() { reject(new Error(url + ' could not be reached or is blocked by CORS policy.')); } }); }); } function updateOrCreateLetterboxdIcon(link) { const existingIcon = document.querySelector('.letterboxd-icon'); const metadataElement = document.querySelector('div[data-testid="metadata-children"]'); if (existingIcon) { existingIcon.href = link; } else if (metadataElement) { const icon = document.createElement('img'); icon.src = letterboxdImg; icon.alt = 'Letterboxd Icon'; icon.classList.add('letterboxd-icon'); icon.style.cssText = 'width: 24px; height: 24px; cursor: pointer;'; const linkElement = document.createElement('a'); linkElement.href = link; //linkElement.target = '_blank'; // Open in a new tab linkElement.appendChild(icon); metadataElement.insertAdjacentElement('afterend', linkElement); } } function buildDefaultLetterboxdUrl(title, year) { const titleSlug = title.trim().toLowerCase() .replace(/&/g, 'and') .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-'); const letterboxdBaseUrl = 'https://letterboxd.com/film/'; return `${letterboxdBaseUrl}${titleSlug}-${year}/`; } function removeYearFromUrl(url) { const yearPattern = /-\d{4}(?=\/$)/; return url.replace(yearPattern, ''); } function replaceFilmWithDirector(url) { return url.replace('film','director'); } function buildLetterboxdUrl(title, year) { let defaultUrl = buildDefaultLetterboxdUrl(title, year); return checkLink(defaultUrl).then(result => { if (result.accessible) { console.log(result.url, 'is accessible, status:', result.status); return result.url; } else { console.log(result.url, 'is not accessible, status:', result.status); let yearRemovedUrl = removeYearFromUrl(result.url); console.log('Trying URL without year:', yearRemovedUrl); return checkLink(yearRemovedUrl).then(yearRemovedResult => { if (yearRemovedResult.accessible) { console.log(yearRemovedUrl, 'is accessible, status:', yearRemovedResult.status); return yearRemovedUrl; } else { console.log(yearRemovedUrl, 'is not accessible, status:', yearRemovedResult.status); let directorUrl = replaceFilmWithDirector(yearRemovedUrl); console.log('Trying director URL:', directorUrl); return directorUrl; } }); } }).catch(error => { console.error('Error after checking both film and year:', error.message); let newUrl = removeYearFromUrl(defaultUrl); return newUrl; }); } // Main if(document.readyState === 'complete' || document.readyState === 'loaded' || document.readyState === 'interactive') { main(); } else { document.addEventListener('DOMContentLoaded', main); } function main() { function observerCallback(mutationsList, observer) { extractData(); if (lastTitle && lastYear) { buildLetterboxdUrl(lastTitle, lastYear).then(url => { updateOrCreateLetterboxdIcon(url); }).catch(error => { console.error('Error building Letterboxd URL:', error); }); } else { console.log('Title or year not found, not updating Letterboxd icon.'); } } const observer = new MutationObserver(observerCallback); observer.observe(document.body, { childList: true, characterData: true, subtree: true }); } })();