Compare commits

..

3 commits

2 changed files with 51 additions and 2 deletions

View file

@ -2,7 +2,7 @@
* @name Sonarr - Trigger Manual Import * @name Sonarr - Trigger Manual Import
* @description Safely trigger Sonarr ManualImport for exactly one processed episode file. * @description Safely trigger Sonarr ManualImport for exactly one processed episode file.
* @author FileFlows + Sascha * @author FileFlows + Sascha
* @revision 12 * @revision 13
* @output Import erfolgreich * @output Import erfolgreich
* @output Fehler * @output Fehler
* @param {string} URL Sonarr root URL and port * @param {string} URL Sonarr root URL and port
@ -293,10 +293,24 @@ function resolvePackEpisode(matches, episodePattern, sonarr) {
.map(item => Number(item?.seriesId)) .map(item => Number(item?.seriesId))
.filter(Boolean) .filter(Boolean)
.filter((value, index, values) => values.indexOf(value) === index); .filter((value, index, values) => values.indexOf(value) === index);
if (seriesIds.length !== 1) return null; if (seriesIds.length !== 1) {
Logger.WLog('Tracked season-pack rows point to multiple series; refusing fallback parsing.');
return { conflict: true };
}
try { try {
const seriesId = seriesIds[0]; const seriesId = seriesIds[0];
const trackedSeries = sonarr.fetchJson(`series/${seriesId}`) || {};
const cleanTitle = normalizeReleaseTitle(trackedSeries.cleanTitle || trackedSeries.title || '');
const normalizedPattern = normalizeReleaseTitle(episodePattern);
if (!cleanTitle || !normalizedPattern.startsWith(cleanTitle)) {
Logger.WLog(
`Tracked release title '${episodePattern}' does not match Sonarr series `
+ `'${trackedSeries.title || seriesId}'; refusing fallback parsing.`
);
return { conflict: true };
}
const query = buildQueryParams({ seriesId }); const query = buildQueryParams({ seriesId });
const episodes = sonarr.fetchJson('episode', query) || []; const episodes = sonarr.fetchJson('episode', query) || [];
const candidates = episodes.filter(item => const candidates = episodes.filter(item =>

View file

@ -22,6 +22,7 @@ class FakeSonarr {
calls.push({type: 'fetch', endpoint, query}); calls.push({type: 'fetch', endpoint, query});
if (endpoint === 'queue') return scenario.queueResponse || {records: []}; if (endpoint === 'queue') return scenario.queueResponse || {records: []};
if (endpoint === 'history') return scenario.historyResponse || {records: []}; if (endpoint === 'history') return scenario.historyResponse || {records: []};
if (endpoint.startsWith('series/')) return scenario.seriesResponse || {};
if (endpoint === 'episode') return scenario.episodeResponse || []; if (endpoint === 'episode') return scenario.episodeResponse || [];
if (endpoint === 'parse') return scenario.parseResponse || {}; if (endpoint === 'parse') return scenario.parseResponse || {};
if (endpoint === 'manualimport') return scenario.candidates || []; if (endpoint === 'manualimport') return scenario.candidates || [];
@ -105,6 +106,7 @@ class SonarrImportSafetyTests(unittest.TestCase):
"scopeRoot": "/tdarr/converted/serien", "scopeRoot": "/tdarr/converted/serien",
"queueResponse": {"records": queue_records}, "queueResponse": {"records": queue_records},
"historyResponse": {"records": history_records}, "historyResponse": {"records": history_records},
"seriesResponse": {"id": 794, "title": "Lucky", "cleanTitle": "lucky"},
"episodeResponse": [ "episodeResponse": [
{"id": 64000 + number, "seasonNumber": 1, "episodeNumber": number} {"id": 64000 + number, "seasonNumber": 1, "episodeNumber": number}
for number in range(1, 8) for number in range(1, 8)
@ -127,6 +129,38 @@ class SonarrImportSafetyTests(unittest.TestCase):
self.assertEqual(imported["episodeIds"], [64005]) self.assertEqual(imported["episodeIds"], [64005])
self.assertEqual(imported["releaseGroup"], "FritzBox") self.assertEqual(imported["releaseGroup"], "FritzBox")
def test_pack_resolution_fails_closed_when_tracked_series_title_differs(self):
pack = "Lucky.S01.Complete.2026.GERMAN.aac.720p.WEB.h265-FritzBox"
episode = "Lucky.S01E05.Are.We.Bad.People.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": 64000 + number}
for number in range(1, 8)
]
},
"historyResponse": {"records": []},
"seriesResponse": {"id": 794, "title": "Dutton Ranch", "cleanTitle": "duttonranch"},
"episodeResponse": [
{"id": 64005, "seasonNumber": 1, "episodeNumber": 5}
],
"parseResponse": {
"series": {"id": 821, "title": "Lucky"},
"episodes": [{"id": 61648}],
},
"candidates": [{"path": exact, "quality": {"quality": {"id": 6}}}],
}
)
self.assertEqual(result["result"], -1)
self.assertFalse(any(call["type"] == "command" for call in result["calls"]))
self.assertFalse(any(call.get("endpoint") == "parse" for call in result["calls"]))
def test_pack_resolution_fails_closed_when_series_ids_differ(self): def test_pack_resolution_fails_closed_when_series_ids_differ(self):
pack = "Lucky.S01.Complete.2026.GERMAN.aac.720p.WEB.h265-FritzBox" pack = "Lucky.S01.Complete.2026.GERMAN.aac.720p.WEB.h265-FritzBox"
episode = "Lucky.S01E05.German.Title.2026.GERMAN.aac.720p.WEB.h265-FritzBox" episode = "Lucky.S01E05.German.Title.2026.GERMAN.aac.720p.WEB.h265-FritzBox"
@ -194,6 +228,7 @@ class SonarrImportSafetyTests(unittest.TestCase):
"scopeRoot": "/tdarr/converted/serien4k", "scopeRoot": "/tdarr/converted/serien4k",
"queueResponse": {"records": queue_records}, "queueResponse": {"records": queue_records},
"historyResponse": {"records": history_records}, "historyResponse": {"records": history_records},
"seriesResponse": {"id": 600, "title": "Marshals", "cleanTitle": "marshals"},
"parseResponse": { "parseResponse": {
"series": {"id": 600, "title": "Marshals"}, "series": {"id": 600, "title": "Marshals"},
"episodes": [{"id": 37505}], "episodes": [{"id": 37505}],