From 81a01d5880e8e865710ace88e4bdc3a4eca3b0e3 Mon Sep 17 00:00:00 2001 From: sascha Date: Sat, 12 Sep 2026 12:03:07 +0200 Subject: [PATCH] fix: preserve Arr metadata and fail closed --- .../scripts/Sonarr_-_Trigger_Manual_Import.js | 68 +++++++++++++++---- 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/fileflows/scripts/Sonarr_-_Trigger_Manual_Import.js b/fileflows/scripts/Sonarr_-_Trigger_Manual_Import.js index 6e06bcd..48dc729 100644 --- a/fileflows/scripts/Sonarr_-_Trigger_Manual_Import.js +++ b/fileflows/scripts/Sonarr_-_Trigger_Manual_Import.js @@ -2,7 +2,7 @@ * @name Sonarr - Trigger Manual Import * @description Safely trigger Sonarr ManualImport for exactly one processed episode file. * @author FileFlows + Sascha - * @revision 9 + * @revision 10 * @output Import erfolgreich * @output Fehler * @param {string} URL Sonarr root URL and port @@ -43,14 +43,22 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) { const sonarr = new Sonarr(URL, ApiKey); /*── seriesId / episodeId detection ──────────────────────────────────*/ - const searchPattern = Variables.file.Orig.FileNameNoExtension; + const filePattern = Variables.file.Orig.FileNameNoExtension; + const folderPattern = System.IO.Path.GetFileName(System.IO.Path.GetDirectoryName(exactImportPath)); + const searchPatterns = [folderPattern, filePattern] + .filter(Boolean) + .filter((value, index, values) => values.indexOf(value) === index); let seriesId = Variables['Sonarr.seriesId'] ?? Variables.TVShowInfo?.id ?? null; let episodeIds = Variables['Sonarr.episodeIds'] ?? Variables.TVShowInfo?.EpisodesInfo ?? null; if (!(seriesId && episodeIds)) { - const trackedEpisode = findEpisodeFromQueue(searchPattern, sonarr) - ?? findEpisodeFromGrabHistory(searchPattern, sonarr) - ?? parseSeries(searchPattern, sonarr); + let trackedEpisode = null; + for (const pattern of searchPatterns) { + trackedEpisode = findEpisodeFromQueue(pattern, sonarr) + ?? findEpisodeFromGrabHistory(pattern, sonarr); + if (trackedEpisode) break; + } + if (!trackedEpisode) trackedEpisode = parseSeries(searchPatterns[0] || '', sonarr); seriesId = trackedEpisode?.id ?? null; episodeIds = trackedEpisode?.episodeIds ?? null; } @@ -60,11 +68,12 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) { Logger.ILog(`Import Mode: ${importMode}`); if (seriesId && episodeIds) { + const releaseMetadata = findGrabMetadata(searchPatterns, sonarr, seriesId, episodeIds); Logger.ILog(`seriesId=${seriesId}, episodeIds=[${episodeIds.join(', ')}] → ManualImport`); - return manualImportWorkflow(sonarr, exactImportPath, importMode, seriesId, episodeIds, TimeOut); + return manualImportWorkflow(sonarr, exactImportPath, importMode, seriesId, episodeIds, releaseMetadata, TimeOut); } - 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; } @@ -80,7 +89,7 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) { * @param {number} timeout - Timeout in milliseconds * @returns {number} 1 on success, −1 on failure */ -function manualImportWorkflow(sonarr, path, mode, seriesId, episodeIds, timeout) { +function manualImportWorkflow(sonarr, path, mode, seriesId, episodeIds, releaseMetadata, timeout) { const candidates = getManualImportCandidates(sonarr, path); if (!candidates.length) { Logger.WLog('No candidates returned by fetching ManualImport'); @@ -98,7 +107,7 @@ function manualImportWorkflow(sonarr, path, mode, seriesId, episodeIds, timeout) return -1; } - const fileObj = buildManualImportFile(cand, seriesId, episodeIds, path); + const fileObj = buildManualImportFile(cand, seriesId, episodeIds, path, releaseMetadata); const cmdBody = { name: 'ManualImport', files: [fileObj], importMode: mode }; const cmdId = sendManualImportCommand(sonarr, cmdBody); if (cmdId === null) { @@ -137,23 +146,52 @@ function getManualImportCandidates(sonarr, importPath) { * @param {number} episodeIds – Sonarr episodeIds to which the file is linked * @returns {object} File descriptor for ManualImport */ -function buildManualImportFile(src, seriesId, episodeIds, importPath) { +function buildManualImportFile(src, seriesId, episodeIds, importPath, releaseMetadata) { const path = src.path || importPath; const fallbackFolder = System.IO.Path.GetFileName(System.IO.Path.GetDirectoryName(path)) ?? Variables.folder.Name; + const metadata = releaseMetadata || {}; + const metadataData = metadata.data || {}; return { path: path, folderName: src.folderName || src.name || fallbackFolder, seriesId: seriesId, episodeIds: episodeIds, - releaseGroup: src.releaseGroup || 'Sonarr', - quality: src.quality, - languages: src.languages || [ { id:0, name:'Unknown' } ], - indexerFlags: src.indexerFlags ?? 0, - releaseType: src.releaseType ?? 'unknown' + releaseGroup: metadataData.releaseGroup || metadata.releaseGroup || src.releaseGroup || 'Sonarr', + quality: metadata.quality || src.quality, + languages: metadata.languages || src.languages || [ { id:0, name:'Unknown' } ], + indexerFlags: Number(metadataData.indexerFlags ?? metadata.indexerFlags ?? src.indexerFlags ?? 0), + releaseType: metadataData.releaseType || metadata.releaseType || src.releaseType || 'unknown' }; } +function findGrabMetadata(searchPatterns, sonarr, seriesId, episodeIds) { + try { + const query = buildQueryParams({ + page: 1, pageSize: 250, eventType: 1, + sortKey: 'date', sortDirection: 'descending' + }); + const response = sonarr.fetchJson('history', query) || {}; + const records = Array.isArray(response) ? response : (response.records || []); + const wanted = searchPatterns.map(normalizeReleaseTitle); + const matching = records.filter(item => + item?.seriesId === seriesId + && episodeIds.includes(item?.episodeId) + && wanted.includes(normalizeReleaseTitle(item.sourceTitle || '')) + ); + if (!matching.length) return null; + const targetKeys = matching + .map(item => `${item.seriesId}:${item.episodeId}`) + .filter((key, index, values) => values.indexOf(key) === index); + if (targetKeys.some(key => !episodeIds.includes(Number(key.split(':')[1])))) return null; + Logger.ILog(`Using metadata from exact Sonarr grab history match (${matching[0].sourceTitle}).`); + return matching[0]; + } catch (e) { + Logger.WLog(`Unable to recover Sonarr grab metadata: ${e.message}`); + return null; + } +} + /** * @description Send the **ManualImport** command to Sonarr and return command-id * @param {Sonarr} sonarr – Sonarr API instance