Rust companion service for Sonarr root folder classification
6.2K
Rooterr is a small Rust companion service for Sonarr. It listens for Sonarr SeriesAdd webhooks, enriches the series metadata, asks a local LLM to choose the best existing Sonarr root folder, applies the new path with moveFiles=true, and stores the decision and reason in SQLite.
Copy the example config:
cp rooterr.toml.example rooterr.toml
Edit rooterr.toml with your Sonarr URL, Sonarr API key, local LLM endpoint, and optional TMDB/TVDB credentials.
Run locally:
cargo run
Open the UI at http://localhost:9898.
Rooterr creates the SQLite database automatically on startup when it is missing. By default it stores the database at ./data/rooterr.sqlite3; the parent data/ directory is created automatically.
Rooterr loads configuration in this order:
rooterr.toml, or the file pointed to by ROOTERR_CONFIG, when the file exists.ROOTERR_* environment variables.This means Docker deployments can be configured entirely with environment variables and do not need to mount a TOML file. Supported environment variables:
| Variable | TOML setting |
|---|---|
ROOTERR_CONFIG | Path to an optional TOML config file |
ROOTERR_SERVER_BIND_ADDRESS | server.bind_address |
ROOTERR_SONARR_BASE_URL | sonarr.base_url |
ROOTERR_SONARR_API_KEY | sonarr.api_key |
ROOTERR_SONARR_WEBHOOK_TOKEN | sonarr.webhook_token |
ROOTERR_LLM_PROVIDER | llm.provider (ollama or openai_compatible) |
ROOTERR_LLM_BASE_URL | llm.base_url |
ROOTERR_LLM_MODEL | llm.model |
ROOTERR_LLM_API_KEY | llm.api_key |
ROOTERR_LLM_AUTO_PULL | llm.auto_pull |
ROOTERR_LLM_STARTUP_WAIT_SECONDS | llm.startup_wait_seconds |
ROOTERR_LLM_PULL_TIMEOUT_SECONDS | llm.pull_timeout_seconds |
ROOTERR_LLM_THINK | llm.think |
ROOTERR_LLM_AUTO_NUM_CTX | llm.auto_num_ctx |
ROOTERR_LLM_MIN_NUM_CTX | llm.min_num_ctx |
ROOTERR_LLM_MAX_NUM_CTX | llm.max_num_ctx |
ROOTERR_LLM_RESERVED_OUTPUT_TOKENS | llm.reserved_output_tokens |
ROOTERR_LLM_TIMEOUT_SECONDS | llm.timeout_seconds |
ROOTERR_LLM_TEMPERATURE | llm.temperature |
ROOTERR_TMDB_BEARER_TOKEN | metadata.tmdb_bearer_token |
ROOTERR_TVDB_API_KEY | metadata.tvdb_api_key |
ROOTERR_TVDB_PIN | metadata.tvdb_pin |
ROOTERR_DATABASE_SQLITE_PATH | database.sqlite_path |
ROOTERR_CLASSIFICATION_MIN_CONFIDENCE | classification.min_confidence |
ROOTERR_CLASSIFICATION_ROOT_FOLDERS_JSON | classification.root_folders |
Boolean values accept 1, true, yes, on, 0, false, no, and off. Empty optional secret values clear the setting.
Use ROOTERR_CLASSIFICATION_ROOT_FOLDERS_JSON for root-folder hints:
{
"/data/kids": {
"label": "Kids",
"description": "Children's and family-oriented shows."
},
"/data/scripted": {
"label": "Scripted",
"description": "Default scripted drama, comedy, action, sci-fi, and general TV."
}
}
In Sonarr, create a dedicated Webhook connection for Rooterr:
Go to Settings -> Connect -> + -> Webhook.
Set Name to Rooterr.
Under Notification Triggers, enable only On Series Add / Series Add.
Leave unrelated triggers disabled, including grab, download/import, upgrade, rename, delete, health, application update, and manual interaction.
Set Webhook URL:
http://rooterr:9898/webhooks/sonarr
Use that URL when Sonarr and Rooterr run in the same Docker Compose network. For a host or LAN install, use:
http://<rooterr-host>:9898/webhooks/sonarr
Set Method to POST.
Leave Username blank.
Leave Password blank.
Add this header when sonarr.webhook_token is set in rooterr.toml:
X-Rooterr-Token: <sonarr.webhook_token>
X-Rooterr-Token is the recommended authentication method because it keeps the secret out of URLs, logs, browser history, and proxy request lines. Rooterr also accepts this alternative header:
Authorization: Bearer <sonarr.webhook_token>
Use a query-string token only if Sonarr cannot send custom headers:
http://<rooterr-host>:9898/webhooks/sonarr?token=<sonarr.webhook_token>
Do not use Sonarr's Username and Password fields for Rooterr; those configure HTTP Basic authentication, which Rooterr does not use for webhooks. Sonarr already sends JSON, so no custom Content-Type header is needed.
If sonarr.webhook_token is omitted, Rooterr accepts the webhook without authentication. Only do this on a trusted private network. Sonarr's test button may send a Test event; Rooterr accepts authenticated requests but ignores anything that is not SeriesAdd.
Rooterr supports:
POST /api/chatPOST /v1/chat/completionsFor Ollama, Rooterr can automatically pull the configured model before the web server starts:
[llm]
provider = "ollama"
base_url = "http://ollama:11434"
model = "qwen3:0.6b"
auto_pull = true
startup_wait_seconds = 60
pull_timeout_seconds = 900
think = false
auto_num_ctx = true
min_num_ctx = 4096
max_num_ctx = 0
reserved_output_tokens = 512
The first startup can take several minutes and requires the Ollama container to have internet access. startup_wait_seconds lets Rooterr wait for the Ollama service before checking local models, and pull_timeout_seconds controls the download timeout. The model is stored in the Ollama volume, so later restarts should skip the download. If you prefer to manage models manually, leave auto_pull = false and pull the model yourself:
docker exec ollama ollama pull qwen3:0.6b
When auto_num_ctx = true, Rooterr estimates the final classification prompt size and sends an Ollama-only options.num_ctx value rounded up to a stable context bucket. max_num_ctx = 0 lets Rooterr detect the model limit from Ollama; set a positive value to override it.
The model is required to return JSON with:
{
"root_folder_path": "/data/kids",
"confidence": 0.91,
"reason": "Animated children's series with kids genre metadata.",
"signals": ["Animation", "Children"]
}
The selected root_folder_path must exactly match a root folder returned by Sonarr.
Rooterr is a one-crate Rust application using a lightweight hexagonal architecture with screaming names:
src/
domain/ Rooterr-owned concepts such as decisions, series, metadata, and root folders
ports/ Traits for repository, Sonarr, metadata, and classifier boundaries
use_cases/ Product workflows such as accepting a series add, processing a decision, and retrying
adapters/ Axum web, SQLite, Sonarr HTTP, metadata APIs, and local LLM implementations
bootstrap.rs wires concrete adapters into Arc<dyn Trait> ports. The web adapter stays thin: it parses HTTP input, calls use cases, spawns background processing, and renders server-side HTML.
docker compose -f docker-compose.example.yml up --build
The example Compose file configures Rooterr entirely with environment variables and mounts only ./data to /app/data, matching the default database.sqlite_path = "./data/rooterr.sqlite3".
For file-based deployments, keep using rooterr.toml and set ROOTERR_CONFIG to the mounted path:
volumes:
- ./rooterr.toml:/config/rooterr.toml:ro
- ./data:/app/data
environment:
ROOTERR_CONFIG: /config/rooterr.toml
Content type
Image
Digest
sha256:dbf294899…
Size
30.9 MB
Last updated
3 months ago
docker pull gnarr/rooterr