I built this app specifically for building up ratio quickly on new private torrent trackers using freeleech torrents but it can be used for downloading anything via RSS.
Automated RSS‑to‑qBittorrent downloader with three-rule logic, containerised in a lightweight multi-stage distroless Docker image (about 21 MB) for improved security (Docker hub repo)
Disclaimer: this tool was 100% vibe-coded, then hardened for production. It only makes outbound calls (RSS + qBittorrent WebAPI) and does not require any open inbound ports.
-
Polls a torrent RSS feed at configurable intervals.
-
Use with a freeleech RSS torrent feed to build your ratio on a new private tracker!
-
Applies three rules before downloading:
- Skip if the torrent GUID was already processed.
- Skip if the torrent is older than 10 minutes for maximum freshness and opportunity to seed.
- Skip if a download occurred too recently (cooldown derived from torrent size ÷ configured download speed, fallback 2 h).
The cooldown enforces one torrent at a time so the available bandwidth goes to seeding what you just grabbed, improving ratio performance.
-
Optional Telegram notification when a torrent is added.
-
Downloads new torrents via the qBittorrent WebAPI with custom parameters (save path, category, tags, share ratio, seeding time).
-
Logs actions and reasons for skips with clear text markers.
-
Persists state in a JSON file (last GUID and timestamp).
-
Configurable entirely via environment variables or a
.envfile. -
Containerised using a minimal multi-stage distroless Docker image (~21 MB) for a smaller attack surface.
flowchart LR
loop[Polling loop every INTERVAL_MINUTES]
rss[(RSS feed)]
entry[Newest feed entry]
rules{All 3 rules pass?}
api[qBittorrent WebAPI]
daemon[qBittorrent daemon]
state[(state.json<br/>last_guid<br/>last_dl_ts<br/>cooldown_until)]
tg[Telegram bot]
loop --> rss
rss --> entry
entry --> rules
rules -- no --> loop
rules -- yes --> api
api --> daemon
api --> state
api --> tg
loop --> state
- The script runs in a loop (every
INTERVAL_MINUTES). - It loads
state.jsonto check last download GUID and cooldown. - It fetches the RSS feed, picks the newest entry, and applies the three rules (duplicate, freshness, cooldown).
- If all pass, it calls the qBittorrent WebAPI to add the torrent with configured options.
- It updates
state.jsonand logs the actions.
- Python 3.11+
feedparser(RSS parsing)requests(HTTP client)python-dotenv(optional, for.envloading)
On Docker, everything is packaged—no host Python needed; the image ships with Python 3.11 distroless.
Copy the provided .env.example to .env and fill in your values:
# qBittorrent API endpoint
QB_URL=http://localhost:8080
# qBittorrent WebUI credentials
QB_USER=user
QB_PASS=password
# Your torrent RSS feed
RSS_URL=https://url.com
# Script options
INTERVAL_MINUTES=5
LOG_FILE=./logs/ratioking.log
# STATE_FILE=./ratioking.state.json # optional custom path
DOWNLOAD_SPEED_MBPS=10 # Size / speed = cooldown duration
TELEGRAM_BOT_TOKEN= # Optional: send alerts via Telegram
TELEGRAM_CHAT_ID= # Optional: recipient chat ID
# HTTP_TIMEOUT=20 # Seconds; applies to RSS and torrent fetches
# MAX_TORRENT_BYTES=5242880 # Safety cap for torrent file prefetch
# USER_AGENT=ratioking/1.0 # Override if your feed requires it
# Download parameters
SAVE_PATH=/mnt/path/
CATEGORY=category
TAGS=tags
RATIO_LIMIT=-1 # -1 = unlimited ratio
SEEDING_TIME_LIMIT=-1 # Minutes; -1 = no time limitEach variable has a sensible default if not set.
-
Clone the repository:
git clone https://.../ratioking.git cd ratioking -
Create a Python virtual environment (optional for local runs):
python3 -m venv .venv source .venv/bin/activate -
Generate
requirements.txtautomatically:pip install feedparser requests python-dotenv pip freeze --local > requirements.txt -
Copy configuration:
cp .env.example .env # Edit .env with your values
# Activate venv if used
source .venv/bin/activate
# Install deps
pip install -r requirements.txt
# Run the downloader
python ratioking.pyLogs appear on stdout and in LOG_FILE. Press Ctrl+C to stop.
docker build -t ratioking:latest .Use the provided docker-compose.yml (matching the distroless image and bind mounts, Docker hub repo):
services:
ratioking:
image: battermanz/ratioking:latest
env_file:
- .env
environment:
TZ: Europe/Amsterdam
STATE_FILE: /app/data/ratioking.state.json
# Optional: run as host UID/GID to avoid bind-mount permission issues
# PUID: "1000"
# PGID: "1000"
volumes:
- ./logs:/app/logs
- ./data:/app/data # persists cooldown/last GUID across restarts
restart: unless-stoppedLaunch:
docker-compose up -d-
Logs: stdout (captured by Docker logs) and file (
LOG_FILE). -
State: JSON in
STATE_FILE:{ "last_guid": "<torrent GUID>", "last_dl_ts": 168XYZ } -
Cooldown: derived from torrent size ÷
DOWNLOAD_SPEED_MBPS(fallback 2 h) to ensure only one torrent downloads at a time and the link is freed quickly for seeding.
-
Optional; disabled unless both
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_IDare set. -
Configure in
.envordocker-compose.yml:TELEGRAM_BOT_TOKEN=123456:ABC... TELEGRAM_CHAT_ID=123456789
-
Bot setup: create a bot with BotFather, copy the token, and use your chat ID (or the ID of the group the bot is in).
-
The app sends an HTML-formatted message when a torrent is added. Example:
📥 Added torrent Natsumes Book of Friends S02 1080p CR WEB-DL Dual-Audio AAC 2 0 x264-Fool Size: 1.20 GB Cooldown: 45.0 min Ends: 2025-12-09 16:45:36
If credentials are missing or Telegram rejects the call, the downloader continues without notifications.
- Distroless runtime (Python 3.11) for a minimal attack surface; no shell or package manager in the final image.
- Non-root container by default (configurable via
PUID/PGIDin compose) to align file permissions with host binds. - Outbound-only design: talks to RSS and qBittorrent WebAPI; no inbound ports exposed by this service.
- TLS-capable HTTP client with configurable
HTTP_TIMEOUTandMAX_TORRENT_BYTESto avoid hanging or oversized torrent fetches. - Torrent prefetch size cap (default 5 MB) and required http/https schemes for torrent URLs to avoid unsafe protocols.
- Atomic state writes and resilient state loading to reduce corruption risk.
- Optional Telegram notifications use HTTPS; they are skipped if not configured.
- Change timing constants in code for different intervals.
- Tweak download options via
.envwithout code changes. - Use a different RSS feed by updating
RSS_URL.
- Skipping too much? Check logs for which rule is firing (duplicate, freshness, cooldown).
- API errors? Validate credentials and test
curlagainstQB_URL. - Feed issues? Run
rss_debug.pyto inspect feed structure. - Docker build fails? Ensure
requirements.txtis up to date.
Enjoy your automated downloads.
GPL-3.0-or-later. See LICENSE.