Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions nova3/engines/jackett.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# VERSION: 4.13
# VERSION: 4.14
# AUTHORS: Diego de las Heras (ngosang@hotmail.es)
# CONTRIBUTORS: ukharley
# hannsen (github.com/hannsen)
Expand All @@ -7,6 +7,7 @@
import datetime
import json
import os
import sys
import urllib.request
import xml.etree.ElementTree
from http.cookiejar import CookieJar
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion nova3/engines/versions.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
eztv: 1.24
jackett: 4.13
jackett: 4.14
limetorrents: 4.15
piratebay: 3.10
solidtorrents: 2.9
Expand Down
73 changes: 71 additions & 2 deletions nova3/tests/test_jackett.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import pytest

from ..engines import jackett
Expand All @@ -8,5 +10,72 @@ 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,
'<indexers />',
],
)
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

def fake_response(_query: str) -> str | None:
return response

monkeypatch.setattr(engine, 'get_response', fake_response)
engine.search('linux', 'all')

assert capfd.readouterr().err


@pytest.mark.parametrize('response', [
'<indexers',
'<error code="100" description="Invalid API Key" />',
])
def test_jackett_raises_for_bad_response(monkeypatch: pytest.MonkeyPatch, response: str) -> None:
engine = jackett.jackett()
engine.api_key = 'test-key'

def fake_response(_query: str) -> str:
return response

monkeypatch.setattr(engine, 'get_response', fake_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
searches: list[tuple[str, list[str] | None, str]] = []

def fake_response(_query: str) -> str:
return '<indexers><indexer id="thepiratebay" /></indexers>'

def fake_search(what: str, category: list[str] | None, indexer_id: str) -> None:
searches.append((what, category, indexer_id))

monkeypatch.setattr(engine, 'get_response', fake_response)
monkeypatch.setattr(engine, 'search_jackett_indexer', fake_search)
engine.search('linux', 'all')

assert searches == [('linux', None, 'all')]