From 280802e7704182364a6884aff51eece6bee8b906 Mon Sep 17 00:00:00 2001 From: dnzbk Date: Fri, 21 Aug 2026 17:50:58 +0300 Subject: [PATCH] Notify SickChill about failed downloads and add Delete option --- main.py | 44 ++++++++++++++++----- manifest.json | 17 +++++++- tests.py | 107 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 13 deletions(-) diff --git a/main.py b/main.py index 533d643..6040d96 100644 --- a/main.py +++ b/main.py @@ -43,6 +43,7 @@ "NZBPO_PROCESSMETHOD", "NZBPO_FORCEREPLACE", "NZBPO_ISPRIORITY", + "NZBPO_DELETE", "NZBPO_VERBOSE", ] @@ -115,18 +116,29 @@ def ping_sickchill(url: str) -> int: def start_post_proccessing( - url: str, path: str, process_method: str, force_replace: int, is_priority: int + url: str, + path: str, + process_method: str, + force_replace: int, + is_priority: int, + delete: int = 0, + failed: int = 0, + nzb_name: str = None, ) -> int: try: - encoded_params = urllib.parse.urlencode( - { - "cmd": "postprocess", - "path": path, - "process_method": process_method, - "force_replace": force_replace, - "is_priority": is_priority, - } - ) + params = { + "cmd": "postprocess", + "path": path, + "process_method": process_method, + "force_replace": force_replace, + "is_priority": is_priority, + "delete": delete, + "failed": failed, + } + if nzb_name: + params["nzbName"] = nzb_name + + encoded_params = urllib.parse.urlencode(params) if VERBOSE: print(f"[INFO] PARAMS:", encoded_params) @@ -148,6 +160,7 @@ def start_post_proccessing( except JSONDecodeError as ex: print("[ERROR] Wrong API Key?") + return ERROR except Exception as ex: print("[ERROR] Unexpected exception:", ex) @@ -162,10 +175,18 @@ def start_post_proccessing( PROCESS_METHOD = METHODS_MAP[os.environ["NZBPO_PROCESSMETHOD"]] FORCE_REPLACE = int(os.environ["NZBPO_FORCEREPLACE"] == "yes") IS_PRIORITY = int(os.environ["NZBPO_ISPRIORITY"] == "yes") +DELETE = int(os.environ["NZBPO_DELETE"] == "yes") + +TOTAL_STATUS = os.environ.get("NZBPP_TOTALSTATUS") +STATUS = os.environ.get("NZBPP_STATUS", "") +FAILED = int(TOTAL_STATUS not in (None, "SUCCESS") or STATUS.startswith("FAILURE")) +NZB_NAME = os.environ.get("NZBPP_NZBNAME") if VERBOSE: print("[INFO] PATH:", PATH) + if FAILED: + print("[INFO] Download status indicates FAILURE. Sending failed post-processing notification.") sys.exit( @@ -175,5 +196,8 @@ def start_post_proccessing( PROCESS_METHOD, FORCE_REPLACE, IS_PRIORITY, + DELETE, + FAILED, + NZB_NAME, ) ) diff --git a/manifest.json b/manifest.json index 153b1b8..2426e4c 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "homepage": "https://github.com/nzbgetcom/Extension-NotifySickChill/", "kind": "POST-PROCESSING", "displayName": "Notify SickChill", - "version": "1.0", + "version": "1.1", "author": "Denis", "license": "GNU", "about": "Post-Processing extension to trigger SickChill after downloads.", @@ -37,7 +37,7 @@ "displayName": "API Key", "value": "", "description": [ - "Used to authenticate and authorize access to an SickChill API.", + "Used to authenticate and authorize access to the SickChill API.", "", "Can be found in the settings of the web interface of SickChill." ], @@ -77,6 +77,19 @@ ], "select": ["no", "yes"] }, + { + "name": "delete", + "displayName": "Delete", + "value": "no", + "description": [ + "Delete files and folders after completion of Post-Processing.", + "", + "Recommended when Process Method is Copy, Hard link or Symbolic link:", + "original files remain in the download directory and are removed after processing.", + "With Move it cleans up leftover files and empty folders." + ], + "select": ["no", "yes"] + }, { "name": "Verbose", "displayName": "Verbose", diff --git a/tests.py b/tests.py index 0ed0977..7a2e95e 100644 --- a/tests.py +++ b/tests.py @@ -57,8 +57,67 @@ def do_GET(self): process_method = query_params.get("process_method", [""])[0] == "move" force_replace = query_params.get("force_replace", [""])[0] == "1" is_priority = query_params.get("is_priority", [""])[0] == "1" + delete = query_params.get("delete", ["0"])[0] in ["0", "1"] + failed = query_params.get("failed", ["0"])[0] in ["0", "1"] - if cmd and path and process_method and force_replace and is_priority: + if cmd and path and process_method and force_replace and is_priority and delete and failed: + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + data = {"data": {}, "message": "Started", "result": "success"} + response = json.dumps(data) + self.wfile.write(response.encode("utf-8")) + else: + self.send_response(400) + self.send_header("Content-type", "application/json") + self.end_headers() + data = {"data": {}, "message": "Failure", "result": "failure"} + response = json.dumps(data) + self.wfile.write(response.encode("utf-8")) + + +class HttpServerPostprocFailedMock(http.server.BaseHTTPRequestHandler): + def do_GET(self): + parsed_url = urlparse(self.path) + query_params = parse_qs(parsed_url.query) + cmd = query_params.get("cmd", [""])[0] == "postprocess" + path = query_params.get("path", [""])[0] == ROOT_DIR + process_method = query_params.get("process_method", [""])[0] == "move" + force_replace = query_params.get("force_replace", [""])[0] == "1" + is_priority = query_params.get("is_priority", [""])[0] == "1" + delete = query_params.get("delete", ["0"])[0] in ["0", "1"] + failed = query_params.get("failed", [""])[0] == "1" + nzb_name = query_params.get("nzbName", [""])[0] == "Test.Show.S01E01" + + if cmd and path and process_method and force_replace and is_priority and delete and failed and nzb_name: + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + data = {"data": {}, "message": "Started", "result": "success"} + response = json.dumps(data) + self.wfile.write(response.encode("utf-8")) + else: + self.send_response(400) + self.send_header("Content-type", "application/json") + self.end_headers() + data = {"data": {}, "message": "Failure", "result": "failure"} + response = json.dumps(data) + self.wfile.write(response.encode("utf-8")) + + +class HttpServerPostprocDeleteMock(http.server.BaseHTTPRequestHandler): + def do_GET(self): + parsed_url = urlparse(self.path) + query_params = parse_qs(parsed_url.query) + cmd = query_params.get("cmd", [""])[0] == "postprocess" + path = query_params.get("path", [""])[0] == ROOT_DIR + process_method = query_params.get("process_method", [""])[0] == "move" + force_replace = query_params.get("force_replace", [""])[0] == "1" + is_priority = query_params.get("is_priority", [""])[0] == "1" + delete = query_params.get("delete", [""])[0] == "1" + failed = query_params.get("failed", [""])[0] == "0" + + if cmd and path and process_method and force_replace and is_priority and delete and failed: self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() @@ -101,6 +160,7 @@ def set_default_env(): os.environ["NZBPO_PROCESSMETHOD"] = "Copy" os.environ["NZBPO_FORCEREPLACE"] = "yes" os.environ["NZBPO_ISPRIORITY"] = "yes" + os.environ["NZBPO_DELETE"] = "no" os.environ["NZBPO_VERBOSE"] = "yes" @@ -129,6 +189,51 @@ def test_postproc(self): thread.join() self.assertEqual(code, SUCCESS) + def test_postproc_delete(self): + set_default_env() + os.environ["NZBPO_PROCESSMETHOD"] = "Move" + os.environ["NZBPO_DELETE"] = "yes" + os.environ["NZBPP_TOTALSTATUS"] = "SUCCESS" + server = http.server.HTTPServer((HOST, int(PORT)), HttpServerPostprocDeleteMock) + thread = threading.Thread(target=server.serve_forever) + thread.start() + [_, code, _] = run_script() + server.shutdown() + server.server_close() + thread.join() + self.assertEqual(code, SUCCESS) + + def test_postproc_failed_totalstatus(self): + set_default_env() + os.environ["NZBPO_PROCESSMETHOD"] = "Move" + os.environ["NZBPP_TOTALSTATUS"] = "FAILURE" + os.environ["NZBPP_NZBNAME"] = "Test.Show.S01E01" + server = http.server.HTTPServer((HOST, int(PORT)), HttpServerPostprocFailedMock) + thread = threading.Thread(target=server.serve_forever) + thread.start() + [out, code, _] = run_script() + server.shutdown() + server.server_close() + thread.join() + self.assertTrue("Download status indicates FAILURE" in out) + self.assertEqual(code, SUCCESS) + + def test_postproc_failed_status(self): + set_default_env() + os.environ["NZBPO_PROCESSMETHOD"] = "Move" + os.environ["NZBPP_TOTALSTATUS"] = "SUCCESS" + os.environ["NZBPP_STATUS"] = "FAILURE/HEALTH" + os.environ["NZBPP_NZBNAME"] = "Test.Show.S01E01" + server = http.server.HTTPServer((HOST, int(PORT)), HttpServerPostprocFailedMock) + thread = threading.Thread(target=server.serve_forever) + thread.start() + [out, code, _] = run_script() + server.shutdown() + server.server_close() + thread.join() + self.assertTrue("Download status indicates FAILURE" in out) + self.assertEqual(code, SUCCESS) + def test_unsupported_method(self): set_default_env() os.environ["NZBPO_PROCESSMETHOD"] = "Unsupported method"