FileFlows: Radarr-Import auf exakte Datei begrenzen #1
3 changed files with 329 additions and 61 deletions
|
|
@ -15,12 +15,27 @@ Exportiert aus FileFlows am 17.07.2026.
|
||||||
| Radarr_Trigger_Manual_Import | 0e522a46-... | Radarr Import trigger |
|
| Radarr_Trigger_Manual_Import | 0e522a46-... | Radarr Import trigger |
|
||||||
| Sonarr_Trigger_Manual_Import | 8fe63a47-... | Sonarr Import trigger |
|
| Sonarr_Trigger_Manual_Import | 8fe63a47-... | Sonarr Import trigger |
|
||||||
|
|
||||||
|
## Sicherheit beim Arr-Import
|
||||||
|
|
||||||
|
- Radarr-Import arbeitet nur mit dem exakten `Variables['file.FullName']`.
|
||||||
|
- Die konfigurierte `ImportPath` dient ausschließlich als Zuständigkeits-Guard für UHD/FHD.
|
||||||
|
- Filmzuordnung erfolgt über einen eindeutigen Release-Match in Radarr Queue bzw. Grab-History; Radarr-Parsing ist nur Fallback.
|
||||||
|
- Ohne eindeutige Movie-ID oder exakten ManualImport-Kandidaten schlägt der Flow sicher fehl und lässt die Datei zur manuellen Wiederherstellung liegen.
|
||||||
|
- Ein Scan des gesamten `/tdarr/converted/video/`- oder `/video4k/`-Roots ist ausdrücklich verboten.
|
||||||
|
|
||||||
|
Regressionstests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m unittest discover -s fileflows/tests -v
|
||||||
|
node --check fileflows/scripts/Radarr_-_Trigger_Manual_Import.js
|
||||||
|
```
|
||||||
|
|
||||||
## Bekannte Issues
|
## Bekannte Issues
|
||||||
|
|
||||||
- QSV-Namen sind falsch, Code verwendet NVENC/cuda
|
- QSV-Namen sind falsch, Code verwendet NVENC/cuda
|
||||||
- AV1/HEVC Flow-Parameter passen nicht zur Script-Signatur
|
- AV1/HEVC Flow-Parameter passen nicht zur Script-Signatur
|
||||||
- 05_Finalize_Move hat Datenverlust-Bug bei fehlgeschlagener SRT-Extraktion
|
- 05_Finalize_Move hat Datenverlust-Bug bei fehlgeschlagener SRT-Extraktion
|
||||||
- Arr-Import wird immer fuer alle 4 Instanzen ausgefuehrt
|
- Der Flow ruft weiterhin alle vier Arr-Nodes seriell auf; deren Pfad-Guards verhindern unzuständige API-Imports.
|
||||||
|
|
||||||
## Flow JSON
|
## Flow JSON
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* @name Radarr - Trigger Manual Import
|
* @name Radarr - Trigger Manual Import
|
||||||
* @description Trigger Radarr ManualImport or downloadedMoviesScan with skip-guard.
|
* @description Safely trigger Radarr ManualImport for exactly one processed file.
|
||||||
* @author FileFlows + Sascha
|
* @author FileFlows + Sascha
|
||||||
* @revision 9
|
* @revision 10
|
||||||
* @output Import erfolgreich
|
* @output Import erfolgreich
|
||||||
* @output Fehler
|
* @output Fehler
|
||||||
* @param {string} URL Radarr root URL and port
|
* @param {string} URL Radarr root URL and port
|
||||||
|
|
@ -18,11 +18,18 @@ import { Radarr } from 'Shared/Radarr';
|
||||||
function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) {
|
function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) {
|
||||||
URL = (URL || Variables['Radarr.Url'] || Variables['Radarr.URI']).replace(/\/+$/g, '');
|
URL = (URL || Variables['Radarr.Url'] || Variables['Radarr.URI']).replace(/\/+$/g, '');
|
||||||
ApiKey = ApiKey || Variables['Radarr.ApiKey'];
|
ApiKey = ApiKey || Variables['Radarr.ApiKey'];
|
||||||
ImportPath = ImportPath || Variables.file.FullName;
|
const currentFile = Variables['file.FullName'] || Variables.file?.FullName || '';
|
||||||
|
ImportPath = ImportPath || currentFile;
|
||||||
TimeOut = TimeOut ? Math.min(TimeOut, 3600) * 1000 : 60000; // ms
|
TimeOut = TimeOut ? Math.min(TimeOut, 3600) * 1000 : 60000; // ms
|
||||||
ImportPath = UseUnmappedPath ? Flow.UnMapPath(ImportPath) : ImportPath;
|
ImportPath = UseUnmappedPath ? Flow.UnMapPath(ImportPath) : ImportPath;
|
||||||
|
const exactImportPath = UseUnmappedPath ? Flow.UnMapPath(currentFile) : currentFile;
|
||||||
const importMode = MoveMode ? 'move' : 'copy';
|
const importMode = MoveMode ? 'move' : 'copy';
|
||||||
|
|
||||||
|
if (!exactImportPath) {
|
||||||
|
Logger.ELog('No exact current file path available; refusing broad Radarr scan.');
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// ── SKIP-GUARD: Nur API-Call machen, wenn der aktuelle ImportPath
|
// ── SKIP-GUARD: Nur API-Call machen, wenn der aktuelle ImportPath
|
||||||
// mit dem konfigurierten Pfad uebereinstimmt. Sonst sofort OK.
|
// mit dem konfigurierten Pfad uebereinstimmt. Sonst sofort OK.
|
||||||
// Das verhindert 3 unnoetige API-Calls pro Datei (4 Arr-Instanzen).
|
// Das verhindert 3 unnoetige API-Calls pro Datei (4 Arr-Instanzen).
|
||||||
|
|
@ -32,7 +39,6 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) {
|
||||||
// Die Flow-Konfiguration setzt ImportPath z.B. auf /tdarr/converted/video4k/
|
// Die Flow-Konfiguration setzt ImportPath z.B. auf /tdarr/converted/video4k/
|
||||||
// oder /tdarr/converted/video/. Wenn der aktuelle file.FullName nicht
|
// oder /tdarr/converted/video/. Wenn der aktuelle file.FullName nicht
|
||||||
// darunter liegt, sind wir nicht zustaendig.
|
// darunter liegt, sind wir nicht zustaendig.
|
||||||
var currentFile = Variables['file.FullName'] || '';
|
|
||||||
if (currentFile && ImportPath && currentFile.indexOf(ImportPath) === -1) {
|
if (currentFile && ImportPath && currentFile.indexOf(ImportPath) === -1) {
|
||||||
Logger.ILog(`Skip: current file '${currentFile}' not under ImportPath '${ImportPath}' - not responsible`);
|
Logger.ILog(`Skip: current file '${currentFile}' not under ImportPath '${ImportPath}' - not responsible`);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -43,26 +49,23 @@ function Script(URL, ApiKey, ImportPath, UseUnmappedPath, MoveMode, TimeOut) {
|
||||||
|
|
||||||
/*── movieId detection ──────────────────────────────────*/
|
/*── movieId detection ──────────────────────────────────*/
|
||||||
const searchPattern = Variables.file.Orig.FileNameNoExtension;
|
const searchPattern = Variables.file.Orig.FileNameNoExtension;
|
||||||
let movieId = Variables['Radarr.movieId'] ?? Variables.MovieInfo?.id ?? parseMovie(searchPattern, radarr) ?? null;
|
let movieId = Variables['Radarr.movieId']
|
||||||
|
?? findMovieIdFromQueue(searchPattern, radarr)
|
||||||
|
?? findMovieIdFromGrabHistory(searchPattern, radarr)
|
||||||
|
?? parseMovie(searchPattern, radarr)
|
||||||
|
?? null;
|
||||||
|
|
||||||
Logger.ILog(`Radarr URL: ${URL}`);
|
Logger.ILog(`Radarr URL: ${URL}`);
|
||||||
Logger.ILog(`Triggering Path: ${ImportPath}`);
|
Logger.ILog(`Triggering Path: ${exactImportPath}`);
|
||||||
Logger.ILog(`Import Mode: ${importMode}`);
|
Logger.ILog(`Import Mode: ${importMode}`);
|
||||||
Logger.ILog(movieId ? `movieId: ${movieId} → ManualImport` : 'No movieId → downloadedMoviesScan');
|
Logger.ILog(movieId ? `movieId: ${movieId} → ManualImport` : 'No unambiguous movieId → fail closed');
|
||||||
|
|
||||||
/*─ Execute first workflow, then fail-over if needed ─*/
|
if (!movieId) {
|
||||||
let result;
|
Logger.ELog(`Unable to identify '${searchPattern}' uniquely; file remains for manual recovery.`);
|
||||||
if (movieId) {
|
return -1;
|
||||||
result = manualImportWorkflow(radarr, ImportPath, importMode, movieId, TimeOut);
|
|
||||||
if (result !== 1) {
|
|
||||||
Logger.WLog('ManualImport failed, falling back to downloadedMoviesScan.');
|
|
||||||
result = scanWorkflow(radarr, ImportPath, importMode, TimeOut);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result = scanWorkflow(radarr, ImportPath, importMode, TimeOut);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return manualImportWorkflow(radarr, exactImportPath, importMode, movieId, TimeOut);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,10 +87,14 @@ function manualImportWorkflow(radarr, importPath, mode, movieId, timeout) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* choose the first candidate that has quality info */
|
/* accept only the candidate for the exact processed file */
|
||||||
const cand = candidates.find(c => c.quality && c.quality.quality);
|
const exactPath = normalizeImportPath(importPath);
|
||||||
|
const matchingCandidates = candidates.filter(c =>
|
||||||
|
normalizeImportPath(c.path || '') === exactPath
|
||||||
|
);
|
||||||
|
const cand = matchingCandidates.find(c => c.quality && c.quality.quality);
|
||||||
if (!cand) {
|
if (!cand) {
|
||||||
Logger.WLog('No ManualImport candidate contained quality information.');
|
Logger.WLog(`No ManualImport candidate matched exact path '${importPath}'.`);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,45 +166,6 @@ function sendManualImportCommand(radarr, cmdBody) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*──────────────────────────── downloadedMoviesScan ────────────────────────────*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Perform the downloadedMoviesScan flow (when movieId is absent).
|
|
||||||
* @param {Radarr} radarr – Radarr API instance
|
|
||||||
* @param {string} importPath – Folder/File path for downloadedMoviesScan
|
|
||||||
* @param {string} mode – 'move' or 'copy'
|
|
||||||
* @param {number} timeout – Timeout in milliseconds
|
|
||||||
* @returns {number} 1 on success, −1 on failure
|
|
||||||
*/
|
|
||||||
function scanWorkflow(radarr, importPath, mode, timeout) {
|
|
||||||
const cmdId = sendDownloadedMoviesScan(radarr, importPath, mode);
|
|
||||||
if (cmdId === null) {
|
|
||||||
Logger.WLog('Faild sending downloadedMoviesScan command');
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return waitForCommand(radarr, cmdId, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Send the **downloadedMoviesScan** command.
|
|
||||||
* @param {Radarr} radarr - Radarr API instance
|
|
||||||
* @param {string} importPath - Folder/File path for the scan
|
|
||||||
* @param {string} mode - 'move' or 'copy'
|
|
||||||
* @returns {number|null} command id or null
|
|
||||||
*/
|
|
||||||
function sendDownloadedMoviesScan(radarr, importPath, mode) {
|
|
||||||
try {
|
|
||||||
const resp = radarr.sendCommand('downloadedMoviesScan', { path: importPath, importMode: mode });
|
|
||||||
const cmdId = resp?.id;
|
|
||||||
Logger.ILog(cmdId ? `downloadedMoviesScan queued (cmdId=${cmdId}).` : 'downloadedMoviesScan failed.');
|
|
||||||
return cmdId ?? null;
|
|
||||||
} catch (e) {
|
|
||||||
Logger.ELog(`Send command downloadedMoviesScan failed: ${e.message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*────────────────────────────── helpers ──────────────────────────────────*/
|
/*────────────────────────────── helpers ──────────────────────────────────*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -218,6 +186,93 @@ function waitForCommand(radarr, cmdId, timeoutMs) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the movie from Radarr's active download queue. The queue title is
|
||||||
|
* the original release name and therefore survives FileFlows renaming.
|
||||||
|
* Only one exact normalized match is accepted.
|
||||||
|
*/
|
||||||
|
function findMovieIdFromQueue(searchPattern, radarr) {
|
||||||
|
if (!searchPattern) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query = buildQueryParams({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
includeUnknownMovieItems: true
|
||||||
|
});
|
||||||
|
const response = radarr.fetchJson('queue', query) || {};
|
||||||
|
const records = Array.isArray(response) ? response : (response.records || []);
|
||||||
|
const wanted = normalizeReleaseTitle(searchPattern);
|
||||||
|
const matches = records.filter(item =>
|
||||||
|
item?.movieId && normalizeReleaseTitle(item.title || '') === wanted
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matches.length === 1) {
|
||||||
|
Logger.ILog(`Matched Radarr queue movieId ${matches[0].movieId}.`);
|
||||||
|
return matches[0].movieId;
|
||||||
|
}
|
||||||
|
if (matches.length > 1) {
|
||||||
|
Logger.WLog(`Ambiguous Radarr queue match (${matches.length} records).`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
Logger.ELog(`Error fetching Radarr queue: ${e.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a release that has already left the active queue from Radarr's grab
|
||||||
|
* history. Repeated grabs are accepted only when every exact-title match points
|
||||||
|
* to the same movie.
|
||||||
|
*/
|
||||||
|
function findMovieIdFromGrabHistory(searchPattern, radarr) {
|
||||||
|
if (!searchPattern) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query = buildQueryParams({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 250,
|
||||||
|
eventType: 1, // Radarr HistoryEventType.Grabbed
|
||||||
|
sortKey: 'date',
|
||||||
|
sortDirection: 'descending'
|
||||||
|
});
|
||||||
|
const response = radarr.fetchJson('history', query) || {};
|
||||||
|
const records = Array.isArray(response) ? response : (response.records || []);
|
||||||
|
const wanted = normalizeReleaseTitle(searchPattern);
|
||||||
|
const movieIds = records
|
||||||
|
.filter(item =>
|
||||||
|
item?.movieId && normalizeReleaseTitle(item.sourceTitle || '') === wanted
|
||||||
|
)
|
||||||
|
.map(item => item.movieId)
|
||||||
|
.filter((id, index, values) => values.indexOf(id) === index);
|
||||||
|
|
||||||
|
if (movieIds.length === 1) {
|
||||||
|
Logger.ILog(`Matched Radarr grab history movieId ${movieIds[0]}.`);
|
||||||
|
return movieIds[0];
|
||||||
|
}
|
||||||
|
if (movieIds.length > 1) {
|
||||||
|
Logger.WLog(`Ambiguous Radarr grab history match (${movieIds.length} movies).`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
Logger.ELog(`Error fetching Radarr history: ${e.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReleaseTitle(value) {
|
||||||
|
return (value || '').toLowerCase().replace(/[^a-z0-9]+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeImportPath(value) {
|
||||||
|
return (value || '').replace(/\\/g, '/').replace(/\/+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Parse the movie name using Radarr parsing based on the search pattern.
|
* @description Parse the movie name using Radarr parsing based on the search pattern.
|
||||||
* @param {string} searchPattern - The search string (file or folder name)
|
* @param {string} searchPattern - The search string (file or folder name)
|
||||||
|
|
|
||||||
198
fileflows/tests/test_radarr_import_safety.py
Normal file
198
fileflows/tests/test_radarr_import_safety.py
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT = Path(__file__).parents[1] / "scripts" / "Radarr_-_Trigger_Manual_Import.js"
|
||||||
|
|
||||||
|
NODE_HARNESS = r"""
|
||||||
|
const fs = require('fs');
|
||||||
|
const vm = require('vm');
|
||||||
|
const path = require('path').posix;
|
||||||
|
const scenario = JSON.parse(process.argv[1]);
|
||||||
|
let source = fs.readFileSync(process.argv[2], 'utf8')
|
||||||
|
.replace(/^\/\*\*[\s\S]*?\*\/\s*/, '')
|
||||||
|
.replace(/^import .*$/m, '');
|
||||||
|
source += '\n;globalThis.__exports = { Script };';
|
||||||
|
const calls = [];
|
||||||
|
class FakeRadarr {
|
||||||
|
constructor(url, apiKey) { this.url = url; this.apiKey = apiKey; }
|
||||||
|
fetchJson(endpoint, query) {
|
||||||
|
calls.push({type: 'fetch', endpoint, query});
|
||||||
|
if (endpoint === 'queue') return scenario.queueResponse || {records: []};
|
||||||
|
if (endpoint === 'parse') return scenario.parseResponse || {};
|
||||||
|
if (endpoint === 'manualimport') return scenario.candidates || [];
|
||||||
|
if (endpoint === 'history') return scenario.historyResponse || {records: []};
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
sendCommand(name, body) {
|
||||||
|
calls.push({type: 'command', name, body});
|
||||||
|
return {id: 42};
|
||||||
|
}
|
||||||
|
waitForCompletion(id, timeout) {
|
||||||
|
calls.push({type: 'wait', id, timeout});
|
||||||
|
return scenario.waitSuccess !== false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const exactPath = scenario.exactPath;
|
||||||
|
const context = {
|
||||||
|
Radarr: FakeRadarr,
|
||||||
|
Variables: {
|
||||||
|
'file.FullName': exactPath,
|
||||||
|
file: {
|
||||||
|
FullName: exactPath,
|
||||||
|
Orig: {FileNameNoExtension: scenario.releaseTitle}
|
||||||
|
},
|
||||||
|
folder: {Name: path.basename(path.dirname(exactPath))}
|
||||||
|
},
|
||||||
|
Flow: {UnMapPath: value => value},
|
||||||
|
Logger: {ILog(){}, WLog(){}, ELog(){}, DLog(){}},
|
||||||
|
System: {IO: {Path: {GetFileName: path.basename, GetDirectoryName: path.dirname}}},
|
||||||
|
Sleep(){},
|
||||||
|
encodeURIComponent,
|
||||||
|
console
|
||||||
|
};
|
||||||
|
vm.createContext(context);
|
||||||
|
vm.runInContext(source, context);
|
||||||
|
const result = context.__exports.Script(
|
||||||
|
'http://radarr-fhd:7879',
|
||||||
|
'test-key',
|
||||||
|
scenario.scopeRoot,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
300
|
||||||
|
);
|
||||||
|
process.stdout.write(JSON.stringify({result, calls}));
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def run_script(scenario):
|
||||||
|
completed = subprocess.run(
|
||||||
|
["node", "-e", NODE_HARNESS, json.dumps(scenario), str(SCRIPT)],
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
return json.loads(completed.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
class RadarrImportSafetyTests(unittest.TestCase):
|
||||||
|
def test_queue_match_imports_only_the_exact_processed_file(self):
|
||||||
|
release = (
|
||||||
|
"Operation.Kabul.-.13.Days.13.Nights.-.13.jours.13.nuits.2025."
|
||||||
|
"German.DTSHD.DL.1080p.BluRay.AVC.Remux-MAMA"
|
||||||
|
)
|
||||||
|
exact = f"/tdarr/converted/video/{release}/{release}.mkv"
|
||||||
|
result = run_script(
|
||||||
|
{
|
||||||
|
"releaseTitle": release,
|
||||||
|
"exactPath": exact,
|
||||||
|
"scopeRoot": "/tdarr/converted/video/",
|
||||||
|
"queueResponse": {
|
||||||
|
"records": [
|
||||||
|
{"title": release, "movieId": 6658}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"parseResponse": {},
|
||||||
|
"candidates": [
|
||||||
|
{
|
||||||
|
"path": exact,
|
||||||
|
"quality": {"quality": {"id": 7, "name": "Bluray-1080p"}},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result["result"], 1)
|
||||||
|
commands = [call for call in result["calls"] if call["type"] == "command"]
|
||||||
|
self.assertEqual(len(commands), 1)
|
||||||
|
self.assertEqual(commands[0]["name"], "ManualImport")
|
||||||
|
self.assertEqual(commands[0]["body"]["files"][0]["path"], exact)
|
||||||
|
self.assertEqual(commands[0]["body"]["files"][0]["movieId"], 6658)
|
||||||
|
self.assertNotIn(
|
||||||
|
"downloadedMoviesScan",
|
||||||
|
[call.get("name") for call in commands],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unknown_movie_fails_closed_without_scanning_the_category_root(self):
|
||||||
|
release = "Unknown.Multilingual.Release.2026.1080p"
|
||||||
|
exact = f"/tdarr/converted/video/{release}/{release}.mkv"
|
||||||
|
result = run_script(
|
||||||
|
{
|
||||||
|
"releaseTitle": release,
|
||||||
|
"exactPath": exact,
|
||||||
|
"scopeRoot": "/tdarr/converted/video/",
|
||||||
|
"queueResponse": {"records": []},
|
||||||
|
"parseResponse": {},
|
||||||
|
"candidates": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result["result"], -1)
|
||||||
|
commands = [call for call in result["calls"] if call["type"] == "command"]
|
||||||
|
self.assertEqual(commands, [])
|
||||||
|
|
||||||
|
def test_manual_import_rejects_a_candidate_for_a_different_file(self):
|
||||||
|
release = "Expected.Movie.2026.1080p"
|
||||||
|
exact = f"/tdarr/converted/video/{release}/{release}.mkv"
|
||||||
|
wrong = "/tdarr/converted/video/Other.Movie/Other.Movie.mkv"
|
||||||
|
result = run_script(
|
||||||
|
{
|
||||||
|
"releaseTitle": release,
|
||||||
|
"exactPath": exact,
|
||||||
|
"scopeRoot": "/tdarr/converted/video/",
|
||||||
|
"queueResponse": {"records": [{"title": release, "movieId": 7001}]},
|
||||||
|
"candidates": [
|
||||||
|
{
|
||||||
|
"path": wrong,
|
||||||
|
"quality": {"quality": {"id": 7, "name": "Bluray-1080p"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": exact,
|
||||||
|
"quality": {"quality": {"id": 7, "name": "Bluray-1080p"}},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
command = next(call for call in result["calls"] if call["type"] == "command")
|
||||||
|
self.assertEqual(command["body"]["files"][0]["path"], exact)
|
||||||
|
|
||||||
|
def test_grab_history_match_recovers_when_download_is_no_longer_in_queue(self):
|
||||||
|
release = "Tracked.Movie.2026.German.1080p-TEST"
|
||||||
|
exact = f"/tdarr/converted/video/{release}/{release}.mkv"
|
||||||
|
result = run_script(
|
||||||
|
{
|
||||||
|
"releaseTitle": release,
|
||||||
|
"exactPath": exact,
|
||||||
|
"scopeRoot": "/tdarr/converted/video/",
|
||||||
|
"queueResponse": {"records": []},
|
||||||
|
"historyResponse": {
|
||||||
|
"records": [
|
||||||
|
{"sourceTitle": release, "movieId": 7111, "eventType": "grabbed"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"parseResponse": {},
|
||||||
|
"candidates": [
|
||||||
|
{
|
||||||
|
"path": exact,
|
||||||
|
"quality": {"quality": {"id": 7, "name": "Bluray-1080p"}},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
commands = [call for call in result["calls"] if call["type"] == "command"]
|
||||||
|
self.assertEqual(len(commands), 1)
|
||||||
|
self.assertEqual(commands[0]["body"]["files"][0]["movieId"], 7111)
|
||||||
|
history_fetch = next(
|
||||||
|
call
|
||||||
|
for call in result["calls"]
|
||||||
|
if call["type"] == "fetch" and call["endpoint"] == "history"
|
||||||
|
)
|
||||||
|
self.assertIn("eventType=1", history_fetch["query"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue