Compare commits
No commits in common. "e205763bbaddf8574ed698b7f7855341c7c12b46" and "48f8719618d1309a79af8c721935f57305082af5" have entirely different histories.
e205763bba
...
48f8719618
3 changed files with 9 additions and 135 deletions
|
|
@ -386,7 +386,7 @@
|
||||||
"ImportPath": "/tdarr/converted/serien4k",
|
"ImportPath": "/tdarr/converted/serien4k",
|
||||||
"UseUnmappedPath": false,
|
"UseUnmappedPath": false,
|
||||||
"MoveMode": true,
|
"MoveMode": true,
|
||||||
"TimeOut": 900,
|
"TimeOut": 300,
|
||||||
"Color": ""
|
"Color": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -420,7 +420,7 @@
|
||||||
"ImportPath": "/tdarr/converted/serien",
|
"ImportPath": "/tdarr/converted/serien",
|
||||||
"UseUnmappedPath": false,
|
"UseUnmappedPath": false,
|
||||||
"MoveMode": true,
|
"MoveMode": true,
|
||||||
"TimeOut": 900,
|
"TimeOut": 300,
|
||||||
"Color": ""
|
"Color": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 11
|
||||||
* @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
|
||||||
|
|
@ -57,8 +57,8 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) {
|
||||||
if (!(seriesId && episodeIds)) {
|
if (!(seriesId && episodeIds)) {
|
||||||
let trackedEpisode = null;
|
let trackedEpisode = null;
|
||||||
for (const pattern of searchPatterns) {
|
for (const pattern of searchPatterns) {
|
||||||
trackedEpisode = findEpisodeFromQueue(pattern, sonarr, filePattern)
|
trackedEpisode = findEpisodeFromQueue(pattern, sonarr)
|
||||||
?? findEpisodeFromGrabHistory(pattern, sonarr, filePattern);
|
?? findEpisodeFromGrabHistory(pattern, sonarr);
|
||||||
if (trackedEpisode) break;
|
if (trackedEpisode) break;
|
||||||
}
|
}
|
||||||
if (!trackedEpisode) {
|
if (!trackedEpisode) {
|
||||||
|
|
@ -226,7 +226,7 @@ function sendManualImportCommand(sonarr, cmdBody) {
|
||||||
|
|
||||||
/*────────────────────────────── helpers ──────────────────────────────────*/
|
/*────────────────────────────── helpers ──────────────────────────────────*/
|
||||||
|
|
||||||
function findEpisodeFromQueue(searchPattern, sonarr, episodePattern) {
|
function findEpisodeFromQueue(searchPattern, sonarr) {
|
||||||
if (!searchPattern) return null;
|
if (!searchPattern) return null;
|
||||||
try {
|
try {
|
||||||
const query = buildQueryParams({ page: 1, pageSize: 200, includeUnknownSeriesItems: true });
|
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}.`);
|
Logger.ILog(`Matched Sonarr queue seriesId ${matches[0].seriesId}, episodeId ${matches[0].episodeId}.`);
|
||||||
return { id: matches[0].seriesId, episodeIds: [matches[0].episodeId] };
|
return { id: matches[0].seriesId, episodeIds: [matches[0].episodeId] };
|
||||||
}
|
}
|
||||||
if (matches.length > 1) {
|
if (matches.length > 1) Logger.WLog(`Ambiguous Sonarr queue match (${matches.length} records).`);
|
||||||
const resolved = resolvePackEpisode(matches, episodePattern, sonarr);
|
|
||||||
if (resolved) return resolved;
|
|
||||||
Logger.WLog(`Ambiguous Sonarr queue match (${matches.length} records).`);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.ELog(`Error fetching Sonarr queue: ${e.message}`);
|
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;
|
if (!searchPattern) return null;
|
||||||
try {
|
try {
|
||||||
const query = buildQueryParams({
|
const query = buildQueryParams({
|
||||||
|
|
@ -273,11 +269,7 @@ function findEpisodeFromGrabHistory(searchPattern, sonarr, episodePattern) {
|
||||||
Logger.ILog(`Matched Sonarr grab history seriesId ${seriesId}, episodeId ${episodeId}.`);
|
Logger.ILog(`Matched Sonarr grab history seriesId ${seriesId}, episodeId ${episodeId}.`);
|
||||||
return { id: seriesId, episodeIds: [episodeId] };
|
return { id: seriesId, episodeIds: [episodeId] };
|
||||||
}
|
}
|
||||||
if (keys.length > 1) {
|
if (keys.length > 1) Logger.WLog(`Ambiguous Sonarr grab history match (${keys.length} episodes).`);
|
||||||
const resolved = resolvePackEpisode(matches, episodePattern, sonarr);
|
|
||||||
if (resolved) return resolved;
|
|
||||||
Logger.WLog(`Ambiguous Sonarr grab history match (${keys.length} episodes).`);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.ELog(`Error fetching Sonarr history: ${e.message}`);
|
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) {
|
function normalizeReleaseTitle(value) {
|
||||||
return (value || '').toLowerCase().replace(/[^a-z0-9]+/g, '');
|
return (value || '').toLowerCase().replace(/[^a-z0-9]+/g, '');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ 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 === '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 || [];
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -78,82 +77,6 @@ def run_script(scenario):
|
||||||
|
|
||||||
|
|
||||||
class SonarrImportSafetyTests(unittest.TestCase):
|
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):
|
def test_fhd_scope_does_not_match_uhd_sibling_prefix(self):
|
||||||
exact = (
|
exact = (
|
||||||
"/tdarr/converted/serien4k/Marshals.2026.S01.GERMAN.DUBBED/"
|
"/tdarr/converted/serien4k/Marshals.2026.S01.GERMAN.DUBBED/"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue