mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-05 13:11:13 +01:00
Feature: rTorrent client support (#463)
This adds in rtorrent for https://github.com/calibrain/shelfmark/issues/420 The one weird thing I noticed is that the download path needs to be the same for both (that's not how I typically set it up) But it definitely adds to rtorrent and gives progress. **rTorrent client integration:** * Added a new `RTorrentClient` class in `shelfmark/release_sources/prowlarr/clients/rtorrent.py` that implements the download client interface for rTorrent using XML-RPC, supporting adding, removing, and querying torrent status. * Registered the rTorrent client in the client registry in `shelfmark/release_sources/prowlarr/clients/__init__.py`. **Settings and configuration:** * Extended the Prowlarr client settings UI and backend (`shelfmark/release_sources/prowlarr/settings.py`) to add rTorrent as a selectable client, provide rTorrent-specific configuration fields (URL, username, password, label, download directory), and implement a connection test action. [[1]](diffhunk://#diff-052272b85804cb61162870f262cc7544ef321596ff3ebf08117a6c25afaa3ec5R390) [[2]](diffhunk://#diff-052272b85804cb61162870f262cc7544ef321596ff3ebf08117a6c25afaa3ec5R539-R582) [[3]](diffhunk://#diff-052272b85804cb61162870f262cc7544ef321596ff3ebf08117a6c25afaa3ec5R198-R225) **Test environment and scripts:** * Updated `docker-compose.test-clients.yml` to add an rTorrent service for local testing, including configuration, ports, and documentation updates. [[1]](diffhunk://#diff-a9fe4200dec6a29947e21c338305d04c8b64a7bddd9b0e519f4ab5382c478ba6R17-L19) [[2]](diffhunk://#diff-a9fe4200dec6a29947e21c338305d04c8b64a7bddd9b0e519f4ab5382c478ba6R39) [[3]](diffhunk://#diff-a9fe4200dec6a29947e21c338305d04c8b64a7bddd9b0e519f4ab5382c478ba6R66) [[4]](diffhunk://#diff-a9fe4200dec6a29947e21c338305d04c8b64a7bddd9b0e519f4ab5382c478ba6R164-R181) * Enhanced `scripts/test_clients.py` to include rTorrent in the test suite, with logic for connecting, adding, and removing torrents via XML-RPC. [[1]](diffhunk://#diff-c7146552cddc9665e380aec1473363fd8592535ab85f06443478464da8f5a99eR26) [[2]](diffhunk://#diff-c7146552cddc9665e380aec1473363fd8592535ab85f06443478464da8f5a99eR57) [[3]](diffhunk://#diff-c7146552cddc9665e380aec1473363fd8592535ab85f06443478464da8f5a99eR88-R90) [[4]](diffhunk://#diff-c7146552cddc9665e380aec1473363fd8592535ab85f06443478464da8f5a99eR395-R468) [[5]](diffhunk://#diff-c7146552cddc9665e380aec1473363fd8592535ab85f06443478464da8f5a99eR492) Closes https://github.com/calibrain/shelfmark/issues/420
This commit is contained in:
@@ -14,9 +14,9 @@
|
||||
# - Deluge: http://localhost:8112 (admin / deluge)
|
||||
# - NZBGet: http://localhost:6789 (nzbget / tegbzn6789)
|
||||
# - SABnzbd: http://localhost:8085 (complete setup wizard for API key)
|
||||
# - rTorrent: http://localhost:8000 (admin / admin - if auth enabled)
|
||||
#
|
||||
|
||||
|
||||
services:
|
||||
shelfmark:
|
||||
build:
|
||||
@@ -36,6 +36,7 @@ services:
|
||||
# - Deluge host: deluge (port 58846)
|
||||
# - NZBGet: http://nzbget:6789
|
||||
# - SABnzbd: http://sabnzbd:8080
|
||||
# - rTorrent: http://rtorrent:80 (XMLRPC via HTTP) or rtorrent (port 5000 for SCGI)
|
||||
ports:
|
||||
- "8084:8084"
|
||||
volumes:
|
||||
@@ -62,6 +63,7 @@ services:
|
||||
- qbittorrent
|
||||
- transmission
|
||||
- deluge
|
||||
- rtorrent
|
||||
restart: unless-stopped
|
||||
|
||||
prowlarr:
|
||||
@@ -159,3 +161,21 @@ services:
|
||||
- "6881:6881"
|
||||
- "6881:6881/udp"
|
||||
restart: unless-stopped
|
||||
|
||||
rtorrent:
|
||||
image: crazymax/rtorrent-rutorrent:latest # linuxserver has deprecated their rtorrent image
|
||||
container_name: test-rtorrent
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=UTC
|
||||
volumes:
|
||||
- ./.local/test-clients/rtorrent/config:/config
|
||||
- ./.local/test-clients/downloads:/downloads
|
||||
ports:
|
||||
- "8000:8000" # XMLRPC
|
||||
- "8089:8080" # ruTorrent Web UI
|
||||
- "9000:9000" # SCGI port
|
||||
- "50000:50000" # Incoming connections
|
||||
- "6881:6881/udp"
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -23,6 +23,7 @@ Web UIs:
|
||||
- Deluge: http://localhost:8112
|
||||
- NZBGet: http://localhost:6789
|
||||
- SABnzbd: http://localhost:8085
|
||||
- rTorrent: http://localhost:8000 (web ui http://localhost:8089 via ruTorrent)
|
||||
|
||||
Prerequisites (for running this script locally):
|
||||
pip install requests transmission-rpc deluge-client qbittorrent-api
|
||||
@@ -53,6 +54,7 @@ First-Time Setup:
|
||||
|
||||
import sys
|
||||
import time
|
||||
from xmlrpc import client
|
||||
|
||||
# Test configuration - matches docker-compose.test-clients.yml
|
||||
CONFIG = {
|
||||
@@ -83,6 +85,9 @@ CONFIG = {
|
||||
"username": "admin",
|
||||
"password": "admin",
|
||||
},
|
||||
"rtorrent": {
|
||||
"url": "http://localhost:8000/RPC2",
|
||||
},
|
||||
}
|
||||
|
||||
# Test magnet link (Ubuntu ISO - legal, small metadata)
|
||||
@@ -387,6 +392,80 @@ def test_deluge():
|
||||
print(" 3. Or access Web UI at http://localhost:8112 (password: deluge)")
|
||||
return False
|
||||
|
||||
def test_rtorrent():
|
||||
"""Test rTorrent connection."""
|
||||
print("\n" + "=" * 50)
|
||||
print("Testing rTorrent")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
import xmlrpc.client
|
||||
|
||||
url = "http://localhost:8000/RPC2"
|
||||
client = xmlrpc.client.ServerProxy(url)
|
||||
|
||||
# Test connection
|
||||
version = client.system.library_version()
|
||||
print(f" Connected to rTorrent {version}")
|
||||
|
||||
# Get torrent list
|
||||
torrents = client.download_list()
|
||||
print(f" Active torrents: {len(torrents)}")
|
||||
|
||||
# Test adding a torrent (then remove it)
|
||||
print(" Testing add/remove torrent...")
|
||||
|
||||
label = "automated"
|
||||
|
||||
commands = []
|
||||
if label:
|
||||
commands.append(f"d.custom1.set={label}")
|
||||
|
||||
download_dir = "/downloads"
|
||||
if download_dir:
|
||||
commands.append(f"d.directory_base.set={download_dir}")
|
||||
|
||||
# rtorrent is weird in that it doesn't return the torrent ID/hash on add
|
||||
client.load.start("", TEST_MAGNET, ";".join(commands))
|
||||
|
||||
# but we know that it is 3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0 from the magnet link
|
||||
torrent_id = "3B245504CF5F11BBDBE1201CEA6A6BF45AEE1BC0" # rtorrent uses uppercase hashes
|
||||
print(f" Added test torrent: {torrent_id}")
|
||||
|
||||
torrent_list = client.d.multicall.filtered(
|
||||
"",
|
||||
"default",
|
||||
f"equal=d.hash=,cat={torrent_id}",
|
||||
"d.hash=",
|
||||
"d.state=",
|
||||
"d.completed_bytes=",
|
||||
"d.size_bytes=",
|
||||
"d.down.rate=",
|
||||
"d.up.rate=",
|
||||
"d.custom1=",
|
||||
"d.complete=",
|
||||
)
|
||||
torrent = torrent_list[0]
|
||||
if not torrent:
|
||||
print(" ERROR: Could not find added torrent in list")
|
||||
return False
|
||||
|
||||
|
||||
client.d.erase(torrent_id)
|
||||
print(" Removed test torrent")
|
||||
|
||||
print(" SUCCESS: rTorrent is working!")
|
||||
return True
|
||||
|
||||
except ImportError:
|
||||
print(" ERROR: xmlrpc.client not available")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}")
|
||||
if "Connection refused" in str(e):
|
||||
print(" Is the container running? docker ps | grep rtorrent")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
print("Download Client Test Suite")
|
||||
@@ -410,6 +489,7 @@ def main():
|
||||
results["qbittorrent"] = test_qbittorrent()
|
||||
results["transmission"] = test_transmission()
|
||||
results["deluge"] = test_deluge()
|
||||
results["rtorrent"] = test_rtorrent()
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 50)
|
||||
|
||||
@@ -298,3 +298,4 @@ from shelfmark.release_sources.prowlarr.clients import nzbget # noqa: F401, E40
|
||||
from shelfmark.release_sources.prowlarr.clients import sabnzbd # noqa: F401, E402
|
||||
from shelfmark.release_sources.prowlarr.clients import transmission # noqa: F401, E402
|
||||
from shelfmark.release_sources.prowlarr.clients import deluge # noqa: F401, E402
|
||||
from shelfmark.release_sources.prowlarr.clients import rtorrent # noqa: F401, E402
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
"""
|
||||
rTorrent download client for Prowlarr integration.
|
||||
|
||||
Uses xmlrpc to communicate with rTorrent's RPC interface.
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from shelfmark.core.config import config
|
||||
from shelfmark.core.logger import setup_logger
|
||||
from shelfmark.release_sources.prowlarr.clients import (
|
||||
DownloadClient,
|
||||
DownloadStatus,
|
||||
register_client,
|
||||
)
|
||||
from shelfmark.release_sources.prowlarr.clients.torrent_utils import (
|
||||
extract_torrent_info,
|
||||
)
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
@register_client("torrent")
|
||||
class RTorrentClient(DownloadClient):
|
||||
"""rTorrent download client using xmlrpc."""
|
||||
|
||||
protocol = "torrent"
|
||||
name = "rtorrent"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize rTorrent client with settings from config."""
|
||||
from xmlrpc.client import ServerProxy
|
||||
|
||||
url = config.get("RTORRENT_URL", "")
|
||||
if not url:
|
||||
raise ValueError("RTORRENT_URL is required")
|
||||
|
||||
self._base_url = url.rstrip("/")
|
||||
|
||||
username = config.get("RTORRENT_USERNAME", "")
|
||||
password = config.get("RTORRENT_PASSWORD", "")
|
||||
|
||||
if username and password:
|
||||
parsed = urlparse(self._base_url)
|
||||
self._base_url = (
|
||||
f"{parsed.scheme}://{username}:{password}@{parsed.netloc}{parsed.path}"
|
||||
)
|
||||
|
||||
self._rpc = ServerProxy(self._base_url)
|
||||
self._download_dir = config.get("RTORRENT_DOWNLOAD_DIR", "")
|
||||
self._label = config.get("RTORRENT_LABEL", "")
|
||||
|
||||
@staticmethod
|
||||
def is_configured() -> bool:
|
||||
"""Check if rTorrent is configured and selected as the torrent client."""
|
||||
client = config.get("PROWLARR_TORRENT_CLIENT", "")
|
||||
url = config.get("RTORRENT_URL", "")
|
||||
return client == "rtorrent" and bool(url)
|
||||
|
||||
def test_connection(self) -> Tuple[bool, str]:
|
||||
"""Test connection to rTorrent."""
|
||||
try:
|
||||
version = self._rpc.system.client_version()
|
||||
return True, f"Connected to rTorrent {version}"
|
||||
except Exception as e:
|
||||
return False, f"Connection failed: {str(e)}"
|
||||
|
||||
def add_download(self, url: str, name: str, category: str = None) -> str:
|
||||
"""
|
||||
Add torrent by URL (magnet or .torrent).
|
||||
|
||||
Args:
|
||||
url: Magnet link or .torrent URL
|
||||
name: Display name for the torrent
|
||||
category: Category for organization (uses configured label if not specified)
|
||||
|
||||
Returns:
|
||||
Torrent hash (info_hash).
|
||||
|
||||
Raises:
|
||||
Exception: If adding fails.
|
||||
"""
|
||||
try:
|
||||
torrent_info = extract_torrent_info(url)
|
||||
|
||||
commands = []
|
||||
|
||||
label = category or self._label
|
||||
if label:
|
||||
logger.debug(f"Setting rTorrent label: {label}")
|
||||
commands.append(f"d.custom1.set={label}")
|
||||
|
||||
download_dir = self._download_dir or self._get_download_dir()
|
||||
if download_dir:
|
||||
logger.debug(f"Setting rTorrent download directory: {download_dir}")
|
||||
commands.append(f"d.directory_base.set={download_dir}")
|
||||
|
||||
if torrent_info.torrent_data:
|
||||
logger.debug(f"Adding torrent data directly to rTorrent for: {name} with commands: {commands} with data size: {len(torrent_info.torrent_data)}")
|
||||
self._rpc.load.raw_start(
|
||||
"", torrent_info.torrent_data, ";".join(commands)
|
||||
)
|
||||
else:
|
||||
logger.debug(f"Adding torrent URL to rTorrent for: {name} with commands: {commands} with URL: {url}")
|
||||
add_url = torrent_info.magnet_url or url
|
||||
self._rpc.load.start("", add_url, ";".join(commands))
|
||||
|
||||
torrent_hash = torrent_info.info_hash
|
||||
if not torrent_hash:
|
||||
raise Exception("Could not determine torrent hash from URL")
|
||||
|
||||
logger.debug(f"Added torrent to rTorrent: {torrent_hash}")
|
||||
return torrent_hash
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"rTorrent add failed: {e}")
|
||||
raise
|
||||
|
||||
def get_status(self, download_id: str) -> DownloadStatus:
|
||||
"""
|
||||
Get torrent status by hash.
|
||||
|
||||
Args:
|
||||
download_id: Torrent info_hash
|
||||
|
||||
Returns:
|
||||
Current download status.
|
||||
"""
|
||||
try:
|
||||
# rtorrent is somehow case sensitive and requires uppercase hashes for look
|
||||
download_id = download_id.upper()
|
||||
torrent_list = self._rpc.d.multicall.filtered(
|
||||
"",
|
||||
"default",
|
||||
f"equal={{d.hash=,cat={download_id}}}",
|
||||
"d.hash=",
|
||||
"d.state=",
|
||||
"d.completed_bytes=",
|
||||
"d.size_bytes=",
|
||||
"d.down.rate=",
|
||||
"d.up.rate=",
|
||||
"d.custom1=",
|
||||
"d.complete=",
|
||||
)
|
||||
logger.debug(f"Fetched torrent status from rTorrent for: {download_id} - {torrent_list}")
|
||||
if not torrent_list:
|
||||
logger.warning(f"Torrent not found in rTorrent: {download_id}")
|
||||
return DownloadStatus.error("Torrent not found")
|
||||
|
||||
torrent = torrent_list[0]
|
||||
if not torrent:
|
||||
logger.warning(f"Torrent data is empty for: {download_id}")
|
||||
return DownloadStatus.error("Torrent not found")
|
||||
|
||||
logger.debug(f"Torrent data for {download_id}: {torrent}")
|
||||
(
|
||||
torrent_hash,
|
||||
state,
|
||||
bytes_downloaded,
|
||||
bytes_total,
|
||||
down_rate,
|
||||
up_rate,
|
||||
custom_category,
|
||||
complete,
|
||||
) = torrent
|
||||
|
||||
if bytes_total > 0:
|
||||
progress = (bytes_downloaded / bytes_total) * 100
|
||||
else:
|
||||
progress = 0
|
||||
|
||||
bytes_left = max(0, bytes_total - bytes_downloaded)
|
||||
|
||||
state_map = {
|
||||
0: ("stopped", "Stopped"),
|
||||
1: ("started", "Started"),
|
||||
}
|
||||
|
||||
state_str, message = state_map.get(state, ("unknown", "Unknown state"))
|
||||
|
||||
if complete and not message:
|
||||
message = "Complete"
|
||||
|
||||
eta = None
|
||||
if down_rate > 0 and bytes_left > 0:
|
||||
eta_seconds = bytes_left / down_rate
|
||||
if eta_seconds < 604800:
|
||||
eta = int(eta_seconds)
|
||||
|
||||
file_path = None
|
||||
if complete:
|
||||
file_path = self._get_torrent_path(download_id)
|
||||
|
||||
return DownloadStatus(
|
||||
progress=min(100, progress),
|
||||
state="complete" if complete else state_str,
|
||||
message=message,
|
||||
complete=complete,
|
||||
file_path=file_path,
|
||||
download_speed=down_rate if down_rate > 0 else None,
|
||||
eta=eta,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_type = type(e).__name__
|
||||
logger.error(f"rTorrent get_status failed ({error_type}): {e}")
|
||||
return DownloadStatus.error(f"{error_type}: {e}")
|
||||
|
||||
def remove(self, download_id: str, delete_files: bool = False) -> bool:
|
||||
"""
|
||||
Remove a torrent from rTorrent.
|
||||
|
||||
Args:
|
||||
download_id: Torrent info_hash
|
||||
delete_files: Whether to also delete files
|
||||
|
||||
Returns:
|
||||
True if successful.
|
||||
"""
|
||||
try:
|
||||
if delete_files:
|
||||
self._rpc.d.delete_tied(download_id)
|
||||
self._rpc.d.erase(download_id)
|
||||
else:
|
||||
self._rpc.d.stop(download_id)
|
||||
self._rpc.d.erase(download_id)
|
||||
|
||||
logger.info(
|
||||
f"Removed torrent from rTorrent: {download_id}"
|
||||
+ (" (with files)" if delete_files else "")
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
error_type = type(e).__name__
|
||||
logger.error(f"rTorrent remove failed ({error_type}): {e}")
|
||||
return False
|
||||
|
||||
def get_download_path(self, download_id: str) -> Optional[str]:
|
||||
"""
|
||||
Get the path where torrent files are located.
|
||||
|
||||
Args:
|
||||
download_id: Torrent info_hash
|
||||
|
||||
Returns:
|
||||
Content path (file or directory), or None.
|
||||
"""
|
||||
try:
|
||||
return self._get_torrent_path(download_id)
|
||||
except Exception as e:
|
||||
error_type = type(e).__name__
|
||||
logger.debug(f"rTorrent get_download_path failed ({error_type}): {e}")
|
||||
return None
|
||||
|
||||
def find_existing(self, url: str) -> Optional[Tuple[str, DownloadStatus]]:
|
||||
"""Check if a torrent for this URL already exists in rTorrent."""
|
||||
try:
|
||||
torrent_info = extract_torrent_info(url)
|
||||
if not torrent_info.info_hash:
|
||||
return None
|
||||
|
||||
try:
|
||||
status = self.get_status(torrent_info.info_hash)
|
||||
if status.state != DownloadStatus.error("").state:
|
||||
return (torrent_info.info_hash, status)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking for existing torrent: {e}")
|
||||
return None
|
||||
|
||||
def _get_download_dir(self) -> str:
|
||||
"""Get the download directory from rTorrent config."""
|
||||
try:
|
||||
download_dir = self._rpc.directory.default()
|
||||
return download_dir
|
||||
except Exception:
|
||||
return "/downloads"
|
||||
|
||||
def _get_torrent_path(self, download_id: str) -> Optional[str]:
|
||||
"""Get the file path of a torrent by hash."""
|
||||
try:
|
||||
base_path = self._rpc.d.directory(download_id)
|
||||
return base_path if base_path else None
|
||||
except Exception:
|
||||
return None
|
||||
@@ -195,6 +195,34 @@ def _test_deluge_connection(current_values: Dict[str, Any] = None) -> Dict[str,
|
||||
return {"success": False, "message": f"Connection failed: {str(e)}"}
|
||||
|
||||
|
||||
def _test_rtorrent_connection(current_values: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""Test the rTorrent connection using current form values."""
|
||||
from shelfmark.core.config import config
|
||||
from urllib.parse import urlparse
|
||||
from xmlrpc.client import ServerProxy
|
||||
|
||||
current_values = current_values or {}
|
||||
|
||||
url = current_values.get("RTORRENT_URL") or config.get("RTORRENT_URL", "")
|
||||
username = current_values.get("RTORRENT_USERNAME") or config.get("RTORRENT_USERNAME", "")
|
||||
password = current_values.get("RTORRENT_PASSWORD") or config.get("RTORRENT_PASSWORD", "")
|
||||
|
||||
if not url:
|
||||
return {"success": False, "message": "rTorrent URL is required"}
|
||||
|
||||
try:
|
||||
# Add HTTP auth to URL if credentials provided
|
||||
if username and password:
|
||||
parsed = urlparse(url)
|
||||
url = f"{parsed.scheme}://{username}:{password}@{parsed.netloc}{parsed.path}"
|
||||
|
||||
rpc = ServerProxy(url.rstrip("/"))
|
||||
version = rpc.system.client_version()
|
||||
return {"success": True, "message": f"Connected to rTorrent {version}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"Connection failed: {str(e)}"}
|
||||
|
||||
|
||||
def _test_nzbget_connection(current_values: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""Test the NZBGet connection using current form values."""
|
||||
import requests
|
||||
@@ -359,6 +387,7 @@ def prowlarr_clients_settings():
|
||||
{"value": "qbittorrent", "label": "qBittorrent"},
|
||||
{"value": "transmission", "label": "Transmission"},
|
||||
{"value": "deluge", "label": "Deluge"},
|
||||
{"value": "rtorrent", "label": "rTorrent"},
|
||||
],
|
||||
default="",
|
||||
),
|
||||
@@ -507,6 +536,50 @@ def prowlarr_clients_settings():
|
||||
default="",
|
||||
show_when={"field": "PROWLARR_TORRENT_CLIENT", "value": "deluge"},
|
||||
),
|
||||
|
||||
# --- rTorrent Settings ---
|
||||
TextField(
|
||||
key="RTORRENT_URL",
|
||||
label="rTorrent URL",
|
||||
description="XML-RPC URL of your rTorrent instance",
|
||||
placeholder="http://rtorrent:6881/RPC2",
|
||||
show_when={"field": "PROWLARR_TORRENT_CLIENT", "value": "rtorrent"},
|
||||
),
|
||||
TextField(
|
||||
key="RTORRENT_USERNAME",
|
||||
label="Username",
|
||||
description="HTTP Basic auth username (if authentication enabled)",
|
||||
show_when={"field": "PROWLARR_TORRENT_CLIENT", "value": "rtorrent"},
|
||||
),
|
||||
PasswordField(
|
||||
key="RTORRENT_PASSWORD",
|
||||
label="Password",
|
||||
description="HTTP Basic auth password",
|
||||
show_when={"field": "PROWLARR_TORRENT_CLIENT", "value": "rtorrent"},
|
||||
),
|
||||
ActionButton(
|
||||
key="test_rtorrent",
|
||||
label="Test Connection",
|
||||
description="Verify your rTorrent configuration",
|
||||
style="primary",
|
||||
callback=_test_rtorrent_connection,
|
||||
show_when={"field": "PROWLARR_TORRENT_CLIENT", "value": "rtorrent"},
|
||||
),
|
||||
TextField(
|
||||
key="RTORRENT_LABEL",
|
||||
label="Book Label",
|
||||
description="Label to assign to book downloads in rTorrent",
|
||||
placeholder="cwabd",
|
||||
default="cwabd",
|
||||
show_when={"field": "PROWLARR_TORRENT_CLIENT", "value": "rtorrent"},
|
||||
),
|
||||
TextField(
|
||||
key="RTORRENT_DOWNLOAD_DIR",
|
||||
label="Download Directory",
|
||||
description="Server-side directory where torrents are downloaded (optional, uses rTorrent default if not specified)",
|
||||
placeholder="/downloads",
|
||||
show_when={"field": "PROWLARR_TORRENT_CLIENT", "value": "rtorrent"},
|
||||
),
|
||||
# Note: Torrent client download path must be mounted identically in both containers.
|
||||
# Torrents are always copied (not moved) to preserve seeding capability.
|
||||
|
||||
|
||||
@@ -0,0 +1,679 @@
|
||||
"""
|
||||
Unit tests for the rTorrent client.
|
||||
|
||||
These tests mock the xmlrpc library to test the client logic
|
||||
without requiring a running rTorrent instance.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
import sys
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients import DownloadStatus
|
||||
|
||||
|
||||
def make_config_getter(values):
|
||||
"""Create a config.get function that returns values from a dict."""
|
||||
|
||||
def getter(key, default=""):
|
||||
return values.get(key, default)
|
||||
|
||||
return getter
|
||||
|
||||
|
||||
def create_mock_xmlrpc_module():
|
||||
"""Create a mock xmlrpc.client module."""
|
||||
mock_module = MagicMock()
|
||||
mock_module.ServerProxy = MagicMock()
|
||||
return mock_module
|
||||
|
||||
|
||||
class TestRTorrentClientIsConfigured:
|
||||
"""Tests for RTorrentClient.is_configured()."""
|
||||
|
||||
def test_is_configured_when_all_set(self, monkeypatch):
|
||||
"""Test is_configured returns True when properly configured."""
|
||||
config_values = {
|
||||
"PROWLARR_TORRENT_CLIENT": "rtorrent",
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import RTorrentClient
|
||||
|
||||
assert RTorrentClient.is_configured() is True
|
||||
|
||||
def test_is_configured_wrong_client(self, monkeypatch):
|
||||
"""Test is_configured returns False when different client selected."""
|
||||
config_values = {
|
||||
"PROWLARR_TORRENT_CLIENT": "qbittorrent",
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import RTorrentClient
|
||||
|
||||
assert RTorrentClient.is_configured() is False
|
||||
|
||||
def test_is_configured_no_url(self, monkeypatch):
|
||||
"""Test is_configured returns False when URL not set."""
|
||||
config_values = {
|
||||
"PROWLARR_TORRENT_CLIENT": "rtorrent",
|
||||
"RTORRENT_URL": "",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import RTorrentClient
|
||||
|
||||
assert RTorrentClient.is_configured() is False
|
||||
|
||||
|
||||
class TestRTorrentClientTestConnection:
|
||||
"""Tests for RTorrentClient.test_connection()."""
|
||||
|
||||
def test_test_connection_success(self, monkeypatch):
|
||||
"""Test successful connection."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_rpc.system.client_version.return_value = "0.9.8"
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
success, message = client.test_connection()
|
||||
|
||||
assert success is True
|
||||
assert "0.9.8" in message
|
||||
|
||||
def test_test_connection_failure(self, monkeypatch):
|
||||
"""Test failed connection."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_rpc.system.client_version.side_effect = Exception("Connection refused")
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
success, message = client.test_connection()
|
||||
|
||||
assert success is False
|
||||
assert "failed" in message.lower()
|
||||
|
||||
def test_test_connection_with_auth(self, monkeypatch):
|
||||
"""Test connection with HTTP authentication."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "testuser",
|
||||
"RTORRENT_PASSWORD": "testpass",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_rpc.system.client_version.return_value = "0.9.8"
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
success, message = client.test_connection()
|
||||
|
||||
assert success is True
|
||||
assert "0.9.8" in message
|
||||
|
||||
|
||||
class TestRTorrentClientAddDownload:
|
||||
"""Tests for RTorrentClient.add_download()."""
|
||||
|
||||
def test_add_download_magnet(self, monkeypatch):
|
||||
"""Test adding a torrent via magnet link."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
# Mock extract_torrent_info
|
||||
mock_torrent_info = MagicMock()
|
||||
mock_torrent_info.torrent_data = None
|
||||
mock_torrent_info.magnet_url = "magnet:?xt=urn:btih:abc123def456"
|
||||
mock_torrent_info.info_hash = "abc123def456"
|
||||
mock_torrent_info.is_magnet = True
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
with patch(
|
||||
"shelfmark.release_sources.prowlarr.clients.torrent_utils.extract_torrent_info",
|
||||
return_value=mock_torrent_info,
|
||||
):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules[
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent"
|
||||
]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
result_hash = client.add_download(
|
||||
"magnet:?xt=urn:btih:abc123def456", "Test Torrent"
|
||||
)
|
||||
|
||||
assert result_hash == "abc123def456"
|
||||
mock_rpc.load.start.assert_called_once()
|
||||
args = mock_rpc.load.start.call_args[0]
|
||||
assert args[1] == "magnet:?xt=urn:btih:abc123def456"
|
||||
assert "d.custom1.set=cwabd" in args[2]
|
||||
assert "d.directory_base.set=/downloads" in args[2]
|
||||
|
||||
def test_add_download_torrent_file(self, monkeypatch):
|
||||
"""Test adding a torrent via raw torrent data."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
# Mock extract_torrent_info
|
||||
mock_torrent_info = MagicMock()
|
||||
mock_torrent_info.torrent_data = b"raw_torrent_data"
|
||||
mock_torrent_info.magnet_url = None
|
||||
mock_torrent_info.info_hash = "abc123def456"
|
||||
mock_torrent_info.is_magnet = False
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
with patch(
|
||||
"shelfmark.release_sources.prowlarr.clients.torrent_utils.extract_torrent_info",
|
||||
return_value=mock_torrent_info,
|
||||
):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules[
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent"
|
||||
]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
result_hash = client.add_download(
|
||||
"http://example.com/test.torrent", "Test Torrent"
|
||||
)
|
||||
|
||||
assert result_hash == "abc123def456"
|
||||
mock_rpc.load.raw_start.assert_called_once()
|
||||
args = mock_rpc.load.raw_start.call_args[0]
|
||||
assert args[1] == b"raw_torrent_data"
|
||||
assert "d.custom1.set=cwabd" in args[2]
|
||||
|
||||
def test_add_download_failure(self, monkeypatch):
|
||||
"""Test failure when adding a download."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_rpc.load.start.side_effect = Exception("RPC Error")
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
mock_torrent_info = MagicMock()
|
||||
mock_torrent_info.torrent_data = None
|
||||
mock_torrent_info.magnet_url = "magnet:..."
|
||||
mock_torrent_info.info_hash = "abc"
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
with patch(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.extract_torrent_info",
|
||||
return_value=mock_torrent_info,
|
||||
):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules[
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent"
|
||||
]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
client.add_download("magnet:...", "Test")
|
||||
|
||||
assert "RPC Error" in str(excinfo.value)
|
||||
|
||||
|
||||
class TestRTorrentClientGetStatus:
|
||||
"""Tests for RTorrentClient.get_status()."""
|
||||
|
||||
def test_get_status_downloading(self, monkeypatch):
|
||||
"""Test status for downloading torrent."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_rpc.d.multicall.filtered.return_value = [
|
||||
[
|
||||
"abc123def456",
|
||||
2,
|
||||
524288000,
|
||||
1048576000,
|
||||
1024000,
|
||||
0,
|
||||
"cwabd",
|
||||
]
|
||||
]
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
status = client.get_status("abc123def456")
|
||||
|
||||
assert status.progress == 50.0
|
||||
assert status.state_value == "downloading"
|
||||
assert status.complete is False
|
||||
assert status.download_speed == 1024000
|
||||
|
||||
def test_get_status_seeding(self, monkeypatch):
|
||||
"""Test status for seeding (complete) torrent."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_rpc.d.multicall.filtered.return_value = [
|
||||
[
|
||||
"abc123def456",
|
||||
4,
|
||||
1048576000,
|
||||
1048576000,
|
||||
0,
|
||||
2048000,
|
||||
"cwabd",
|
||||
]
|
||||
]
|
||||
mock_rpc.d.get_base_path.return_value = "/downloads/test-torrent"
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
status = client.get_status("abc123def456")
|
||||
|
||||
assert status.progress == 100.0
|
||||
assert status.state_value == "complete"
|
||||
assert status.complete is True
|
||||
assert status.file_path == "/downloads/test-torrent"
|
||||
assert status.message == "Seeding"
|
||||
|
||||
def test_get_status_torrent_not_found(self, monkeypatch):
|
||||
"""Test status when torrent not found."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_rpc.d.multicall.filtered.return_value = []
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
status = client.get_status("nonexistent")
|
||||
|
||||
assert status.state_value == "error"
|
||||
assert status.complete is False
|
||||
|
||||
def test_get_status_with_eta(self, monkeypatch):
|
||||
"""Test status with ETA calculation."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_rpc.d.multicall.filtered.return_value = [
|
||||
[
|
||||
"abc123def456",
|
||||
2,
|
||||
524288000,
|
||||
1048576000,
|
||||
1048576,
|
||||
0,
|
||||
"cwabd",
|
||||
]
|
||||
]
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
status = client.get_status("abc123def456")
|
||||
|
||||
assert status.eta is not None
|
||||
assert status.eta == 500
|
||||
|
||||
|
||||
class TestRTorrentClientRemove:
|
||||
"""Tests for RTorrentClient.remove()."""
|
||||
|
||||
def test_remove_success(self, monkeypatch):
|
||||
"""Test successful torrent removal."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
result = client.remove("abc123def456", delete_files=False)
|
||||
|
||||
assert result is True
|
||||
mock_rpc.d.stop.assert_called_once_with("abc123def456")
|
||||
mock_rpc.d.erase.assert_called_once_with("abc123def456")
|
||||
|
||||
def test_remove_with_files(self, monkeypatch):
|
||||
"""Test torrent removal with file deletion."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
result = client.remove("abc123def456", delete_files=True)
|
||||
|
||||
assert result is True
|
||||
mock_rpc.d.delete_tied.assert_called_once_with("abc123def456")
|
||||
mock_rpc.d.erase.assert_called_once_with("abc123def456")
|
||||
|
||||
def test_remove_failure(self, monkeypatch):
|
||||
"""Test failed torrent removal."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_rpc.d.stop.side_effect = Exception("Connection lost")
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
result = client.remove("abc123def456")
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestRTorrentClientGetDownloadPath:
|
||||
"""Tests for RTorrentClient.get_download_path()."""
|
||||
|
||||
def test_get_download_path_success(self, monkeypatch):
|
||||
"""Test getting download path successfully."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_rpc.d.get_base_path.return_value = "/downloads/test-file"
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
path = client.get_download_path("abc123def456")
|
||||
|
||||
assert path == "/downloads/test-file"
|
||||
|
||||
def test_get_download_path_failure(self, monkeypatch):
|
||||
"""Test getting download path when torrent not found."""
|
||||
config_values = {
|
||||
"RTORRENT_URL": "http://localhost:8080/RPC2",
|
||||
"RTORRENT_USERNAME": "",
|
||||
"RTORRENT_PASSWORD": "",
|
||||
"RTORRENT_DOWNLOAD_DIR": "/downloads",
|
||||
"RTORRENT_LABEL": "cwabd",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"shelfmark.release_sources.prowlarr.clients.rtorrent.config.get",
|
||||
make_config_getter(config_values),
|
||||
)
|
||||
|
||||
mock_rpc = MagicMock()
|
||||
mock_rpc.d.get_base_path.side_effect = Exception("Torrent not found")
|
||||
|
||||
mock_xmlrpc = create_mock_xmlrpc_module()
|
||||
mock_xmlrpc.ServerProxy.return_value = mock_rpc
|
||||
|
||||
with patch.dict("sys.modules", {"xmlrpc.client": mock_xmlrpc}):
|
||||
if "shelfmark.release_sources.prowlarr.clients.rtorrent" in sys.modules:
|
||||
del sys.modules["shelfmark.release_sources.prowlarr.clients.rtorrent"]
|
||||
|
||||
from shelfmark.release_sources.prowlarr.clients.rtorrent import (
|
||||
RTorrentClient,
|
||||
)
|
||||
|
||||
client = RTorrentClient()
|
||||
path = client.get_download_path("nonexistent")
|
||||
|
||||
assert path is None
|
||||
Reference in New Issue
Block a user