# 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](./download-clients.md#per-client-custom-download-path) 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:** ```typescript // 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 ```typescript 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 | 'downloading' | 'uploading' | 'stalledDL' | 'stalledUP' | 'pausedDL' | 'pausedUP' | 'queuedDL' | 'queuedUP' | 'checkingDL' | 'checkingUP' | 'error' | 'missingFiles' | 'allocating' // Forced states (user clicked "Force Resume") | 'forcedDL' | 'forcedUP' // Metadata fetching | 'metaDL' | 'forcedMetaDL' // qBittorrent v5.0+ (renamed paused → stopped) | 'stoppedDL' | 'stoppedUP' // 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) - `forcedUP` → `seeding`/`completed` enables monitor to trigger import - `stoppedDL`/`stoppedUP` → `paused` ensures qBittorrent v5.x compatibility ## Tech Stack - axios (HTTP + cookie mgmt) - parse-torrent (bencode + hash extraction) - form-data (multipart uploads) ## Related - See [File Organization](./file-organization.md) for seeding support