FileFlows: Radarr-Import auf exakte Datei begrenzen #1
1 changed files with 115 additions and 60 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue