From 620f264861062f3f67a4985da29a2c0a211fc692 Mon Sep 17 00:00:00 2001 From: Gronod Date: Tue, 19 May 2026 11:53:51 +0100 Subject: [PATCH] fix: remove auto-appending of /RPC2 from RTorrentClient and finalize PDCA documentation - Remove auto-appending of /RPC2 from RTorrentClient constructor - Use exact URL from config (supports custom paths like whatbox.ca/xmlrpc) - Update .env.sample with clear URL path documentation and examples - Update README.md with comprehensive PDCA section and all download clients - Add URL path verification tests (whatbox.ca, custom paths, no auth) - Update architecture diagram to include Transmission and rTorrent - Update Docker Compose example to include all download clients - Update prerequisites to mention all supported download clients - Update "What It Does" and "The Matching Process" sections --- .env.sample | 12 +++-- README.md | 53 ++++++++++++++++++++--- server/clients/RTorrentClient.js | 6 +-- tests/unit/clients/RTorrentClient.test.js | 38 ++++++++++++++-- 4 files changed, 93 insertions(+), 16 deletions(-) diff --git a/.env.sample b/.env.sample index 41d2560..b59ad0b 100644 --- a/.env.sample +++ b/.env.sample @@ -99,12 +99,18 @@ QBITTORRENT_INSTANCES=[{"name":"main","url":"https://qbittorrent.example.com","u # Add one or more rTorrent instances as a single-line JSON array # Uses username/password authentication (optional) # Format: [{"name":"instance-name","url":"https://...","username":"...","password":"..."}] -# XML-RPC endpoint is automatically appended: ${url}/RPC2 +# IMPORTANT: XML-RPC endpoint must be included in the url field (no automatic appending). +# Standard installs use /RPC2. Some providers (e.g. whatbox.ca) use /xmlrpc. Other +# installations may use a custom path. Always supply the complete RPC endpoint. +# Examples: +# Standard: http://rtorrent.local:8080/RPC2 +# whatbox.ca: https://user.whatbox.ca/xmlrpc +# Custom: https://example.com/custom/rpc/path # ============================================================================= -# RTORRENT_INSTANCES=[{"name":"main","url":"https://rtorrent.example.com","username":"rtorrent","password":"rtorrent"}] +# RTORRENT_INSTANCES=[{"name":"main","url":"http://rtorrent.example.com/RPC2","username":"rtorrent","password":"rtorrent"}] # Legacy single-instance format (optional - still supported) -# RTORRENT_URL=https://rtorrent.example.com +# RTORRENT_URL=http://rtorrent.example.com/RPC2 # RTORRENT_USERNAME=rtorrent # RTORRENT_PASSWORD=rtorrent diff --git a/README.md b/README.md index 840c161..f521386 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## What It Does sofarr connects to your media stack and shows you a personalized view of: -- **Active Downloads** - See what's currently downloading from Usenet (SABnzbd) and BitTorrent (qBittorrent) +- **Active Downloads** - See what's currently downloading from Usenet (SABnzbd) and BitTorrent (qBittorrent, Transmission, rTorrent) - **Progress Tracking** - Real-time progress bars with speed, ETA, and completion estimates - **Recently Completed** - History tab showing imported and failed downloads from Sonarr/Radarr with deduplication and upgrade-awareness - **User Matching** - Downloads are matched to you based on tags in Sonarr/Radarr @@ -21,7 +21,9 @@ sofarr connects to your media stack and shows you a personalized view of: ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────────┐ │ Browser │────▶│ sofarr │────▶│ SABnzbd (Usenet downloads) │ │ (User) │◀────│ Server │ │ qBittorrent (Torrents) │ -└─────────────┘ └──────────────┘ │ Sonarr (TV management) │ +└─────────────┘ └──────────────┘ │ Transmission (Torrents) │ + │ │ rTorrent (Torrents) │ + │ │ Sonarr (TV management) │ │ │ Radarr (Movie management) │ │ │ Emby (User authentication) │ ▼ └─────────────────────────────┘ @@ -34,10 +36,10 @@ sofarr connects to your media stack and shows you a personalized view of: ### The Matching Process 1. **User Authentication**: Login via Emby credentials -2. **Tag-Based Matching**: +2. **Tag-Based Matching**: - Your media in Sonarr/Radarr is tagged with your username (e.g., "gordon") - sofarr checks Sonarr/Radarr activity to find items tagged with your name - - Downloads (from SABnzbd/qBittorrent) are matched by title to that activity + - Downloads (from SABnzbd, qBittorrent, Transmission, or rTorrent) are matched by title to that activity - Only your downloads appear on your dashboard ### Multi-Instance Support @@ -53,7 +55,7 @@ SONARR_INSTANCES=[{"name":"main","url":"...","apiKey":"..."}] ## Prerequisites - **Docker** (recommended), or Node.js (v22+) for manual installation -- At least one of: SABnzbd or qBittorrent +- At least one download client: SABnzbd, qBittorrent, Transmission, or rTorrent - Sonarr (optional, for TV tracking) - Radarr (optional, for movie tracking) - Emby (for user authentication) @@ -108,6 +110,8 @@ docker run -d \ -e RADARR_INSTANCES='[{"name":"main","url":"http://radarr:7878","apiKey":"your-key"}]' \ -e SABNZBD_INSTANCES='[{"name":"main","url":"http://sabnzbd:8080","apiKey":"your-key"}]' \ -e QBITTORRENT_INSTANCES='[{"name":"main","url":"http://qbit:8080","username":"admin","password":"pass"}]' \ + -e TRANSMISSION_INSTANCES='[{"name":"main","url":"http://transmission:9091","username":"admin","password":"pass"}]' \ + -e RTORRENT_INSTANCES='[{"name":"main","url":"http://rtorrent:8080/RPC2","username":"rtorrent","password":"rtorrent"}]' \ -e LOG_LEVEL=info \ -e POLL_INTERVAL=5000 \ docker.i3omb.com/sofarr:latest @@ -131,6 +135,8 @@ services: - RADARR_INSTANCES=[{"name":"main","url":"http://radarr:7878","apiKey":"your-key"}] - SABNZBD_INSTANCES=[{"name":"main","url":"http://sabnzbd:8080","apiKey":"your-key"}] - QBITTORRENT_INSTANCES=[{"name":"main","url":"http://qbit:8080","username":"admin","password":"pass"}] + - TRANSMISSION_INSTANCES=[{"name":"main","url":"http://transmission:9091","username":"admin","password":"pass"}] + - RTORRENT_INSTANCES=[{"name":"main","url":"http://rtorrent:8080/RPC2","username":"rtorrent","password":"rtorrent"}] - LOG_LEVEL=info - POLL_INTERVAL=5000 ``` @@ -188,6 +194,19 @@ POLL_INTERVAL=5000 # Background polling interval in ms (default # Set to 0 or "off" to disable (on-demand mode) ``` +### Download Clients (PDCA) + +sofarr uses a **Pluggable Download Client Architecture (PDCA)** that provides a unified interface for all download clients. This enables consistent data normalization, easy addition of new client types, and centralized configuration management. + +**Supported Download Clients:** + +| Client | Protocol | Auth Method | Notes | +|--------|----------|-------------|-------| +| SABnzbd | REST API | API Key | Usenet downloads | +| qBittorrent | Sync API | Username/Password | BitTorrent with incremental updates | +| Transmission | JSON-RPC | Username/Password | BitTorrent with session management | +| rTorrent | XML-RPC | HTTP Basic Auth | BitTorrent, requires full endpoint path | + ### Service Instances (JSON Array Format) All services support multi-instance configuration via single-line JSON arrays: @@ -199,10 +218,20 @@ SABNZBD_INSTANCES=[{"name":"primary","url":"https://sabnzbd.example.com","apiKey # qBittorrent Instances (uses username/password, not API key) QBITTORRENT_INSTANCES=[{"name":"main","url":"https://qbittorrent.example.com","username":"admin","password":"secret"}] +# Transmission Instances (uses username/password) +TRANSMISSION_INSTANCES=[{"name":"main","url":"http://transmission:9091/transmission/rpc","username":"admin","password":"pass"}] + +# rTorrent Instances (uses username/password, URL must include full RPC endpoint) +# Standard installs use /RPC2. Some providers like whatbox.ca use /xmlrpc. +RTORRENT_INSTANCES=[{"name":"main","url":"http://rtorrent:8080/RPC2","username":"rtorrent","password":"rtorrent"}] + +# For whatbox.ca (example): +# RTORRENT_INSTANCES=[{"name":"whatbox","url":"https://user.whatbox.ca/xmlrpc","username":"user","password":"pass"}] + # Sonarr Instances SONARR_INSTANCES=[{"name":"hd","url":"https://sonarr.example.com","apiKey":"your-api-key"}] -# Radarr Instances +# Radarr Instances RADARR_INSTANCES=[{"name":"movies","url":"https://radarr.example.com","apiKey":"your-api-key"}] # Emby (single instance for authentication) @@ -216,6 +245,18 @@ If you only have one instance, you can use the legacy format: ```bash SABNZBD_URL=https://sabnzbd.example.com SABNZBD_API_KEY=your-api-key + +QBITTORRENT_URL=https://qbittorrent.example.com +QBITTORRENT_USERNAME=admin +QBITTORRENT_PASSWORD=secret + +TRANSMISSION_URL=http://transmission:9091/transmission/rpc +TRANSMISSION_USERNAME=admin +TRANSMISSION_PASSWORD=pass + +RTORRENT_URL=http://rtorrent:8080/RPC2 +RTORRENT_USERNAME=rtorrent +RTORRENT_PASSWORD=rtorrent ``` ## Setting Up User Tags diff --git a/server/clients/RTorrentClient.js b/server/clients/RTorrentClient.js index ba28454..0717fc0 100644 --- a/server/clients/RTorrentClient.js +++ b/server/clients/RTorrentClient.js @@ -5,18 +5,18 @@ const { logToFile } = require('../utils/logger'); /** * rTorrent download client implementation. - * Communicates via XML-RPC over HTTP (typically ${url}/RPC2). + * Communicates via XML-RPC over HTTP. * Supports HTTP Basic Auth when username/password are configured. + * The URL field must include the full XML-RPC endpoint path (e.g., http://rtorrent.local:8080/RPC2 or https://user.whatbox.ca/xmlrpc). */ class RTorrentClient extends DownloadClient { constructor(instance) { super(instance); - this.rpcUrl = `${this.url}/RPC2`; this._createClient(); } _createClient() { - const clientOptions = { url: this.rpcUrl }; + const clientOptions = { url: this.url }; if (this.username && this.password) { clientOptions.headers = { diff --git a/tests/unit/clients/RTorrentClient.test.js b/tests/unit/clients/RTorrentClient.test.js index d2bd485..bae78a7 100644 --- a/tests/unit/clients/RTorrentClient.test.js +++ b/tests/unit/clients/RTorrentClient.test.js @@ -39,12 +39,11 @@ describe('RTorrentClient', () => { expect(client.getInstanceId()).toBe('test-rtorrent'); expect(client.name).toBe('Test rTorrent'); expect(client.url).toBe('http://localhost:8080'); - expect(client.rpcUrl).toBe('http://localhost:8080/RPC2'); }); - it('should create xmlrpc client with basic auth when credentials provided', () => { + it('should create xmlrpc client with exact URL from config (no auto-append)', () => { expect(xmlrpc.createClient).toHaveBeenCalledWith({ - url: 'http://localhost:8080/RPC2', + url: 'http://localhost:8080', headers: { Authorization: `Basic ${Buffer.from('rtorrent:rtorrent').toString('base64')}` } @@ -56,13 +55,44 @@ describe('RTorrentClient', () => { const noAuthConfig = { id: 'test-rtorrent-noauth', name: 'Test rTorrent No Auth', - url: 'http://localhost:8080' + url: 'http://localhost:8080/RPC2' }; new RTorrentClient(noAuthConfig); expect(xmlrpc.createClient).toHaveBeenCalledWith({ url: 'http://localhost:8080/RPC2' }); }); + + it('should use whatbox.ca-style /xmlrpc path exactly as configured', () => { + xmlrpc.createClient.mockClear(); + const whatboxConfig = { + id: 'test-whatbox', + name: 'Whatbox', + url: 'https://user.whatbox.ca/xmlrpc', + username: 'user', + password: 'pass' + }; + new RTorrentClient(whatboxConfig); + expect(xmlrpc.createClient).toHaveBeenCalledWith({ + url: 'https://user.whatbox.ca/xmlrpc', + headers: { + Authorization: `Basic ${Buffer.from('user:pass').toString('base64')}` + } + }); + }); + + it('should use custom RPC path exactly as configured', () => { + xmlrpc.createClient.mockClear(); + const customConfig = { + id: 'test-custom', + name: 'Custom', + url: 'https://example.com/custom/rpc/path' + }; + new RTorrentClient(customConfig); + expect(xmlrpc.createClient).toHaveBeenCalledWith({ + url: 'https://example.com/custom/rpc/path' + }); + }); }); describe('Connection Test', () => {