tdarr/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js

265 lines
No EOL
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @name Radarr - Trigger Manual Import
* @description Trigger Radarr ManualImport or downloadedMoviesScan with skip-guard.
* @author FileFlows + Sascha
* @revision 9
* @output Import erfolgreich
* @output Fehler
* @param {string} URL Radarr root URL and port
* @param {string} ApiKey Radarr API Key
* @param {string} ImportPath Output path for import triggering
* @param {bool} UseUnmappedPath Whether to Unmap the path
* @param {bool} MoveMode Import mode: true=move, false=copy
* @param {int} TimeOut Timeout in seconds (max 3600)
*/
import { Radarr } from 'Shared/Radarr';
function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) {
URL = (URL || Variables['Radarr.Url'] || Variables['Radarr.URI']).replace(/\/+$/g, '');
ApiKey = ApiKey || Variables['Radarr.ApiKey'];
ImportPath = ImportPath || Variables.file.FullName;
TimeOut = TimeOut ? Math.min(TimeOut, 3600) * 1000 : 60000; // ms
ImportPath = UseUnmappedPath ? Flow.UnMapPath(ImportPath) : ImportPath;
const importMode = MoveMode ? 'move' : 'copy';
// ── SKIP-GUARD: Nur API-Call machen, wenn der aktuelle ImportPath
// mit dem konfigurierten Pfad uebereinstimmt. Sonst sofort OK.
// Das verhindert 3 unnoetige API-Calls pro Datei (4 Arr-Instanzen).
if (ImportPath && ImportPath.indexOf(URL) === -1) {
// ImportPath ist ein Dateipfad, URL ist die Radarr-Instanz-URL.
// Pruefe ob der ImportPath unter dem erwarteten Verzeichnis liegt.
// Die Flow-Konfiguration setzt ImportPath z.B. auf /tdarr/converted/video4k/
// oder /tdarr/converted/video/. Wenn der aktuelle file.FullName nicht
// darunter liegt, sind wir nicht zustaendig.
var currentFile = Variables['file.FullName'] || '';
if (currentFile && ImportPath && currentFile.indexOf(ImportPath) === -1) {
Logger.ILog(`Skip: current file '${currentFile}' not under ImportPath '${ImportPath}' - not responsible`);
return 1;
}
}
const radarr = new Radarr(URL, ApiKey);
/*── movieId detection ──────────────────────────────────*/
const searchPattern = Variables.file.Orig.FileNameNoExtension;
let movieId = Variables['Radarr.movieId'] ?? Variables.MovieInfo?.id ?? parseMovie(searchPattern, radarr) ?? null;
Logger.ILog(`Radarr URL: ${URL}`);
Logger.ILog(`Triggering Path: ${ImportPath}`);
Logger.ILog(`Import Mode: ${importMode}`);
Logger.ILog(movieId ? `movieId: ${movieId} → ManualImport` : 'No movieId → downloadedMoviesScan');
/*─ Execute first workflow, then fail-over if needed ─*/
let result;
if (movieId) {
result = manualImportWorkflow(radarr, ImportPath, importMode, movieId, TimeOut);
if (result !== 1) {
Logger.WLog('ManualImport failed, falling back to downloadedMoviesScan.');
result = scanWorkflow(radarr, ImportPath, importMode, TimeOut);
}
} else {
result = scanWorkflow(radarr, ImportPath, importMode, TimeOut);
}
return result;
}
/*───────────────────────────── ManualImport ─────────────────────────────*/
/**
* @description Perform ManualImport flow (when movieId is present) and wait until it finishes (or times out).
* @param {Radarr} radarr - Radarr API instance
* @param {string} importPath - Folder/File path given to movie import
* @param {string} mode - 'move' or 'copy'
* @param {number} movieId - Radarr movieId to attach to the import
* @param {number} timeout - Timeout in milliseconds
* @returns {number} 1 on success, 1 on failure
*/
function manualImportWorkflow(radarr, importPath, mode, movieId, timeout) {
const candidates = getManualImportCandidates(radarr, importPath);
if (!candidates.length) {
Logger.WLog('No candidates returned by fetching ManualImport');
return -1;
}
/* choose the first candidate that has quality info */
const cand = candidates.find(c => c.quality && c.quality.quality);
if (!cand) {
Logger.WLog('No ManualImport candidate contained quality information.');
return -1;
}
const fileObj = buildManualImportFile(cand, movieId, importPath);
const cmdBody = { name: 'ManualImport', files: [fileObj], importMode: mode };
const cmdId = sendManualImportCommand(radarr, cmdBody);
if (cmdId === null) {
Logger.WLog('Failed sending ManualImport command');
return -1;
}
return waitForCommand(radarr, cmdId, timeout);
}
/**
* @description Retrieve candidate files from Radarrs **Manual Import** analyser.
* @param {Radarr} radarr Radarr API instance
* @param {string} importPath Folder/File path to inspect
* @returns {Array<object>} Array of candidate objects (empty array on error)
*/
function getManualImportCandidates(radarr, importPath) {
try {
const query = buildQueryParams({ folder: importPath });
const resp = radarr.fetchJson('manualimport', query) || [];
Logger.ILog(`ManualImport returned ${resp.length} candidate(s).`);
return resp;
} catch (e) {
Logger.ELog(`Fetch candidates from ManualImport failed: ${e.message}`);
return [];
}
}
/**
* @description Build the minimal file-object required by Radarr's ManualImport POST body.
* @param {object} src Single candidate object returned by ManualImport
* @param {number} movieId Radarr movieId to which the file is linked
* @returns {object} File descriptor for ManualImport
*/
function buildManualImportFile(src, movieId, importPath) {
const path = src.path || importPath
const fallbackFolder = System.IO.Path.GetFileName(System.IO.Path.GetDirectoryName(path)) ?? Variables.folder.Name;
return {
path: path,
folderName: src.folderName || src.name || fallbackFolder,
movieId: movieId,
releaseGroup: src.releaseGroup || 'Radarr',
quality: src.quality,
languages: src.languages || [ { id:0, name:'Unknown' } ],
indexerFlags: src.indexerFlags ?? 0
};
}
/**
* @description Send the **ManualImport** command to Radarr and return command-id
* @param {Radarr} radarr Radarr API instance
* @param {object} cmdBody Complete payload for ManualImport command
* @returns {number|null} Command id on success, null on failure
*/
function sendManualImportCommand(radarr, cmdBody) {
try {
const resp = radarr.sendCommand('ManualImport', cmdBody);
const cmdId = resp?.id;
Logger.ILog(cmdId ? `ManualImport queued (cmdId=${cmdId}).` : 'ManualImport failed.');
return cmdId ?? null;
} catch (e) {
Logger.ELog(`Send command ManualImport failed: ${e.message}`);
return null;
}
}
/*──────────────────────────── downloadedMoviesScan ────────────────────────────*/
/**
* @description Perform the downloadedMoviesScan flow (when movieId is absent).
* @param {Radarr} radarr Radarr API instance
* @param {string} importPath Folder/File path for downloadedMoviesScan
* @param {string} mode 'move' or 'copy'
* @param {number} timeout Timeout in milliseconds
* @returns {number} 1 on success, 1 on failure
*/
function scanWorkflow(radarr, importPath, mode, timeout) {
const cmdId = sendDownloadedMoviesScan(radarr, importPath, mode);
if (cmdId === null) {
Logger.WLog('Faild sending downloadedMoviesScan command');
return -1;
}
return waitForCommand(radarr, cmdId, timeout);
}
/**
* @description Send the **downloadedMoviesScan** command.
* @param {Radarr} radarr - Radarr API instance
* @param {string} importPath - Folder/File path for the scan
* @param {string} mode - 'move' or 'copy'
* @returns {number|null} command id or null
*/
function sendDownloadedMoviesScan(radarr, importPath, mode) {
try {
const resp = radarr.sendCommand('downloadedMoviesScan', { path: importPath, importMode: mode });
const cmdId = resp?.id;
Logger.ILog(cmdId ? `downloadedMoviesScan queued (cmdId=${cmdId}).` : 'downloadedMoviesScan failed.');
return cmdId ?? null;
} catch (e) {
Logger.ELog(`Send command downloadedMoviesScan failed: ${e.message}`);
return null;
}
}
/*────────────────────────────── helpers ──────────────────────────────────*/
/**
* @description Poll Radarr until the command id completes or timeout is reached.
* @param {Radarr} radarr - Radarr API instance
* @param {number} cmdId - Command id returned by sendCommand
* @param {number} timeoutMs - Maximum time to wait in milliseconds
* @returns {number} 1 on success, 1 on timeout
*/
function waitForCommand(radarr, cmdId, timeoutMs) {
const ok = radarr.waitForCompletion(cmdId, timeoutMs);
if (ok) {
Logger.DLog(`Command ${cmdId} completed.`);
Logger.ILog('Import completed successfully');
return 1;
}
Logger.WLog('Import timed out or failed.');
return -1;
}
/**
* @description Parse the movie name using Radarr parsing based on the search pattern.
* @param {string} searchPattern - The search string (file or folder name)
* @param {Object} radarr - Radarr API instance
* @returns {Object|null} Parsed movie object, or null if none.
*/
function parseMovie(searchPattern, radarr) {
let endpoint = 'parse'
let sp = null;
Logger.ILog(`Trying to Parse Movie name using Radarr Parsing`);
if (!searchPattern) {
Logger.WLog('No pattern passed in to find movie');
return null;
} else {
sp = searchPattern.toLowerCase();
}
try {
const queryParams = buildQueryParams({ title: sp });
const item = radarr.fetchJson(endpoint, queryParams);
if (item?.movie?.title) {
Logger.ILog(`Found Movie: ${item.movie.title}`);
return item.movie.id;
}
Logger.WLog(`The ${endpoint} endpoint did not recognise this title.`);
return null;
} catch (e) {
Logger.ELog(`Error fetching Radarr ${endpoint} endpoint: ${e.message}`);
return null;
}
}
/**
* @description Constructs a query string from the given parameters
* @param {Object} params - Key-value pairs to be converted into a query string
* @returns {string} The constructed query string
*/
function buildQueryParams(obj) {
return Object.entries(obj)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
}