Plex Letterboxd links

Add Letterboxd link to its corresponding Plex film's page

当前为 2023-12-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Plex Letterboxd links
  3. // @namespace http://tampermonkey.net/
  4. // @description Add Letterboxd link to its corresponding Plex film's page
  5. // @author CarnivalHipster
  6. // @match https://app.plex.tv/*
  7. // @icon https://www.google.com/s2/favicons?sz=64&domain=plex.tv
  8. // @license MIT
  9. // @grant GM_xmlhttpRequest
  10. // @version 1.1
  11. // ==/UserScript==
  12. (function() {
  13. 'use strict';
  14. const letterboxdImg = 'https://www.google.com/s2/favicons?sz=64&domain=letterboxd.com';
  15. var lastTitle = undefined;
  16. var lastYear = undefined;
  17. //Function Definitions
  18. function extractData() {
  19. const titleElement = document.querySelector('h1[data-testid="metadata-title"]');
  20. const yearElement = document.querySelector('span[data-testid="metadata-line1"]');
  21.  
  22. if (titleElement) {
  23. const title = titleElement.textContent.trim() || titleElement.innerText.trim();
  24. if (title !== lastTitle) {
  25. lastTitle = title;
  26. console.log('The title is:', lastTitle);
  27. }
  28. } else {
  29. lastTitle = ''; // Reset if no title is found
  30. }
  31.  
  32. if (yearElement) {
  33. const text = yearElement.textContent.trim() || yearElement.innerText.trim();
  34. const match = text.match(/\b\d{4}\b/);
  35. if (match && match[0] !== lastYear) {
  36. lastYear = match[0];
  37. console.log('The year is:', lastYear);
  38. }
  39. } else {
  40. lastYear = ''; // Reset if no year is found
  41. }
  42. }
  43.  
  44.  
  45. function checkLink(url) {
  46. return new Promise((resolve, reject) => {
  47. GM.xmlHttpRequest({
  48. method: 'HEAD',
  49. url: url,
  50. onload: function(response) {
  51. if (response.status >= 200 && response.status < 300) {
  52. resolve({url: url, status: response.status, accessible: true});
  53. } else {
  54. resolve({url: url, status: response.status, accessible: false});
  55. }
  56. },
  57. onerror: function() {
  58. reject(new Error(url + ' could not be reached or is blocked by CORS policy.'));
  59. }
  60. });
  61. });
  62. }
  63.  
  64. function updateOrCreateLetterboxdIcon(link) {
  65. const existingIcon = document.querySelector('.letterboxd-icon');
  66. const metadataElement = document.querySelector('div[data-testid="metadata-children"]');
  67.  
  68. if (existingIcon) {
  69. existingIcon.href = link;
  70. } else if (metadataElement) {
  71. const icon = document.createElement('img');
  72. icon.src = letterboxdImg;
  73. icon.alt = 'Letterboxd Icon';
  74. icon.classList.add('letterboxd-icon');
  75. icon.style.cssText = 'width: 24px; height: 24px; cursor: pointer;';
  76.  
  77. const linkElement = document.createElement('a');
  78. linkElement.href = link;
  79. //linkElement.target = '_blank'; // Open in a new tab
  80. linkElement.appendChild(icon);
  81.  
  82. metadataElement.insertAdjacentElement('afterend', linkElement);
  83. }
  84. }
  85.  
  86. function buildDefaultLetterboxdUrl(title, year) {
  87. const titleSlug = title.trim().toLowerCase()
  88. .replace(/&/g, 'and')
  89. .replace(/[^\w\s-]/g, '')
  90. .replace(/\s+/g, '-');
  91.  
  92. const letterboxdBaseUrl = 'https://letterboxd.com/film/';
  93. return `${letterboxdBaseUrl}${titleSlug}-${year}/`;
  94. }
  95.  
  96.  
  97. function removeYearFromUrl(url) {
  98. const yearPattern = /-\d{4}(?=\/$)/;
  99. return url.replace(yearPattern, '');
  100. }
  101.  
  102. function replaceFilmWithDirector(url) {
  103. return url.replace('film','director');
  104. }
  105.  
  106. function buildLetterboxdUrl(title, year) {
  107. let defaultUrl = buildDefaultLetterboxdUrl(title, year);
  108. return checkLink(defaultUrl).then(result => {
  109. if (result.accessible) {
  110. console.log(result.url, 'is accessible, status:', result.status);
  111. return result.url;
  112. } else {
  113. console.log(result.url, 'is not accessible, status:', result.status);
  114. let yearRemovedUrl = removeYearFromUrl(result.url);
  115. console.log('Trying URL without year:', yearRemovedUrl);
  116. return checkLink(yearRemovedUrl).then(yearRemovedResult => {
  117. if (yearRemovedResult.accessible) {
  118. console.log(yearRemovedUrl, 'is accessible, status:', yearRemovedResult.status);
  119. return yearRemovedUrl;
  120. } else {
  121. console.log(yearRemovedUrl, 'is not accessible, status:', yearRemovedResult.status);
  122. let directorUrl = replaceFilmWithDirector(yearRemovedUrl);
  123. console.log('Trying director URL:', directorUrl);
  124. return directorUrl;
  125. }
  126. });
  127. }
  128. }).catch(error => {
  129. console.error('Error after checking both film and year:', error.message);
  130. let newUrl = removeYearFromUrl(defaultUrl);
  131. return newUrl;
  132. });
  133. }
  134.  
  135.  
  136.  
  137. // Main
  138. if(document.readyState === 'complete' || document.readyState === 'loaded' || document.readyState === 'interactive') {
  139. main();
  140. } else {
  141. document.addEventListener('DOMContentLoaded', main);
  142. }
  143.  
  144. function main() {
  145. function observerCallback(mutationsList, observer) {
  146. extractData();
  147. if (lastTitle && lastYear) {
  148. buildLetterboxdUrl(lastTitle, lastYear).then(url => {
  149. updateOrCreateLetterboxdIcon(url);
  150. }).catch(error => {
  151. console.error('Error building Letterboxd URL:', error);
  152. });
  153. } else {
  154. console.log('Title or year not found, not updating Letterboxd icon.');
  155. }
  156. }
  157. const observer = new MutationObserver(observerCallback);
  158. observer.observe(document.body, {
  159. childList: true,
  160. characterData: true,
  161. subtree: true
  162. });
  163. }
  164.  
  165. })();