Last active
May 12, 2025 08:41
-
-
Save keldian/5749bd0317c74c98b56a5889a9960765 to your computer and use it in GitHub Desktop.
Trakt-to-Arr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Trakt-to-Arr | |
| // @namespace https://trakt.tv/ | |
| // @version 0.2 | |
| // @description Adds buttons to Trakt.tv pages to push titles directly to Radarr or Sonarr | |
| // @author keldian | |
| // @match https://trakt.tv/movies/* | |
| // @match https://trakt.tv/shows/* | |
| // @grant GM.xmlHttpRequest | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_config | |
| // @grant GM_registerMenuCommand | |
| // @connect * | |
| // @require https://openuserjs.org/src/libs/sizzle/GM_config.js | |
| // ==/UserScript== | |
| // Global variables for the configs | |
| var radarrConfig, sonarrConfig; | |
| // Override GM_config frame creation to use our custom styling | |
| const originalCreate = GM_config.create; | |
| GM_config.create = function(config) { | |
| const result = originalCreate.call(this, config); | |
| // Get the newly created frame | |
| if (this.frame) { | |
| // Apply proper sizing directly | |
| this.frame.style.width = '400px'; | |
| this.frame.style.height = 'auto'; | |
| this.frame.style.maxHeight = '80vh'; | |
| this.frame.style.inset = '50% auto auto 50%'; | |
| this.frame.style.transform = 'translate(-50%, -50%)'; | |
| this.frame.style.border = '1px solid #444'; | |
| // Create a custom stylesheet for the iframe content | |
| const frameDoc = this.frame.contentDocument || this.frame.contentWindow.document; | |
| if (frameDoc) { | |
| const frameStyle = frameDoc.createElement('style'); | |
| frameStyle.textContent = ` | |
| body { | |
| height: auto !important; | |
| overflow: visible !important; | |
| } | |
| #${this.id}_wrapper { | |
| height: auto !important; | |
| max-height: none !important; | |
| } | |
| .section_header { | |
| margin-bottom: 10px; | |
| } | |
| .section_desc { | |
| margin: 10px 0; | |
| } | |
| `; | |
| frameDoc.head.appendChild(frameStyle); | |
| } | |
| } | |
| return result; | |
| }; | |
| // Also override the open method to ensure sizing is applied | |
| const originalOpen = GM_config.open; | |
| GM_config.open = function() { | |
| originalOpen.apply(this, arguments); | |
| if (this.frame) { | |
| // Force correct styling again after opening | |
| setTimeout(() => { | |
| this.frame.style.width = '400px'; | |
| this.frame.style.height = 'auto'; | |
| this.frame.style.maxHeight = '80vh'; | |
| this.frame.style.inset = '50% auto auto 50%'; | |
| this.frame.style.transform = 'translate(-50%, -50%)'; | |
| this.frame.style.border = '1px solid #444'; | |
| }, 0); | |
| } | |
| }; | |
| (function() { | |
| 'use strict'; | |
| // Create notification system | |
| function createNotification(message, type = 'info') { | |
| const notification = document.createElement('div'); | |
| notification.style.cssText = ` | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| padding: 10px 20px; | |
| border-radius: 4px; | |
| color: white; | |
| z-index: 9999; | |
| animation: fadeInOut 3s ease-in-out; | |
| `; | |
| switch(type) { | |
| case 'success': | |
| notification.style.backgroundColor = '#28a745'; | |
| break; | |
| case 'error': | |
| notification.style.backgroundColor = '#dc3545'; | |
| break; | |
| default: | |
| notification.style.backgroundColor = '#17a2b8'; | |
| } | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| setTimeout(() => { | |
| if (document.body.contains(notification)) { | |
| document.body.removeChild(notification); | |
| } | |
| }, 3000); | |
| } | |
| // Add CSS for animations and button styling in a single block | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| @keyframes fadeInOut { | |
| 0% { opacity: 0; transform: translateY(-20px); } | |
| 10% { opacity: 1; transform: translateY(0); } | |
| 90% { opacity: 1; transform: translateY(0); } | |
| 100% { opacity: 0; transform: translateY(-20px); } | |
| } | |
| /* Button styling */ | |
| .arr-button { | |
| position: relative; | |
| background-color: transparent; | |
| cursor: pointer; | |
| border: none; | |
| padding: 0; | |
| line-height: 1; | |
| text-shadow: 0 0 20px black; | |
| transition: opacity 0.2s ease; | |
| } | |
| .arr-button:hover { | |
| opacity: 0.7; | |
| } | |
| .arr-button svg { | |
| height: 30px; | |
| width: 30px; | |
| display: block; | |
| } | |
| .arr-button.loading::after { | |
| content: ''; | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| top: 0; | |
| left: 0; | |
| background: rgba(45, 45, 45, 0.8); | |
| border-radius: 3px; | |
| z-index: 10; | |
| } | |
| .arr-plus-icon { | |
| position: absolute; | |
| bottom: -1.4px; | |
| right: -6.9px; | |
| } | |
| .test_button:hover { | |
| background-color: #3D3D3D !important; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| // Initialize Radarr config with fixed event handlers | |
| radarrConfig = new GM_config({ | |
| id: 'RadarrConfig', | |
| title: 'Radarr Configuration', | |
| fields: { | |
| radarrUrl: { | |
| label: 'Radarr URL', | |
| type: 'text', | |
| default: 'http://localhost:7878' | |
| }, | |
| radarrApiKey: { | |
| label: 'Radarr API Key', | |
| type: 'text', | |
| default: '' | |
| } | |
| }, | |
| events: { | |
| open: function(doc) { | |
| // Focus first field | |
| setTimeout(() => { | |
| const urlInput = doc.getElementById('RadarrConfig_field_radarrUrl'); | |
| if (urlInput) urlInput.focus(); | |
| }, 100); | |
| // Click outside handler | |
| const frame = doc.getElementById('RadarrConfig'); | |
| if (frame) { | |
| setTimeout(() => { | |
| function closeOnClickOutside(e) { | |
| if (frame && !frame.contains(e.target)) { | |
| radarrConfig.close(); | |
| document.removeEventListener('click', closeOnClickOutside); | |
| } | |
| } | |
| document.addEventListener('click', closeOnClickOutside); | |
| }, 100); | |
| } | |
| // Direct event handlers for auto-save | |
| const urlField = doc.getElementById('RadarrConfig_field_radarrUrl'); | |
| const apiField = doc.getElementById('RadarrConfig_field_radarrApiKey'); | |
| if (urlField) { | |
| urlField.addEventListener('input', debounce(() => { | |
| console.log('Saving Radarr URL...'); | |
| radarrConfig.save(); | |
| }, 500)); | |
| urlField.addEventListener('blur', () => { | |
| console.log('Saving Radarr URL on blur'); | |
| radarrConfig.save(); | |
| }); | |
| } | |
| if (apiField) { | |
| apiField.addEventListener('input', debounce(() => { | |
| console.log('Saving Radarr API Key...'); | |
| radarrConfig.save(); | |
| }, 500)); | |
| apiField.addEventListener('blur', () => { | |
| console.log('Saving Radarr API Key on blur'); | |
| radarrConfig.save(); | |
| }); | |
| } | |
| }, | |
| save: function() { | |
| createNotification('Radarr settings saved!', 'success'); | |
| } | |
| }, | |
| css: ` | |
| #RadarrConfig { | |
| background-color: #1D1D1D !important; | |
| color: #fff; | |
| width: 400px !important; | |
| height: auto !important; | |
| max-height: auto !important; | |
| margin: auto !important; | |
| position: fixed !important; | |
| inset: 50% auto auto 50% !important; | |
| transform: translate(-50%, -50%) !important; | |
| } | |
| #RadarrConfig_wrapper { | |
| height: auto !important; | |
| max-height: none !important; | |
| padding-bottom: 15px !important; | |
| } | |
| #RadarrConfig .config_header { | |
| color: #fff; | |
| background-color: #ED1C24; | |
| padding: 10px; | |
| margin-bottom: 10px; | |
| font-size: 16px; | |
| font-weight: bold; | |
| } | |
| #RadarrConfig .config_var { | |
| display: flex; | |
| align-items: center; | |
| margin: 10px 0; | |
| padding: 0 10px; | |
| } | |
| #RadarrConfig .field_label { | |
| min-width: 120px; | |
| } | |
| #RadarrConfig input[type="text"] { | |
| flex-grow: 1; | |
| margin-left: 10px; | |
| padding: 5px; | |
| background-color: #2D2D2D; | |
| color: #fff; | |
| border: 1px solid #444; | |
| } | |
| #RadarrConfig_buttons_holder { | |
| display: none !important; | |
| } | |
| ` | |
| }); | |
| // Initialize Sonarr config with the same approach | |
| sonarrConfig = new GM_config({ | |
| id: 'SonarrConfig', | |
| title: 'Sonarr Configuration', | |
| fields: { | |
| sonarrUrl: { | |
| label: 'Sonarr URL', | |
| type: 'text', | |
| default: 'http://localhost:8989' | |
| }, | |
| sonarrApiKey: { | |
| label: 'Sonarr API Key', | |
| type: 'text', | |
| default: '' | |
| } | |
| }, | |
| events: { | |
| open: function(doc) { | |
| // Focus first field | |
| setTimeout(() => { | |
| const urlInput = doc.getElementById('SonarrConfig_field_sonarrUrl'); | |
| if (urlInput) urlInput.focus(); | |
| }, 100); | |
| // Click outside handler | |
| const frame = doc.getElementById('SonarrConfig'); | |
| if (frame) { | |
| setTimeout(() => { | |
| function closeOnClickOutside(e) { | |
| if (frame && !frame.contains(e.target)) { | |
| sonarrConfig.close(); | |
| document.removeEventListener('click', closeOnClickOutside); | |
| } | |
| } | |
| document.addEventListener('click', closeOnClickOutside); | |
| }, 100); | |
| } | |
| // Direct event handlers for auto-save | |
| const urlField = doc.getElementById('SonarrConfig_field_sonarrUrl'); | |
| const apiField = doc.getElementById('SonarrConfig_field_sonarrApiKey'); | |
| if (urlField) { | |
| urlField.addEventListener('input', debounce(() => { | |
| console.log('Saving Sonarr URL...'); | |
| sonarrConfig.save(); | |
| }, 500)); | |
| urlField.addEventListener('blur', () => { | |
| console.log('Saving Sonarr URL on blur'); | |
| sonarrConfig.save(); | |
| }); | |
| } | |
| if (apiField) { | |
| apiField.addEventListener('input', debounce(() => { | |
| console.log('Saving Sonarr API Key...'); | |
| sonarrConfig.save(); | |
| }, 500)); | |
| apiField.addEventListener('blur', () => { | |
| console.log('Saving Sonarr API Key on blur'); | |
| sonarrConfig.save(); | |
| }); | |
| } | |
| }, | |
| save: function() { | |
| createNotification('Sonarr settings saved!', 'success'); | |
| } | |
| }, | |
| css: ` | |
| #SonarrConfig { | |
| background-color: #1D1D1D !important; | |
| color: #fff; | |
| width: 400px !important; | |
| height: auto !important; | |
| max-height: auto !important; | |
| margin: auto !important; | |
| position: fixed !important; | |
| inset: 50% auto auto 50% !important; | |
| transform: translate(-50%, -50%) !important; | |
| } | |
| #SonarrConfig_wrapper { | |
| height: auto !important; | |
| max-height: none !important; | |
| padding-bottom: 15px !important; | |
| } | |
| #SonarrConfig .config_header { | |
| color: #fff; | |
| background-color: #ED1C24; | |
| padding: 10px; | |
| margin-bottom: 10px; | |
| font-size: 16px; | |
| font-weight: bold; | |
| } | |
| #SonarrConfig .config_var { | |
| display: flex; | |
| align-items: center; | |
| margin: 10px 0; | |
| padding: 0 10px; | |
| } | |
| #SonarrConfig .field_label { | |
| min-width: 120px; | |
| } | |
| #SonarrConfig input[type="text"] { | |
| flex-grow: 1; | |
| margin-left: 10px; | |
| padding: 5px; | |
| background-color: #2D2D2D; | |
| color: #fff; | |
| border: 1px solid #444; | |
| } | |
| #SonarrConfig_buttons_holder { | |
| display: none !important; | |
| } | |
| ` | |
| }); | |
| // Simple debounce function to prevent too frequent saves | |
| function debounce(func, wait) { | |
| let timeout; | |
| return function() { | |
| const context = this, args = arguments; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(() => { | |
| func.apply(context, args); | |
| }, wait); | |
| }; | |
| } | |
| // Register menu commands | |
| GM_registerMenuCommand('Configure Radarr', openRadarrConfig); | |
| GM_registerMenuCommand('Configure Sonarr', openSonarrConfig); | |
| // Simplified functions to open configs | |
| function openRadarrConfig() { | |
| radarrConfig.open(); | |
| } | |
| function openSonarrConfig() { | |
| sonarrConfig.open(); | |
| } | |
| // Get IMDb ID from page | |
| function getImdbId() { | |
| console.log('Getting IMDb ID for:', window.location.pathname); | |
| // First try the direct external link method - most reliable | |
| const imdbLink = document.querySelector('#external-link-imdb'); | |
| if (imdbLink) { | |
| console.log('Found IMDb link:', imdbLink.href); | |
| const imdbMatch = imdbLink.href.match(/\/title\/(tt\d+)/); | |
| if (imdbMatch) return imdbMatch[1]; | |
| } | |
| // If direct link fails, try to find IMDb ID from page content | |
| const pageContent = document.documentElement.innerHTML; | |
| const imdbPatterns = [ | |
| /"imdb":"(tt\d+)"/, | |
| /"imdb":\s*"(tt\d+)"/, | |
| /imdb[/"=](tt\d+)/, | |
| /imdb_id=(tt\d+)/ | |
| ]; | |
| for (const pattern of imdbPatterns) { | |
| const match = pageContent.match(pattern); | |
| if (match) { | |
| console.log('Found IMDb ID via pattern:', match[1]); | |
| return match[1]; | |
| } | |
| } | |
| // Last resort - try Trakt.getProps() if available | |
| if (typeof window.Trakt !== 'undefined' && typeof window.Trakt.getProps === 'function') { | |
| try { | |
| const traktProps = window.Trakt.getProps(); | |
| if (traktProps?.show?.ids?.imdb || traktProps?.movie?.ids?.imdb) { | |
| const imdbId = traktProps.show?.ids?.imdb || traktProps.movie?.ids?.imdb; | |
| console.log('Found IMDb ID from Trakt.getProps():', imdbId); | |
| return imdbId; | |
| } | |
| } catch (e) { | |
| console.error('Error accessing Trakt.getProps():', e); | |
| } | |
| } | |
| console.error('Failed to find IMDb ID'); | |
| return null; | |
| } | |
| // Add the request button as a standalone element between ratings and stats | |
| function addRequestButton(type) { | |
| // Look for the specific sections in the summary | |
| const ulWrapper = document.querySelector('.ul-wrapper'); | |
| const ratingsUl = document.querySelector('.ul-wrapper ul.ratings'); | |
| const statsUl = document.querySelector('.ul-wrapper ul.stats'); | |
| // If we can't find both elements, fall back to another method | |
| if (!ulWrapper || !ratingsUl || !statsUl) { | |
| console.log('Could not find ratings/stats section', { ulWrapper, ratingsUl, statsUl }); | |
| return false; | |
| } | |
| // Check if button already exists | |
| if (document.querySelector('.arr-button')) { | |
| return false; | |
| } | |
| const arrColor = type === 'movie' ? '#FFC230' : '#00CCFF'; | |
| // Create a standalone element that matches heart icon styling | |
| const buttonUl = document.createElement('ul'); | |
| buttonUl.className = 'integrations'; | |
| buttonUl.style.cssText = ` | |
| display: inline-block; | |
| list-style: none; | |
| margin: 0; | |
| padding: 0; | |
| `; | |
| const buttonLi = document.createElement('li'); | |
| buttonLi.style.cssText = ` | |
| display: inline-block; | |
| margin-left: 30px; | |
| padding: 0; | |
| transition: all .6s; | |
| `; | |
| const button = document.createElement('button'); | |
| button.className = 'arr-button'; | |
| // Add title attribute for hover tooltip | |
| button.title = `Add to ${type === 'movie' ? 'Radarr' : 'Sonarr'} library`; | |
| // Create SVG content based on type | |
| let svgContent = ''; | |
| if (type === 'movie') { | |
| // Radarr SVG | |
| svgContent = ` | |
| <svg height="30" width="30" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"> | |
| <g transform="translate(70 21.00012)"> | |
| <path d="m105.302 154.943 7.522 714.549c-60.173 7.522-105.30242-22.565-105.30242-82.737l-7.52158-594.205c0-188.03894 172.996-233.1684 278.298-157.9526l534.032 308.3846c75.216 52.651 90.259 150.431 52.651 218.125-7.521-52.651-30.086-82.737-75.216-112.823l-601.726-338.471c-45.129-30.0862-82.737-22.5646-82.737 45.13z" fill="#808080"/> | |
| <path d="m0 376.079c45.1295 15.043 90.259 7.521 127.867-15.043l616.769-361.036c37.608 52.651 30.087 105.302-15.043 135.388l-518.989 300.863c-75.216 37.608-172.9961 0-210.604-60.172z" fill="#808080" transform="translate(60.17249 531.0214)"/> | |
| <path d="m0 413.687 368.557-210.604-361.03543-203.083z" fill="#ffc230" transform="translate(240.6902 282.8092)"/> | |
| </g> | |
| </svg>`; | |
| } else { | |
| // Sonarr SVG | |
| svgContent = ` | |
| <svg height="30" width="30" viewBox="0 0 216.7 216.9" xmlns="http://www.w3.org/2000/svg"> | |
| <g clip-rule="evenodd"> | |
| <path d="m216.7 108.45c0 29.833-10.533 55.4-31.6 76.7-.7.833-1.483 1.6-2.35 2.3-3.466 3.4-7.133 6.484-11 9.25-18.267 13.467-39.367 20.2-63.3 20.2-23.967 0-45.033-6.733-63.2-20.2-4.8-3.4-9.3-7.25-13.5-11.55-16.367-16.266-26.417-35.167-30.15-56.7-.733-4.2-1.217-8.467-1.45-12.8-.1-2.4-.15-4.8-.15-7.2 0-2.533.05-4.95.15-7.25 0-.233.066-.467.2-.7 1.567-26.6 12.033-49.583 31.4-68.95 21.3-21.033 46.867-31.55 76.7-31.55 29.933 0 55.484 10.517 76.65 31.55 21.067 21.433 31.6 47.067 31.6 76.9z" fill="#eee" fill-rule="evenodd"/> | |
| <path d="m194.65 42.5-22.4 22.4c-13.098 13.098-14.25 24.5-14.25 44.6 0 17.934 2.852 34.352 16.2 47.7 9.746 9.746 19 18.95 19 18.95-2.5 3.067-5.2 6.067-8.1 9-.7.833-1.483 1.6-2.35 2.3-2.533 2.5-5.167 4.817-7.9 6.95l-17.55-17.55c-15.598-15.6-27.996-17.1-48.6-17.1-19.77 0-33.223 1.822-47.7 16.3-8.647 8.647-18.55 18.6-18.55 18.6-3.767-2.867-7.333-6.034-10.7-9.5-2.8-2.8-5.417-5.667-7.85-8.6 0 0 9.798-9.848 19.15-19.2 13.852-13.853 16.1-29.916 16.1-47.85 0-17.5-2.874-33.823-15.6-46.55-8.835-8.836-21.05-21-21.05-21 2.833-3.6 5.917-7.067 9.25-10.4 2.934-2.867 5.934-5.55 9-8.05l20.35 20.35c13.002 13.002 29.667 16.35 47.6 16.35 18.467 0 35.077-3.577 48.6-17.1 8.32-8.32 19.3-19.25 19.3-19.25 2.9 2.367 5.733 4.933 8.5 7.7 3.467 3.533 6.65 7.183 9.55 10.95z" fill="#3a3f51" fill-rule="evenodd"/> | |
| <path d="m78.7 114c-.2-1.167-.332-2.35-.4-3.55-.032-.667-.05-1.333-.05-2 0-.7.018-1.367.05-2 0-.067.018-.133.05-.2.435-7.367 3.334-13.733 8.7-19.1 5.9-5.833 12.984-8.75 21.25-8.75 8.3 0 15.384 2.917 21.25 8.75 5.834 5.934 8.75 13.033 8.75 21.3s-2.916 15.35-8.75 21.25c-.2.233-.416.45-.65.65-.966.933-1.982 1.783-3.05 2.55-5.065 3.733-10.916 5.6-17.55 5.6s-12.466-1.866-17.5-5.6c-1.332-.934-2.582-2-3.75-3.2-4.532-4.5-7.316-9.734-8.35-15.7z" fill="#0cf" fill-rule="evenodd"/> | |
| <g fill="none" stroke="#0cf" stroke-miterlimit="1"> | |
| <path d="m157.8 59.75-15 14.65m-112.015-41.874 40.865 40.724m84.6 84.25 27.808 28.78m1.855-153.894-28.113 27.364m-125.45 126 27.35-27.4" stroke-width="2"/> | |
| <path d="m157.8 59.75-16.95 17.2m-81.88-16.346 17.2 17.15m-16.547 80.676 16.75-17.4m61.928-1.396 18.028 17.945" stroke-width="7"/> | |
| </g> | |
| </g> | |
| </svg>`; | |
| } | |
| // Create a plus icon element (same for both types) | |
| const plusIcon = `<i class="fa-solid fa-circle-plus arr-plus-icon" style="color: var(--action-collect);"></i>`; | |
| // Set the button's content with SVG and plus icon | |
| button.innerHTML = svgContent + plusIcon; | |
| // Add click handler | |
| button.onclick = () => sendToArr(type, button); | |
| // Add button to list item | |
| buttonLi.appendChild(button); | |
| // Add the list item to our UL | |
| buttonUl.appendChild(buttonLi); | |
| // Insert the new UL between ratings and stats | |
| ulWrapper.insertBefore(buttonUl, statsUl); | |
| console.log('Button added successfully with FontAwesome plus icon'); | |
| return true; | |
| } | |
| // Send title to Radarr or Sonarr using IMDb ID | |
| async function sendToArr(type, button) { | |
| const imdbId = getImdbId(); | |
| if (!imdbId) { | |
| createNotification('Could not find IMDb ID', 'error'); | |
| console.error('Failed to find IMDb ID. URL:', window.location.href); | |
| return; | |
| } | |
| const isMovie = type === 'movie'; | |
| // Get configuration values | |
| const config = { | |
| url: isMovie | |
| ? radarrConfig.get('radarrUrl')?.replace(/\/$/, '') | |
| : sonarrConfig.get('sonarrUrl')?.replace(/\/$/, ''), | |
| apiKey: isMovie | |
| ? radarrConfig.get('radarrApiKey') | |
| : sonarrConfig.get('sonarrApiKey') | |
| }; | |
| if (!config.url || !config.apiKey) { | |
| createNotification(`${isMovie ? 'Radarr' : 'Sonarr'} not configured`, 'error'); | |
| if (isMovie) { | |
| radarrConfig.open(); | |
| } else { | |
| sonarrConfig.open(); | |
| } | |
| return; | |
| } | |
| // Add loading state to button | |
| button.classList.add('loading'); | |
| button.disabled = true; | |
| createNotification(`Sending to ${isMovie ? 'Radarr' : 'Sonarr'}...`, 'info'); | |
| try { | |
| // Get title for easier identification | |
| const title = document.querySelector('h1')?.textContent.trim() || 'Unknown Title'; | |
| // Construct lookup URL using IMDb ID for both services | |
| const lookupUrl = `${config.url}/api/v3/${isMovie ? 'movie' : 'series'}/lookup?term=imdb:${imdbId}`; | |
| console.log(`Looking up ${isMovie ? 'movie' : 'show'} with IMDb ID ${imdbId}`); | |
| GM.xmlHttpRequest({ | |
| method: 'GET', | |
| url: lookupUrl, | |
| headers: { | |
| 'X-Api-Key': config.apiKey | |
| }, | |
| onload: function(response) { | |
| if (response.status === 200) { | |
| const data = JSON.parse(response.responseText); | |
| if (!data || data.length === 0) { | |
| button.classList.remove('loading'); | |
| button.disabled = false; | |
| createNotification(`${isMovie ? 'Movie' : 'Show'} not found with IMDb ID ${imdbId}`, 'error'); | |
| return; | |
| } | |
| // Get the first result | |
| const mediaData = data[0]; | |
| // Check if movie/show already exists | |
| const idField = isMovie ? 'imdbId' : 'tvdbId'; | |
| const idValue = isMovie ? mediaData.imdbId : mediaData.tvdbId; | |
| GM.xmlHttpRequest({ | |
| method: 'GET', | |
| url: `${config.url}/api/v3/${isMovie ? 'movie' : 'series'}`, | |
| headers: { | |
| 'X-Api-Key': config.apiKey | |
| }, | |
| onload: function(listResponse) { | |
| const items = JSON.parse(listResponse.responseText); | |
| const existing = items.find(item => item[idField] === idValue); | |
| if (existing) { | |
| button.classList.remove('loading'); | |
| button.disabled = false; | |
| createNotification(`${title} already exists in ${isMovie ? 'Radarr' : 'Sonarr'}`, 'info'); | |
| // Open the item's page in a new tab - FIXED URL FORMAT | |
| try { | |
| if (isMovie) { | |
| // For Radarr: use tmdbId | |
| const tmdbId = existing.tmdbId || mediaData.tmdbId; | |
| const itemUrl = `${config.url}/movie/${tmdbId}`; | |
| window.open(itemUrl, '_blank'); | |
| } else { | |
| // For Sonarr: use titleSlug | |
| const titleSlug = existing.titleSlug || mediaData.titleSlug; | |
| if (titleSlug) { | |
| const itemUrl = `${config.url}/series/${titleSlug}`; | |
| window.open(itemUrl, '_blank'); | |
| } else { | |
| // Fallback to ID if no titleSlug | |
| window.open(`${config.url}/series/${existing.id}`, '_blank'); | |
| } | |
| } | |
| } catch (e) { | |
| console.error('Error opening item in *arr:', e); | |
| } | |
| return; | |
| } | |
| // Get root folders to use the first one | |
| GM.xmlHttpRequest({ | |
| method: 'GET', | |
| url: `${config.url}/api/v3/rootfolder`, | |
| headers: { | |
| 'X-Api-Key': config.apiKey | |
| }, | |
| onload: function(folderResponse) { | |
| let rootFolderPath = ''; | |
| try { | |
| const folders = JSON.parse(folderResponse.responseText); | |
| if (folders && folders.length > 0) { | |
| rootFolderPath = folders[0].path; | |
| } | |
| } catch (e) { | |
| console.error('Error parsing root folders:', e); | |
| } | |
| // Get quality profiles | |
| GM.xmlHttpRequest({ | |
| method: 'GET', | |
| url: `${config.url}/api/v3/qualityprofile`, | |
| headers: { | |
| 'X-Api-Key': config.apiKey | |
| }, | |
| onload: function(profileResponse) { | |
| let qualityProfileId = 1; | |
| try { | |
| const profiles = JSON.parse(profileResponse.responseText); | |
| if (profiles && profiles.length > 0) { | |
| qualityProfileId = profiles[0].id; | |
| } | |
| } catch (e) { | |
| console.error('Error parsing quality profiles:', e); | |
| } | |
| // For Sonarr, try to get language profiles if available | |
| if (!isMovie) { | |
| GM.xmlHttpRequest({ | |
| method: 'GET', | |
| url: `${config.url}/api/v3/languageprofile`, | |
| headers: { | |
| 'X-Api-Key': config.apiKey | |
| }, | |
| onload: function(langResponse) { | |
| try { | |
| let languageProfileId = 1; | |
| if (langResponse.status === 200) { | |
| const languages = JSON.parse(langResponse.responseText); | |
| if (languages && languages.length > 0) { | |
| languageProfileId = languages[0].id; | |
| } | |
| } | |
| // Add show with language profile | |
| addMediaToArr(mediaData, { | |
| qualityProfileId, | |
| languageProfileId, | |
| rootFolderPath, | |
| isMovie, | |
| title, | |
| button, | |
| config | |
| }); | |
| } catch (e) { | |
| console.warn('Error with language profiles, continuing without:', e); | |
| // Add show without language profile | |
| addMediaToArr(mediaData, { | |
| qualityProfileId, | |
| rootFolderPath, | |
| isMovie, | |
| title, | |
| button, | |
| config | |
| }); | |
| } | |
| }, | |
| onerror: function() { | |
| // Continue without language profile | |
| addMediaToArr(mediaData, { | |
| qualityProfileId, | |
| rootFolderPath, | |
| isMovie, | |
| title, | |
| button, | |
| config | |
| }); | |
| } | |
| }); | |
| } else { | |
| // Add movie (no language profile needed) | |
| addMediaToArr(mediaData, { | |
| qualityProfileId, | |
| rootFolderPath, | |
| isMovie, | |
| title, | |
| button, | |
| config | |
| }); | |
| } | |
| }, | |
| onerror: function(error) { | |
| button.classList.remove('loading'); | |
| button.disabled = false; | |
| createNotification('Error getting quality profiles', 'error'); | |
| console.error(`${isMovie ? 'Radarr' : 'Sonarr'} error:`, error); | |
| } | |
| }); | |
| }, | |
| onerror: function(error) { | |
| button.classList.remove('loading'); | |
| button.disabled = false; | |
| createNotification('Error getting root folders', 'error'); | |
| console.error(`${isMovie ? 'Radarr' : 'Sonarr'} error:`, error); | |
| } | |
| }); | |
| }, | |
| onerror: function(error) { | |
| button.classList.remove('loading'); | |
| button.disabled = false; | |
| createNotification(`Error checking existing ${isMovie ? 'movies' : 'shows'}`, 'error'); | |
| console.error(`${isMovie ? 'Radarr' : 'Sonarr'} error:`, error); | |
| } | |
| }); | |
| } else { | |
| button.classList.remove('loading'); | |
| button.disabled = false; | |
| createNotification(`Lookup failed: ${response.statusText}`, 'error'); | |
| } | |
| }, | |
| onerror: function(error) { | |
| button.classList.remove('loading'); | |
| button.disabled = false; | |
| createNotification(`Error connecting to ${isMovie ? 'Radarr' : 'Sonarr'}`, 'error'); | |
| console.error(`${isMovie ? 'Radarr' : 'Sonarr'} lookup error:`, error); | |
| } | |
| }); | |
| } catch (error) { | |
| button.classList.remove('loading'); | |
| button.disabled = false; | |
| createNotification(`Error: ${error.message}`, 'error'); | |
| console.error('Error sending to Arr:', error); | |
| } | |
| } | |
| // Helper function for the final add API call | |
| function addMediaToArr(mediaData, options) { | |
| const { | |
| qualityProfileId, | |
| languageProfileId, | |
| rootFolderPath, | |
| isMovie, | |
| title, | |
| button, | |
| config | |
| } = options; | |
| // Prepare media data for adding | |
| const dataToAdd = { | |
| ...mediaData, | |
| qualityProfileId: qualityProfileId, | |
| rootFolderPath: rootFolderPath, | |
| monitored: true, | |
| addOptions: { | |
| [isMovie ? 'searchForMovie' : 'searchForMissingEpisodes']: true | |
| } | |
| }; | |
| // Add language profile for TV shows | |
| if (!isMovie && languageProfileId) { | |
| dataToAdd.languageProfileId = languageProfileId; | |
| } | |
| // Add to Radarr or Sonarr | |
| GM.xmlHttpRequest({ | |
| method: 'POST', | |
| url: `${config.url}/api/v3/${isMovie ? 'movie' : 'series'}`, | |
| headers: { | |
| 'X-Api-Key': config.apiKey, | |
| 'Content-Type': 'application/json' | |
| }, | |
| data: JSON.stringify(dataToAdd), | |
| onload: function(addResponse) { | |
| button.classList.remove('loading'); | |
| button.disabled = false; | |
| if (addResponse.status >= 200 && addResponse.status < 300) { | |
| createNotification(`${title} added to ${isMovie ? 'Radarr' : 'Sonarr'}!`, 'success'); | |
| } else { | |
| createNotification(`Failed to add ${isMovie ? 'movie' : 'show'}: ${addResponse.statusText}`, 'error'); | |
| console.error(`${isMovie ? 'Radarr' : 'Sonarr'} error:`, addResponse.responseText); | |
| } | |
| }, | |
| onerror: function(error) { | |
| button.classList.remove('loading'); | |
| button.disabled = false; | |
| createNotification(`Error adding ${isMovie ? 'movie' : 'show'}`, 'error'); | |
| console.error(`${isMovie ? 'Radarr' : 'Sonarr'} error:`, error); | |
| } | |
| }); | |
| } | |
| // Initialize and add buttons based on page URL | |
| function initialize() { | |
| // Just check the first directory part to decide on movie or show | |
| const path = window.location.pathname; | |
| if (path.includes('/movies')) { | |
| console.log('Detected movie context:', path); | |
| addRequestButton('movie'); | |
| } else if (path.includes('/shows')) { | |
| console.log('Detected show context:', path); | |
| addRequestButton('show'); | |
| } else { | |
| console.log('Not a supported page type:', path); | |
| } | |
| } | |
| // Run initialization when DOM is fully loaded | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', initialize); | |
| } else { | |
| // DOM is already ready | |
| initialize(); | |
| } | |
| })(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment