FileFlows Arr-Handoff robust gegen Duplicate-Loops #3

Merged
sascha merged 12 commits from fix/fileflows-arr-handoff-robustness-20260912 into main 2026-09-12 12:10:56 +02:00
Showing only changes of commit 3c714618e6 - Show all commits

View file

@ -2,7 +2,7 @@
* @name Radarr - Trigger Manual Import * @name Radarr - Trigger Manual Import
* @description Safely trigger Radarr ManualImport for exactly one processed file. * @description Safely trigger Radarr ManualImport for exactly one processed file.
* @author FileFlows + Sascha * @author FileFlows + Sascha
* @revision 10 * @revision 11
* @output Import erfolgreich * @output Import erfolgreich
* @output Fehler * @output Fehler
* @param {string} URL Radarr root URL and port * @param {string} URL Radarr root URL and port
@ -48,12 +48,20 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) {
const radarr = new Radarr(URL, ApiKey); const radarr = new Radarr(URL, ApiKey);
/*── movieId detection ──────────────────────────────────*/ /*── movieId detection ──────────────────────────────────*/
const searchPattern = Variables.file.Orig.FileNameNoExtension; const filePattern = Variables.file.Orig.FileNameNoExtension;
let movieId = Variables['Radarr.movieId'] const folderPattern = System.IO.Path.GetFileName(System.IO.Path.GetDirectoryName(exactImportPath));
?? findMovieIdFromQueue(searchPattern, radarr) const searchPatterns = [folderPattern, filePattern]
?? findMovieIdFromGrabHistory(searchPattern, radarr) .filter(Boolean)
?? parseMovie(searchPattern, radarr) .filter((value, index, values) => values.indexOf(value) === index);
?? null; let movieId = Variables['Radarr.movieId'] ?? null;
for (const pattern of searchPatterns) {
if (!movieId) {
movieId = findMovieIdFromQueue(pattern, radarr)
?? findMovieIdFromGrabHistory(pattern, radarr);
}
if (movieId) break;
}
if (!movieId) movieId = parseMovie(searchPatterns[0] || '', radarr);
Logger.ILog(`Radarr URL: ${URL}`); Logger.ILog(`Radarr URL: ${URL}`);
Logger.ILog(`Triggering Path: ${exactImportPath}`); Logger.ILog(`Triggering Path: ${exactImportPath}`);
@ -61,11 +69,12 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) {
Logger.ILog(movieId ? `movieId: ${movieId} → ManualImport` : 'No unambiguous movieId → fail closed'); Logger.ILog(movieId ? `movieId: ${movieId} → ManualImport` : 'No unambiguous movieId → fail closed');
if (!movieId) { if (!movieId) {
Logger.ELog(`Unable to identify '${searchPattern}' uniquely; file remains for manual recovery.`); Logger.ELog(`Unable to identify '${searchPatterns.join("' or '")}' uniquely; file remains for manual recovery.`);
return -1; return -1;
} }
return manualImportWorkflow(radarr, exactImportPath, importMode, movieId, TimeOut); const releaseMetadata = findGrabMetadata(searchPatterns, radarr, movieId);
return manualImportWorkflow(radarr, exactImportPath, importMode, movieId, releaseMetadata, TimeOut);
} }
@ -80,7 +89,7 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) {
* @param {number} timeout - Timeout in milliseconds * @param {number} timeout - Timeout in milliseconds
* @returns {number} 1 on success, −1 on failure * @returns {number} 1 on success, −1 on failure
*/ */
function manualImportWorkflow(radarr, importPath, mode, movieId, timeout) { function manualImportWorkflow(radarr, importPath, mode, movieId, releaseMetadata, timeout) {
const candidates = getManualImportCandidates(radarr, importPath); const candidates = getManualImportCandidates(radarr, importPath);
if (!candidates.length) { if (!candidates.length) {
Logger.WLog('No candidates returned by fetching ManualImport'); Logger.WLog('No candidates returned by fetching ManualImport');
@ -98,7 +107,7 @@ function manualImportWorkflow(radarr, importPath, mode, movieId, timeout) {
return -1; return -1;
} }
const fileObj = buildManualImportFile(cand, movieId, importPath); const fileObj = buildManualImportFile(cand, movieId, importPath, releaseMetadata);
const cmdBody = { name: 'ManualImport', files: [fileObj], importMode: mode }; const cmdBody = { name: 'ManualImport', files: [fileObj], importMode: mode };
const cmdId = sendManualImportCommand(radarr, cmdBody); const cmdId = sendManualImportCommand(radarr, cmdBody);
if (cmdId === null) { if (cmdId === null) {
@ -133,21 +142,49 @@ function getManualImportCandidates(radarr, importPath) {
* @param {number} movieId – Radarr movieId to which the file is linked * @param {number} movieId – Radarr movieId to which the file is linked
* @returns {object} File descriptor for ManualImport * @returns {object} File descriptor for ManualImport
*/ */
function buildManualImportFile(src, movieId, importPath) { function buildManualImportFile(src, movieId, importPath, releaseMetadata) {
const path = src.path || importPath const path = src.path || importPath
const fallbackFolder = System.IO.Path.GetFileName(System.IO.Path.GetDirectoryName(path)) ?? Variables.folder.Name; const fallbackFolder = System.IO.Path.GetFileName(System.IO.Path.GetDirectoryName(path)) ?? Variables.folder.Name;
const metadata = releaseMetadata || {};
const metadataData = metadata.data || {};
return { return {
path: path, path: path,
folderName: src.folderName || src.name || fallbackFolder, folderName: src.folderName || src.name || fallbackFolder,
movieId: movieId, movieId: movieId,
releaseGroup: src.releaseGroup || 'Radarr', releaseGroup: metadataData.releaseGroup || metadata.releaseGroup || src.releaseGroup || 'Radarr',
quality: src.quality, quality: metadata.quality || src.quality,
languages: src.languages || [ { id:0, name:'Unknown' } ], languages: metadata.languages || src.languages || [ { id:0, name:'Unknown' } ],
indexerFlags: src.indexerFlags ?? 0 indexerFlags: Number(metadataData.indexerFlags ?? metadata.indexerFlags ?? src.indexerFlags ?? 0)
}; };
} }
function findGrabMetadata(searchPatterns, radarr, movieId) {
try {
const query = buildQueryParams({
page: 1, pageSize: 250, eventType: 1,
sortKey: 'date', sortDirection: 'descending'
});
const response = radarr.fetchJson('history', query) || {};
const records = Array.isArray(response) ? response : (response.records || []);
const wanted = searchPatterns.map(normalizeReleaseTitle);
const matching = records.filter(item =>
item?.movieId === movieId
&& wanted.includes(normalizeReleaseTitle(item.sourceTitle || ''))
);
if (!matching.length) return null;
const movieIds = matching
.map(item => item.movieId)
.filter((id, index, values) => values.indexOf(id) === index);
if (movieIds.length !== 1) return null;
Logger.ILog(`Using metadata from exact Radarr grab history match (${matching[0].sourceTitle}).`);
return matching[0];
} catch (e) {
Logger.WLog(`Unable to recover Radarr grab metadata: ${e.message}`);
return null;
}
}
/** /**
* @description Send the **ManualImport** command to Radarr and return command-id * @description Send the **ManualImport** command to Radarr and return command-id
* @param {Radarr} radarr – Radarr API instance * @param {Radarr} radarr – Radarr API instance