Adds quick link buttons to search movies on SIMKL and PassThePopcorn from Radarr movie pages
Size
11.8 KB
Version
1.2.3
Created
Mar 3, 2026
Updated
about 1 month ago
1// ==UserScript==
2// @name Radarr SIMKL & PTP Link Buttons
3// @description Adds quick link buttons to search movies on SIMKL and PassThePopcorn from Radarr movie pages
4// @version 1.2.3
5// @match http://192.168.1.50/*
6// @icon http://192.168.1.50:7881/radarr/favicon.ico
7// @grant GM.openInTab
8// @grant GM.xmlhttpRequest
9// ==/UserScript==
10(function() {
11 'use strict';
12
13 console.log('Radarr SIMKL & PTP Link Buttons extension loaded - VERSION 2.0 WITH API FIX');
14
15 let currentMovieId = null;
16 const RADARR_API_KEY = '24c3546a26d847bbbb70422a1cf39ba3';
17
18 // Function to get movie ID from URL
19 function getMovieIdFromUrl() {
20 const match = window.location.pathname.match(/\/movie\/(\d+)/);
21 return match ? match[1] : null;
22 }
23
24 // Function to get movie info from the page
25 function getMovieInfo() {
26 const titleElement = document.querySelector('.MovieDetails-title-yaEzx span[title]');
27 const yearElement = document.querySelector('.MovieDetails-year-FZGC1 span');
28 const pathElement = document.querySelector('.MovieDetails-path-UGxp_');
29
30 const title = titleElement ? titleElement.textContent.trim() : '';
31 const year = yearElement ? yearElement.textContent.trim() : '';
32
33 // Extract TMDb ID from path (format: {tmdb-123456})
34 let tmdbId = null;
35 if (pathElement) {
36 const pathText = pathElement.textContent;
37 const tmdbMatch = pathText.match(/\{tmdb-(\d+)\}/);
38 if (tmdbMatch) {
39 tmdbId = tmdbMatch[1];
40 }
41 }
42
43 // Extract IMDb ID from path (format: {imdb-tt1234567})
44 let imdbId = null;
45 if (pathElement) {
46 const pathText = pathElement.textContent;
47 const imdbMatch = pathText.match(/\{imdb-(tt\d+)\}/);
48 if (imdbMatch) {
49 imdbId = imdbMatch[1];
50 }
51 }
52
53 console.log('Movie info extracted from page:', { title, year, tmdbId, imdbId });
54 return { title, year, tmdbId, imdbId };
55 }
56
57 // Function to fetch movie data from Radarr API
58 async function getMovieDataFromAPI() {
59 const movieId = getMovieIdFromUrl();
60 if (!movieId) {
61 console.error('Could not extract movie ID from URL');
62 return null;
63 }
64
65 try {
66 console.log('Fetching movie data from API for TMDb ID:', movieId);
67
68 const xmlHttpRequest = typeof GM !== 'undefined' && GM.xmlhttpRequest ? GM.xmlhttpRequest : GM_xmlhttpRequest;
69 console.log('Using xmlHttpRequest:', typeof xmlHttpRequest);
70
71 // Fetch all movies since the URL contains TMDb ID, not Radarr internal ID
72 const response = await new Promise((resolve, reject) => {
73 xmlHttpRequest({
74 method: 'GET',
75 url: 'http://192.168.1.50:7881/radarr/api/v3/movie',
76 headers: {
77 'Accept': 'application/json',
78 'X-Api-Key': RADARR_API_KEY
79 },
80 onload: (resp) => {
81 console.log('API onload called, status:', resp.status);
82 resolve(resp);
83 },
84 onerror: (err) => {
85 console.error('API onerror called:', err);
86 reject(err);
87 },
88 ontimeout: () => {
89 console.error('API ontimeout called');
90 reject(new Error('Timeout'));
91 }
92 });
93 });
94
95 console.log('API response status:', response.status);
96
97 if (response.status !== 200) {
98 console.error('API returned non-200 status:', response.status);
99 return null;
100 }
101
102 const movies = JSON.parse(response.responseText);
103 console.log('Parsed movies, total count:', movies.length);
104
105 // Find movie by TMDb ID (the ID in the URL is TMDb ID, not Radarr internal ID)
106 const movie = movies.find(m => m.tmdbId === parseInt(movieId));
107
108 if (!movie) {
109 console.error('Movie not found in Radarr database with TMDb ID:', movieId);
110 return null;
111 }
112
113 console.log('Movie data fetched successfully from API:', {
114 title: movie.title,
115 imdbId: movie.imdbId,
116 tmdbId: movie.tmdbId,
117 year: movie.year
118 });
119 return movie;
120 } catch (error) {
121 console.error('Error fetching movie data from API:', error);
122 return null;
123 }
124 }
125
126 // Debounce function to prevent excessive calls
127 function debounce(func, wait) {
128 let timeout;
129 return function executedFunction(...args) {
130 const later = () => {
131 clearTimeout(timeout);
132 func(...args);
133 };
134 clearTimeout(timeout);
135 timeout = setTimeout(later, wait);
136 };
137 }
138
139 // Function to create and add buttons
140 async function addButtons() {
141 const movieId = getMovieIdFromUrl();
142
143 // Check if we're on a movie page
144 if (!movieId) {
145 console.log('Not on a movie page');
146 return false;
147 }
148
149 // Check if we already added buttons for this movie
150 if (currentMovieId === movieId && (document.getElementById('simkl-button') || document.getElementById('ptp-button'))) {
151 console.log('Buttons already exist for this movie');
152 return true;
153 }
154
155 const toolbar = document.querySelector('.PageToolbarSection-section-hL6TV.PageToolbarSection-left-E_sPJ');
156
157 if (!toolbar) {
158 console.log('Toolbar not found, waiting...');
159 return false;
160 }
161
162 // Remove old buttons if they exist
163 const oldSimklButton = document.getElementById('simkl-button');
164 const oldPtpButton = document.getElementById('ptp-button');
165 const oldSeparator = document.getElementById('custom-separator');
166 if (oldSimklButton) oldSimklButton.remove();
167 if (oldPtpButton) oldPtpButton.remove();
168 if (oldSeparator) oldSeparator.remove();
169
170 // Get movie info from the page first
171 let { title, year, tmdbId, imdbId } = getMovieInfo();
172
173 // If no IMDb ID in path, try to get it from API
174 if (!imdbId) {
175 console.log('No IMDb ID in path, fetching from API...');
176 const apiData = await getMovieDataFromAPI();
177 console.log('API data received:', apiData);
178 if (apiData && apiData.imdbId) {
179 imdbId = apiData.imdbId;
180 console.log('IMDb ID fetched from API:', imdbId);
181 } else {
182 console.log('No IMDb ID returned from API');
183 }
184 // Also get TMDb ID from API if not in path
185 if (!tmdbId && apiData && apiData.tmdbId) {
186 tmdbId = apiData.tmdbId;
187 }
188 }
189
190 if (!imdbId && !tmdbId && !title) {
191 console.error('Could not get IMDb ID, TMDb ID, or movie title');
192 return false;
193 }
194
195 console.log('Creating buttons with:', { imdbId, tmdbId, title, year });
196
197 // Use GM.openInTab if available (modern), otherwise fall back to window.open
198 const openInTab = typeof GM !== 'undefined' && GM.openInTab ? GM.openInTab :
199 (url) => {
200 window.open(url, '_blank');
201 };
202
203 // Create SIMKL button
204 const simklButton = document.createElement('button');
205 simklButton.id = 'simkl-button';
206 simklButton.className = 'PageToolbarButton-toolbarButton-j8a_b Link-link-RInnp Link-link-RInnp';
207 simklButton.title = 'Search on SIMKL';
208 simklButton.innerHTML = `
209 <div class="PageToolbarButton-labelContainer-QhTz_">
210 <div class="PageToolbarButton-label-QIVQh">SIMKL</div>
211 </div>
212 `;
213
214 simklButton.addEventListener('click', async () => {
215 let simklUrl;
216 if (imdbId) {
217 // Use IMDb ID in search (SIMKL will redirect to the movie page)
218 simklUrl = `https://simkl.com/search/?q=${imdbId}`;
219 console.log('Opening SIMKL with IMDb ID:', imdbId);
220 } else if (tmdbId) {
221 // Use TMDb ID in search as fallback
222 simklUrl = `https://simkl.com/search/?q=tmdb:${tmdbId}`;
223 console.log('Opening SIMKL with TMDb ID:', tmdbId);
224 } else {
225 // Last resort: search by title and year
226 simklUrl = `https://simkl.com/search/?q=${encodeURIComponent(title + ' ' + year)}`;
227 console.log('Opening SIMKL with title search:', title, year);
228 }
229 console.log('Opening SIMKL URL:', simklUrl);
230 await openInTab(simklUrl, false);
231 });
232
233 // Create PTP button
234 const ptpButton = document.createElement('button');
235 ptpButton.id = 'ptp-button';
236 ptpButton.className = 'PageToolbarButton-toolbarButton-j8a_b Link-link-RInnp Link-link-RInnp';
237 ptpButton.title = 'Search on PassThePopcorn';
238 ptpButton.innerHTML = `
239 <div class="PageToolbarButton-labelContainer-QhTz_">
240 <div class="PageToolbarButton-label-QIVQh">PTP</div>
241 </div>
242 `;
243
244 ptpButton.addEventListener('click', async () => {
245 const ptpUrl = imdbId
246 ? `https://passthepopcorn.me/torrents.php?imdb=${imdbId}`
247 : `https://passthepopcorn.me/torrents.php?searchstr=${encodeURIComponent(title)}`;
248 console.log('Opening PTP:', ptpUrl);
249 await openInTab(ptpUrl, false);
250 });
251
252 // Add separator before buttons
253 const separator = document.createElement('div');
254 separator.id = 'custom-separator';
255 separator.className = 'PageToolbarSeparator-separator-N1WHF';
256
257 // Insert buttons into toolbar
258 toolbar.appendChild(separator);
259 toolbar.appendChild(simklButton);
260 toolbar.appendChild(ptpButton);
261
262 currentMovieId = movieId;
263 const idType = imdbId ? `IMDb ID: ${imdbId}` : (tmdbId ? `TMDb ID: ${tmdbId}` : 'title search');
264 console.log('SIMKL and PTP buttons added successfully with', idType);
265 return true;
266 }
267
268 // Debounced version of addButtons
269 const debouncedAddButtons = debounce(addButtons, 300);
270
271 // Initialize the extension
272 function init() {
273 console.log('Initializing extension...');
274
275 // Try to add buttons immediately
276 addButtons();
277
278 // Watch for DOM changes (for SPA navigation)
279 const observer = new MutationObserver(() => {
280 debouncedAddButtons();
281 });
282
283 observer.observe(document.body, {
284 childList: true,
285 subtree: true
286 });
287
288 // Also watch for URL changes (for SPA navigation)
289 let lastUrl = location.href;
290 new MutationObserver(() => {
291 const url = location.href;
292 if (url !== lastUrl) {
293 lastUrl = url;
294 console.log('URL changed to:', url);
295 currentMovieId = null; // Reset current movie ID
296 setTimeout(addButtons, 500); // Wait a bit for content to load
297 }
298 }).observe(document, { subtree: true, childList: true });
299 }
300
301 // Start when DOM is ready
302 if (document.readyState === 'loading') {
303 document.addEventListener('DOMContentLoaded', init);
304 } else {
305 init();
306 }
307})();