您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add Letterboxd link and rating to its corresponding Plex film's page
当前为
- // ==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.46
- // ==/UserScript==
- (function() {
- 'use strict';
- const letterboxdImg = 'data:image/png;base64,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';
- var lastTitle = undefined;
- var lastYear = undefined;
- 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) {
- let metadataElement = document.querySelector('div[data-testid="metadata-ratings"]');
- if (!metadataElement) {
- metadataElement = document.querySelector('div[data-testid="metadata-children"]');
- }
- const existingContainer = document.querySelector('.letterboxd-container');
- if (existingContainer) {
- existingContainer.querySelector('a').href = link;
- const ratingElement = existingContainer.querySelector('.letterboxd-rating');
- if (ratingElement) {
- ratingElement.textContent = rating ? `Rating: ${rating}` : 'Rating not available';
- }
- } 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; // ? rating : "Director's Page"; That was neat for director's pages, but shows up with films that don't have ratings
- ratingText.style.cssText = 'font-size: 14px;'; // Style as needed
- const linkElement = document.createElement('a');
- linkElement.href = link;
- linkElement.appendChild(icon);
- container.appendChild(linkElement);
- container.appendChild(ratingText);
- metadataElement.insertAdjacentElement('afterend', container);
- }
- }
- function buildDefaultLetterboxdUrl(title, year) {
- const normalizedTitle = title.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
- const titleSlug = normalizedTitle.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;
- }
- }
- 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) {
- const isAlbumPage = document.querySelector('[class^="AlbumDisc"]');
- if (isAlbumPage) {
- console.log('Detected an album page, not proceeding with Letterboxd icon creation.');
- return;
- }
- extractData();
- if (lastTitle !== lastProcessedTitle || lastYear !== lastProcessedYear) {
- lastProcessedTitle = lastTitle;
- lastProcessedYear = lastYear;
- if (lastTitle && lastYear) {
- buildLetterboxdUrl(lastTitle, lastYear).then(url => {
- fetchLetterboxdPage(url).then(html => {
- //console.log(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
- });
- }
- })();