Categories and Routing - Kheopsian/Hydranos GitHub Wiki
Applies to Hydranos 4.4.
This page is for anyone who adds torrents to Hydranos, by hand or from Sonarr, Radarr or autobrr. It explains how Hydranos picks the engine and the folder for a new torrent, how to create and change categories, and what tags do. Read it before you create your first category: there is no default save path, so without a category that has one, most adds are refused.
If unsure, file your categories as Hoard. Race is for racing fresh uploads, and the race drain can delete race torrents with their data (see Engines: Race and Hoard). Terms used here (engine, graduation, race drain, seed obligation) are defined on Concepts and Glossary.
A category is a name with four settings:
| Field | UI label | What it does |
|---|---|---|
mode |
Mode (Race / Hoard) | Picks the engine: hoard → the engine hoard, race → the engine race. Only these two values are accepted (any case, stored in lower case). |
save_path |
Save Path | The folder new torrents in this category are written to. |
graduate_to |
Graduate to (hoard category) | Race categories only: the hoard category the race drain moves a torrent to when it must leave the race disk before its seed obligation is met (a graduation). |
transit |
Transit area | Hoard categories only: if some category names this one in Graduate to, every torrent filed in it is deleted with its files once its tracker's seed obligation is declared and met (see Race categories and graduation below). |
Categories are stored in the store (hydra.db), not in default.toml. Editing the config file never changes them. If the store has no categories yet, Hydranos falls back to reading <data_dir>/categories.json, but it never writes that file: the first edit puts the whole list into the store.
Names are compared exactly: Movies and movies are two different categories. The mode is not: Hoard or HOARD is stored as hoard, and any other value is refused (see With the REST API below).
A torrent carries its category per copy. If the same torrent sits in two engines, each copy has its own category.
Accepted but ignored in 4.4: category fields
placement,agents,strategyandmin_free_bytesare kept when you save a category, but nothing reads them. Placement usesmodealone. At startup Hydranos logs one warning per such field, naming the categories that carry it. See Upgrading from 3.x.
Every add path (the + Add Torrent tab, the REST API, the qBittorrent shim, the MCP server, watched folders, magnets once their metadata arrives, a fetch from another node) runs the same rule:
-
Engine. An explicit
enginethat exists on this instance wins. That is the only way to place a torrent on an extra engine such asvpn1. Next, on the native add routes and the Add tab, amode(raceorhoard) picks the engine of that role. Otherwise the category decides: modehoard→ enginehoard; moderace, an unknown category, or no category → enginerace. -
Save path. A save path in the request wins. Otherwise, the category's save path is used. If there is neither, the add is refused with
no save path: category "x" is unknown and no savepath was given. You get the same message for a known category whose save path is empty. The qBittorrent shim alone has a last resort: the folder a client set withsetPreferencessave_path(see qBittorrent Shim and Automation).
Two add paths check the category first: MCP add_torrent refuses a missing or unknown category before placement, even when a save path is given, and a watched folder can only be saved with an existing category.
flowchart TD
A["New torrent: category, optional save path, optional engine or mode"] --> B{"engine given and it exists on this instance?"}
B -- yes --> E1["Engine = the named engine"]
B -- "not given" --> MO{"mode given? (native routes, Add tab)"}
B -- "given, unknown: native routes, MCP file" --> RX["Refused: no engine X (400 on native routes; MCP: tool error unknown engine X)"]
B -- "given, unknown: MCP magnet, watched folder whose engine was removed" --> C
MO -- yes --> E2["Engine = the engine with that role"]
MO -- no --> C{"Category known?"}
C -- no --> R1["Engine = race"]
C -- yes --> D{"Category mode is hoard?"}
D -- yes --> H["Engine = hoard"]
D -- "no (race)" --> R2["Engine = race"]
E1 --> S{"Save path in the request?"}
E2 --> S
R1 --> S
H --> S
R2 --> S
S -- yes --> P1["Use the request's save path"]
S -- no --> T{"Category known and its save path not empty?"}
T -- yes --> P2["Use the category's save path"]
T -- no --> X["Refused: no save path"]
Details that matter:
-
Where you can name an engine: only the REST API (
engineonPOST /api/torrents/uploadandPOST /api/torrents; required onPOST /api/nodes/:name/fetch), the MCPadd_torrenttool and watched folders configured through the API. The qBittorrent shim never takes one: Sonarr and Radarr route by category alone. The Add tab has no engine field, only the Mode switch. -
Where you can give a mode:
mode(raceorhoard, any case) onPOST /api/torrents/uploadandPOST /api/torrents, and the Add tab's Mode switch. It picks the engine with that role, the one namedraceorhoardfirst.enginewins when both are sent. -
An engine or mode that does not exist is refused with 400 by
POST /api/torrents/uploadandPOST /api/torrents(magnets included):no engine X,mode must be race or hoard, not "x", orno engine has the hoard role. MCPadd_torrentrefuses an unknown engine for a file, as a tool error (unknown engine `x`; this node runs: …), not an HTTP 400. A magnet from MCP ignores it and falls back to the category; a watched folder refuses an unknownenginewhen saved (400), and only falls back if that engine is removed later. - The save path is not checked at add time: no test that it is absolute or exists. Paths are as Hydranos sees them (inside the container on Docker).
-
Layout on disk: a multi-file torrent goes to
<save path>/<torrent name>/…, a single-file torrent to<save path>/<file>, or<save path>/<file name without extension>/<file>with the subfolder option; see Adding Torrents and Existing Data. A path collision (isolate) moves either kind into<save path>/<first component of its name> [<first 8 hex digits of its info hash>]/(Adding Torrents and Existing Data). -
On the Add tab, picking a category replaces the Save Path field with the category's save path (emptied if the category has none) and sets the Mode switch to the category's mode. The form then sends both as explicit values, so the add follows the category; change Mode or Save Path afterwards and your choice wins. Mode starts on Race and is sent with every add, even with
- none -as category (see Adding Torrents). - After placement, the add may still be refused by the race admission check, and data already on disk is rechecked or trusted. That pipeline is on Adding Torrents and Existing Data.
Open the Categories tab and click + New. The form has Name, Mode, Save Path, and then, depending on the mode, Graduate to (hoard category) (race) or Transit area (hoard). The UI requires a name and a save path. Graduate to only lists hoard categories that have a save path. The table shows Name, Mode, Save Path, Graduate to and Edit / Delete buttons, followed by the orphaned labels (see Deleting and renaming below). Saving a new category under a name that already exists fails with category "x" already exists.
Changing the save path or the mode of a category only affects torrents added afterwards. Torrents already in it stay where they are, in the same engine. To move them, use Change category + move files › (below).
# Create (201, the category is echoed back)
curl -X POST http://localhost:8199/api/categories \
-H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"tv","mode":"hoard","save_path":"/data/library/tv"}'
curl -X POST http://localhost:8199/api/categories \
-H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"tv-race","mode":"race","save_path":"/data/race/tv","graduate_to":"tv"}'
# List (array, sorted by name, case-insensitive)
curl -H "X-Api-Key: YOUR_API_KEY" http://localhost:8199/api/categoriesWhat the routes check:
| Route | Checks and answer |
|---|---|
POST /api/categories |
name required (400 name is required). mode must be race or hoard, any case, stored in lower case; absent → race; anything else → 400 mode must be race or hoard, not "x". A name that already exists → 409 category "x" already exists. 201 with the category echoed back. |
PUT /api/categories/:name |
Updates the entry: the fields in the body replace the stored ones, and a field you do not send (mode included) keeps its value, so {"save_path":"/new"} changes the save path only. Same mode check. A name in the body that differs from the one in the path renames the category (409 if the new name is taken). An unknown :name is created, not refused. Answer {"status":"ok","name","relabelled"}, plus followed on a rename (see Deleting and renaming). |
DELETE /api/categories/:name |
Removes the definition and clears the label from every torrent that wore it. Answer {"cleared","cleared_stored","status":"ok","was_orphan"}. Also works on an orphaned label. |
GET /api/categories/orphans |
Labels worn by torrents that match no category: [{"name","torrents","mode","save_path"}]. |
Nothing checks save_path (an empty one is stored), nor that Graduate to names an existing hoard category.
Sonarr, Radarr and other qBittorrent clients can manage categories too, with different results:
| Call | What Hydranos does |
|---|---|
POST /api/v2/torrents/createCategory (category, savePath) |
Creates the category with mode hoard, always. An existing name answers 409 Category already exists and is left as it was; an empty name answers 400. |
POST /api/v2/torrents/editCategory |
Changes only the save path. Creates a hoard category if the name is unknown. |
POST /api/v2/torrents/removeCategories |
Removes the definitions (newline- or comma-separated names) and clears the label from the torrents, like qBittorrent. |
/api/v2/torrents/categories (any method) |
{name: {"name", "savePath"}}. The mode is not visible to the client. |
If a client creates a category without a save path, it is stored empty and every later add to it that does not send its own savepath is refused for lack of a save path. Create your categories in Hydranos first, with the right mode and path, and use the same names in Sonarr/Radarr. See Migrating from qBittorrent.
Right-click one or more torrents. The Torrent group has two category entries:
- Change category ›: label only. The engine and files stay where they are, even if the new category has another mode or another save path.
- Change category + move files ›: the data follows the category's save path, as a background job.
Both run as a bulk task with a progress panel. The submenus list the existing categories; Change category › also starts with No category, which clears the label (label only: there is no folder to move to, so + move files does not offer it).
With + move files, Hydranos decides per copy:
flowchart TD
A["Change category + move files"] --> B{"Category known and has a save path?"}
B -- no --> E["400, nothing changes"]
B -- yes --> C{"Category mode matches the role<br/>of the engine holding this copy?"}
C -- yes --> D{"Data already at the category's save path?"}
D -- yes --> L["Label changed, nothing moves (200)"]
D -- no --> M["Job move_data, same engine"]
C -- no --> G["Job graduate, to engine race or hoard"]
M --> K{"Pre-checks pass?"}
G --> K
K -- no --> F["409: shared, unsafe_path, destination_taken,<br/>hardlinks or space, or a move already queued"]
K -- yes --> J["202: job queued, see the Jobs tab"]
- A mode change becomes a graduation: the torrent moves to the engine
raceorhoardand its data moves with it, in either direction. A copy in an extra engine whose role already matches the mode stays in that engine. The context menu acts on the copy the row shows (it names the extra engine with?engine=), so a row ofvpn1moves thevpn1copy. -
destination_takenmeans files with the same names already exist in the target folder; moving onto them would overwrite them. -
hardlinksmeans some files are hardlinked elsewhere (usually your Sonarr/Radarr library) and the target is on another filesystem (on Docker, check that save paths and library sit under the same mount: see Docker and Jobs, Bulk Actions and Moving Data). The UI asks once for the whole selection (Move them anyway?) and retries with consent. The other reasons cannot be overridden. - With a
move_datajob (same engine) the torrent keeps seeding while the data is copied. A graduation first removes the torrent from its engine, then moves the files, then adds it to the other engine with its state (counters, trackers, stopped or not): it does not seed during the move. A failure is rolled back: the files go back and the torrent returns to its engine as it was. The notice after the action says which case you are in: It keeps seeding while its data is copied for amove_datajob, which runs in another engine: it stops seeding while its data is copied, then carries on there as it was for a graduation. Its category label changes when the job finishes. Job behaviour and failure cases are on Jobs, Bulk Actions and Moving Data.
REST equivalent, per torrent (/api/race/… or /api/hoard/… picks the engine holding the copy; add ?engine=vpn1 for an extra engine):
curl -X POST http://localhost:8199/api/race/torrents/0123456789abcdef0123456789abcdef01234567/category \
-H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"category":"tv","move_files":true}'Without "move_files": true the route only relabels that copy. Add "allow_breaking_hardlinks": true to accept a hardlinks refusal.
Other ways to change a category:
| Where | Effect |
|---|---|
qBit POST /api/v2/torrents/setCategory
|
Label only, on every copy of each hash. No data move, no engine change, no check that the category exists. An empty category clears it. |
| Workflow action set category (files stay) | Label only, on the matched copy. See Workflows. |
MCP set_category / move_to_category
|
Label only / with move. Both refuse an unknown category. See MCP Server. |
On rows coming from another node, both category entries are sent to the node that holds the torrent, which applies its own category of that name (its own mode and save path): see Nodes and Multiple Machines.
Renaming. Click Edit, change Name, save. The definition moves to the new name and every torrent labelled with the old name gets the new one (the API answers how many in relabelled). Files and engines do not change. If the new name is already a category, the save fails with category "x" already exists.
Everything else that names the category follows the rename: the Graduate to of other categories, RSS rules, watched folders, workflow conditions category is / is not and set category actions, magnets still waiting for their metadata, and queued jobs. The API answers what followed in followed ({"rss_rules","watch_folders","workflows","magnets","jobs"}, counts). A workflow condition that matches the name with contains or a regular expression is your text and is not changed: check those by hand.
Deleting. Delete asks Delete category "x"?, removes the definition and clears the label from every torrent that wore it (cleared in the answer). The torrents stay where they are, in the same engine. New adds that still name it route like an unknown category: engine race, refused without an explicit save path.
Orphaned labels. A label worn by torrents with no category of that name (left by an upgrade, or by a client calling qBit setCategory with a name that does not exist) is listed under the categories as <name> orphaned, with Save Path reading no longer configured, still on N torrent(s), and two buttons:
- Adopt opens the + New form filled in from those torrents: the name, the mode of the engine role most of them run in, and the save path most of them use, with the message Filled in from the {n} torrent(s) already labelled {label}: they run in {mode}, mostly under {path}. Check it, then save to adopt them. Saving creates the category; the torrents belong to it at once, nothing moves.
- Delete clears the label from those torrents.
In the qBittorrent shim, a torrent with no category is reported under its engine's name, such as race or hoard. A client asking torrents/info?category=race therefore gets the race torrents labelled race and every uncategorised race torrent. Avoid naming your own categories race or hoard unless that is what you want. The shim lists every engine, extra engines included (an uncategorised torrent there is reported under the engine's id), but adds only into race or hoard, by category (see qBittorrent Shim and Automation).
Give each *arr its own category. If unsure, make it Hoard: a race torrent can be deleted by the race drain once its seed obligation is met, whether or not Sonarr or Radarr has imported it yet. Use race only for racing fresh uploads, with Graduate to set.
A race category with Graduate to set tells the race drain where to move a torrent that must leave the race disk before its seed obligation is met. A hoard category with Transit area ticked deletes, with their files, all torrents filed in it once their seed obligation is declared and met, including torrents added straight into it. It only does this if some category names it in Graduate to; a torrent whose tracker declares no obligation is never deleted. Do not tick Transit area on a library category. The full rules are on Race Drain, Graduation and Seed Obligations.
Tags are free labels. They have no effect on placement, engine or files: use them to filter and to drive workflows.
| Where tags are set | Notes |
|---|---|
| Context menu Edit tags › | Add an existing tag, remove one, or type a new one (new tag + Enter). Runs as a bulk task. Shown only when the selection contains a hoard torrent (or is a whole-list selection); race copies are skipped. |
| Add requests |
tags field on POST /api/torrents/upload, POST /api/torrents, qBit torrents/add, MCP add_torrent. The Add tab has no tags field; watched folders add none. |
qBit addTags / removeTags / createTags
|
addTags/removeTags change every copy of the hash. createTags only registers the name. |
REST POST /api/{hoard,race}/torrents/:hash/tags
|
{"tags":["a","b"]} replaces the whole set. |
| Workflow actions add tags / remove tags | See Workflows. |
Where tags filter: the Tags facet on the Hoard tab (see Web UI Tour), qBit torrents/info?tag= (exact match), bulk selections by filter, and workflow conditions has_tag / not_has_tag. GET /api/v2/torrents/tags lists every registered tag.
qBit deleteTags removes the tags from the list and from every torrent; details on qBittorrent Shim and Automation.
- Adding Torrents and Existing Data: the add pipeline after placement.
- Engines: Race and Hoard: what the two roles do, extra engines.
- Race Drain, Graduation and Seed Obligations.
- Jobs, Bulk Actions and Moving Data.
- qBittorrent Shim and Automation.
- REST API: every category and tag route.