Root cause: Singleton caching issue
The qBittorrent service uses a singleton pattern with a configLoaded flag.
Once initialized, it NEVER re-reads the database config, even when the
user changes settings via the admin settings page.
Flow showing the bug:
1. Wizard saves download_dir to database ✓
2. First torrent: service reads config, creates singleton, sets configLoaded=true ✓
3. User changes download_dir in settings page ✓ (database updated)
4. Next torrent: getQBittorrentService() returns CACHED singleton ✗
5. Cached singleton has OLD download_dir value in this.defaultSavePath ✗
6. Category check shows "already has correct save path: /old/path" ✗
7. Download goes to wrong location ✗
The singleton check (line 745):
if (!qbittorrentService || !configLoaded) {
// Only runs if service doesn't exist or config failed
}
Once both exist, this block is SKIPPED forever!
Fix:
1. Added invalidateQBittorrentService() function
- Resets qbittorrentService = null
- Resets configLoaded = false
- Forces reload from database on next use
2. Call invalidation from settings APIs:
- After updating paths (download_dir, media_dir)
- After updating download client (URL, credentials)
3. Next torrent addition:
- getQBittorrentService() sees null singleton
- Re-reads config from database
- Creates new service with current download_dir
- Category updated with correct path
Benefits:
- Settings changes take effect immediately
- No app restart needed
- Category save path always matches current config
- Download client credentials always current
Updated documentation to explain singleton invalidation pattern.
4.6 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:
- Extract
info_hashfrom magnet URI (deterministic) - Upload via
urlsparameter - Return extracted hash immediately
Torrent Files:
- Download .torrent file to memory
- Parse with
parse-torrent(bencode decoder) - Extract
info_hash(SHA-1 of info dict) - Upload file content via
torrentsparameter (multipart/form-data) - 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 URLdownload_client_username- qBittorrent usernamedownload_client_password- qBittorrent passworddownload_dir- Download save path (passed to qBittorrent for all torrents)
Validation: All fields checked before service initialization.
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_dirconfig - Handles config changes: if user changes
download_dir, category updates automatically - Uses both
createCategoryandeditCategoryAPIs for reliability
Why Both Create and Edit:
- Create: Ensures category exists (idempotent, won't fail if exists)
- 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.
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
Tech Stack
- axios (HTTP + cookie mgmt)
- parse-torrent (bencode + hash extraction)
- form-data (multipart uploads)
Related
- See File Organization for seeding support