diff --git a/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js b/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js index 5b24baf..5ebc556 100644 --- a/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js +++ b/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js @@ -1,8 +1,8 @@ /** * @name Radarr - Trigger Manual Import - * @description Trigger Radarr ManualImport or downloadedMoviesScan with skip-guard. + * @description Safely trigger Radarr ManualImport for exactly one processed file. * @author FileFlows + Sascha - * @revision 9 + * @revision 10 * @output Import erfolgreich * @output Fehler * @param {string} URL Radarr root URL and port @@ -18,11 +18,18 @@ 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; + const currentFile = Variables['file.FullName'] || Variables.file?.FullName || ''; + ImportPath = ImportPath || currentFile; TimeOut = TimeOut ? Math.min(TimeOut, 3600) * 1000 : 60000; // ms ImportPath = UseUnmappedPath ? Flow.UnMapPath(ImportPath) : ImportPath; + const exactImportPath = UseUnmappedPath ? Flow.UnMapPath(currentFile) : currentFile; const importMode = MoveMode ? 'move' : 'copy'; + if (!exactImportPath) { + Logger.ELog('No exact current file path available; refusing broad Radarr scan.'); + return -1; + } + // ── 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). @@ -32,7 +39,6 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) { // 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; @@ -43,26 +49,23 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) { /*── movieId detection ──────────────────────────────────*/ const searchPattern = Variables.file.Orig.FileNameNoExtension; - let movieId = Variables['Radarr.movieId'] ?? Variables.MovieInfo?.id ?? parseMovie(searchPattern, radarr) ?? null; + let movieId = Variables['Radarr.movieId'] + ?? findMovieIdFromQueue(searchPattern, radarr) + ?? findMovieIdFromGrabHistory(searchPattern, radarr) + ?? parseMovie(searchPattern, radarr) + ?? null; Logger.ILog(`Radarr URL: ${URL}`); - Logger.ILog(`Triggering Path: ${ImportPath}`); + Logger.ILog(`Triggering Path: ${exactImportPath}`); Logger.ILog(`Import Mode: ${importMode}`); - Logger.ILog(movieId ? `movieId: ${movieId} → ManualImport` : 'No movieId → downloadedMoviesScan'); + Logger.ILog(movieId ? `movieId: ${movieId} → ManualImport` : 'No unambiguous movieId → fail closed'); - /*─ 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); + if (!movieId) { + Logger.ELog(`Unable to identify '${searchPattern}' uniquely; file remains for manual recovery.`); + return -1; } - return result; + return manualImportWorkflow(radarr, exactImportPath, importMode, movieId, TimeOut); } @@ -84,10 +87,14 @@ function manualImportWorkflow(radarr, importPath, mode, movieId, timeout) { return -1; } - /* choose the first candidate that has quality info */ - const cand = candidates.find(c => c.quality && c.quality.quality); + /* accept only the candidate for the exact processed file */ + const exactPath = normalizeImportPath(importPath); + const matchingCandidates = candidates.filter(c => + normalizeImportPath(c.path || '') === exactPath + ); + const cand = matchingCandidates.find(c => c.quality && c.quality.quality); if (!cand) { - Logger.WLog('No ManualImport candidate contained quality information.'); + Logger.WLog(`No ManualImport candidate matched exact path '${importPath}'.`); return -1; } @@ -159,45 +166,6 @@ function sendManualImportCommand(radarr, cmdBody) { } } -/*──────────────────────────── 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 ──────────────────────────────────*/ /** @@ -218,6 +186,93 @@ function waitForCommand(radarr, cmdId, timeoutMs) { return -1; } +/** + * Resolve the movie from Radarr's active download queue. The queue title is + * the original release name and therefore survives FileFlows renaming. + * Only one exact normalized match is accepted. + */ +function findMovieIdFromQueue(searchPattern, radarr) { + if (!searchPattern) { + return null; + } + + try { + const query = buildQueryParams({ + page: 1, + pageSize: 200, + includeUnknownMovieItems: true + }); + const response = radarr.fetchJson('queue', query) || {}; + const records = Array.isArray(response) ? response : (response.records || []); + const wanted = normalizeReleaseTitle(searchPattern); + const matches = records.filter(item => + item?.movieId && normalizeReleaseTitle(item.title || '') === wanted + ); + + if (matches.length === 1) { + Logger.ILog(`Matched Radarr queue movieId ${matches[0].movieId}.`); + return matches[0].movieId; + } + if (matches.length > 1) { + Logger.WLog(`Ambiguous Radarr queue match (${matches.length} records).`); + } + return null; + } catch (e) { + Logger.ELog(`Error fetching Radarr queue: ${e.message}`); + return null; + } +} + +/** + * Resolve a release that has already left the active queue from Radarr's grab + * history. Repeated grabs are accepted only when every exact-title match points + * to the same movie. + */ +function findMovieIdFromGrabHistory(searchPattern, radarr) { + if (!searchPattern) { + return null; + } + + try { + const query = buildQueryParams({ + page: 1, + pageSize: 250, + eventType: 1, // Radarr HistoryEventType.Grabbed + sortKey: 'date', + sortDirection: 'descending' + }); + const response = radarr.fetchJson('history', query) || {}; + const records = Array.isArray(response) ? response : (response.records || []); + const wanted = normalizeReleaseTitle(searchPattern); + const movieIds = records + .filter(item => + item?.movieId && normalizeReleaseTitle(item.sourceTitle || '') === wanted + ) + .map(item => item.movieId) + .filter((id, index, values) => values.indexOf(id) === index); + + if (movieIds.length === 1) { + Logger.ILog(`Matched Radarr grab history movieId ${movieIds[0]}.`); + return movieIds[0]; + } + if (movieIds.length > 1) { + Logger.WLog(`Ambiguous Radarr grab history match (${movieIds.length} movies).`); + } + return null; + } catch (e) { + Logger.ELog(`Error fetching Radarr history: ${e.message}`); + return null; + } +} + +function normalizeReleaseTitle(value) { + return (value || '').toLowerCase().replace(/[^a-z0-9]+/g, ''); +} + +function normalizeImportPath(value) { + return (value || '').replace(/\\/g, '/').replace(/\/+$/g, ''); +} + /** * @description Parse the movie name using Radarr parsing based on the search pattern. * @param {string} searchPattern - The search string (file or folder name)