Greasy Fork 支持简体中文。

IMDb Watcher

Watch movies & series for free on IMDb

  1. // ==UserScript==
  2. // @name IMDb Watcher
  3. // @namespace https://www.imdb.com/
  4. // @icon https://vidsrc.net/template/vidsrc-ico.png
  5. // @version 2.1.1
  6. // @description Watch movies & series for free on IMDb
  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: 'VidLink',
  19. urls: {
  20. movie: 'https://vidlink.pro/movie/{id}?autoplay=true',
  21. tv: 'https://vidlink.pro/tv/{id}/{season}/{episode}?autoplay=true'
  22. },
  23. tmdb: true,
  24. },
  25. {
  26. name: 'Embed.su',
  27. urls: {
  28. movie: 'https://embed.su/embed/movie/{id}/1/1',
  29. tv: 'https://embed.su/embed/tv/{id}/{season}/{episode}'
  30. },
  31. },
  32. {
  33. name: 'SuperEmbed',
  34. urls: {
  35. movie: 'https://multiembed.mov/?video_id={id}',
  36. tv: 'https://multiembed.mov/?video_id={id}&s={season}&e={episode}'
  37. },
  38. },
  39. {
  40. name: '2Embed',
  41. urls: {
  42. movie: 'https://www.2embed.stream/embed/movie/{id}',
  43. tv: 'https://www.2embed.stream/embed/tv/{id}/{season}/{episode}'
  44. },
  45. }
  46. ];
  47.  
  48. const buttonSpacing = 10; // Space between buttons
  49. const tmdbCache = {}; // Cache the imdb -> tmdb resps
  50.  
  51. // helper to convert IMDb IDs to TMDb IDs
  52. async function getTmdbID(imdbId) {
  53. // check cache
  54. if (tmdbCache[imdbId]) {
  55. console.log(`Cache hit for ${imdbId}`);
  56. return tmdbCache[imdbId];
  57. }
  58.  
  59. // fetch
  60. console.log(`Fetching TMDb ID for ${imdbId}`);
  61. return new Promise((resolve, reject) => {
  62. GM_xmlhttpRequest({
  63. method: "GET",
  64. url: `https://api.themoviedb.org/3/find/${imdbId}?api_key=8d6d91941230817f7807d643736e8a49&language=en-US&external_source=imdb_id`,
  65. onload: function(response) {
  66. const respJson = JSON.parse(response.responseText);
  67. const tmdbId =
  68. respJson.tv_results?.[0]?.id ||
  69. respJson.movie_results?.[0]?.id ||
  70. respJson.tv_episode_results?.[0]?.show_id;
  71. if (!tmdbId) {
  72. reject("No TMDb ID found");
  73. return;
  74. }
  75. console.log(`TMDb ID: ${tmdbId}`);
  76. tmdbCache[imdbId] = tmdbId; // cache the result
  77. resolve(tmdbId);
  78. },
  79. });
  80. });
  81. }
  82.  
  83. // extract season and episode numbers from the IMDb page
  84. function extractSeasonEpisode() {
  85. const seasonEpisodeDiv = document.querySelector('[data-testid="hero-subnav-bar-season-episode-numbers-section"]');
  86. if (seasonEpisodeDiv) {
  87. const seasonEpisodeText = seasonEpisodeDiv.textContent.trim();
  88. const match = seasonEpisodeText.match(/S(\d+).E(\d+)/);
  89. if (match) {
  90. return {
  91. season: match[1],
  92. episode: match[2],
  93. };
  94. }
  95. }
  96. return null;
  97. }
  98.  
  99. // extract the series ID from the IMDb page
  100. function extractSeriesId() {
  101. const seriesLink = document.querySelector('[data-testid="hero-title-block__series-link"]');
  102. if (seriesLink) {
  103. const href = seriesLink.getAttribute('href');
  104. const match = href.match(/\/title\/(tt\d+)\//);
  105. if (match) {
  106. return match[1];
  107. }
  108. }
  109. return null;
  110. }
  111.  
  112. // generate the URL for a specific source
  113. async function generateUrl(urls, isMovie, isEpisode, imdbId, seriesId, seasonEpisode, isTmdb) {
  114. imdbId = seriesId || imdbId;
  115. if (isTmdb) {
  116. imdbId = await getTmdbID(imdbId);
  117. }
  118.  
  119. if (isMovie && isEpisode) {
  120. return urls.movie.replace('{id}', imdbId);
  121. }
  122.  
  123. if (seasonEpisode && seriesId) {
  124. const {
  125. season,
  126. episode
  127. } = seasonEpisode;
  128. return urls.tv.replace('{id}', imdbId).replace('{season}', season).replace('{episode}', episode);
  129. } else {
  130. return urls.tv.replace('{id}', imdbId).replace('{season}', '1').replace('{episode}', '1');
  131. }
  132. }
  133.  
  134. // create the iframe only once
  135. const createIframe = (defaultUrl) => {
  136. const mainEl = document.querySelector('main');
  137. const iframe = document.createElement('iframe');
  138. iframe.setAttribute('style', 'height:800px; width:100%;');
  139. iframe.id = "Player";
  140. iframe.allowFullscreen = "true";
  141. iframe.webkitallowfullscreen = "true";
  142. iframe.mozallowfullscreen = "true";
  143. iframe.src = defaultUrl;
  144. mainEl.before(iframe);
  145. return iframe;
  146. };
  147.  
  148. const imdbId = window.location.pathname.split('/')[2];
  149. const isMovie = document.title.indexOf('TV Series') === -1;
  150. const isEpisode = document.title.indexOf('TV Episode') === -1;
  151. const seasonEpisode = extractSeasonEpisode();
  152. const seriesId = extractSeriesId();
  153.  
  154. // initialize the iframe with the first source as default
  155. (async () => {
  156. const defaultUrl = await generateUrl(
  157. sources[0].urls,
  158. isMovie,
  159. isEpisode,
  160. imdbId,
  161. seriesId,
  162. seasonEpisode,
  163. !!sources[0].tmdb
  164. );
  165. const iframe = createIframe(defaultUrl);
  166. document.querySelector('section.ipc-page-section').setAttribute('style', 'padding-top: 10px;')
  167.  
  168. // add buttons to switch sources
  169. const mainEl = document.querySelector('main');
  170. const buttonContainer = document.createElement('div');
  171. buttonContainer.style.position = 'absolute';
  172. buttonContainer.style.top = '10px';
  173. buttonContainer.style.right = '10px';
  174. buttonContainer.style.display = 'flex';
  175. buttonContainer.style.flexDirection = 'column';
  176. buttonContainer.style.zIndex = '1000';
  177.  
  178. let selectedButton = null;
  179.  
  180. sources.forEach((source, index) => {
  181. const button = document.createElement('button');
  182. button.textContent = `📽 Source - ${source.name}`;
  183. button.style.fontFamily = 'Arial';
  184. button.style.marginBottom = `${buttonSpacing}px`;
  185. button.style.padding = '10px';
  186. button.style.background = '#3B3A3C';
  187. button.style.color = '#ffffff';
  188. button.style.border = 'none';
  189. button.style.cursor = 'pointer';
  190. button.style.fontWeight = 'bold';
  191. button.style.borderRadius = '6px';
  192. button.style.boxShadow =
  193. '0 10px 8px rgb(0 0 0 / 0.05), 0 4px 3px rgb(0 0 0 / 0.1)';
  194.  
  195. // highlight selected button
  196. const highlightButton = (btn) => {
  197. if (selectedButton) {
  198. selectedButton.style.border = 'none';
  199. }
  200. selectedButton = btn;
  201. btn.style.border = '2px solid #FFD700';
  202. };
  203.  
  204. // button click handler
  205. button.addEventListener('click', async () => {
  206. highlightButton(button);
  207. button.textContent = "⏳ Loading...";
  208. button.disabled = true;
  209.  
  210. try {
  211. const url = await generateUrl(source.urls, isMovie, isEpisode, imdbId, seriesId, seasonEpisode, !!source.tmdb);
  212. console.log('Reloading iframe');
  213. document.querySelector('iframe#Player').remove()
  214. createIframe(url);
  215. } catch (error) {
  216. console.error("Error generating URL:", error);
  217. alert(`Failed to generate URL: ${error}`);
  218. } finally {
  219. button.textContent = `📽 Source - ${source.name}`;
  220. button.disabled = false;
  221. }
  222. });
  223.  
  224. // set the first button as highlighted
  225. if (index === 0) {
  226. highlightButton(button);
  227. }
  228.  
  229. buttonContainer.appendChild(button);
  230. });
  231.  
  232. // Add "Open in Popup" button
  233. const popupButton = document.createElement('button');
  234. popupButton.textContent = "⛶ Open in Popup";
  235. popupButton.style.position = 'absolute';
  236. popupButton.style.top = '10px';
  237. popupButton.style.left = '10px';
  238. popupButton.style.padding = '10px';
  239. popupButton.style.background = '#3B3A3C';
  240. popupButton.style.color = '#ffffff';
  241. popupButton.style.border = 'none';
  242. popupButton.style.cursor = 'pointer';
  243. popupButton.style.fontWeight = 'bold';
  244. popupButton.style.borderRadius = '6px';
  245. popupButton.style.boxShadow =
  246. '0 10px 8px rgb(0 0 0 / 0.02), 0 4px 3px rgb(0 0 0 / 0.1)';
  247. popupButton.addEventListener('click', () => {
  248. window.open(document.querySelector('iframe#Player').src, '_blank', 'width=800,height=600,scrollbars=yes');
  249. });
  250.  
  251. mainEl.style.position = 'relative';
  252. mainEl.appendChild(buttonContainer);
  253. mainEl.appendChild(popupButton);
  254. })();
  255. })();