Pluggable Download Client Architecture (PDCA) – Unified Interface + Expanded Client Support #18

Closed
opened 2026-05-19 11:01:12 +01:00 by Gandalf · 0 comments
Owner

Problem Statement

Sofarr currently handles download clients in a semi-coupled way:

SABnzbd logic lives mostly inside the poller.
qBittorrent has its own dedicated client class (recently enhanced with the Sync API).

This makes adding new clients (rtorrent, uTorrent, Transmission, Deluge) expensive and risks inconsistent data shapes in the cache and dashboard. Every new client duplicates authentication, error handling, normalization, and caching logic.

Goal

Introduce a clean, pluggable abstraction layer for all download clients (Usenet + BitTorrent) so that:

The poller and cache become client-agnostic.
Adding a new client is a matter of implementing a small, well-defined interface.
All clients produce a normalized download object that the rest of the application can consume without knowing the underlying client.

Scope

  • Define a DownloadClient abstract base class / interface with these core methods:

    • getActiveDownloads() → normalized array
    • getClientStatus() (optional but recommended)
    • testConnection() / health check
  • Refactor existing clients to implement the interface:

    • SABnzbdClient
    • QBittorrentClient (preserve the Sync API work already done)
  • Create a DownloadClientFactory + registry (config-driven).

  • Update the poller to iterate over registered clients generically.

  • Normalize all download objects to a common schema (see below).

  • Comprehensive unit + integration tests for the new abstraction.

  • Update ARCHITECTURE.md and add a small “Adding a New Download Client” guide.

Normalized Download Object (Proposed Schema)
All clients must return objects matching this shape (extensible via raw or clientSpecific):

interface NormalizedDownload {
  id: string;                    // client-specific unique id
  title: string;
  type: 'usenet' | 'torrent';
  client: string;                // 'sabnzbd' | 'qbittorrent' | 'transmission' ...
  instanceId: string;
  instanceName: string;
  status: string;                // normalized (Downloading, Seeding, Paused, etc.)
  progress: number;              // 0-100
  size: number;                  // bytes
  downloaded: number;            // bytes
  speed: number;                 // bytes/sec
  eta: number | null;            // seconds
  category?: string;
  tags?: string[];
  savePath?: string;
  addedOn?: string;
  // For matching to Sonarr/Radarr
  arrQueueId?: number;
  arrType?: 'series' | 'movie';
  // Escape hatch
  raw?: any;                     // original client response (for advanced use)
}

Success Metrics

  • 100% of existing functionality preserved (no regressions in SABnzbd or qBittorrent).
  • Poller becomes ~30-40% smaller and easier to reason about.
  • Passes all existing integration tests.
**Problem Statement** Sofarr currently handles download clients in a semi-coupled way: SABnzbd logic lives mostly inside the poller. qBittorrent has its own dedicated client class (recently enhanced with the Sync API). This makes adding new clients (rtorrent, uTorrent, Transmission, Deluge) expensive and risks inconsistent data shapes in the cache and dashboard. Every new client duplicates authentication, error handling, normalization, and caching logic. **Goal** Introduce a clean, pluggable abstraction layer for all download clients (Usenet + BitTorrent) so that: The poller and cache become client-agnostic. Adding a new client is a matter of implementing a small, well-defined interface. All clients produce a normalized download object that the rest of the application can consume without knowing the underlying client. **Scope** - Define a DownloadClient abstract base class / interface with these core methods: - getActiveDownloads() → normalized array - getClientStatus() (optional but recommended) - testConnection() / health check - Refactor existing clients to implement the interface: - SABnzbdClient - QBittorrentClient (preserve the Sync API work already done) - Create a DownloadClientFactory + registry (config-driven). - Update the poller to iterate over registered clients generically. - Normalize all download objects to a common schema (see below). - Comprehensive unit + integration tests for the new abstraction. - Update ARCHITECTURE.md and add a small “Adding a New Download Client” guide. **Normalized Download Object (Proposed Schema)** All clients must return objects matching this shape (extensible via raw or clientSpecific): ```ts interface NormalizedDownload { id: string; // client-specific unique id title: string; type: 'usenet' | 'torrent'; client: string; // 'sabnzbd' | 'qbittorrent' | 'transmission' ... instanceId: string; instanceName: string; status: string; // normalized (Downloading, Seeding, Paused, etc.) progress: number; // 0-100 size: number; // bytes downloaded: number; // bytes speed: number; // bytes/sec eta: number | null; // seconds category?: string; tags?: string[]; savePath?: string; addedOn?: string; // For matching to Sonarr/Radarr arrQueueId?: number; arrType?: 'series' | 'movie'; // Escape hatch raw?: any; // original client response (for advanced use) } ``` **Success Metrics** - 100% of existing functionality preserved (no regressions in SABnzbd or qBittorrent). - Poller becomes ~30-40% smaller and easier to reason about. - Passes all existing integration tests.
Gandalf added the Kind/Enhancement label 2026-05-19 11:01:12 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Gandalf/sofarr#18