An automated torrent management system that integrates with qBittorrent's Web API to organize and process torrents based on their origin (Empornium, OneJav) and content type. The system handles categorization, file organization, cleanup of non-media files, and provides reliable monitoring alternatives to qBittorrent's external program feature.
This handler automatically manages torrents throughout their lifecycle:
- Torrent Addition: Identifies torrent origin from metadata/comments and assigns appropriate categories via qBittorrent API
- Download Completion: Processes finished downloads by moving video files to organized destinations, removing non-video content, and cleaning up source directories
- Reliable Monitoring: Uses periodic polling and systemd service to ensure no torrents are missed due to qBittorrent's unreliable external program execution
The system supports two operational modes:
- Traditional Mode: Direct integration with qBittorrent's external program feature (less reliable in recent versions)
- Monitor Mode: Recommended approach using continuous monitoring with periodic polling (production-ready)
- Metadata Analysis: Parses torrent comments for "empornium" or "onejav" keywords (case-insensitive)
- Tracker URL Fallback: Uses tracker patterns when metadata is unavailable
- Category Inheritance: Uses qBittorrent category parameter to avoid race conditions
- Default Handling: Graceful fallback for unknown sources
- Single-File Torrents: Direct rename/copy to destination with cross-filesystem support
- Multi-File Torrents:
- Removes non-video files (images, text files, metadata, etc.)
- Creates subdirectories for torrents with 10+ video files
- Preserves original filenames
- Cross-Filesystem Moves: Automatic detection and fallback from
renametocopy+remove - Protected Directories: Prevents accidental deletion of important folders
- qBittorrent Auto-Move Detection: Skips redundant file operations when qBittorrent has already moved files
- Supported Formats: MP4, MKV, AVI, MOV, WMV, FLV, WebM, M4V, MPG, MPEG, M2V, M2TS, TS, VOB, DIVX, 3GP, OGV, MTS, ASF
- Image File Detection: JPG, JPEG, PNG, GIF, BMP, WebP, TIFF, TIF, SVG, RAW, HEIC, HEIF, ICO, PSD, AI, EPS, JFIF, AVIF (removed during cleanup)
- Size Limits: Configurable maximum file size (default: 10GB)
- Polling Mode: Uses periodic polling of the qBittorrent API for reliable detection of completed torrents (trigger file support removed)
- Fallback Polling: Checks for completed torrents every 5 minutes
- Systemd Integration: Automatic startup, restart on failure, and log management
- Duplicate Prevention: Tracks processed torrent hashes to avoid re-processing
- Resource Limits: Memory (512MB) and CPU (50%) limits for stable operation
- Comprehensive Logging: Detailed operation logs with timestamps
- Error Tracking: Separate error log for troubleshooting
- Retry Logic: Configurable API retries with exponential backoff
- Graceful Degradation: Continues processing even if individual operations fail
- Race Condition Protection: Uses category inheritance to avoid API timing issues
qBittorrent Event → torrentProcessor.js → handleAddedTorrent.js / handleFinishedTorrent.js → qBittorrent API
Pros: Direct, simple integration Cons: Unreliable execution in qBittorrent v5.1.2+, race conditions, limited error recovery
qBittorrent Event → monitor-torrents.js (polling) → handle* functions → qBittorrent API
Pros: Reliable, immediate response, continuous operation, comprehensive error handling Cons: Requires systemd service setup, additional file I/O
torrentProcessor.js: Main entry point for traditional mode, parses qBittorrent arguments and routes to appropriate handlershandleAddedTorrent.js: Categorizes newly added torrents based on origin detectionhandleFinishedTorrent.js: Main processing logic for completed downloads including file movement, cleanup, and torrent removalsharedFunctions.js:getTorrentOrigin(): Determines torrent source from metadata/trackerlogMessage(): Unified logging systemexecPromise(): Secure shell command execution with increased buffer limitsgetConfig(): Lazy-loaded configuration management
-- monitor-torrents.js: Continuous monitoring service using periodic polling
- Polls qBittorrent API for completed torrents every 5 minutes
- Fallback polling for completed torrents every 5 minutes
- Duplicate torrent hash tracking
- Systemd/journald logging integration
--
trigger-monitor.js(removed): Lightweight trigger script that was used to create JSON trigger files — no longer required by the monitor qbittorrent-monitor.service: Systemd service file for automatic service management- Automatic startup on boot
- Restart on failure (10s delay)
- Resource limits (512MB memory, 50% CPU)
- Secure paths and permissions
config.js: Centralized configuration with environment variable support and validation- qBittorrent API settings (host, port, auth)
- File paths (destinations, temporary directories)
- Processing options (retries, timeouts, file limits)
- Logging configuration (files, levels, rotation)
- OneJav fetcher settings
validate-config.js: Comprehensive setup validation tool- Tests qBittorrent API connectivity
- Validates file paths and permissions
- Tests module imports and logging
- Provides configuration summary
All configuration is managed through environment variables with sensible defaults:
QB_HOST=localhost # qBittorrent host
QB_PORT=12389 # Web UI port (your setup)
QB_PROTOCOL=http # HTTP/HTTPS
QB_USERNAME=admin # Web UI username (if auth enabled)
QB_PASSWORD=yourpassword # Web UI passwordEMPORNIUM_FINISHED_PATH="/mnt/ST26/Torrents/Fertig/Porn/EmporniumFinished/"
ONEJAV_FINISHED_PATH="/mnt/ST26/Torrents/Fertig/Porn/OnejavFinished/"
TEMP_DOWNLOAD_PATH="/mnt/Torrent/Torrents/Fertig"MAX_RETRIES=3 # API retry attempts
API_TIMEOUT=30000 # API timeout in milliseconds
MAX_FILE_SIZE=10737418240 # Maximum file size (10GB)
CONCURRENT_OPS=5 # Maximum concurrent operationsLOG_FILE="./log.log" # Main log file
ERROR_LOG_FILE="./error.log" # Error log file
LOG_LEVEL="info" # debug, info, warn, error
MAX_LOG_SIZE=1048576 # 1MB max log sizeThe system automatically protects these directories from cleanup:
/mnt/Torrent/Torrents/Fertig/Porn/EmporniumFinished//mnt/Torrent/Torrents/Fertig/Porn/OnejavFinished//mnt/Torrent/Torrents/Fertig/Porn/
- Empornium Destination:
/mnt/ST26/Torrents/Fertig/Porn/EmporniumFinished/ - OneJav Destination:
/mnt/ST26/Torrents/Fertig/Porn/OnejavFinished/ - Temporary Downloads:
/mnt/Torrent/Torrents/Fertig - Logs:
./log.logand./error.log - Trigger Files: Removed — the monitor does not use trigger files anymore; it relies on periodic polling
- Deno: Version 1.40+ (install via
curl -fsSL https://deno.land/install.sh | sh) - qBittorrent: Version 5.1.2+ with Web UI enabled (port 12389 in your setup)
- System Access: Root access for systemd service installation
- File System: Write access to destination paths and log directories
git clone <repository-url>
cd qbittorrent_downloadhandler
chmod +x *.js # Make scripts executable# Test API connectivity and paths
deno run --allow-net --allow-read --allow-write --allow-env validate-config.jsExpected output:
🔍 Validating configuration...
✅ qBittorrent API connected (http://localhost:12389) - Version: 5.1.2
✅ Path exists: /mnt/ST26/Torrents/Fertig/Porn/EmporniumFinished/
✅ Path exists: /mnt/ST26/Torrents/Fertig/Porn/OnejavFinished/
✅ Path exists: /mnt/Torrent/Torrents/Fertig
✅ 19 video extensions configured
✅ 19 image extensions configured
✅ Configuration validation complete
# Copy systemd service
sudo cp qbittorrent-monitor.service /etc/systemd/system/
# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable qbittorrent-monitor.service
sudo systemctl start qbittorrent-monitor.service
# Verify service is running
sudo systemctl status qbittorrent-monitor.serviceService configuration (qbittorrent-monitor.service):
[Unit]
Description=qBittorrent Torrent Monitor Service
After=network.target
[Service]
Type=simple
User=franz
Group=franz
WorkingDirectory=/mnt/MG09/JD/IT/ownSoftware/qbittorrent_downloadhandler
ExecStart=/home/franz/.deno/bin/deno run --allow-net --allow-read --allow-write --allow-run --allow-env monitor-torrents.js
Restart=always
RestartSec=10
Environment=QB_PORT=12389
# Security
NoNewPrivileges=yes
ProtectHome=read-only
ProtectSystem=strict
ReadWritePaths=/mnt/MG09/JD/IT/ownSoftware/qbittorrent_downloadhandler /mnt/ST26/Torrents/Fertig/Porn /mnt/Torrent/Torrents/Fertig
# Resources
MemoryLimit=512M
CPUQuota=50%
StandardOutput=journal
StandardError=journal
SyslogIdentifier=qbittorrent-monitorMonitor Mode (Recommended):
No external program configuration is required — monitor-torrents.js runs as a systemd service and polls the qBittorrent API for completed torrents. If you prefer immediate processing on event, continue to use the Traditional Mode with torrentProcessor.js instead.
Traditional Mode (Fallback):
- On torrent added:
/home/franz/.deno/bin/deno run --allow-all /mnt/MG09/JD/IT/ownSoftware/qbittorrent_downloadhandler/torrentProcessor.js "%N" "%L" "%G" "%F" "%R" "%D" "%C" "%Z" "%T" "%I" "%J" "%K" added - On torrent finished:
/home/franz/.deno/bin/deno run --allow-all /mnt/MG09/JD/IT/ownSoftware/qbittorrent_downloadhandler/torrentProcessor.js "%N" "%L" "%G" "%F" "%R" "%D" "%C" "%Z" "%T" "%I" "%J" "%K" finished
# Start monitor service (polling)
sudo systemctl start qbittorrent-monitor.service
# Test traditional processor
deno run --allow-all torrentProcessor.js "TestTorrent" "onejav" "" "/tmp/testpath" "/tmp/root" "/tmp/save" "1" "1048576" "http://onejav.com" "testhash" "" "testid" "added"
# View logs
tail -f log.log# Check service logs
sudo journalctl -u qbittorrent-monitor.service -f
# Expected startup output:
# 🎬 Starting qBittorrent torrent monitor...
# 📊 Regular polling every 5 minutes
# ✅ Polling timer activeqBittorrent passes these parameters to external programs:
| Parameter | Variable | Description |
|---|---|---|
%N |
torrentName |
Name of the torrent |
%L |
category |
Current torrent category |
%G |
tags |
Torrent tags |
%F |
contentPath |
Path to torrent content |
%R |
rootPath |
Root path of torrent |
%D |
savePath |
Save path configured in qBittorrent |
%C |
numberOfFiles |
Number of files in torrent |
%Z |
torrentSize |
Total size in bytes |
%T |
currentTracker |
Current tracker URL |
%I |
v1hash |
Info hash v1 |
%J |
v2hash |
Info hash v2 |
%K |
torrentId |
Internal torrent ID |
%S |
programStatus |
Event type: added, finished |
The handler interacts with qBittorrent's Web API (version 2):
- Authentication: Automatic login on first API call
- Version Check:
GET /api/v2/app/version - Torrent Info:
GET /api/v2/torrents/info?hashes={hash} - Set Category:
POST /api/v2/torrents/setCategory - Get Files:
GET /api/v2/torrents/files?hash={hash} - Delete Torrent:
POST /api/v2/torrents/delete?hashes={hash}&deleteFiles={true/false}
The system uses these categories:
- empornium: For torrents from Empornium tracker
- onejav: For torrents from OneJav/Sukebei trackers
- uncategorized: Default when origin detection fails
1. Check if file is already in destination
2. If not: rename to target path (or copy+remove if cross-filesystem)
3. Remove empty parent directory (if not protected)
4. Remove torrent from qBittorrent
1. Count video files via qBittorrent API
2. Remove non-video files from content directory
3. Determine target: flat directory or subdirectory (if ≥10 videos)
4. Move all remaining files to target directory
5. Create subdirectory if needed (sanitized torrent name)
6. Remove empty source directory (if not protected)
7. Remove torrent from qBittorrent
Directories matching these patterns are never deleted:
/mnt/Torrent/Torrents/Fertig/Porn/EmporniumFinished//mnt/Torrent/Torrents/Fertig/Porn/OnejavFinished//mnt/Torrent/Torrents/Fertig/Porn/
Before deletion, the system verifies directories are empty to prevent accidental data loss.
Symptoms: systemctl status qbittorrent-monitor.service shows errors
Solutions:
# Check service logs
sudo journalctl -u qbittorrent-monitor.service -n 50
# Verify Deno path
which deno
ls -la /home/franz/.deno/bin/deno
# Check working directory permissions
ls -la /mnt/MG09/JD/IT/ownSoftware/qbittorrent_downloadhandler/
# Test manual execution
cd /mnt/MG09/JD/IT/ownSoftware/qbittorrent_downloadhandler
/home/franz/.deno/bin/deno run --allow-net --allow-read --allow-write --allow-run --allow-env monitor-torrents.jsService File Issues:
- Update
ExecStartwith correct Deno path - Verify
WorkingDirectoryexists and is accessible - Check
ReadWritePathsmatch your mount points - Ensure
User/Groupexist and have permissions
Symptoms: Log shows "Failed to get torrent info" or validation fails Solutions:
# Test API connectivity
curl "http://localhost:12389/api/v2/app/version"
# Check qBittorrent Web UI settings
# Tools → Options → Web UI → Enable Web UI (port 12389)
# Verify environment variables
echo $QB_PORT # Should be 12389
echo $QB_HOST # Should be localhost
# Test with authentication (if enabled)
curl -u admin:yourpassword "http://localhost:12389/api/v2/auth/login"Symptoms: Torrents complete but files remain in download directory Solutions:
# Check log for specific errors
grep "Error moving files" log.log
grep "cross-device" log.log
# Verify destination paths exist and are writable
ls -la /mnt/ST26/Torrents/Fertig/Porn/EmporniumFinished/
touch /mnt/ST26/Torrents/Fertig/Porn/EmporniumFinished/test.txt
rm /mnt/ST26/Torrents/Fertig/Porn/EmporniumFinished/test.txt
# Test cross-filesystem move manually
ls -la /tmp/testfile
mv /tmp/testfile /mnt/ST26/Torrents/Fertig/Porn/testfile 2>&1
# Check if qBittorrent auto-move is enabled
# Tools → Options → Downloads → Keep incomplete torrents in and move completed toSymptoms: Torrents show as "uncategorized" or wrong category Solutions:
# Check origin detection logs
grep "Determined origin" log.log
# Test manual categorization
curl -X POST "http://localhost:12389/api/v2/torrents/setCategory" \
-d "hashes=testid123&category=onejav"
# Verify torrent comments contain expected keywords
curl "http://localhost:12389/api/v2/torrents/info?hashes=testid123" | jq '.[0].comment'
# Check tracker URLs
curl "http://localhost:12389/api/v2/torrents/info?hashes=testid123" | jq '.[0].trackers[] | .url'Symptoms: Images, text files, or metadata remain after processing Solutions:
# Check video extension matching
grep -A 10 "videoExtensions" config.js
# Test file detection manually
deno run --allow-read test-file-detection.js image.jpg # Should return false for non-videos
# Verify cleanup logs
grep "Removed non-video file" log.log
# Check protected directories
grep "Skipping deletion of protected directory" log.logSymptoms: Same torrent processed multiple times or some torrents skipped Solutions:
# Check processed torrent tracking (monitor mode)
grep "Already processed" log.log
# Verify monitor operation
# Check that the monitor performs regular polling and processes torrents as expected
grep "Regular polling check" log.log
grep "Found.*completed torrent" log.log
# Check fallback polling
grep "Regular polling check" log.log
# Monitor service for missed events
sudo journalctl -u qbittorrent-monitor.service | grep "Found.*completed torrent"# Monitor main application log
tail -f log.log
# Monitor service log
sudo journalctl -u qbittorrent-monitor.service -f
# Monitor both simultaneously
multitail log.log -cS qbittorrent '(sudo journalctl -u qbittorrent-monitor.service -f)'# Find recent errors
grep "ERROR\|Failed\|Exception" log.log | tail -20
# Cross-reference with service logs
sudo journalctl -u qbittorrent-monitor.service --since "1 hour ago" | grep -i error
# Find specific torrent issues
grep "torrentId: abc123" log.log# Check processing times
grep "Processed torrent" log.log | awk '{print $1, $NF}' | sort | uniq -c
# Monitor API call success rate
grep "curl.*api/v2" log.log | wc -l
grep "Failed to.*API" log.log | wc -l
# File operation statistics
grep "Moved\|Copied\|Removed" log.log | sort | uniq -cexport LOG_LEVEL=debug
# Restart monitor service
sudo systemctl restart qbittorrent-monitor.serviceAdd to sharedFunctions.js for detailed API responses:
// In execPromise function, before return
if (command.includes('/api/v2/')) {
console.log(`API Response [${command}]:`, stdout.substring(0, 1000));
}Test file operations manually:
# Test single file move
deno run --allow-read --allow-write single-file-test.js /tmp/source.mp4 /mnt/ST26/Torrents/Fertig/Porn/EmporniumFinished/
# Test multi-file cleanup
deno run --allow-read --allow-write multi-file-test.js /tmp/test-torrent/ /mnt/ST26/Torrents/Fertig/Porn/OnejavFinished/- Memory: Monitor service limited to 512MB
- CPU: 50% quota prevents system impact
- Disk I/O: Minimal during normal operation, spikes during file processing
- Network: Only qBittorrent API calls (local), no external dependencies during processing
- Concurrent Operations: Limited to 5 simultaneous file operations
- Large Torrents: Handles torrents up to 10GB by default (configurable)
- High Volume: Processes multiple torrents sequentially, suitable for 50-100 torrents/day
- Use SSDs for temporary download paths to speed up file operations
- Same Filesystem for source and destination when possible (avoids copy+remove)
- Pre-create Directories to avoid permission issues during processing
- Monitor Logs Regularly to catch issues early
- Regular Backups of log files for troubleshooting
- Web UI Access: Restrict to localhost only (
127.0.0.1:12389) - Strong Authentication: Use complex username/password for Web UI
- API Permissions: Handler only needs read/write torrent operations
- Network Isolation: No external network access required for core functions
- Path Validation: All paths validated against protected directories
- Sanitization: Torrent names sanitized for subdirectory creation
- Permission Checks: Verify write access before file operations
- Error Handling: Graceful failures prevent system crashes
- Systemd Hardening:
NoNewPrivileges=yes: Prevents privilege escalationProtectHome=read-only: Prevents home directory accessProtectSystem=strict: Prevents system file modificationReadWritePaths: Explicitly limits filesystem access
- User Isolation: Runs as non-root user (
franz) - Resource Limits: Prevents resource exhaustion attacks
- Sensitive Data: API responses filtered to remove passwords
- File Permissions: Logs readable only by service user
- Rotation: Configurable log rotation prevents disk exhaustion
- Audit Trail: Comprehensive logging for troubleshooting and compliance
The handler works seamlessly with the OneJav Torrent Fetcher:
- Automated Fetching:
fetchOnejav.jsdownloads torrents to./torrents/ - Automatic Processing: qBittorrent loads torrents and handler categorizes them
- Origin Detection: OneJav torrents automatically detected and organized
Usage workflow:
# 1. Fetch new torrents (cron job)
0 8 * * * cd /path/to/project && deno run --allow-net --allow-write fetchOnejav.js >> fetch.log 2>&1
# 2. qBittorrent loads torrents from watch directory
# 3. Handler automatically processes and organizesIntegrate with log rotation tools:
# /etc/logrotate.d/qbittorrent-handler
/mnt/MG09/JD/IT/ownSoftware/qbittorrent_downloadhandler/log.log {
daily
rotate 7
compress
missingok
notifempty
create 644 franz franz
postrotate
systemctl reload qbittorrent-monitor.service
endscript
}Add to monitoring systems:
# Check service health
systemctl is-active --quiet qbittorrent-monitor.service && echo "OK" || echo "CRITICAL"
# Check recent processing
[ $(grep "Successfully processed" log.log | wc -l) -gt 0 ] && echo "OK" || echo "WARNING"
# Alert on errors
grep "Failed to process\|CRITICAL" log.log | tail -1 | grep -q . && echo "ALERT: Processing errors detected"src/
├── config/ # Configuration management
│ └── config.js
├── core/ # Main processing logic
│ ├── handleAddedTorrent.js
│ ├── handleFinishedTorrent.js
│ └── torrentProcessor.js
├── monitor/ # Monitoring system
│ ├── monitor-torrents.js
│ └── (trigger-monitor.js removed)
├── utils/ # Shared utilities
│ └── sharedFunctions.js
└── tools/ # Maintenance tools
└── validate-config.js
- Unit Tests: Test individual functions (planned enhancement)
- Integration Tests: Validate end-to-end workflows
- Configuration Tests:
validate-config.jsfor setup verification - Manual Testing: Use
test_args.txtfor command-line testing
- Configuration Health Checks: Integrate validation into systemd service
- Automatic Log Rotation: Implement file size/date-based rotation
- Additional Sources: Extensible origin detection framework
- Test Suite: Comprehensive Deno test framework integration
- Web Dashboard: Monitoring interface for status and configuration
- v1.0: Initial external program implementation
- v1.1: Cross-filesystem moves, error handling improvements
- v1.2: Race condition fixes, buffer limit increases
- v2.0: Monitor system with file watching and systemd integration (current)
# Test with Empornium-like comment
echo '{"comment": "Empornium - Movie Title"}' | \
curl -X POST "http://localhost:12389/api/v2/torrents/info?hashes=test123" -d @- | \
jq '.comment'
# Expected: handler detects "empornium" and sets categoryCreate test directory:
mkdir -p /tmp/test-torrent/{videos,images}
touch /tmp/test-torrent/videos/movie.mp4
touch /tmp/test-torrent/images/poster.jpg
touch /tmp/test-torrent/info.nfo
# Test processing
deno run --allow-all handleFinishedTorrent.js test123 "Test Movie" "/tmp/test-torrent" "onejav" ""
# Verify: only movie.mp4 moved, others removed
ls /mnt/ST26/Torrents/Fertig/Porn/OnejavFinished/# Ensure monitor service is running
sudo systemctl start qbittorrent-monitor.service
# Create a sample torrent using the traditional processor to simulate an event (or use the UI)
deno run --allow-all torrentProcessor.js "TestTrigger" "onejav" "" "/tmp/completed" "/tmp/root" "/tmp/save" "5" "5242880" "udp://tracker.onejav.com" "triggerhash" "" "triggerid" "finished"
# Verify monitor processes it via logs
sudo journalctl -u qbittorrent-monitor.service -f- Check Logs: Start with
tail -f log.logand service logs - Validation: Run
validate-config.jsfor setup issues - Documentation: Refer to this file and main README
- Issues: Check todo.md for known limitations
- Bug Reports: Include log excerpts and reproduction steps
- Features: Open issues for discussion before implementation
- Code: Follow Deno style guidelines, add tests
- Documentation: Keep docs updated with code changes
- qBittorrent v5.1.2: External program feature unreliable (use monitor mode)
- Cross-Filesystem: Copy+remove slower than rename (inherent limitation)
- Large Torrents: API responses limited to 10MB (configurable)
- Single Origin: Currently supports only Empornium/OneJav (extensible)
Current Status: Production-ready with monitor system. Handles 100+ torrents daily reliably.
Next Steps: See todo.md for planned enhancements including log rotation, additional sources, and comprehensive testing.
Created: October 2025
Updated: Based on current codebase analysis