A webhook service that "grooms" rough releases into presentable ones — automatically renaming torrents in qBittorrent when Sonarr or Radarr grabs a release.
Fixes infinite download loops (Couldn't add release X from Indexer Y to download queue.) in Sonarr and Radarr by renaming torrents to match their original release titles, ensuring that metadata (Custom Formats, Release Groups) lost during filename parsing is preserved.
- Receives webhooks from Sonarr/Radarr on Grab events
- Renames torrents, folders, and files in qBittorrent
- Configurable trigger filters (indexer, quality, custom formats, etc.)
- Customizable rename rules (regex patterns, prefix/suffix)
- Multiple rename modes (torrent only, folder, files)
- Handles timing issues with automatic retry/polling
- Score validation: Optionally validates renames against Sonarr/Radarr API to ensure custom format scores aren't negatively impacted
# Copy example config
cp config/rename_rules.yaml.example config/rename_rules.yaml
# Edit config (optional - defaults work out of the box)
nano config/rename_rules.yaml# Edit docker-compose.yml with your qBittorrent credentials
docker-compose up -dIn Radarr:
- Go to Settings → Connect → + → Webhook
- Name:
Groomarr - On Grab: ✓ (enable)
- URL:
http://groomarr:8000/webhook/radarr - Method: POST
- Click Test, then Save
In Sonarr:
- Go to Settings → Connect → + → Webhook
- Name:
Groomarr - On Grab: ✓ (enable)
- URL:
http://groomarr:8000/webhook/sonarr - Method: POST
- Click Test, then Save
| Variable | Default | Description |
|---|---|---|
QBITTORRENT_URL |
http://qbittorrent:8080 |
qBittorrent Web UI URL |
QBITTORRENT_USERNAME |
admin |
qBittorrent username |
QBITTORRENT_PASSWORD |
adminadmin |
qBittorrent password |
API_PORT |
8000 |
Port for webhook API |
LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR) |
LOG_FORMAT |
text |
Log format: text or json (for log aggregation) |
RENAME_MODE |
torrent_and_folder |
What to rename (see below) |
DRY_RUN |
false |
If true, logs what would be renamed without making changes |
INITIAL_DELAY |
2 |
Seconds to wait before first torrent lookup |
MAX_RETRIES |
10 |
Max attempts to find torrent |
RETRY_DELAY |
3 |
Base seconds between retries (uses exponential backoff) |
SONARR_URL |
null |
Sonarr API URL (for score validation) |
SONARR_API_KEY |
null |
Sonarr API key (Settings → General) |
RADARR_URL |
null |
Radarr API URL (for score validation) |
RADARR_API_KEY |
null |
Radarr API key (Settings → General) |
| Mode | Description |
|---|---|
torrent_only |
Only rename torrent display name in qBittorrent UI |
torrent_and_folder |
Rename torrent name + root folder (default) |
torrent_folder_files |
Rename torrent + folder + all files |
folder_only |
Only rename root folder |
files_only |
Only rename files |
Create config/rename_rules.yaml to customize behavior. The configuration supports two formats:
- Legacy flat format: All rules at root level (backward compatible)
- Hierarchical format:
globalsection +trackerslist for per-indexer rules
When a webhook is received:
- Check if the indexer matches any tracker in the
trackerslist (first match wins) - If matched → use that tracker's rules exclusively (global rules are NOT applied)
- If no match → use global rules
# Global rules - used when no tracker-specific config matches
global:
# Trigger filters
indexers_exclude:
- ".*Public.*"
qualities_exclude:
- "CAM"
- "TS"
# Rename rules
prefix: ""
suffix: ""
# Score validation
validate_custom_format_score: false
score_validation_policy: "block"
# Tracker-specific rules - first matching tracker wins
trackers:
- name: "my-private-tracker"
match:
- "MyPrivateTracker (API)" # Exact match (case-insensitive)
- "MyPrivateTracker*" # Wildcard match
rules:
qualities_include:
- "Bluray.*"
- "Remux"
prefix: "[MPT] "
validate_custom_format_score: true
- name: "anime-tracker"
match:
- "Nyaa*"
- "AnimeBytes*"
- "/.*anime.*/i" # Regex match (wrapped in slashes)
rules:
release_groups_include:
- "SubsPlease"
- "Erai-raws"
remove_patterns:
- "\\[.*?\\]" # Remove [tags] common in anime
suffix: " [Anime]"| Pattern | Example | Matches |
|---|---|---|
| Exact string | "TrackerName" |
Case-insensitive exact match |
| Wildcard | "Tracker*", "*Cinema*" |
Shell-style glob (* = any chars, ? = single char) |
| Regex | "/Tracker.*API/" |
Regular expression (wrapped in slashes) |
For simpler setups without tracker-specific rules:
# All rules at root level (no 'global:' or 'trackers:' sections)
indexers_include:
- "TrackerA.*"
- "IndexerB"
indexers_exclude:
- ".*Public.*"
qualities_exclude:
- "CAM"
- "TS"
prefix: ""
suffix: ""
validate_custom_format_score: false
score_validation_policy: "block"| Filter | Description |
|---|---|
indexers_include |
Only process these indexers (regex) |
indexers_exclude |
Skip these indexers |
qualities_include |
Only process these qualities |
qualities_exclude |
Skip these qualities |
customformats_require_any |
Require any of these custom formats |
customformats_exclude |
Skip if any of these present |
min_customformat_score |
Minimum score threshold (null = disabled) |
download_clients_include |
Only process from these clients |
download_clients_exclude |
Skip these download clients |
release_groups_include |
Only process these release groups |
release_groups_exclude |
Skip these release groups |
| Rule | Description |
|---|---|
prefix |
Add prefix to renamed titles |
suffix |
Add suffix to renamed titles |
remove_patterns |
Regex patterns to remove from title |
replace_patterns |
Pattern → replacement mapping |
skip_title_patterns |
Skip renaming if title matches these |
Score validation is an optional feature that uses the Sonarr/Radarr API to compare custom format scores before and after renaming. This helps ensure that your rename rules don't accidentally remove information that Sonarr/Radarr uses for matching.
How it works:
- When a rename is triggered, Groomarr calls the
/api/v3/parseendpoint with both the original and new name - It compares the
customFormatScorevalues returned by the API - Based on the policy, it either blocks or warns if the new name would have a lower score
Configuration:
- Set the environment variables for Sonarr/Radarr API access
- Enable in
rename_rules.yaml:
validate_custom_format_score: true
score_validation_policy: "block" # or "warn"Policies:
block(default): Skip the rename if the score would decreasewarn: Log a warning but proceed with the rename anyway
Example log output:
# Score validation passed
[radarr] Score validation: 'Original.Name' (11200) -> 'New.Name' (11200), change=0, safe=True
# Score decrease blocked
[radarr] Skipping rename: score would decrease from 11200 to 8500 (-2700)
# API unreachable
[radarr] Skipping rename: Radarr API unreachable at http://radarr:7878
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check |
/webhook/radarr |
POST | Radarr webhook receiver |
/webhook/sonarr |
POST | Sonarr webhook receiver |
/rename/manual |
POST | Manually rename a torrent by hash |
/rename/preview |
POST | Preview what a rename operation would do without making changes |
/reload |
GET | Reload rename rules |
/docs |
GET | Swagger API documentation |
The /rename/manual endpoint allows direct renaming of torrents without a webhook event:
curl -X POST http://localhost:8000/rename/manual \
-H "Content-Type: application/json" \
-d '{
"torrent_hash": "AF35BC0E03A9D8405779A69FC9A438F1BFE90C5F",
"new_name": "Movie.2024.1080p.BluRay.x264-GROUP",
"mode": "torrent_and_folder"
}'Parameters:
torrent_hash(required): The torrent info hashnew_name(required): New name to applymode(optional): Rename mode (default:torrent_and_folder)
The /rename/preview endpoint shows exactly what would happen if you performed a rename operation without actually making any changes. This is useful for testing rename rules or verifying behavior before committing to a rename.
curl -X POST http://localhost:8000/rename/preview \
-H "Content-Type: application/json" \
-d '{
"torrent_hash": "AF35BC0E03A9D8405779A69FC9A438F1BFE90C5F",
"new_name": "Movie.2024.1080p.BluRay.x264-GROUP",
"mode": "torrent_folder_files"
}'Parameters:
torrent_hash(required): The torrent info hashnew_name(required): New name to previewmode(optional): Rename mode to preview (default:torrent_and_folder)
Response includes:
- Current state: torrent name, root folder, total files
- Proposed changes: new torrent name, new folder name, list of file renames
- Change indicators: which items will actually change
- Warnings: any issues detected (e.g., conflicts, missing folders)
This endpoint is read-only and never modifies your torrents, making it safe to use for testing and validation.
services:
groomarr:
image: maksii/groomarr:latest
container_name: groomarr
environment:
- QBITTORRENT_URL=http://qbittorrent:8080
- QBITTORRENT_USERNAME=admin
- QBITTORRENT_PASSWORD=your_password
- RENAME_MODE=torrent_and_folder
# Optional: Enable score validation (requires Arr API access)
# - SONARR_URL=http://sonarr:8989
# - SONARR_API_KEY=your-sonarr-api-key
# - RADARR_URL=http://radarr:7878
# - RADARR_API_KEY=your-radarr-api-key
volumes:
- ./config:/config
ports:
- "8000:8000"
restart: unless-stopped
networks:
- media # Same network as qBittorrent, Sonarr, Radarr
networks:
media:
external: true- Sonarr/Radarr grabs a release and sends webhook to this service
- Service validates the webhook and applies trigger filters
- If filters pass, a background task is queued
- Background task waits for torrent to appear in qBittorrent (with retries)
- Rename rules are applied to generate new name
- Torrent/folder/files are renamed based on configured mode
flowchart LR
subgraph arr [Sonarr/Radarr]
A[Grab Release]
end
subgraph groomarr [Groomarr]
B[Receive Webhook]
C{Validate & Filter}
D[Queue Task]
E[Return 200]
end
subgraph background [Background Task]
F[Wait for Torrent]
G[Apply Rename Rules]
H[Rename in qBit]
end
subgraph qbit [qBittorrent]
I[Torrent Renamed]
end
A -->|POST webhook| B
B --> C
C -->|Pass| D
C -->|Fail| E
D --> E
D -.->|async| F
F --> G
G --> H
H --> I
- Increase
MAX_RETRIESorRETRY_DELAY - Check qBittorrent is accessible from the container
- Verify
QBITTORRENT_URLis correct
- Check Sonarr/Radarr can reach the service URL
- Verify firewall/network settings
- Check logs:
docker logs groomarr
- All filter patterns are regex (case-insensitive)
- Use
docker logsto see skip reasons - Test patterns at https://regex101.com
docker logs -f groomarr# Install dependencies
pip install -r requirements.txt
# Run
python -m uvicorn src.main:app --reload --port 8000pip install pytest
pytest tests/ -v- qBittorrent v4.2.1+ (for file renaming support)
- Sonarr v3+ / Radarr v3+
- Docker (recommended)
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Run the tests (
pytest tests/ -v) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT