diff --git a/fileflows/flow/flow_auto_movie_pipeline.json b/fileflows/flow/flow_auto_movie_pipeline.json index 3446d87..95bc959 100644 --- a/fileflows/flow/flow_auto_movie_pipeline.json +++ b/fileflows/flow/flow_auto_movie_pipeline.json @@ -386,7 +386,7 @@ "ImportPath": "/tdarr/converted/serien4k", "UseUnmappedPath": false, "MoveMode": true, - "TimeOut": 900, + "TimeOut": 300, "Color": "" } }, @@ -420,7 +420,7 @@ "ImportPath": "/tdarr/converted/serien", "UseUnmappedPath": false, "MoveMode": true, - "TimeOut": 900, + "TimeOut": 300, "Color": "" } }, diff --git a/fileflows/scripts/Sonarr_-_Trigger_Manual_Import.js b/fileflows/scripts/Sonarr_-_Trigger_Manual_Import.js index 7dd870b..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 12 + * @revision 11 * @output Import erfolgreich * @output Fehler * @param {string} URL Sonarr root URL and port @@ -57,8 +57,8 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) { if (!(seriesId && episodeIds)) { let trackedEpisode = null; for (const pattern of searchPatterns) { - trackedEpisode = findEpisodeFromQueue(pattern, sonarr, filePattern) - ?? findEpisodeFromGrabHistory(pattern, sonarr, filePattern); + trackedEpisode = findEpisodeFromQueue(pattern, sonarr) + ?? findEpisodeFromGrabHistory(pattern, sonarr); if (trackedEpisode) break; } if (!trackedEpisode) { @@ -226,7 +226,7 @@ function sendManualImportCommand(sonarr, cmdBody) { /*────────────────────────────── helpers ──────────────────────────────────*/ -function findEpisodeFromQueue(searchPattern, sonarr, episodePattern) { +function findEpisodeFromQueue(searchPattern, sonarr) { if (!searchPattern) return null; try { const query = buildQueryParams({ page: 1, pageSize: 200, includeUnknownSeriesItems: true }); @@ -240,11 +240,7 @@ function findEpisodeFromQueue(searchPattern, sonarr, episodePattern) { Logger.ILog(`Matched Sonarr queue seriesId ${matches[0].seriesId}, episodeId ${matches[0].episodeId}.`); return { id: matches[0].seriesId, episodeIds: [matches[0].episodeId] }; } - if (matches.length > 1) { - const resolved = resolvePackEpisode(matches, episodePattern, sonarr); - if (resolved) return resolved; - Logger.WLog(`Ambiguous Sonarr queue match (${matches.length} records).`); - } + if (matches.length > 1) Logger.WLog(`Ambiguous Sonarr queue match (${matches.length} records).`); return null; } catch (e) { Logger.ELog(`Error fetching Sonarr queue: ${e.message}`); @@ -252,7 +248,7 @@ function findEpisodeFromQueue(searchPattern, sonarr, episodePattern) { } } -function findEpisodeFromGrabHistory(searchPattern, sonarr, episodePattern) { +function findEpisodeFromGrabHistory(searchPattern, sonarr) { if (!searchPattern) return null; try { const query = buildQueryParams({ @@ -273,11 +269,7 @@ function findEpisodeFromGrabHistory(searchPattern, sonarr, episodePattern) { Logger.ILog(`Matched Sonarr grab history seriesId ${seriesId}, episodeId ${episodeId}.`); return { id: seriesId, episodeIds: [episodeId] }; } - if (keys.length > 1) { - const resolved = resolvePackEpisode(matches, episodePattern, sonarr); - if (resolved) return resolved; - Logger.WLog(`Ambiguous Sonarr grab history match (${keys.length} episodes).`); - } + if (keys.length > 1) Logger.WLog(`Ambiguous Sonarr grab history match (${keys.length} episodes).`); return null; } catch (e) { Logger.ELog(`Error fetching Sonarr history: ${e.message}`); @@ -285,47 +277,6 @@ function findEpisodeFromGrabHistory(searchPattern, sonarr, episodePattern) { } } -function resolvePackEpisode(matches, episodePattern, sonarr) { - const coordinates = extractEpisodeCoordinates(episodePattern); - if (!coordinates) return null; - - const seriesIds = matches - .map(item => Number(item?.seriesId)) - .filter(Boolean) - .filter((value, index, values) => values.indexOf(value) === index); - if (seriesIds.length !== 1) return null; - - try { - const seriesId = seriesIds[0]; - const query = buildQueryParams({ seriesId }); - const episodes = sonarr.fetchJson('episode', query) || []; - const candidates = episodes.filter(item => - Number(item?.seasonNumber) === coordinates.seasonNumber - && Number(item?.episodeNumber) === coordinates.episodeNumber - ); - if (candidates.length !== 1) return null; - - const episodeId = Number(candidates[0]?.id); - const trackedIds = matches.map(item => Number(item?.episodeId)).filter(Boolean); - if (!episodeId || !trackedIds.includes(episodeId)) return null; - - Logger.ILog( - `Resolved season-pack match via exact filename S${coordinates.seasonNumber}` - + `E${coordinates.episodeNumber}: seriesId ${seriesId}, episodeId ${episodeId}.` - ); - return { id: seriesId, episodeIds: [episodeId] }; - } catch (e) { - Logger.WLog(`Unable to resolve season-pack episode: ${e.message}`); - return null; - } -} - -function extractEpisodeCoordinates(value) { - const match = String(value || '').match(/(?:^|[^a-z0-9])s(\d{1,2})e(\d{1,3})(?:[^a-z0-9]|$)/i); - if (!match) return null; - return { seasonNumber: Number(match[1]), episodeNumber: Number(match[2]) }; -} - function normalizeReleaseTitle(value) { return (value || '').toLowerCase().replace(/[^a-z0-9]+/g, ''); } diff --git a/fileflows/tests/test_sonarr_import_safety.py b/fileflows/tests/test_sonarr_import_safety.py index 9512215..26f6308 100644 --- a/fileflows/tests/test_sonarr_import_safety.py +++ b/fileflows/tests/test_sonarr_import_safety.py @@ -22,7 +22,6 @@ class FakeSonarr { calls.push({type: 'fetch', endpoint, query}); if (endpoint === 'queue') return scenario.queueResponse || {records: []}; if (endpoint === 'history') return scenario.historyResponse || {records: []}; - if (endpoint === 'episode') return scenario.episodeResponse || []; if (endpoint === 'parse') return scenario.parseResponse || {}; if (endpoint === 'manualimport') return scenario.candidates || []; return null; @@ -78,82 +77,6 @@ def run_script(scenario): class SonarrImportSafetyTests(unittest.TestCase): - def test_pack_with_ambiguous_queue_resolves_exact_episode_from_filename(self): - pack = "Lucky.S01.Complete.2026.GERMAN.aac.720p.WEB.h265-FritzBox" - episode = "Lucky.S01E05.German.Title.2026.GERMAN.aac.720p.WEB.h265-FritzBox" - exact = f"/tdarr/converted/serien/{pack}/{episode}.mp4" - queue_records = [ - {"title": pack, "seriesId": 794, "episodeId": 64000 + number} - for number in range(1, 8) - ] - history_records = [ - { - "sourceTitle": pack, - "seriesId": 794, - "episodeId": 64000 + number, - "eventType": "grabbed", - "quality": {"quality": {"id": 6, "name": "WEBDL-720p"}}, - "languages": [{"id": 4, "name": "German"}], - "data": {"releaseGroup": "FritzBox", "releaseType": "SeasonPack"}, - } - for number in range(1, 8) - ] - result = run_script( - { - "releaseTitle": episode, - "exactPath": exact, - "scopeRoot": "/tdarr/converted/serien", - "queueResponse": {"records": queue_records}, - "historyResponse": {"records": history_records}, - "episodeResponse": [ - {"id": 64000 + number, "seasonNumber": 1, "episodeNumber": number} - for number in range(1, 8) - ], - "parseResponse": {}, - "candidates": [ - { - "path": exact, - "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["seriesId"], 794) - self.assertEqual(imported["episodeIds"], [64005]) - self.assertEqual(imported["releaseGroup"], "FritzBox") - - def test_pack_resolution_fails_closed_when_series_ids_differ(self): - pack = "Lucky.S01.Complete.2026.GERMAN.aac.720p.WEB.h265-FritzBox" - episode = "Lucky.S01E05.German.Title.2026.GERMAN.aac.720p.WEB.h265-FritzBox" - exact = f"/tdarr/converted/serien/{pack}/{episode}.mp4" - result = run_script( - { - "releaseTitle": episode, - "exactPath": exact, - "scopeRoot": "/tdarr/converted/serien", - "queueResponse": { - "records": [ - {"title": pack, "seriesId": 794, "episodeId": 64005}, - {"title": pack, "seriesId": 999, "episodeId": 99005}, - ] - }, - "historyResponse": {"records": []}, - "episodeResponse": [ - {"id": 64005, "seasonNumber": 1, "episodeNumber": 5} - ], - "parseResponse": {}, - "candidates": [], - } - ) - - self.assertEqual(result["result"], -1) - self.assertFalse(any(call["type"] == "command" for call in result["calls"])) - def test_fhd_scope_does_not_match_uhd_sibling_prefix(self): exact = ( "/tdarr/converted/serien4k/Marshals.2026.S01.GERMAN.DUBBED/"