fix(rtorrent): apply the audiobook label to audiobook downloads (#1239)

add_download() picks self._audiobook_label from a content_type kwarg,
but the only call site never passed one, so is_audiobook was always
False and every download got RTORRENT_LABEL. category does not fill
the gap: _get_category_for_task() returns None for rTorrent, which has
no category concept, leaving content_type as its only audiobook signal.

Pass task.content_type through from base_handler, and match it with the
shared is_audiobook() helper instead of == "audiobook".
normalize_content_type()
treats "book (audiobook)" as an audiobook, so the exact-string check
would have mislabeled that value even once it arrived.

The existing rTorrent tests passed content_type straight to the client,
which is why nothing caught the missing wiring; the new handler test
covers the call site itself.

Post-processing was never affected: destination.py reads
task.content_type directly, so files already landed in
DESTINATION_AUDIOBOOK correctly.

Fixes #1235
This commit is contained in:
CaliBrain
2026-08-20 10:39:50 -04:00
committed by GitHub
parent 12d554a92f
commit 7193036626
4 changed files with 97 additions and 2 deletions
@@ -843,6 +843,9 @@ class ExternalClientHandler(DownloadHandler, ABC):
expected_hash=request.expected_hash,
seeding_time_limit=request.seeding_time_limit,
ratio_limit=request.ratio_limit,
# rTorrent has no category concept, so its audiobook label
# can only be chosen from the content type (#1235).
content_type=task.content_type,
)
except Exception as e:
if not refresh_attempted:
+8 -2
View File
@@ -11,7 +11,12 @@ from urllib.parse import urlparse
from shelfmark.core.config import config
from shelfmark.core.logger import setup_logger
from shelfmark.core.utils import get_hardened_xmlrpc_client
from shelfmark.core.utils import (
get_hardened_xmlrpc_client,
)
from shelfmark.core.utils import (
is_audiobook as check_audiobook,
)
from shelfmark.download.clients import (
DownloadClient,
DownloadStatus,
@@ -173,7 +178,8 @@ class RTorrentClient(DownloadClient):
commands = []
is_audiobook = kwargs.get("content_type") == "audiobook"
content_type = kwargs.get("content_type")
is_audiobook = check_audiobook(content_type if isinstance(content_type, str) else None)
default_label = (
self._audiobook_label if is_audiobook and self._audiobook_label else self._label
)
+56
View File
@@ -727,6 +727,62 @@ class TestProwlarrHandlerSeedCriteria:
assert call_kwargs["ratio_limit"] == 1.25
class TestProwlarrHandlerContentType:
"""Regression tests for issue #1235 — content type must reach the client."""
def test_download_passes_content_type_to_client(self):
"""rTorrent picks its audiobook label from content_type, not category."""
mock_client = MagicMock()
mock_client.name = "rtorrent"
mock_client.find_existing.return_value = None
mock_client.add_download.return_value = "download_id"
with (
patch(
"shelfmark.release_sources.prowlarr.handler.get_release",
return_value={
"protocol": "torrent",
"title": "Test Release",
"magnetUrl": "magnet:?xt=urn:btih:abc123",
},
),
patch(
"shelfmark.release_sources.prowlarr.handler.get_client",
return_value=mock_client,
),
patch(
"shelfmark.release_sources.prowlarr.handler.remove_release",
),
patch("shelfmark.release_sources.prowlarr.handler.config.get", return_value=True),
patch.object(
ProwlarrHandler,
"_poll_and_complete",
return_value=None,
),
):
handler = ProwlarrHandler()
task = DownloadTask(
task_id="content-type-pass-through",
source="prowlarr",
title="Test Audiobook",
content_type="audiobook",
)
cancel_flag = Event()
recorder = ProgressRecorder()
handler.download(
task=task,
cancel_flag=cancel_flag,
progress_callback=recorder.progress_callback,
status_callback=recorder.status_callback,
)
call_kwargs = mock_client.add_download.call_args.kwargs
assert call_kwargs["content_type"] == "audiobook"
# rTorrent gets no category, so content_type is its only audiobook signal.
assert call_kwargs["category"] is None
class TestProwlarrHandlerExistingDownload:
"""Tests for handling existing downloads."""
+30
View File
@@ -563,6 +563,36 @@ class TestRTorrentClientAudiobookLabel:
assert "d.custom1.set=books" in args[2]
assert "d.custom1.set=audiobooks" not in args[2]
def test_uses_audiobook_label_for_compound_content_type(self, monkeypatch):
"""Issue #1235 — an exact "audiobook" match missed forms like "book (audiobook)"."""
config_values = {
"RTORRENT_URL": "http://localhost:8080/RPC2",
"RTORRENT_LABEL": "books",
"RTORRENT_AUDIOBOOK_LABEL": "audiobooks",
"RTORRENT_DOWNLOAD_DIR": "/downloads",
}
mock_rpc, mock_xmlrpc, mock_torrent_info = self._make_client(monkeypatch, config_values)
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
with patch(
"shelfmark.download.clients.torrent_utils.extract_torrent_info",
return_value=mock_torrent_info,
):
if "shelfmark.download.clients.rtorrent" in sys.modules:
del sys.modules["shelfmark.download.clients.rtorrent"]
from shelfmark.download.clients.rtorrent import RTorrentClient
client = RTorrentClient()
client.add_download(
"magnet:?xt=urn:btih:abc123",
"Test Audiobook",
content_type="Book (Audiobook)",
)
args = mock_rpc.load.start.call_args[0]
assert "d.custom1.set=audiobooks" in args[2]
assert "d.custom1.set=books" not in args[2]
class TestRTorrentClientGetStatus:
"""Tests for RTorrentClient.get_status()."""