diff --git a/nova3/engines/jackett.py b/nova3/engines/jackett.py
index 0ea675fa..429ed90b 100644
--- a/nova3/engines/jackett.py
+++ b/nova3/engines/jackett.py
@@ -1,4 +1,4 @@
-# VERSION: 4.13
+# VERSION: 4.14
# AUTHORS: Diego de las Heras (ngosang@hotmail.es)
# CONTRIBUTORS: ukharley
# hannsen (github.com/hannsen)
@@ -7,6 +7,7 @@
import datetime
import json
import os
+import sys
import urllib.request
import xml.etree.ElementTree
from http.cookiejar import CookieJar
@@ -146,15 +147,15 @@ def search(self, what: str, cat: str = 'all') -> None:
# check api_key
if self.api_key == "YOUR_API_KEY_HERE":
- self.handle_error("api key error", what)
+ self.handle_error("API key not configured in jackett.json", what)
return
# search in Jackett API
- if self.thread_count > 1:
- indexers = self.get_jackett_indexers(what)
- if not indexers:
- return
+ indexers = self.get_jackett_indexers(what)
+ if not indexers:
+ return
+ if self.thread_count > 1:
args = ((what, category, indexer) for indexer in indexers)
with Pool(min(len(indexers), self.thread_count)) as pool:
pool.starmap(self.search_jackett_indexer, args)
@@ -170,13 +171,21 @@ def get_jackett_indexers(self, what: str) -> List[str]:
jacket_url = f"{self.url}/api/v2.0/indexers/all/results/torznab/api?{params}"
response = self.get_response(jacket_url)
if response is None:
- self.handle_error("connection error getting indexer list", what)
+ self.handle_error("cannot contact Jackett; check that it is running and the API key is correct", what)
return []
# process results
- response_xml = xml.etree.ElementTree.fromstring(response)
+ try:
+ response_xml = xml.etree.ElementTree.fromstring(response)
+ except xml.etree.ElementTree.ParseError as error:
+ raise RuntimeError(f"Invalid response from Jackett. {what}") from error
+ if response_xml.tag == 'error':
+ description = response_xml.attrib.get('description', 'unknown error')
+ raise RuntimeError(f"Jackett API error: {description}. {what}")
indexers: List[str] = []
for indexer in response_xml.findall('indexer'):
indexers.append(indexer.attrib['id'])
+ if not indexers:
+ self.handle_error("no indexers configured in Jackett; add one in the Jackett UI", what)
return indexers
def search_jackett_indexer(self, what: str, category: Union[List[str], None], indexer_id: str) -> None:
@@ -278,6 +287,7 @@ def get_response(self, query: str) -> Union[str, None]:
return response
def handle_error(self, error_msg: str, what: str) -> None:
+ print(f"Jackett: {error_msg}. Search: {what}", file=sys.stderr)
# we need to print the search text to be displayed in qBittorrent when
# 'Torrent names only' is enabled
self.pretty_printer_thread_safe({
diff --git a/nova3/engines/versions.txt b/nova3/engines/versions.txt
index 3c684d05..c7829a47 100644
--- a/nova3/engines/versions.txt
+++ b/nova3/engines/versions.txt
@@ -1,5 +1,5 @@
eztv: 1.24
-jackett: 4.13
+jackett: 4.14
limetorrents: 4.15
piratebay: 3.10
solidtorrents: 2.9
diff --git a/nova3/tests/test_jackett.py b/nova3/tests/test_jackett.py
index a47a0992..a59c7344 100644
--- a/nova3/tests/test_jackett.py
+++ b/nova3/tests/test_jackett.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from unittest.mock import Mock
+
import pytest
from ..engines import jackett
@@ -8,5 +12,56 @@ def test_jackett(capfd: pytest.CaptureFixture[str]) -> None:
engine.search('linux', 'all')
capturedOutput = capfd.readouterr()
- assert capturedOutput.err == ""
- assert len(capturedOutput.out) >= 0
+ assert capturedOutput.err or capturedOutput.out
+
+
+def test_jackett_reports_missing_api_key(capfd: pytest.CaptureFixture[str]) -> None:
+ engine = jackett.jackett()
+ engine.api_key = 'YOUR_API_KEY_HERE'
+ engine.search('linux', 'all')
+
+ assert capfd.readouterr().err
+
+
+@pytest.mark.parametrize(
+ 'response',
+ [
+ None,
+ '',
+ ],
+)
+def test_jackett_reports_setup_errors(monkeypatch: pytest.MonkeyPatch, capfd: pytest.CaptureFixture[str], response: str | None) -> None:
+ engine = jackett.jackett()
+ engine.api_key = 'test-key'
+ engine.thread_count = 1
+
+ monkeypatch.setattr(engine, 'get_response', Mock(return_value=response))
+ engine.search('linux', 'all')
+
+ assert capfd.readouterr().err
+
+
+@pytest.mark.parametrize('response', [
+ '',
+])
+def test_jackett_raises_for_bad_response(monkeypatch: pytest.MonkeyPatch, response: str) -> None:
+ engine = jackett.jackett()
+ engine.api_key = 'test-key'
+
+ monkeypatch.setattr(engine, 'get_response', Mock(return_value=response))
+
+ with pytest.raises(RuntimeError):
+ engine.search('linux', 'all')
+
+
+def test_jackett_checks_indexers_without_concurrency(monkeypatch: pytest.MonkeyPatch) -> None:
+ engine = jackett.jackett()
+ engine.api_key = 'test-key'
+ engine.thread_count = 1
+ search_mock = Mock()
+ monkeypatch.setattr(engine, 'get_response', Mock(return_value=''))
+ monkeypatch.setattr(engine, 'search_jackett_indexer', search_mock)
+ engine.search('linux', 'all')
+
+ search_mock.assert_called_once_with('linux', None, 'all')