Files
kikootwo d70f6c9957 Add Deluge integration; revamp admin Jobs & Logs UI
Introduce Deluge download client service and tests, remove obsolete rdtclient service, and update qbittorrent integration/tests and download-client interfaces/manager. Large UI refactor for admin pages: Jobs and Logs were redesigned to be responsive (mobile card views + desktop tables), improved headers, dialogs, controls, and better status/detail rendering. Also updated DownloadClient components (card, management, modal), organize-files processor, audible-series integration, and related unit tests to align with integration changes. Minor UX and accessibility tweaks, cron handling/validation adjustments, and a few formatting/cleanup fixes throughout.
2026-02-20 20:44:26 -05:00

14 KiB

qBittorrent Integration

Status: Implemented

Free, open-source BitTorrent client with comprehensive Web API.

Enterprise Torrent Addition

Challenge: /api/v2/torrents/add returns only "Ok." without torrent hash.

Solution (Professional):

Magnet Links:

  1. Extract info_hash from magnet URI (deterministic)
  2. Upload via urls parameter
  3. Return extracted hash immediately

Torrent Files:

  1. Download .torrent file to memory
  2. Parse with parse-torrent (bencode decoder)
  3. Extract info_hash (SHA-1 of info dict)
  4. Upload file content via torrents parameter (multipart/form-data)
  5. Return extracted hash immediately

Benefits: Deterministic, no race conditions, works with Docker networking, handles expired URLs

API Endpoints

Base: http://qbittorrent:8080/api/v2 Auth: Cookie-based (login required)

POST /auth/login - Get session cookie POST /torrents/add - Add torrent (supports urls and torrents params, savepath override) GET /torrents/info?hashes={hash} - Get status/progress POST /torrents/pause - Pause torrent POST /torrents/resume - Resume POST /torrents/delete - Delete torrent GET /torrents/files - Get file list POST /torrents/createCategory - Create category with save path POST /torrents/editCategory - Update category save path POST /torrents/setCategory - Set category for torrent

Config

Required (database only, no env fallbacks):

  • download_client_url - qBittorrent Web UI URL (supports HTTP and HTTPS)
  • download_client_username - qBittorrent username
  • download_client_password - qBittorrent password
  • download_dir - Base download save path (joined with per-client customPath if configured)

Optional (SSL/TLS):

  • download_client_disable_ssl_verify - Disable SSL certificate verification for HTTPS (boolean as string "true"/"false", default: "false")
    • Use when connecting to qBittorrent with self-signed certificates
    • ⚠️ Security warning: Only use on trusted private networks
    • Enhanced error messages guide users when SSL issues detected

Optional (Remote Path Mapping):

  • download_client_remote_path_mapping_enabled - Enable path mapping (boolean as string "true"/"false")
  • download_client_remote_path - Remote path prefix from qBittorrent
  • download_client_local_path - Local path prefix for ReadMeABook

Validation: All required fields checked before service initialization. Path mapping fields validated when enabled.

