From cb054cbac70cbce2b8552a86d1bc3dfdcd28c44a Mon Sep 17 00:00:00 2001 From: sascha Date: Mon, 14 Sep 2026 09:15:04 +0200 Subject: [PATCH 1/2] Fix Sonarr path boundary and exact episode parsing --- .../scripts/Sonarr_-_Trigger_Manual_Import.js | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/fileflows/scripts/Sonarr_-_Trigger_Manual_Import.js b/fileflows/scripts/Sonarr_-_Trigger_Manual_Import.js index 48dc729..1adfe02 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 11 * @output Import erfolgreich * @output Fehler * @param {string} URL Sonarr root URL and port @@ -31,13 +31,16 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) { } // ── 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). - if (ImportPath && ImportPath.indexOf(URL) === -1) { - if (currentFile && ImportPath && currentFile.indexOf(ImportPath) === -1) { - Logger.ILog(`Skip: current file '${currentFile}' not under ImportPath '${ImportPath}' - not responsible`); - return 1; - } + // mit dem konfigurierten Pfad als vollständiges Pfadsegment + // übereinstimmt. Ein nacktes Prefix würde z. B. /serien4k + // fälschlich dem FHD-Root /serien zuordnen. + const normalizedCurrentPath = normalizeImportPath(exactImportPath); + const normalizedScopeRoot = normalizeImportPath(ImportPath); + const isInScope = normalizedCurrentPath === normalizedScopeRoot + || normalizedCurrentPath.startsWith(normalizedScopeRoot + '/'); + if (!isInScope) { + Logger.ILog(`Skip: current file '${currentFile}' not under ImportPath '${ImportPath}' - not responsible`); + return 1; } const sonarr = new Sonarr(URL, ApiKey); @@ -58,7 +61,18 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) { ?? findEpisodeFromGrabHistory(pattern, sonarr); if (trackedEpisode) break; } - if (!trackedEpisode) trackedEpisode = parseSeries(searchPatterns[0] || '', sonarr); + if (!trackedEpisode) { + // Ein Staffelpaket-Ordner ist absichtlich mehrdeutig. Zuerst + // den exakten Episoden-Dateinamen parsen, danach erst den + // Ordnernamen als letzten Fallback versuchen. + const parsePatterns = [filePattern, folderPattern] + .filter(Boolean) + .filter((value, index, values) => values.indexOf(value) === index); + for (const pattern of parsePatterns) { + trackedEpisode = parseSeries(pattern, sonarr); + if (trackedEpisode?.id && trackedEpisode?.episodeIds?.length) break; + } + } seriesId = trackedEpisode?.id ?? null; episodeIds = trackedEpisode?.episodeIds ?? null; } From 3153e66cc8d7d3c4b1cdd82e07ab08a5cfb2c7fd Mon Sep 17 00:00:00 2001 From: sascha Date: Mon, 14 Sep 2026 09:15:05 +0200 Subject: [PATCH 2/2] Add season-pack and sibling-prefix regression tests --- fileflows/tests/test_sonarr_import_safety.py | 63 ++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/fileflows/tests/test_sonarr_import_safety.py b/fileflows/tests/test_sonarr_import_safety.py index 7c45df6..26f6308 100644 --- a/fileflows/tests/test_sonarr_import_safety.py +++ b/fileflows/tests/test_sonarr_import_safety.py @@ -77,6 +77,69 @@ def run_script(scenario): class SonarrImportSafetyTests(unittest.TestCase): + def test_fhd_scope_does_not_match_uhd_sibling_prefix(self): + exact = ( + "/tdarr/converted/serien4k/Marshals.2026.S01.GERMAN.DUBBED/" + "Marshals.2026.S01E05.GERMAN.DUBBED.mkv" + ) + result = run_script( + { + "releaseTitle": "Marshals.2026.S01E05.GERMAN.DUBBED", + "exactPath": exact, + "scopeRoot": "/tdarr/converted/serien", + } + ) + + self.assertEqual(result["result"], 1) + self.assertEqual(result["calls"], []) + + def test_season_pack_history_uses_exact_episode_filename_for_parse(self): + pack = "Marshals.2026.S01.GERMAN.DUBBED.DL.2160p.WEB.H265-TSCC" + episode = "Marshals.2026.S01E05.GERMAN.DUBBED.DL.2160p.WEB.H265-TSCC" + exact = f"/tdarr/converted/serien4k/{pack}/{episode}.mkv" + queue_records = [ + {"title": pack, "seriesId": 600, "episodeId": 37500 + number} + for number in range(1, 14) + ] + history_records = [ + { + "sourceTitle": pack, + "seriesId": 600, + "episodeId": 37500 + number, + "eventType": "grabbed", + } + for number in range(1, 14) + ] + result = run_script( + { + "releaseTitle": episode, + "exactPath": exact, + "scopeRoot": "/tdarr/converted/serien4k", + "queueResponse": {"records": queue_records}, + "historyResponse": {"records": history_records}, + "parseResponse": { + "series": {"id": 600, "title": "Marshals"}, + "episodes": [{"id": 37505}], + }, + "candidates": [ + { + "path": exact, + "quality": {"quality": {"id": 19, "name": "WEBDL-2160p"}}, + "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]["episodeIds"], [37505]) + parse_call = next( + call for call in result["calls"] + if call["type"] == "fetch" and call["endpoint"] == "parse" + ) + self.assertIn("s01e05", parse_call["query"].lower()) + def test_percent_title_matches_queue_and_imports_exact_episode_file(self): source_title = ( "100%.Wolf.S01E01.Die.Aufnahmeprufung.German.1080p."