Radarr SIMKL & PTP Link Buttons

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})();