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
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:
12
.env.sample
12
.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
|
# Add one or more rTorrent instances as a single-line JSON array
|
||||||
# Uses username/password authentication (optional)
|
# Uses username/password authentication (optional)
|
||||||
# Format: [{"name":"instance-name","url":"https://...","username":"...","password":"..."}]
|
# 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)
|
# Legacy single-instance format (optional - still supported)
|
||||||
# RTORRENT_URL=https://rtorrent.example.com
|
# RTORRENT_URL=http://rtorrent.example.com/RPC2
|
||||||
# RTORRENT_USERNAME=rtorrent
|
# RTORRENT_USERNAME=rtorrent
|
||||||
# RTORRENT_PASSWORD=rtorrent
|
# RTORRENT_PASSWORD=rtorrent
|
||||||
|
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -7,7 +7,7 @@
|
|||||||
## What It Does
|
## What It Does
|
||||||
|
|
||||||
sofarr connects to your media stack and shows you a personalized view of:
|
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
|
- **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
|
- **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
|
- **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) │
|
│ Browser │────▶│ sofarr │────▶│ SABnzbd (Usenet downloads) │
|
||||||
│ (User) │◀────│ Server │ │ qBittorrent (Torrents) │
|
│ (User) │◀────│ Server │ │ qBittorrent (Torrents) │
|
||||||
└─────────────┘ └──────────────┘ │ Sonarr (TV management) │
|
└─────────────┘ └──────────────┘ │ Transmission (Torrents) │
|
||||||
|
│ │ rTorrent (Torrents) │
|
||||||
|
│ │ Sonarr (TV management) │
|
||||||
│ │ Radarr (Movie management) │
|
│ │ Radarr (Movie management) │
|
||||||
│ │ Emby (User authentication) │
|
│ │ Emby (User authentication) │
|
||||||
▼ └─────────────────────────────┘
|
▼ └─────────────────────────────┘
|
||||||
@@ -37,7 +39,7 @@ sofarr connects to your media stack and shows you a personalized view of:
|
|||||||
2. **Tag-Based Matching**:
|
2. **Tag-Based Matching**:
|
||||||
- Your media in Sonarr/Radarr is tagged with your username (e.g., "gordon")
|
- 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
|
- 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
|
- Only your downloads appear on your dashboard
|
||||||
|
|
||||||
### Multi-Instance Support
|
### Multi-Instance Support
|
||||||
@@ -53,7 +55,7 @@ SONARR_INSTANCES=[{"name":"main","url":"...","apiKey":"..."}]
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Docker** (recommended), or Node.js (v22+) for manual installation
|
- **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)
|
- Sonarr (optional, for TV tracking)
|
||||||
- Radarr (optional, for movie tracking)
|
- Radarr (optional, for movie tracking)
|
||||||
- Emby (for user authentication)
|
- Emby (for user authentication)
|
||||||
@@ -108,6 +110,8 @@ docker run -d \
|
|||||||
-e RADARR_INSTANCES='[{"name":"main","url":"http://radarr:7878","apiKey":"your-key"}]' \
|
-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 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 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 LOG_LEVEL=info \
|
||||||
-e POLL_INTERVAL=5000 \
|
-e POLL_INTERVAL=5000 \
|
||||||
docker.i3omb.com/sofarr:latest
|
docker.i3omb.com/sofarr:latest
|
||||||
@@ -131,6 +135,8 @@ services:
|
|||||||
- RADARR_INSTANCES=[{"name":"main","url":"http://radarr:7878","apiKey":"your-key"}]
|
- RADARR_INSTANCES=[{"name":"main","url":"http://radarr:7878","apiKey":"your-key"}]
|
||||||
- SABNZBD_INSTANCES=[{"name":"main","url":"http://sabnzbd:8080","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"}]
|
- 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
|
- LOG_LEVEL=info
|
||||||
- POLL_INTERVAL=5000
|
- 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)
|
# 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)
|
### Service Instances (JSON Array Format)
|
||||||
|
|
||||||
All services support multi-instance configuration via single-line JSON arrays:
|
All services support multi-instance configuration via single-line JSON arrays:
|
||||||
@@ -199,6 +218,16 @@ SABNZBD_INSTANCES=[{"name":"primary","url":"https://sabnzbd.example.com","apiKey
|
|||||||
# qBittorrent Instances (uses username/password, not API key)
|
# qBittorrent Instances (uses username/password, not API key)
|
||||||
QBITTORRENT_INSTANCES=[{"name":"main","url":"https://qbittorrent.example.com","username":"admin","password":"secret"}]
|
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
|
||||||
SONARR_INSTANCES=[{"name":"hd","url":"https://sonarr.example.com","apiKey":"your-api-key"}]
|
SONARR_INSTANCES=[{"name":"hd","url":"https://sonarr.example.com","apiKey":"your-api-key"}]
|
||||||
|
|
||||||
@@ -216,6 +245,18 @@ If you only have one instance, you can use the legacy format:
|
|||||||
```bash
|
```bash
|
||||||
SABNZBD_URL=https://sabnzbd.example.com
|
SABNZBD_URL=https://sabnzbd.example.com
|
||||||
SABNZBD_API_KEY=your-api-key
|
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
|
## Setting Up User Tags
|
||||||
|
|||||||
@@ -5,18 +5,18 @@ const { logToFile } = require('../utils/logger');
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* rTorrent download client implementation.
|
* 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.
|
* 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 {
|
class RTorrentClient extends DownloadClient {
|
||||||
constructor(instance) {
|
constructor(instance) {
|
||||||
super(instance);
|
super(instance);
|
||||||
this.rpcUrl = `${this.url}/RPC2`;
|
|
||||||
this._createClient();
|
this._createClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
_createClient() {
|
_createClient() {
|
||||||
const clientOptions = { url: this.rpcUrl };
|
const clientOptions = { url: this.url };
|
||||||
|
|
||||||
if (this.username && this.password) {
|
if (this.username && this.password) {
|
||||||
clientOptions.headers = {
|
clientOptions.headers = {
|
||||||
|
|||||||
@@ -39,12 +39,11 @@ describe('RTorrentClient', () => {
|
|||||||
expect(client.getInstanceId()).toBe('test-rtorrent');
|
expect(client.getInstanceId()).toBe('test-rtorrent');
|
||||||
expect(client.name).toBe('Test rTorrent');
|
expect(client.name).toBe('Test rTorrent');
|
||||||
expect(client.url).toBe('http://localhost:8080');
|
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({
|
expect(xmlrpc.createClient).toHaveBeenCalledWith({
|
||||||
url: 'http://localhost:8080/RPC2',
|
url: 'http://localhost:8080',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${Buffer.from('rtorrent:rtorrent').toString('base64')}`
|
Authorization: `Basic ${Buffer.from('rtorrent:rtorrent').toString('base64')}`
|
||||||
}
|
}
|
||||||
@@ -56,13 +55,44 @@ describe('RTorrentClient', () => {
|
|||||||
const noAuthConfig = {
|
const noAuthConfig = {
|
||||||
id: 'test-rtorrent-noauth',
|
id: 'test-rtorrent-noauth',
|
||||||
name: 'Test rTorrent No Auth',
|
name: 'Test rTorrent No Auth',
|
||||||
url: 'http://localhost:8080'
|
url: 'http://localhost:8080/RPC2'
|
||||||
};
|
};
|
||||||
new RTorrentClient(noAuthConfig);
|
new RTorrentClient(noAuthConfig);
|
||||||
expect(xmlrpc.createClient).toHaveBeenCalledWith({
|
expect(xmlrpc.createClient).toHaveBeenCalledWith({
|
||||||
url: 'http://localhost:8080/RPC2'
|
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', () => {
|
describe('Connection Test', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user