diff --git a/fileflows/flow/flow_auto_movie_pipeline.json b/fileflows/flow/flow_auto_movie_pipeline.json index 91cffd0..35498dc 100644 --- a/fileflows/flow/flow_auto_movie_pipeline.json +++ b/fileflows/flow/flow_auto_movie_pipeline.json @@ -310,7 +310,7 @@ "ErrorConnection": { "Input": 1, "Output": -1, - "InputNode": "6bf4e65f-5376-4b52-a754-bc0972620be4" + "InputNode": "64a5eb5c-13d3-47d7-b67a-a013fad58145" }, "Type": 9, "Model": { @@ -343,7 +343,7 @@ "ErrorConnection": { "Input": 1, "Output": -1, - "InputNode": "6bf4e65f-5376-4b52-a754-bc0972620be4" + "InputNode": "64a5eb5c-13d3-47d7-b67a-a013fad58145" }, "Type": 9, "Model": { @@ -377,7 +377,7 @@ "ErrorConnection": { "Input": 1, "Output": -1, - "InputNode": "6bf4e65f-5376-4b52-a754-bc0972620be4" + "InputNode": "64a5eb5c-13d3-47d7-b67a-a013fad58145" }, "Type": 9, "Model": { @@ -411,7 +411,7 @@ "ErrorConnection": { "Input": 1, "Output": -1, - "InputNode": "6bf4e65f-5376-4b52-a754-bc0972620be4" + "InputNode": "64a5eb5c-13d3-47d7-b67a-a013fad58145" }, "Type": 9, "Model": { diff --git a/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js b/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js index b4c4844..5ebc556 100644 --- a/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js +++ b/fileflows/scripts/Radarr_-_Trigger_Manual_Import.js @@ -2,7 +2,7 @@ * @name Radarr - Trigger Manual Import * @description Safely trigger Radarr ManualImport for exactly one processed file. * @author FileFlows + Sascha - * @revision 11 + * @revision 10 * @output Import erfolgreich * @output Fehler * @param {string} URL Radarr root URL and port @@ -48,20 +48,12 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) { const radarr = new Radarr(URL, ApiKey); /*── movieId detection ──────────────────────────────────*/ - 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 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); + const searchPattern = Variables.file.Orig.FileNameNoExtension; + let movieId = Variables['Radarr.movieId'] + ?? findMovieIdFromQueue(searchPattern, radarr) + ?? findMovieIdFromGrabHistory(searchPattern, radarr) + ?? parseMovie(searchPattern, radarr) + ?? null; Logger.ILog(`Radarr URL: ${URL}`); Logger.ILog(`Triggering Path: ${exactImportPath}`); @@ -69,12 +61,11 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) { Logger.ILog(movieId ? `movieId: ${movieId} → ManualImport` : 'No unambiguous movieId → fail closed'); if (!movieId) { - Logger.ELog(`Unable to identify '${searchPatterns.join("' or '")}' uniquely; file remains for manual recovery.`); + Logger.ELog(`Unable to identify '${searchPattern}' uniquely; file remains for manual recovery.`); return -1; } - const releaseMetadata = findGrabMetadata(searchPatterns, radarr, movieId); - return manualImportWorkflow(radarr, exactImportPath, importMode, movieId, releaseMetadata, TimeOut); + return manualImportWorkflow(radarr, exactImportPath, importMode, movieId, TimeOut); } @@ -89,7 +80,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(radarr, importPath, mode, movieId, releaseMetadata, timeout) { +function manualImportWorkflow(radarr, importPath, mode, movieId, timeout) { const candidates = getManualImportCandidates(radarr, importPath); if (!candidates.length) { Logger.WLog('No candidates returned by fetching ManualImport'); @@ -107,7 +98,7 @@ function manualImportWorkflow(radarr, importPath, mode, movieId, releaseMetadata return -1; } - const fileObj = buildManualImportFile(cand, movieId, importPath, releaseMetadata); + const fileObj = buildManualImportFile(cand, movieId, importPath); const cmdBody = { name: 'ManualImport', files: [fileObj], importMode: mode }; const cmdId = sendManualImportCommand(radarr, cmdBody); if (cmdId === null) { @@ -142,49 +133,21 @@ function getManualImportCandidates(radarr, importPath) { * @param {number} movieId – Radarr movieId to which the file is linked * @returns {object} File descriptor for ManualImport */ -function buildManualImportFile(src, movieId, importPath, releaseMetadata) { +function buildManualImportFile(src, movieId, importPath) { 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, movieId: movieId, - releaseGroup: metadataData.releaseGroup || metadata.releaseGroup || src.releaseGroup || 'Radarr', - quality: metadata.quality || src.quality, - languages: metadata.languages || src.languages || [ { id:0, name:'Unknown' } ], - indexerFlags: Number(metadataData.indexerFlags ?? metadata.indexerFlags ?? src.indexerFlags ?? 0) + releaseGroup: src.releaseGroup || 'Radarr', + quality: src.quality, + languages: src.languages || [ { id:0, name:'Unknown' } ], + 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 * @param {Radarr} radarr – Radarr API instance diff --git a/fileflows/scripts/Sonarr_-_Trigger_Manual_Import.js b/fileflows/scripts/Sonarr_-_Trigger_Manual_Import.js index 48dc729..6e06bcd 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 10 + * @revision 9 * @output Import erfolgreich * @output Fehler * @param {string} URL Sonarr root URL and port @@ -43,22 +43,14 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) { const sonarr = new Sonarr(URL, ApiKey); /*── seriesId / episodeId detection ──────────────────────────────────*/ - 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); + const searchPattern = Variables.file.Orig.FileNameNoExtension; let seriesId = Variables['Sonarr.seriesId'] ?? Variables.TVShowInfo?.id ?? null; let episodeIds = Variables['Sonarr.episodeIds'] ?? Variables.TVShowInfo?.EpisodesInfo ?? null; if (!(seriesId && episodeIds)) { - 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); + const trackedEpisode = findEpisodeFromQueue(searchPattern, sonarr) + ?? findEpisodeFromGrabHistory(searchPattern, sonarr) + ?? parseSeries(searchPattern, sonarr); seriesId = trackedEpisode?.id ?? null; episodeIds = trackedEpisode?.episodeIds ?? null; } @@ -68,12 +60,11 @@ 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, releaseMetadata, TimeOut); + return manualImportWorkflow(sonarr, exactImportPath, importMode, seriesId, episodeIds, TimeOut); } - Logger.ELog(`Unable to identify '${searchPatterns.join("' or '")}' uniquely; file remains for manual recovery.`); + Logger.ELog(`Unable to identify '${searchPattern}' uniquely; file remains for manual recovery.`); return -1; } @@ -89,7 +80,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, releaseMetadata, timeout) { +function manualImportWorkflow(sonarr, path, mode, seriesId, episodeIds, timeout) { const candidates = getManualImportCandidates(sonarr, path); if (!candidates.length) { Logger.WLog('No candidates returned by fetching ManualImport'); @@ -107,7 +98,7 @@ function manualImportWorkflow(sonarr, path, mode, seriesId, episodeIds, releaseM return -1; } - const fileObj = buildManualImportFile(cand, seriesId, episodeIds, path, releaseMetadata); + const fileObj = buildManualImportFile(cand, seriesId, episodeIds, path); const cmdBody = { name: 'ManualImport', files: [fileObj], importMode: mode }; const cmdId = sendManualImportCommand(sonarr, cmdBody); if (cmdId === null) { @@ -146,52 +137,23 @@ 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, releaseMetadata) { +function buildManualImportFile(src, seriesId, episodeIds, importPath) { 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: 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' + releaseGroup: src.releaseGroup || 'Sonarr', + quality: src.quality, + languages: src.languages || [ { id:0, name:'Unknown' } ], + indexerFlags: src.indexerFlags ?? 0, + 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 diff --git a/fileflows/tests/test_flow_import_failure_routing.py b/fileflows/tests/test_flow_import_failure_routing.py deleted file mode 100644 index bdb370c..0000000 --- a/fileflows/tests/test_flow_import_failure_routing.py +++ /dev/null @@ -1,31 +0,0 @@ -import json -import unittest -from pathlib import Path - - -FLOW = Path(__file__).parents[1] / "flow" / "flow_auto_movie_pipeline.json" - - -class FlowImportFailureRoutingTests(unittest.TestCase): - def test_arr_import_failures_route_to_fail_flow(self): - flow = json.loads(FLOW.read_text()) - fail_node = next( - part - for part in flow["Parts"] - if part.get("FlowElementUid") == "FileFlows.BasicNodes.Functions.FailFlow" - ) - import_nodes = [ - part - for part in flow["Parts"] - if part.get("Name") - in {"Sonarr - Trigger Manual Import", "Radarr - Trigger Manual Import"} - ] - - self.assertEqual(len(import_nodes), 4) - for node in import_nodes: - with self.subTest(node=node["Uid"]): - self.assertEqual(node["ErrorConnection"]["InputNode"], fail_node["Uid"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/fileflows/tests/test_radarr_import_safety.py b/fileflows/tests/test_radarr_import_safety.py index b281ec2..a895783 100644 --- a/fileflows/tests/test_radarr_import_safety.py +++ b/fileflows/tests/test_radarr_import_safety.py @@ -193,76 +193,6 @@ class RadarrImportSafetyTests(unittest.TestCase): ) self.assertIn("eventType=1", history_fetch["query"]) - def test_grab_metadata_overrides_unknown_manualimport_metadata(self): - release = "American.Sweatshop.2025.German.DTSHD.DL.1080p.BluRay.AVC.Remux-MAMA" - exact = f"/tdarr/converted/video/{release}/{release}.mkv" - grabbed_quality = {"quality": {"id": 30, "name": "Remux-1080p"}} - grabbed_languages = [ - {"id": 1, "name": "English"}, - {"id": 4, "name": "German"}, - ] - result = run_script( - { - "releaseTitle": release, - "exactPath": exact, - "scopeRoot": "/tdarr/converted/video/", - "queueResponse": {"records": [{"title": release, "movieId": 6425}]}, - "historyResponse": { - "records": [ - { - "sourceTitle": release, - "movieId": 6425, - "eventType": "grabbed", - "quality": grabbed_quality, - "languages": grabbed_languages, - "data": {"releaseGroup": "MAMA", "indexerFlags": "4"}, - } - ] - }, - "candidates": [ - { - "path": exact, - "releaseGroup": "Radarr", - "quality": {"quality": {"id": 0, "name": "Unknown"}}, - "languages": [{"id": 0, "name": "Unknown"}], - } - ], - } - ) - - self.assertEqual(result["result"], 1) - command = next(call for call in result["calls"] if call["type"] == "command") - imported = command["body"]["files"][0] - self.assertEqual(imported["quality"], grabbed_quality) - self.assertEqual(imported["languages"], grabbed_languages) - self.assertEqual(imported["releaseGroup"], "MAMA") - self.assertEqual(imported["indexerFlags"], 4) - - def test_release_folder_matches_when_movie_filename_is_shortened(self): - release = "Example.Movie.2026.German.DL.1080p.WEB.H264-GROUP" - exact = f"/tdarr/converted/video/{release}/movie-final.mkv" - result = run_script( - { - "releaseTitle": "movie-final", - "exactPath": exact, - "scopeRoot": "/tdarr/converted/video/", - "queueResponse": {"records": [{"title": release, "movieId": 7331}]}, - "historyResponse": {"records": []}, - "parseResponse": {}, - "candidates": [ - { - "path": exact, - "quality": {"quality": {"id": 3, "name": "WEBDL-1080p"}}, - "languages": [{"id": 4, "name": "German"}], - } - ], - } - ) - - self.assertEqual(result["result"], 1) - command = next(call for call in result["calls"] if call["type"] == "command") - self.assertEqual(command["body"]["files"][0]["movieId"], 7331) - if __name__ == "__main__": unittest.main() diff --git a/fileflows/tests/test_sonarr_import_safety.py b/fileflows/tests/test_sonarr_import_safety.py index 7c45df6..b05ee18 100644 --- a/fileflows/tests/test_sonarr_import_safety.py +++ b/fileflows/tests/test_sonarr_import_safety.py @@ -119,98 +119,6 @@ class SonarrImportSafetyTests(unittest.TestCase): [call.get("name") for call in commands], ) - def test_release_folder_matches_when_episode_filename_is_scene_shortened(self): - release = ( - "Angie.Tribeca.S01E06.Frettchen.Royale.GERMAN.DL." - "1080p.HDTV.x264-SYNCTV" - ) - exact = f"/tdarr/converted/serien/{release}/synctv-angie.s01e06.1080p.mkv" - result = run_script( - { - "releaseTitle": "synctv-angie.s01e06.1080p", - "exactPath": exact, - "scopeRoot": "/tdarr/converted/serien", - "queueResponse": { - "records": [ - {"title": release, "seriesId": 853, "episodeId": 63336} - ] - }, - "historyResponse": {"records": []}, - "parseResponse": {}, - "candidates": [ - { - "path": exact, - "quality": {"quality": {"id": 4, "name": "HDTV-1080p"}}, - "languages": [{"id": 4, "name": "German"}], - } - ], - } - ) - - self.assertEqual(result["result"], 1) - command = next(call for call in result["calls"] if call["type"] == "command") - imported = command["body"]["files"][0] - self.assertEqual(imported["seriesId"], 853) - self.assertEqual(imported["episodeIds"], [63336]) - - def test_grab_metadata_overrides_unknown_manualimport_metadata(self): - release = ( - "The.Night.Agent.2025.S02E04.Desperate.Measures.2160p.NF." - "WEB-DL.MULTi.DDP5.1.Atmos.H.265-FUZEER" - ) - exact = f"/tdarr/converted/serien4k/{release}/{release}.mkv" - grabbed_quality = {"quality": {"id": 18, "name": "WEBDL-2160p"}} - grabbed_languages = [ - {"id": 1, "name": "English"}, - {"id": 4, "name": "German"}, - ] - result = run_script( - { - "releaseTitle": release, - "exactPath": exact, - "scopeRoot": "/tdarr/converted/serien4k", - "queueResponse": { - "records": [ - {"title": release, "seriesId": 602, "episodeId": 37528} - ] - }, - "historyResponse": { - "records": [ - { - "sourceTitle": release, - "seriesId": 602, - "episodeId": 37528, - "eventType": "grabbed", - "quality": grabbed_quality, - "languages": grabbed_languages, - "data": { - "releaseGroup": "FUZEER", - "indexerFlags": "8", - "releaseType": "SingleEpisode", - }, - } - ] - }, - "candidates": [ - { - "path": exact, - "releaseGroup": "Sonarr", - "quality": {"quality": {"id": 0, "name": "Unknown"}}, - "languages": [{"id": 0, "name": "Unknown"}], - } - ], - } - ) - - self.assertEqual(result["result"], 1) - command = next(call for call in result["calls"] if call["type"] == "command") - imported = command["body"]["files"][0] - self.assertEqual(imported["quality"], grabbed_quality) - self.assertEqual(imported["languages"], grabbed_languages) - self.assertEqual(imported["releaseGroup"], "FUZEER") - self.assertEqual(imported["indexerFlags"], 8) - self.assertEqual(imported["releaseType"], "SingleEpisode") - if __name__ == "__main__": unittest.main()