Skip to content

Instantly share code, notes, and snippets.

@keldian
Last active May 12, 2025 08:41
Show Gist options
  • Save keldian/5749bd0317c74c98b56a5889a9960765 to your computer and use it in GitHub Desktop.
Save keldian/5749bd0317c74c98b56a5889a9960765 to your computer and use it in GitHub Desktop.
Trakt-to-Arr
// ==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