Adding Torrents and Existing Data - Kheopsian/Hydranos GitHub Wiki

🇬🇧 English · 🇫🇷 Français

Adding Torrents and Existing Data

Applies to Hydranos 4.4.

This page is for anyone who puts torrents into Hydranos: by hand, from a script, from Sonarr/Radarr/autobrr, from an RSS rule, from an AI client or through a watched folder. It covers every way to add, which options each one honours, how to add over data you already have on disk without downloading it again, what happens when another torrent already names the same files, the order a torrent downloads in, and why an add can be refused. Terms used here are defined on Concepts and Glossary.

The add pipeline

Every add path ends in the same function, so the same rules apply everywhere. Magnets take a detour: they are checked, stored, and resolved in the background, then enter the pipeline once their metadata has arrived.

flowchart LR
    UI["Add tab: upload or server path"] --> SF
    REST["REST: POST /api/torrents/upload, POST /api/torrents (path or URL)"] --> SF
    QB["qBit shim: POST /api/v2/torrents/add"] --> SF
    RSS["RSS rule or the RSS tab's Download"] --> SF
    SF["Subfolder option: a single-file torrent<br/>gets a folder of its name"] --> P
    MCP["MCP tool add_torrent"] --> P
    WF["Watched folder: .torrent file"] --> P
    NF["Node fetch: bring it here"] --> P
    IMP["Import wizard"] --> P
    MIN["Magnet from the Add tab, REST, shim, RSS, MCP or a .magnet file"] --> MQ{"Link valid, save path known,<br/>not in the target engine?"}
    MQ -- no --> MR["Refused at once"]
    MQ -- yes --> MS["Stored, resolved in the background (BEP 9)"]
    MS -- "metadata arrives, shim subfolder option applied" --> P
    P["Parse the .torrent"] --> PL["Placement: explicit engine, else mode,<br/>else the category's mode, else race"]
    PL --> SP{"Save path: explicit, else the category's"}
    SP -- none --> R1["Refused: no save path"]
    SP -- found --> EN{"Engine exists?"}
    EN -- no --> R0["Refused: no engine"]
    EN -- yes --> RA{"race-role engine and add_block_enabled<br/>and projected space too short?"}
    RA -- yes --> R2["Refused: race disk full"]
    RA -- no --> PC{"Path claim: another torrent<br/>names these files with other bytes?"}
    PC -- "yes, refuse" --> R3["Refused: would overwrite"]
    PC -- "yes, isolate" --> ISO["Own folder NAME [hash8], held paused,<br/>seed mode dropped"]
    PC -- "no, legacy, or seed mode<br/>with every size matching" --> ST["Store row and content key written"]
    ISO --> ST
    ST --> DD{"[dedup] enabled?"}
    DD -- yes --> DL["Try to hardlink identical payload already held"]
    DD -- no --> EA
    DL --> EA{"Hash already in this engine?"}
    EA -- yes --> AA["Refused: already added"]
    EA -- no --> HOLD{"Held by the path claim?"}
    HOLD -- yes --> PR["Path-claim proof in the background,<br/>see Files another torrent already holds"]
    HOLD -- no --> SM{"Seed mode?"}
    SM -- yes --> SEED["Seeding, no hash check"]
    SM -- no --> DOD{"Any file present at the save path?"}
    DOD -- yes --> RC["Recheck: seed, or download the missing pieces"]
    DOD -- no --> DLD["Download"]
    PR --> WK
    SEED --> WK["Workflows 'when a torrent is added' fire<br/>(not for the import wizard)"]
    RC --> WK
    DLD --> WK
Loading

What the diagram means in practice:

  • Placement (which engine, which folder) is decided by the category; the full rule is on Categories and Routing. An explicit engine (or mode, race or hoard, on the native routes and the Add tab) wins over the category. An unknown or empty category goes to the race engine with no save path, so it is refused unless you give a save path explicitly.
  • There is no default save path, with one exception on the qBit shim: a folder a client set with setPreferences save_path is used when an add names neither a save path nor a category that has one (empty until a client sets it).
  • The path claim keeps a new torrent from writing into files another torrent already names with other bytes; see Files another torrent already holds.
  • The "already added" check is per engine. Adding a hash that another engine already holds creates a second copy in the target engine. Only the import wizard and bring it here (node fetch, which answers 409 this node already has that torrent) check every engine.
  • "When a torrent is added" workflows fire for every path except the import wizard (see Workflows); RSS adds included.

