Repository navigation
Architecture Internals
This page maps the codebase itself — which module does what, and how a job crosses process boundaries from HTTP request to qBittorrent API call. If you're looking for the user-facing condition/action evaluation model instead, see Rules Architecture.
| Module | Responsibility |
|---|---|
cli.py |
Entry point. Parses args and dispatches to either server mode (--serve) or client mode (default) |
arguments.py |
Centralized argparse setup shared by both CLI modes |
server.py |
Flask HTTP API (run under Gunicorn) — job submission, status, health, stats endpoints; API-key auth (see HTTP API Reference); also registers the read-only /dashboard* routes (see Dashboard) and, when enabled, /metrics (see Metrics) |
templates/ |
Jinja2 templates for the /dashboard* routes — server-rendered HTML, no frontend build step |
metrics.py |
Optional Prometheus instrumentation (see Metrics) — always safe to import even when prometheus_client isn't installed, since the actual dependency is only ever imported lazily, deferred until metrics.init() runs |
worker.py |
Background thread that dequeues jobs and executes them via the engine; graceful shutdown handling |
scheduler.py |
Background thread that enqueues a job when a configured cron entry fires — see Scheduling |
queue_manager.py |
Abstract job-queue interface (JobStatus constants, enqueue/dequeue/status contract) |
queue_backends/ |
Concrete implementations of that interface — SQLite (default) and Redis |
engine.py |
RulesEngine (orchestrator), ConditionEvaluator, and ActionExecutor — the actual rule evaluation and action dispatch logic |
resolver.py |
RuleResolver — expands $ref: and substitutes ${vars.x} before a rule reaches the engine |
config.py |
Loads and validates config.yml/rules.yml; environment variable + _FILE secret resolution |
api.py |
Thin wrapper around the qbittorrent-api package — multi-version qBittorrent support |
errors.py |
User-facing exception types with actionable messages (no raw stack traces surfaced to users) |
logging.py |
Shared logging setup used by every other module |
utils.py |
Small stateless helpers: parse_tags, is_older_than/is_newer_than, is_larger_than/is_smaller_than
|
__version__.py |
Single source of truth for the package version — see Release Process |
There's one binary (qbt-rules) with two roles, chosen by how it's invoked:
-
Server mode (
--serve): startsserver.py's Flask app under Gunicorn, plusworker.pyandscheduler.pybackground threads in the same process. This is what runs continuously inside the Docker container. -
Client mode (the default, no
--serve): submits a job to a running server over HTTP and, if given a--client-server-url/--client-api-key, optionally waits for and prints the result (--wait). This is what runs when youdocker exec qbt-rules qbt-rules --context X --wait, or when a script hits the HTTP API withcurldirectly instead.
Nothing in client mode touches qBittorrent directly — it only talks to the server's HTTP API. All qBittorrent access happens inside the server process, via the worker.
When metrics are enabled, metrics.py runs into the exact same Gunicorn preload_app/fork consideration scheduler.py already had to solve (see Scheduling) — a naive in-process counter would be silently forked into server.workers independent, unsynchronized copies. See Metrics for how it's handled.
- A job arrives at
server.py(POST /api/execute) or via the CLI client, which is really just an HTTP request under the hood — orscheduler.pyenqueues one directly on a cron tick, bypassing the HTTP layer entirely (no API key check needed, since it's not going over the network). The API key is checked for every other submission path before anything else happens. - Either way, the job (context, optional hash) reaches
queue_manager.py, which persists it through whichever backend is configured (queue_backends/sqlite_queue.pyorredis_queue.py) with statuspending. -
worker.py's background thread dequeues the job, marks itprocessing, and loadsrules.ymlthroughconfig.py— which also initializesresolver.py'sRuleResolverif arefs:block is present. - For each rule, the resolver expands any
$ref:/${vars.x}before the rule reachesengine.py. The engine'sConditionEvaluatorchecksenabled, then context, then evaluatesconditions:per torrent (fetched viaapi.py); matches are handed toActionExecutor, which calls back intoapi.pyto actually act on qBittorrent. - The worker updates the job's status (
completed/failed) and result summary back throughqueue_manager.py, which is what--job-status/--list-jobs(via the client) orGET /api/jobsread back.
queue_manager.py defines the contract; queue_backends/ provides SQLite and Redis implementations behind it. worker.py and server.py only ever talk to the abstract interface, so switching queue.backend between sqlite and redis in config.yml doesn't touch either of them — only which backend module gets instantiated at startup changes. Adding a third backend (e.g. Postgres) means implementing the same interface in a new module under queue_backends/ and wiring it into that instantiation point; no changes to worker.py/server.py should be needed.
qbt-rules | Client-server automation engine for qBittorrent
GitHub • Docker Image • Releases • Issues • Discussions • Contributing • License
Requirements: Docker • qBittorrent with Web UI enabled
Released into the public domain under the Unlicense
- Rules Architecture
- Contexts
- Conditions
- Actions
- Available Fields
- Reusable References
- HTTP API Reference
- Dashboard
- Metrics
- 🚀 Client-server architecture (HTTP API + worker)
- 📦 Persistent job queue (SQLite or Redis)
- ♻️ Reusable references (
$ref,${vars.x}) - 🐳 Docker-first deployment