IMDb Watcher

Adds buttons on IMDb to watch movies/series for free

当前为 2025-01-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name IMDb Watcher
  3. // @namespace https://www.imdb.com/
  4. // @icon https://vidsrc.net/template/vidsrc-ico.png
  5. // @version 1.3.0
  6. // @description Adds buttons on IMDb to watch movies/series for free
  7. // @author cevoj35548
  8. // @license MIT
  9. // @match https://*.imdb.com/title/*
  10. // @grant GM_xmlhttpRequest
  11. // @connect api.themoviedb.org
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const sources = [{
  18. name: 'Embed.su',
  19. urls: {
  20. movie: 'https://embed.su/embed/movie/{id}/1/1',
  21. tv: 'https://embed.su/embed/tv/{id}/{season}/{episode}'
  22. },
  23. color: '#9038ED',
  24. textColor: '#f0f0f0'
  25. },
  26. {
  27. name: 'SuperEmbed',
  28. urls: {
  29. movie: 'https://multiembed.mov/?video_id={id}',
  30. tv: 'https://multiembed.mov/?video_id={id}&s={season}&e={episode}'
  31. },
  32. color: '#5009DE',
  33. textColor: '#bad8eb'
  34. },
  35. {
  36. name: 'VidLink',
  37. urls: {
  38. movie: 'https://vidlink.pro/movie/{id}',
  39. tv: 'https://vidlink.pro/tv/{id}/{season}/{episode}'
  40. },
  41. tmdb: true,
  42. color: '#5009DE',
  43. textColor: '#bad8eb'
  44. },
  45. {
  46. name: '2Embed',
  47. urls: {
  48. movie: 'https://www.2embed.stream/embed/movie/{id}',
  49. tv: 'https://www.2embed.stream/embed/tv/{id}/{season}/{episode}'
  50. },
  51. color: '#0058a4',
  52. textColor: '#bad8eb'
  53. },
  54. {
  55. name: 'VidSrc',
  56. urls: {
  57. movie: 'https://vidsrc.net/embed/movie/{id}',
  58. tv: 'https://vidsrc.net/embed/tv/{id}/{season}/{episode}'
  59. },
  60. color: '#125784',
  61. textColor: '#bad8eb'
  62. },
  63. ];
  64.  
  65. const cornerPadding = 20; // Distance from the corner of the screen
  66. const buttonSpacing = 2; // Space between each button in the stack
  67.  
  68. function gmRequest(options) {
  69. console.log("Requesting", options.url)
  70. return new Promise((resolve, reject) => {
  71. GM_xmlhttpRequest({
  72. ...options,
  73. method: "GET",
  74. onload: (response) => resolve(response),
  75. onerror: (error) => reject(error),
  76. });
  77. });
  78. }
  79.  
  80. // helper to convert imdb IDs to tmdb IDs
  81. // source: https://greasyfork.org/en/scripts/437200
  82. async function getTmdbID(imdbId) {
  83. console.log(`Fetching Tmdb ID for ${imdbId}`);
  84. return await new Promise((resolve, reject) => {
  85. GM_xmlhttpRequest({
  86. method: "GET",
  87. url: "https://api.themoviedb.org/3/find/" + imdbId + "?api_key=8d6d91941230817f7807d643736e8a49&language=en-US&external_source=imdb_id",
  88. onload: function(response) {
  89. let respJson = JSON.parse(response.responseText);
  90. let tmdbId = (respJson.tv_results?.[0]?.id) || (respJson.movie_results?.[0]?.id) || (respJson.tv_episode_results?.[0]?.show_id);
  91. if (!tmdbId) {
  92. reject("No Tmdb ID found");
  93. return;
  94. }
  95. console.log(`Tmdb Id: ${tmdbId}`);
  96. resolve(tmdbId);
  97. },
  98. });
  99. });
  100. }
  101.  
  102. // extract season and episode numbers from the IMDb page
  103. function extractSeasonEpisode() {
  104. const seasonEpisodeDiv = document.querySelector('[data-testid="hero-subnav-bar-season-episode-numbers-section"]');
  105. if (seasonEpisodeDiv) {
  106. const seasonEpisodeText = seasonEpisodeDiv.textContent.trim();
  107. const match = seasonEpisodeText.match(/S(\d+).E(\d+)/);
  108. if (match) {
  109. return {
  110. season: match[1],
  111. episode: match[2]
  112. };
  113. }
  114. }
  115. return null;
  116. }
  117.  
  118. // extract the series ID from the IMDb page
  119. function extractSeriesId() {
  120. const seriesLink = document.querySelector('[data-testid="hero-title-block__series-link"]');
  121. if (seriesLink) {
  122. const href = seriesLink.getAttribute('href');
  123. const match = href.match(/\/title\/(tt\d+)\//);
  124. if (match) {
  125. return match[1];
  126. }
  127. }
  128. return null;
  129. }
  130.  
  131. // generate the URL for a specific source
  132. async function generateUrl(urls, isMovie, isEpisode, imdbId, seriesId, seasonEpisode, isTmdb) {
  133. // get original ID
  134. imdbId = seriesId || imdbId;
  135. if (isTmdb) {
  136. imdbId = await getTmdbID(imdbId);
  137. }
  138.  
  139. if (isMovie && isEpisode) {
  140. return urls.movie.replace('{id}', imdbId);
  141. }
  142.  
  143. if (seasonEpisode && seriesId) {
  144. const { season, episode } = seasonEpisode;
  145. return urls.tv.replace('{id}', imdbId).replace('{season}', season).replace('{episode}', episode);
  146. } else {
  147. // console.log("Found no season/episode pair. Using s01e01")
  148. return urls.tv.replace('{id}', imdbId).replace('{season}', '1').replace('{episode}', '1');
  149. }
  150. }
  151.  
  152. sources.reverse().forEach((source, index) => {
  153. const button = document.createElement('button');
  154. button.textContent = `📽 Watch - ${source.name}`;
  155. button.style.fontFamily = 'Arial';
  156. button.style.position = 'fixed';
  157. button.style.bottom = `${cornerPadding + index * (40 + buttonSpacing)}px`;
  158. button.style.right = `${cornerPadding}px`;
  159. button.style.padding = '10px';
  160. button.style.background = source.color;
  161. button.style.color = source.textColor;
  162. button.style.border = 'none';
  163. button.style.cursor = 'pointer';
  164. button.style.fontWeight = 'bold';
  165. button.style.borderRadius = '6px';
  166. button.style.zIndex = '9999';
  167. button.style.filter = 'drop-shadow(0 10px 8px rgb(0 0 0 / 0.05)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1))';
  168.  
  169. let originalText = null;
  170. let disableBtn = () => {
  171. originalText = button.textContent;
  172. button.textContent = "⏳ Loading...";
  173. button.disabled = true;
  174. };
  175. let enableBtn = () => {
  176. button.textContent = originalText;
  177. button.disabled = false;
  178. };
  179.  
  180. // add click event to redirect to the source URL
  181. button.addEventListener('click', () => {
  182. disableBtn();
  183.  
  184. const imdbId = window.location.pathname.split('/')[2];
  185. const isMovie = document.title.indexOf('TV Series') === -1;
  186. const isEpisode = document.title.indexOf('TV Episode') === -1;
  187. const seasonEpisode = extractSeasonEpisode();
  188. const seriesId = extractSeriesId();
  189.  
  190. generateUrl(source.urls, isMovie, isEpisode, imdbId, seriesId, seasonEpisode, !!source.tmdb)
  191. .then(url => {
  192. console.log(`Opening ${url}`)
  193. window.open(url, '_blank');
  194. enableBtn();
  195. })
  196. .catch(error => {
  197. console.error("Error generating URL:", error);
  198. enableBtn();
  199. alert(`Failed to generate URL: ${error}`);
  200. });
  201. });
  202.  
  203. document.body.appendChild(button);
  204. });
  205. })();