Ways to add

Path Files Magnets .torrent URL Category Save path Tags Start stopped Seed mode Engine
+ Add Torrent tab, upload yes, several (one request each) — — yes yes — yes yes Mode (Race / Hoard)
+ Add Torrent tab, Torrent File Path a file on the daemon's disk — — yes yes — yes yes Mode
+ Add Torrent tab, Magnet link — yes — yes yes — yes yes Mode
POST /api/torrents/upload (multipart) one per request — — category savepath or save_path tags paused or stopped skip_checking, seed_mode or skip_recheck engine or mode (unknown → 400)
POST /api/torrents (JSON) torrent_path (daemon's disk) magnet_uri torrent_url category save_path tags stopped or paused (bool) seed_mode or skip_recheck (bool) engine or mode (unknown → 400)
qBit shim POST /api/v2/torrents/add torrents, file or fileselect, several urls urls category savepath tags paused or stopped skip_checking —
MCP tool add_torrent torrent_path magnet torrent_url required, must exist save_path tags (array) paused never engine
Watched folder .torrent .magnet — the folder's (mandatory) the category's — Add stopped never API only

Boolean form fields accept true or 1; the JSON route wants real JSON booleans. The upload route takes the file under the multipart field name torrent, torrents or file: all three work (with several file parts, only the last one is added). mode (race or hoard, any case) picks the engine of that role, the one named after the role first; engine wins when both are sent. RSS rules and the RSS tab's Download add through the same pipeline, never in seed mode: see RSS and Automatic Downloads. All REST and shim routes need the API key (X-Api-Key header or apikey query parameter) or a logged-in session cookie (what Sonarr/Radarr use through the shim's login); see Security and Access. Every route and field is listed on REST API; enabling and connecting the MCP endpoint is on MCP Server.

Two more paths reuse the pipeline: bring it here on a declared node's torrent (Nodes and Multiple Machines) and the import wizard (Migrating from qBittorrent).

Examples, against an instance on port 8199:

# Upload a .torrent into the "tv" category, trusting the data already on disk
curl -H "X-Api-Key: YOUR_API_KEY" \
  -F "[email protected]" -F "category=tv" -F "seed_mode=true" \
  http://localhost:8199/api/torrents/upload

# Upload into an extra engine, with an explicit save path, stopped
curl -H "X-Api-Key: YOUR_API_KEY" \
  -F "[email protected]" -F "engine=vpn1" \
  -F "save_path=/data/vpn1" -F "stopped=true" \
  http://localhost:8199/api/torrents/upload

# Add a magnet (answers 202 {"info_hash":...,"status":"resolving"})
curl -H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"magnet_uri":"magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567&tr=https%3A%2F%2Ftracker.example.org%2Fannounce","category":"tv"}' \
  http://localhost:8199/api/torrents

# Add from a tracker's .torrent URL into the hoard engine, stopped
curl -H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"torrent_url":"https://tracker.example.org/download/123.torrent","mode":"hoard","category":"movies","stopped":true}' \
  http://localhost:8199/api/torrents

On Windows, in PowerShell, call curl.exe (plain curl is an alias of Invoke-WebRequest in Windows PowerShell 5.1) and keep it on one line; Windows paths are fine after @:

curl.exe -H "X-Api-Key: YOUR_API_KEY" -F "torrent=@D:\Torrents\Film (2020).torrent" -F "category=movies" -F "seed_mode=true" http://localhost:8199/api/torrents/upload

Answers:

  • Upload route: 200 {"info_hash","name","engine","mode"} (engine is the engine the torrent went to, mode its role); 409 {"error"} when that engine already holds it; 400 {"error"} for every other refusal.
  • JSON route: 200 {"info_hash","name","engine","mode"} for a file or URL, 202 {"info_hash","status":"resolving"} for a magnet. A refusal carries {"error","targets":[{"agent":"local","error"}]}: 409 when the engine already holds it and for the other refusals (race disk full, path_collision = "refuse"), 404 when torrent_path does not exist, 400 for the bad requests listed in When an add is refused (a malformed, missing or v2-only magnet hash included), 500 only for a store failure.
  • Shim: Ok. or Fails. like qBittorrent; see qBittorrent Shim and Automation.

