UNPKG

gptdarr

Version:

MCP server for managing Sonarr and Radarr

255 lines (219 loc) 10.4 kB
import logger from '../modules/log.js'; import fs from 'node:fs'; import path from 'node:path'; import { config } from '../config/config.js'; class RadarrController { constructor() { this.radarrUrl = config.get(['radarr', 'url']) || 'http://localhost:7878'; this.radarrApiKey = config.get(['radarr', 'apiKey']); this.qualityProfileId = config.get(['radarr', 'qualityProfileId']) || 1; this.forceSearchOnExisting = config.get(['radarr', 'forceSearchOnExisting']) || true; this.rootFolder = path.normalize(config.get(['radarr', 'rootFolder'])); logger.raw(this); if (!this.radarrUrl.endsWith('/api/v3')) { this.radarrUrl = `${this.radarrUrl}/api/v3`; } if (!this.radarrApiKey) { logger.error('radarr', 'Radarr API key is not set', new Error('Radarr API key is not set')); process.exit(1); } // check if root folder exists if (!fs.existsSync(this.rootFolder)) { logger.error('radarr', 'Radarr root folder does not exist', new Error('Radarr root folder does not exist')); process.exit(1); } this.rootFolder = this.rootFolder.replace(/\/$/, ''); } async lookup(movieName, movieYear) { try { const response = await fetch(`${this.radarrUrl}/movie/lookup?term=${encodeURIComponent(movieName)}%20(${movieYear})`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-Api-key': this.radarrApiKey } }); const data = await response.json(); // Log the lookup request logger.lookupContent('radarr', { movieName, movieYear }, { statusCode: response.status, resultsCount: data.length }); return data; } catch (error) { console.error('Error in lookup method:', error); logger.error('radarr', `Error looking up movie: ${movieName} (${movieYear})`, error); throw error; } } async checkQueueStatus(movieId) { try { const response = await fetch(`${this.radarrUrl}/queue/details?movieId=${movieId}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-Api-key': this.radarrApiKey } }); const data = await response.json(); if (data && data.length > 0) { const status = data[0].status?.toLowerCase(); if (status === 'downloading') { return { status: 'downloading', message: 'Movie is already downloading' }; } else if (status === 'importing') { return { status: 'importing', message: 'Movie is importing, wait a few minutes' }; } } return null; } catch (error) { logger.error('radarr', `Error checking queue status for movie ID: ${movieId}`, error); return null; } } async add(movieName, movieYear) { try { console.log('Adding movie', movieName, movieYear); logger.log('info', 'radarr', { message: `Attempting to add movie: ${movieName} (${movieYear})` }); const lookup = await this.lookup(movieName, movieYear); if (!lookup || lookup.length === 0) { console.log('No results found for', movieName, movieYear); const result = this.generateResponseStructure(false, 'No results found', movieName, movieYear); logger.addContent('radarr', { movieName, movieYear }, result, false); return result; } const request = this.buildRequestObject(lookup[0]); logger.log('info', 'radarr', { message: `Request object: ${JSON.stringify(request)}` }); // Check queue status before attempting to add if (request.id) { const queueStatus = await this.checkQueueStatus(request.id); if (queueStatus) { const result = this.generateResponseStructure(false, queueStatus.message, request.title, request.year, request.imdbId, request.tmdbId); logger.addContent('radarr', { movieName, movieYear }, result, false); return result; } } if (typeof request.id !== 'undefined') { let msg = 'Movie exists, but isn\'t downloaded'; let hasFile = request.movieFileId !== 0; // log the entire request object if ( !request.isAvailable ) { msg = 'Movie is not out yet, still in cinemas or production.'; } else if ( hasFile ) { msg = 'Movie already exists, doing nothing!'; } else if ( !hasFile && request.isAvailable && this.forceSearchOnExisting ) { const url = `${this.radarrUrl}/command`; const postdata = { "name":"MoviesSearch", "movieIds":[request.id] }; logger.log('api', 'radarr', { operation: 'movie-search', movieId: request.id, title: request.title }); const response = await fetch(url, { method: 'POST', body: JSON.stringify(postdata), headers: { 'Content-Type': 'application/json', 'X-Api-key': this.radarrApiKey } }); if ( response.status === 201 ) { msg = 'Movie is downloading now'; } else { msg = 'An error has occurred'; logger.log('error', 'radarr', { operation: 'movie-search-result', response: response, title: request.title }, false); } logger.log('api', 'radarr', { operation: 'movie-search-result', statusCode: response.status, title: request.title }, response.ok); } const result = this.generateResponseStructure(false, msg, request.title, request.year, request.imdbId, request.tmdbId); logger.addContent('radarr', { movieName, movieYear }, result, false); return result; } const response = await this.sendMovieToRadarr(request); if (response.status === 201) { const result = this.generateResponseStructure(true, 'Movie added and downloading', request.title, request.year, request.imdbId, request.tmdbId); logger.addContent('radarr', { movieName, movieYear }, result, true); return result; } const errors = await response.json(); const result = this.generateResponseStructure(false, 'Error adding movie', request.title, request.year, request.imdbId, request.tmdbId, errors); logger.addContent('radarr', { movieName, movieYear }, result, false); return result; } catch (error) { console.error('Error in add method:', error); logger.error('radarr', `Error adding movie: ${movieName} (${movieYear})`, error); const result = this.generateResponseStructure(false, 'Unexpected error occurred'); return result; } } async bulkAdd(movieList) { logger.log('info', 'radarr', { message: `Attempting to add ${movieList.length} movies` }); let results = []; for (const movie of movieList) { const result = await this.add(movie.title, movie.year); results.push(result); } return results; } generateResponseStructure(success, message, title = '', year = '', imdbId = '', tmdbId = '', errors = null) { return { success, message, title, year, imdbId, tmdbId, errors }; } buildRequestObject(lookupResult) { const normalizedFolder = path.normalize(lookupResult.folder); return { ...lookupResult, qualityProfileId: this.qualityProfileId, profileId: this.qualityProfileId, path: path.normalize(path.join(this.rootFolder, normalizedFolder)), monitored: true, minimumAvailability: 'released', addOptions: { ignoreEpisodesWithFiles: false, ignoreEpisodesWithoutFiles: false, searchForMovie: true } }; } async sendMovieToRadarr(request) { try { console.log('Sending movie to Radarr:', request.title); const url = `${this.radarrUrl}/movie`; const response = await fetch(url, { method: 'POST', body: JSON.stringify(request), headers: { 'Content-Type': 'application/json', 'X-Api-key': this.radarrApiKey } }); logger.log('api', 'radarr', { operation: 'add-movie', title: request.title, statusCode: response.status }, response.ok); return response; } catch (error) { logger.error('radarr', `Error sending movie to Radarr: ${request.title}`, error); throw error; } } } export default RadarrController;