Singleton Invalidation: Service uses singleton pattern for performance. When settings change (via admin settings page), singleton is invalidated to force reload:

  • invalidateQBittorrentService() called after updating paths or download client settings
  • Forces service to re-read database config on next torrent addition
  • Ensures category save path, credentials, and customPath resolution are always current
  • Singleton getter resolves customPath from client config (consistent with manager's createService())

Category Management

Category: readmeabook (auto-created for all torrents)

Save Path Synchronization:

  • Category created/updated on every torrent addition
  • Category save path synced with resolved download path (download_dir + per-client customPath)
  • Handles config changes: if user changes download_dir or customPath, category updates automatically
  • Uses both createCategory and editCategory APIs for reliability
  • Remote path mapping applied after customPath resolution (outgoing: local → remote)

Why Both Create and Edit:

  1. Create: Ensures category exists (idempotent, won't fail if exists)
  2. Edit: Updates save path to match current config (handles user changing settings)

This prevents issues where category retains old save path after user changes download_dir setting.

Per-Client Custom Path:

  • If customPath is set (e.g., torrents), category save path becomes /downloads/torrents
  • Remote path mapping applies to the resolved path: reverseTransform(/downloads/torrents) → remote equivalent
  • See download-clients.md for details

Remote Path Mapping

Use Case: qBittorrent runs on different machine/container with different filesystem perspective.

Example Scenario:

  • qBittorrent on Windows expects: F:\Docker\downloads\completed\books
  • ReadMeABook inside Docker sees: /downloads
  • Mapping: Remote F:\Docker\downloads\completed\books ↔ Local /downloads

Configuration:

  1. Admin Settings → Download Client → Enable Remote Path Mapping
  2. Enter remote path (as qBittorrent sees it, e.g., F:\Docker\downloads\completed\books)
  3. Enter local path (as RMAB sees it, e.g., /downloads)
  4. Test connection validates local path exists
  5. Save settings

Bidirectional Path Mapping:

1. Outgoing (RMAB → qBittorrent): When adding torrents

  • RMAB's download path: /downloads
  • Translated to qBit's path: F:\Docker\downloads\completed\books
  • Applied in qbittorrent.service.ts via PathMapper.reverseTransform()
  • Ensures qBittorrent knows where to save files

2. Incoming (qBittorrent → RMAB): When processing completed downloads

  • qBit reports: F:\Docker\downloads\completed\books\Audiobook.Name
  • Translated to RMAB's path: /downloads/Audiobook.Name
  • Applied in monitor-download.processor.ts via PathMapper.transform()
  • Applied in retry-failed-imports.processor.ts for failed imports
  • Ensures RMAB can find and organize files

Implementation:

  • PathMapper utility (src/lib/utils/path-mapper.ts) handles transformation
  • transform(): Remote → Local (qBit → RMAB)
  • reverseTransform(): Local → Remote (RMAB → qBit)
  • Uses simple prefix replacement with path normalization
  • Preserves Windows backslashes when translating to Windows paths
  • Graceful fallback: if path doesn't match prefix, returns unchanged

Path Transformation Examples:

// Outgoing: RMAB → qBittorrent (when adding torrent)
localPath = "/downloads"
config = { remotePath: "F:\\Docker\\downloads\\completed\\books", localPath: "/downloads" }
remotePath = PathMapper.reverseTransform(localPath, config)
// Result: "F:\Docker\downloads\completed\books"

// Incoming: qBittorrent → RMAB (when processing completion)
qbPath = "F:\\Docker\\downloads\\completed\\books\\Audiobook.Name"
config = { remotePath: "F:\\Docker\\downloads\\completed\\books", localPath: "/downloads" }
organizePath = PathMapper.transform(qbPath, config)
// Result: "/downloads/Audiobook.Name"

Validation:

  • Local path accessibility checked during test connection
  • Prevents misconfiguration before save
  • Warning shown for existing downloads (mapping only affects new downloads)

Behavior:

  • Mapping only applies when enabled
  • If path doesn't start with expected prefix, returns original (logs warning)
  • Path normalization handles trailing slashes, backslashes, redundant separators
  • Works with both content_path and constructed save_path + name
  • Preserves native path separators (important for Windows)

Data Models

interface TorrentInfo {
  hash: string;
  name: string;
  size: number;
  progress: number; // 0.0-1.0
  dlspeed: number; // bytes/s
  upspeed: number;
  eta: number; // seconds
  state: TorrentState;
  category: string;
  savePath: string;
  completionDate: number;
}

type TorrentState =
  // Core states (*DL = download phase, *UP = upload/post-download phase)
  | 'downloading' | 'uploading'
  | 'stalledDL' | 'stalledUP'        // stalledUP → completed (download done)
  | 'pausedDL' | 'pausedUP'          // pausedUP → completed (download done, paused seeding)
  | 'queuedDL' | 'queuedUP'          // queuedUP → completed (download done)
  | 'checkingDL' | 'checkingUP'      // checkingUP → completed (download done, rechecking)
  | 'error' | 'missingFiles' | 'allocating'
  // Forced states (user clicked "Force Resume")
  | 'forcedDL' | 'forcedUP'          // forcedUP → completed (download done)
  // Metadata fetching
  | 'metaDL' | 'forcedMetaDL'
  // qBittorrent v5.0+ (renamed paused → stopped)
  | 'stoppedDL' | 'stoppedUP'        // stoppedUP → completed (download done)
  // Other
  | 'checkingResumeData' | 'moving';

Fixed Issues

1. Naive torrent identification - Fixed with deterministic hash extraction 2. Docker networking issues - Fixed by downloading .torrent ourselves 3. Duplicate detection - Check if hash exists before adding 4. Config fallbacks to env - Removed, database only 5. Unclear error messages - List missing fields explicitly 6. Race condition on torrent availability - Fixed with 3s initial delay + exponential backoff retry (500ms, 1s, 2s) 7. Error logging during duplicate check - Removed console.error in getTorrent() during expected "not found" cases (duplicate checking) 8. Prowlarr magnet link redirects - Some indexers return HTTP URLs that redirect to magnet: links. Fixed by intercepting 3xx redirects before axios follows them, extracting the Location header, and routing to magnet flow if target is a magnet: link 9. Category save path not updating - When user changes download_dir setting, category keeps old path. Fixed by:

  • Checking existing categories before create/edit (avoid unnecessary 409 errors)
  • Invalidating service singleton when settings change (forces config reload)
  • Settings API calls invalidateQBittorrentService() after updating paths or credentials 10. Remote seedbox path mismatch - qBittorrent on remote machine reports different filesystem paths. Fixed by:
  • Remote path mapping feature with toggle in admin settings and setup wizard
  • PathMapper utility for prefix replacement transformation
  • Local path validation during test connection
  • Applied in download completion and import retry processors 11. HTTPS SSL certificate errors - Users with seedboxes using self-signed certificates or Let's Encrypt couldn't connect. Fixed by:
  • Optional SSL verification disable toggle in setup wizard and admin settings
  • Custom HTTPS agent with rejectUnauthorized: false when enabled
  • Enhanced error messages identifying SSL/TLS certificate issues with actionable guidance
  • Secure by default (SSL verification enabled), with clear security warnings when disabled
  • URL format: https://qbt.domain.com:443/qbittorrent fully supported 12. CSRF protection HTTP 401 errors - qBittorrent v4.1.0+ has CSRF protection enabled by default, causing authentication failures (HTTP 401) when Referer/Origin headers missing. Browsers work because they auto-send these headers. Fixed by:
  • Adding Referer and Origin headers to all login requests
  • Headers set to qBittorrent base URL (e.g., https://seedbox.example.com:443/qbittorrent)
  • Applied to both login() and testConnectionWithCredentials() methods
  • Works with all qBittorrent versions and configurations
  • Enhanced debug logging for troubleshooting authentication issues (enable with LOG_LEVEL=debug) 13. Nginx/Apache reverse proxy HTTP Basic Auth - Many seedboxes use nginx or Apache reverse proxy with HTTP Basic Authentication in front of qBittorrent. This causes HTTP 401 errors with www-authenticate: Basic header. Browsers handle this by prompting for credentials and sending Authorization: Basic header. Fixed by:
  • Adding HTTP Basic Auth to all axios requests using auth parameter
  • Same credentials used for both Basic Auth (nginx/Apache) and qBittorrent Web UI authentication
  • Applied to axios client instance and all standalone requests
  • Works transparently with or without reverse proxy
  • Compatible with popular seedbox providers (seedit4.me, etc.) 14. Remote path mapping not applied when adding torrents - When qBittorrent runs locally (e.g., Windows) and RMAB runs in Docker, savepath sent to qBittorrent was not translated. qBittorrent received /downloads (RMAB's path) but expected F:\Docker\downloads\completed\books (Windows path), causing "Invalid path" errors. Fixed by:
  • Added PathMapper.reverseTransform() for bidirectional path mapping (local → remote)
  • Applied in qbittorrent.service.ts when setting savepath for torrents
  • Preserves Windows backslashes when translating to Windows paths
  • Path mapping now works in both directions: outgoing (RMAB → qBit) and incoming (qBit → RMAB)
  • Service constructor accepts PathMappingConfig parameter
  • Singleton loads path mapping config from database

15. Missing qBittorrent torrent states - Monitor never detected completion for force-resumed torrents (forcedDL/forcedUP), causing infinite polling at 100%. Also missing metadata states (metaDL/forcedMetaDL), qBittorrent v5.x renamed states (stoppedDL/stoppedUP), and utility states (checkingResumeData/moving). Fixed by:

  • Adding all 8 missing states to TorrentState type union
  • Adding mappings to both mapState() (legacy) and mapStateToDownloadStatus() (unified interface)
  • forcedUPseeding/completed enables monitor to trigger import
  • stoppedDLpaused ensures qBittorrent v5.x compatibility

16. pausedUP/stoppedUP mapped as paused instead of completed - qBittorrent (after ratio limits) transitions directly to pausedUP/stoppedUP without passing through uploading/stalledUP. The *UP suffix means the download phase is complete and the torrent is on the upload side. Both states were incorrectly mapped to 'paused', causing the monitor to re-schedule checks indefinitely instead of triggering file organization. Fixed by:

  • pausedUPseeding (unified) / completed (legacy) — triggers completion in monitor
  • stoppedUPseeding (unified) / completed (legacy) — same fix for qBittorrent v5.x
  • pausedDL/stoppedDL remain paused — download phase genuinely paused
  • Key insight: any *UP state is post-download; any *DL state is pre-completion

Tech Stack

  • axios (HTTP + cookie mgmt)
  • parse-torrent (bencode + hash extraction)
  • form-data (multipart uploads)