.torrent URLs (native torrent_url, shim urls, MCP torrent_url) are fetched with a plain GET through the daemon's own way out (see VPN and Egress): http or https only (else torrent_url must be an http(s) URL), 30 s timeout, at most 15 MiB, no cookies or extra headers. The URL is replaced by <url> in the log and in the native route's error, since a tracker URL usually carries a passkey.

Download, recheck, or seed mode

When a torrent is added, Hydranos decides what to do with the save path:

Situation What happens
Seed mode not asked, no file of the torrent present Normal download.
Seed mode not asked, at least one file present Recheck in the background: every piece is hashed; the torrent ends seeding if all pieces verify, or downloads only what is missing.
Seed mode asked (skip_checking / seed_mode / skip_recheck, the Add tab's Skip the hash check…) Starts seeding at once. Nothing is checked: not whether the files exist, not their size.
Another torrent names some of these files with other bytes, or files on disk are hardlinked elsewhere With path_collision = "isolate" (default): moved to its own folder, held paused while the same-size files are proven, then rechecked. refuse: refused. legacy: rechecked over them. Seed mode over same-size files only: trusted, added at once. Files on disk hardlinked elsewhere and named by no torrent: held in place until proven (not in seed mode). See Files another torrent already holds.

The default — recheck — is the safe one: adding over a complete copy of the data costs one read of the data and no download. It also applies to a torrent added stopped: it is rechecked and stays stopped, so you can see what is on disk before it touches the network.

Use seed mode only when you know the data is complete and at the exact path, typically for cross-seeding (cross-seed sends skip_checking=true through the shim) or a scripted import of a verified library. If you are wrong, the torrent advertises pieces it cannot serve. When in doubt, add without seed mode, or run Recheck later (context menu, shown when the selection holds a hoard torrent; or POST /api/hoard/torrents/<hash>/verify, which finds the torrent in any engine despite the /hoard/ prefix and answers {"status":"ok","checking":true,"paused":…}). Every recheck is guarded by the path claim (see Files another torrent already holds).

Layout on disk, which must match exactly for a recheck or seed mode to find the data:

  • multi-file torrent: <save path>/<torrent name>/<files>
  • single-file torrent: <save path>/<file>
  • single-file torrent with the subfolder option: <save path>/<file name without its extension>/<file>

The torrent name and the file names are used exactly as the .torrent writes them; nothing is renamed or matched loosely. The subfolder option is Put the payload in its own subfolder on the Add tab, create_subfolder on both native routes, contentLayout / root_folder on the shim, with [daemon] create_torrent_folder (default false) as the default for all of them and for RSS adds. It only changes single-file torrents (a multi-file torrent already has its folder). There is no way to strip a multi-file torrent's top folder (the shim refuses contentLayout=NoSubfolder on one). If your folder is named differently (a Plex-style rename, a different year format), the data is not found: without seed mode the torrent downloads everything again into a new folder of the right name; with seed mode it advertises data it does not have. Rename the folder to the torrent's name before adding, or keep a copy laid out the way the torrent expects.

Example on Windows: you already have D:\Media\Movies\Film (2020)\Film.2020.mkv and a multi-file torrent named Film (2020) holding Film.2020.mkv. Give the category Save Path D:\Media\Movies (the parent, not the film's folder) and add the .torrent with the PowerShell line in Ways to add above. With seed_mode=true it seeds at once; without it, Hydranos reads the whole file once to check it (time ≈ data size ÷ the disk's read speed) and then seeds. In UI fields (save path, watched folder, Torrent File Path) type Windows paths with backslashes as usual; the forward-slash advice on Install on Windows is for strings inside default.toml.

Accepted but ignored in 4.4: the subfolder option (create_subfolder, [daemon] create_torrent_folder) on a magnet added through POST /api/torrents or the Add tab, and on watched folders, MCP adds, RSS magnets, node fetches and the import wizard — those keep the plain layout — upload the .torrent instead, or add the magnet through the shim with contentLayout=Subfolder.

Seed mode on a magnet is kept with the request and applied once the metadata arrives (native seed_mode / skip_recheck, the Add tab's box, shim skip_checking). A .magnet in a watched folder, an MCP magnet and an RSS magnet are never added in seed mode.

Download order

A torrent always downloads every file. What you can change is the order pieces are fetched in; Hydranos has no "do not download" (qBittorrent's priority 0), on purpose: a torrent left incomplete is one it can never seed.

  • File priorities: Normal, High, Maximum (qBittorrent's 1, 6, 7). A piece is ranked by the highest priority of the files it covers; the highest rank is fetched first.
  • First/last piece first: the first and last piece of every file come before everything else, above any priority (as in qBittorrent).
  • Sequential download: within one rank, pieces are fetched in index order instead of rarest first. Several pieces stay in flight at once, and the endgame (the last 16 pieces, asked of more than one peer) works as usual.
  • Default: nothing set means plain rarest first; nothing is stored for it.
  • Kept: a change applies at once and is written to the engine's resume database (state.db), so it survives a restart and travels with the torrent when it moves to another engine. A complete torrent keeps and shows its settings; they act again if a recheck finds pieces missing.

Where to set it:

Where What
Detail panel › Content tab Priority column, one selector per file (Normal / High / Maximum). The summary line adds sequential and first/last piece first when set. A row from another node shows priorities read-only. A failure shows Priority not changed: ….
Context menu, Torrent group Sequential download and First/last piece first, ticked when every selected torrent has it on (read for up to 50 selected torrents; a larger, filter-wide or remote selection shows unticked); a click sets the opposite on the whole selection. There is no bulk file priority.
GET / POST /api/torrents/<hash>/download-order GET answers {"info_hash","copies":[{"engine","sequential","first_last_piece","file_priorities","complete"}]}. POST takes any of sequential, first_last_piece, and file_ids (indices from GET /api/torrents/<hash>/files, field index) together with priority. Applies to every local copy unless ?engine= names one. Priority 0 → 409 (Hydranos always downloads the whole torrent…); any other bad body → 400 saying why; unknown hash → 404.
Selection action download_order {"sequential","first_last_piece"} (at least one); see REST API.
qBit shim torrents/filePrio (1, 6, 7; 0 → 409), torrents/toggleSequentialDownload, torrents/toggleFirstLastPiecePrio; on add, sequentialDownload and firstLastPiecePrio, kept with a magnet until its metadata arrives. See qBittorrent Shim and Automation.
# Play file 2 first, and fetch each file's ends early
curl -H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"file_ids":[2],"priority":7,"first_last_piece":true}' \
  http://localhost:8199/api/torrents/<hash>/download-order

Not implemented in 4.4: a download-order setting at add time on the Add tab, POST /api/torrents, the upload route, MCP or a watched folder — only the shim reads one — set it right after the add (context menu or download-order), or add through the shim.

The Add tab

The tab labelled + Add Torrent (heading Add Torrent) has these fields:

Field What it really does
Category (with + New) Sent with the add. Picking one replaces Save Path with the category's path (empty if it has none) and switches Mode to the category's mode. The first entry is - none -.
Torrent File Path Path of a .torrent on the machine running Hydranos (in Docker: inside the container), not on your computer. On a Windows install, a normal path such as D:\Torrents\release.torrent works.
Or Upload .torrent One or several files from your computer, sent one request each. Takes priority over the path and magnet fields.
Preview contents before adding Reads the selected files in your browser and shows them in the Contents column. Nothing is sent.
Magnet link Used only when no file is selected and Torrent File Path is empty.
Advanced, override mode & save path › Save Path Sent as the save path; overrides the category's. Whatever the field shows is what is sent.
Mode (Race / Hoard) Sent with every add (mode); picks the engine of that role, the one named race or hoard first. Starts on Race; it follows the category you pick, and wins over it if you change it afterwards.
Put the payload in its own subfolder create_subfolder: a single-file torrent goes to <save path>/<file name without extension>/. Pre-ticked from [daemon] create_torrent_folder (read through GET /api/torrents/add-defaults; if that read fails, nothing is sent and the daemon setting applies). Not applied to a Magnet link.
Skip the hash check, the data is already on disk Seed mode, for an upload, a path or a magnet. Starts unticked.
Add without starting Added stopped, for an upload, a path or a magnet. A stopped add over existing data is still rechecked.

The success line reads Added <name> → <mode>[ · <category>], with the role of the engine the torrent went to. An error is shown as the server sent it.

There is no tag field and no .torrent URL field on the tab (a URL typed into Torrent File Path is read as a path and refused with no such file; use torrent_url on POST /api/torrents, the shim or MCP), and the engine is chosen by role only. With - none - and an empty Save Path, the add is refused (no save path).

Known bug in 4.4: going back to - none - in Category looks like it resets the form — Save Path and Mode keep the previous category's values and are sent — clear Save Path and pick the mode by hand.

Known bug in 4.4: two help texts under the boxes do not describe the code — Put the payload in its own subfolder says it applies "on the hoard agent" (it applies to any engine), and Skip the hash check… says the add is refused when a file is missing or the wrong size (nothing is checked, the torrent advertises what it does not have) — rely on this page.

Magnets

A magnet is accepted in seconds and resolved in the background:

  1. Checked at once. Refused immediately if the link does not parse, if no save path can be found, or if the target engine already has the hash. An engine (or mode) that does not exist is refused (400) by POST /api/torrents; from an MCP magnet it is ignored and the magnet goes to the category's engine. A watched folder cannot be saved with an unknown engine (PUT /api/watch answers 400 no engine "x"); only an engine removed after the folder was saved falls back to the category. Links need a v1 info hash (xt=urn:btih:, 40 hex or 32 base32). A v2-only magnet (urn:btmh: only) is refused with add the .torrent file instead; a hybrid magnet is fine. tr (including numbered tr.1…), dn and x.pe are used.
  2. Stored. The request (category, save path, tags, stopped flag, seed mode, engine, and the shim's add options: rate limits, download order, content layout) is written to the store and survives a restart.
  3. Resolved. Peers come from the magnet's trackers, x.pe and the DHT; the metadata is fetched over BEP 9 and must hash to the info hash. The engine must be on the network; if it is not and the request is more than 600 s old (counted from when it was first made, not from the last restart or Retry), the request fails.
  4. Retried. Up to 4 attempts, waiting 0, 1, 5 then 15 minutes; after the fourth failure the request is marked failed with the reason.
  5. Added. The .torrent is rebuilt (info dictionary byte for byte, one tracker tier per tr) and goes through the pipeline above, in seed mode if the request asked for it, with the shim's options applied. A shim contentLayout=NoSubfolder on what turns out to be a multi-file torrent fails the request (contentLayout NoSubfolder on a multi-file torrent is not supported). If the pipeline refuses it, the request is marked failed at once (no retry); if the engine already holds it, the request is dropped quietly.
stateDiagram-v2
    state "Refused at once" as Refused
    state "resolving: fetching metadata" as Fetching
    state "waiting to retry" as Waiting
    state "failed" as Failed
    state "Added through the add pipeline" as Added
    state "Request dropped, already in the engine" as Dropped
    [*] --> Refused: bad link, no save path, already there
    [*] --> Fetching: stored in the store, resolution started
    Fetching --> Added: metadata hashes to the info hash, add accepted
    Fetching --> Dropped: the add says already added
    Fetching --> Waiting: no metadata, or metadata does not match
    Waiting --> Fetching: after 1, 5 then 15 min
    Fetching --> Failed: 4th failed attempt, NoSubfolder on a multi-file torrent, or the add refused it
    Fetching --> Failed: engine off the network, request over 600 s old
    Failed --> Fetching: Retry, attempts back to 0
    Failed --> [*]: Remove
    Waiting --> [*]: Remove
    Fetching --> [*]: Remove
    Added --> [*]
    Dropped --> [*]
    Refused --> [*]
Loading

On the Add tab, the Magnets card (hidden when empty) lists pending requests with Name, Engine and State (fetching metadata..., waiting to retry (N min), failed: …), a Retry button on failed ones and Remove. The same through the API:

curl -H "X-Api-Key: YOUR_API_KEY" http://localhost:8199/api/magnets
curl -X POST -H "X-Api-Key: YOUR_API_KEY" http://localhost:8199/api/magnets/<hash>/retry
curl -X DELETE -H "X-Api-Key: YOUR_API_KEY" http://localhost:8199/api/magnets/<hash>

Retry restarts from the first attempt (the 600 s off-network limit is not reset).

Watched folders

Config tab › Watched folders: each row has Folder, Category, Add stopped, On and Status; + Folder adds a row and Save replaces the whole list (PUT /api/watch).

  • Category is mandatory and must exist when you save; it decides the engine and the save path. The folder must be an absolute path to a directory the daemon can see (Linux/macOS: starts with /; Windows: X:\…, C:/… or a UNC share \\server\share\watch).
  • Scanned every 10 seconds, top level only (no subfolders), files ending in .torrent or .magnet (any case). A file is taken once its size and modification time have not changed between two scans, so expect 10–20 s of latency. Files larger than 64 MiB are refused.
  • Each folder is scanned on its own, so a network share that stops answering holds only its own row: after 60 s with no answer, that row shows an error (no answer from this folder for 60 s …), and the other folders go on.
  • A .magnet file holds the link on its first non-empty line.
  • Outcome: an added file, or one already in the engine, moves to <folder>/added/ (name clashes get (2), (3)…). A refused file is renamed <name>.invalid with <name>.invalid.txt beside it giving the reason. Nothing is deleted. For a .magnet, "added" means the request was accepted; resolution follows as in Magnets.
  • Never seed mode: existing data is always rechecked. No tags, no subfolder option.
  • Status shows seen N · added N · refused N and the last refusal: seen is how many .torrent and .magnet files the last scan found in the folder. A folder not scanned since the start shows not scanned yet. These counters are kept in memory and reset on restart.
  • The API also accepts an engine per folder. The UI does not show it, but keeps it when you save the list from the Config tab.

In Docker, the folder is a path inside the container: mount it. On Windows, type it as D:\Watch\movies (or \\server\share\watch) in the Folder field.

A folder on a network share, in Docker. If the share is mounted on the host after the container started, the container still sees the empty mountpoint beneath it: the folder reads fine and nothing is ever picked up. The status shows it: seen 0 while files are being dropped in. Mount the parent of the mountpoint with rslave propagation (- /mnt/remotes:/remotes:rslave, folder /remotes/share/watch) so a later host mount reaches the container, or restart the container after the share is mounted.

flowchart TD
    S["Every 10 s: each enabled folder, top level only"] --> F{"Name ends in .torrent or .magnet?"}
    F -- no --> IGN["Ignored"]
    F -- yes --> ST{"Same size and mtime<br/>as at the previous scan?"}
    ST -- no --> WAIT["Remembered, looked at next scan"]
    ST -- yes --> BIG{"Larger than 64 MiB?"}
    BIG -- yes --> INV["Renamed NAME.invalid,<br/>reason in NAME.invalid.txt"]
    BIG -- no --> K{"A .magnet file?"}
    K -- yes --> MAG["Magnet request, first non-empty line,<br/>folder's category"]
    K -- no --> ADD["Add pipeline, folder's category,<br/>never seed mode"]
    MAG --> R{"Accepted, or already added?"}
    ADD --> R
    R -- yes --> OK["Moved to FOLDER/added/"]
    R -- no --> INV
Loading

Duplicate data (dedup on add)

If you add a torrent whose payload you already hold under another torrent (same content, different info hash: a re-upload on another tracker, a renamed release), Hydranos hardlinks the existing files under the new torrent's names before the engine looks at the save path. Nothing is downloaded again.

  • On by default: [dedup] enabled = true applies even when the section is absent (the shipped default.toml has none).
  • Match = the torrents' piece hashes are byte-identical and the file sizes match one for one. No partial matches. v2-only torrents never match.
  • Every source file must exist with the right size; no destination file may already exist (so dedup never overwrites anything, and an add over data already at the save path just takes the recheck path); source and destination must be on the same filesystem as the daemon sees it (same device id on Linux/macOS, same volume serial on Windows: two folders on one NTFS drive qualify).
  • Any failure is silent and harmless: the links created are removed and the torrent downloads normally.

Docker, Sonarr and Radarr. Hydranos checks the device id, then calls the kernel's link(). Linux refuses a hardlink across two different mount points (error EXDEV) even when both are the same disk, so two separate bind mounts (/mnt/user/downloads:/data/downloads and /mnt/user/media:/data/media) make every dedup link fail, and the torrent downloads instead. Mount the common parent once (for example /mnt/user/data:/data) and put every category's save path under it. Sonarr and Radarr hardlink imports follow the same kernel rule inside their own container: give them the same single parent mount, at the same container path as in Hydranos, so the save path Hydranos reports through the shim is a path they can open. Hydranos itself never links into your library; that import is Sonarr's or Radarr's own step (see qBittorrent Shim and Automation and Install with Docker).

  • The linked torrent is still rechecked (seed mode is not implied), so it reads the data once before seeding. The card's text saying it "seeds at once" is optimistic.

Config tab › Duplicate data › On adding a torrent we already hold: checkbox Link it instead of downloading, button Save and restart. The setting is written to default.toml and applied immediately; the button restarts Hydranos anyway (see Operating Hydranos for what a restart from the UI does). The card also shows counts of identical-payload groups. API: POST /api/dedup/config {"enabled": false}, GET /api/dedup/stats. The key is listed on Configuration: Daemon, Auth and Global Sections.

Files another torrent already holds

Two different torrents can name the same file: a re-upload that kept the release name, a REPACK that did not rename, the same title encoded twice. Before 4.4 the second one found files at its paths, rechecked them, downloaded the pieces that differed and wrote them over the first torrent's file, which went on seeding a mix. Since 4.4 the path claim checks every add, after race admission and before anything is written, against the torrents of every engine that name the same paths (its own copies in other engines excluded). Per file:

  • nobody else names the path: nothing to do;
  • another torrent names it with the same pieces and the same file list (cross-seeding the very same bytes): shared, as always;
  • another torrent names it with another size: a conflict, the file cannot be both;
  • another torrent names it with the same size: unproven, since the same size is not the same bytes; it is hashed before anything is shared.
flowchart TD
    A["Add after race admission:<br/>this torrent's files at its save path"] --> C{"Another torrent in any engine names one of these paths,<br/>not with the same pieces and file list?"}
    C -- no --> F{"Files present on disk, named by no torrent,<br/>with a hardlink count above 1?"}
    F -- "no, or seed mode" --> GO["Added where it asked"]
    F -- yes --> HP["Added paused where it asked"]
    HP --> PV{"Every such file verifies<br/>against this torrent's pieces?"}
    PV -- yes --> RS["Recheck; started unless the add asked stopped;<br/>what is missing downloads"]
    PV -- no --> ER["Stays paused in error, nothing written"]
    C -- yes --> SZ{"Seed mode, and every shared path<br/>at the size this torrent expects?"}
    SZ -- yes --> GO
    SZ -- no --> M{"[dedup] path_collision"}
    M -- legacy --> LG["Added where it asked: the recheck may overwrite,<br/>reported as shared"]
    M -- refuse --> RF["Refused: would overwrite them"]
    M -- isolate --> IS["Added paused in SAVE/TOP [hash8],<br/>seed mode dropped"]
    IS --> PI["Same-size files hashed against its pieces,<br/>those that verify hardlinked in"]
    PI --> RS
Loading

[dedup] path_collision decides the conflict case:

Value What happens
isolate (default) The torrent gets a folder of its own beside the one it asked for: <save path>/<first component of its name> [<first 8 hex digits of its info hash>], for example /data/tv/Show.S01 [1a2b3c4d]. Same-size files that hash to its pieces are hardlinked in, so only the files that really differ are downloaded. Seed mode is dropped (the new folder starts empty). The torrent reports this folder as its save path, to the shim's clients too.
refuse The add is refused: N file(s) of this torrent are already another torrent's with other bytes (path, path, path, ...); adding it there would overwrite them.
legacy The behaviour before 4.4: shared path, the recheck overwrites what differs. Logged and reported every time.

The key is not in the shipped default.toml and the Duplicate data card writes only enabled: add path_collision = "refuse" (or "legacy") under [dedup] by hand and restart.

  • Seed mode (skip_checking, as cross-seed sends it) over same-size files of another torrent is trusted: the add goes ahead where it asked, with an info log line (path claim: skip_checking over files another torrent holds at the same size), not an anomaly. A size conflict is isolated, refused or let through by the setting even in seed mode.
  • Library hardlinks. Files already at the path that no torrent names but that have another hardlink (a Sonarr/Radarr import, typically) are proven before anything can write into them: the torrent is added paused, those files are hashed, and it is released only if they all verify. Otherwise it stays paused in error with N file(s) on disk differ from this torrent and are hardlinked elsewhere; held paused rather than overwrite them. Not done in seed mode.
  • The proof runs in the background (thread pathclaim-proof); the add answers at once. A restart in the middle of an isolation leaves at worst a paused torrent with an empty folder (or one holding only files already proven identical), which downloads when started; run Recheck first so the files already linked in are counted. A torrent held in place (library hardlinks), or stopped by a guarded recheck, stays paused and is not proven again after a restart: Start proves it first, as an add does (the torrent stays paused while those files are hashed and starts only if they all verify; a complete torrent starts at once), and so does Recheck.
  • Two adds naming the same folder at the same moment (the same release grabbed from two trackers) are checked one after the other; an add waits at most 30 s, then goes ahead with a WARN path claim: waited too long on another add, going ahead.
  • On Windows, paths are compared without regard to case.
  • Rechecks are guarded, whatever path_collision says: Recheck in the UI, POST /api/hoard/torrents/<hash>/verify, the bulk and MCP rechecks and the shim's torrents/recheck stop a torrent whose files another torrent names with other bytes, hash those files, and recheck it (restarting it if it was running) only if they all verify. Otherwise it stays paused in error with the hardlinked elsewhere message above, even though the other name is a torrent's file.
  • Where to see it: each isolated, refused, shared (legacy), held and proven event is a WARN log line path collision: … and is kept in memory (the last 500, lost on restart) for the path_collision kind of GET /api/health/anomalies (see Monitoring and Logs). There is no periodic scan of existing torrents: only adds, rechecks and Start are checked.

When an add is refused

Message (abridged) Cause Fix
no save path: category "x" is unknown and no savepath was given No explicit save path, and the category is unknown, empty, or has an empty save path (for example one created through the shim's createCategory without savePath). Create the category with a save path (Categories and Routing) or send a save path.
no save path: give one, or a category that has one The subfolder option on a single-file torrent, with no save path to put the folder in. As above.
no engine … / no engine named … on this node The target engine does not exist. Both native routes refuse an unknown engine (400), magnets included; MCP magnets fall back to the category; a watched folder refuses an unknown engine when it is saved (400), and falls back only if that engine was removed afterwards. Check the engine id on the Nodes tab.
mode must be race or hoard, not "x" / no engine has the hoard role mode is neither race nor hoard, or no engine carries that role. Send race or hoard, or name the engine.
give exactly one of torrent_path, torrent_url or magnet_uri POST /api/torrents got none or several. Send one.
<path>: no such file (404) torrent_path does not exist on the daemon's disk (a URL typed as a path ends here too). Give a path the daemon sees, or use torrent_url.
torrent_url must be an http(s) URL / <url> answered 404 … The .torrent URL is not http(s), or the fetch failed. Check the link and its passkey.
race disk full: … GB free of disk, … GB already promised … Race admission: only for a race-role engine, only when [race_drain] add_block_enabled = true (default false). Free space (of the race quota when one is set: of N.N TB quota) minus what accepted downloads still have to write minus reserve_free_gb (default 0) is smaller than the torrent. An unreadable volume lets the add through. Free space, or see Race Drain, Graduation and Seed Obligations.
N file(s) of this torrent are already another torrent's with other bytes (…); adding it there would overwrite them [dedup] path_collision = "refuse" and another torrent names these files with other bytes. Pick the release, give another save path, or use isolate.
torrent already added / already added That engine already holds the hash. Nothing to do. The shim answers Ok., a watched folder moves the file to added/, a resolved magnet is dropped quietly; the native routes (409) and MCP report it as an error.
torrent file did not parse Not a valid .torrent. —
this magnet has only a v2 info hash (btmh) v2-only magnet. Add the .torrent file.
contentLayout NoSubfolder on a multi-file torrent is not supported Shim add (or its magnet, once resolved) asking to strip a multi-file torrent's folder. Use Original or Subfolder.

Importing a whole library

To bring an existing qBittorrent or Transmission library in one go (categories, stopped states, seed mode for complete torrents whose data is found), use the import wizard: Config › Advanced settings (default.toml) › Import tab › Import… (under the heading Import from another client). It is described step by step on Migrating from qBittorrent.

⚠️ **GitHub.com Fallback** ⚠️