fix: remove auto-appending of /RPC2 from RTorrentClient and finalize PDCA documentation
Some checks failed
Build and Push Docker Image / build (push) Failing after 40s
CI / Security audit (push) Failing after 45s
CI / Tests & coverage (push) Failing after 55s
Docs Check / Markdown lint (push) Successful in 1m1s
Docs Check / Mermaid diagram parse check (push) Successful in 1m23s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 28s

- 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
This commit is contained in:
2026-05-19 11:53:51 +01:00
parent a50e5a7d69
commit 620f264861
4 changed files with 93 additions and 16 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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', () => {