From 8c9960b45a348c8500a56522a97fc6e781317716 Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 17 Jul 2026 19:34:12 +0200 Subject: [PATCH] chore: add Radarr_-_Trigger_Manual_Import.js from live FileFlows export --- .../scripts/Radarr_-_Trigger_Manual_Import.js | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 fileflows/scripts/Radarr_-_Trigger_Manual_Import.js diff --git a/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js b/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js new file mode 100644 index 0000000..4fcea78 --- /dev/null +++ b/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js @@ -0,0 +1,235 @@ +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'; + + 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 Radarr’s **Manual Import** analyser. + * @param {Radarr} radarr – Radarr API instance + * @param {string} importPath – Folder/File path to inspect + * @returns {Array} 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('&'); +} \ No newline at end of file