Files
ReadMeABook/documentation/phase3/qbittorrent.md
T
kikootwo dc7e557694 Add notification system with admin UI and backend
Introduces a full notification system with support for Discord and Pushover backends, event triggers, and message formatting. Adds backend services, processors, and API endpoints for managing notifications, as well as a new Notifications tab in the admin settings UI. Updates documentation, database schema, and tests to cover notification features and approval workflow improvements. Also changes project license from MIT to AGPL v3.
2026-01-28 11:42:00 -05:00

11 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 - Download save path (passed to qBittorrent for all torrents)

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 and credentials are always current

Category Management

Category: readmeabook (auto-created for all torrents)

Save Path Synchronization:

  • Category created/updated on every torrent addition
  • Category save path always synced with download_dir config
  • Handles config changes: if user changes download_dir, category updates automatically
  • Uses both createCategory and editCategory APIs for reliability

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.

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 = 'downloading' | 'uploading' | 'stalledDL' |
  'pausedDL' | 'queuedDL' | 'checkingDL' | 'error' | 'missingFiles';

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

Tech Stack

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