Plex Letterboxd link and rating

Add Letterboxd link and rating to its corresponding Plex film's page

目前为 2023-12-30 提交的版本。查看 最新版本

// ==UserScript==
// @name         Plex Letterboxd link and rating
// @namespace    http://tampermonkey.net/
// @description  Add Letterboxd link and rating 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.4
// ==/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, rating) {
    const existingContainer = document.querySelector('.letterboxd-container');
    const metadataElement = document.querySelector('div[data-testid="metadata-ratings"]');

    if (existingContainer) {
        existingContainer.querySelector('a').href = link;
        const ratingElement = existingContainer.querySelector('.letterboxd-rating');
        if (ratingElement) {
            ratingElement.textContent = rating;
        }
    } else if (metadataElement) {
        const container = document.createElement('div');
        container.classList.add('letterboxd-container');
        container.style.cssText = 'display: flex; align-items: center; gap: 8px;';

        const icon = document.createElement('img');
        icon.src = letterboxdImg;
        icon.alt = 'Letterboxd Icon';
        icon.style.cssText = 'width: 24px; height: 24px; cursor: pointer;';

        const ratingText = document.createElement('span');
        ratingText.classList.add('letterboxd-rating');
        ratingText.textContent = rating;
        ratingText.style.cssText = 'font-size: 14px;';

        const linkElement = document.createElement('a');
        linkElement.href = link;
        // linkElement.target = '_blank'; // Uncomment if you want to open in a new tab
        linkElement.appendChild(icon);
        container.appendChild(linkElement);
        container.appendChild(ratingText);
        metadataElement.insertAdjacentElement('afterend', container);
    }
}


    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;
        });
    }

    function fetchLetterboxdPage(url) {
        return new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: 'GET',
                url: url,
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300) {
                        resolve(response.responseText);
                    } else {
                        reject(new Error('Failed to load Letterboxd page'));
                    }
                },
                onerror: function() {
                    reject(new Error('Network error while fetching Letterboxd page'));
                }
            });
        });
    }

    function roundToOneDecimal(numberString) {
        const number = parseFloat(numberString);
        return isNaN(number) ? null : (Math.round(number * 10) / 10).toFixed(1);
    }

    function extractRating(html) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const ratingElement = doc.querySelector('meta[name="twitter:data2"]');

        if (ratingElement && ratingElement.content) {
            const match = ratingElement.getAttribute('content').match(/\b\d+\.\d{1,2}\b/);
            if (match) {
                return roundToOneDecimal(match[0]);
            }
        } else {
            console.log('Rating element not found.');
            return null;
        }
    }

    // Main
    if(document.readyState === 'complete' || document.readyState === 'loaded' || document.readyState === 'interactive') {
        main();
    } else {
        document.addEventListener('DOMContentLoaded', main);
    }

function main() {
    var lastProcessedTitle = undefined;
    var lastProcessedYear = undefined;

    function observerCallback(mutationsList, observer) {
        extractData();
        if (lastTitle !== lastProcessedTitle || lastYear !== lastProcessedYear) {
            lastProcessedTitle = lastTitle;
            lastProcessedYear = lastYear;

            if (lastTitle && lastYear) {
                buildLetterboxdUrl(lastTitle, lastYear).then(url => {
                    fetchLetterboxdPage(url).then(html => {
                        const rating = extractRating(html);
                        updateOrCreateLetterboxdIcon(url, rating);
                    }).catch(error => {
                        console.error('Error fetching or parsing Letterboxd page:', error);
                    });
                }).catch(error => {
                    console.error('Error building Letterboxd URL:', error);
                });
            }
        }
    }

    const observer = new MutationObserver(observerCallback);
    observer.observe(document.body, {
        childList: true,
        characterData: true,
        subtree: true
    });
}
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址