RSS and Automatic Downloads - Kheopsian/Hydranos GitHub Wiki
π¬π§ English Β· π«π· FranΓ§ais
Applies to Hydranos 4.4.
For anyone who wants Hydranos to pick up new releases from a tracker's RSS feed on its own, the way qBittorrent's RSS downloader does. You will learn what a feed and a rule are, where to set them in the RSS tab, how a rule decides to take an article, which network path a feed is fetched through, what the [rss] section changes, which API routes exist (native and qBittorrent-compatible), and what is still missing in 4.4. Terms: Concepts and Glossary.
RSS is new in 4.4: Hydranos 4.3 had no RSS at all.
- A feed is a URL that returns RSS 2.0 or Atom (a tracker's RSS, usually with your passkey in it). Hydranos fetches it on its own interval and keeps its items, called articles.
- A rule (an automatic download rule) is qBittorrent's rule document: text filters, an optional episode filter, the feeds it watches, and where to add what it takes (a category, optionally a save path).
- When a rule matches an article, the article's torrent is added through the same add pipeline as any other add: the category picks the engine and the folder, the path claim, dedup and the "on add" workflows apply (Adding Torrents and Existing Data).
Do you need it? Sonarr, Radarr and autobrr read RSS themselves and send finished choices to Hydranos through torrents/add: they need nothing on this page. It is for setups without them, and for tools or scripts that drive qBittorrent's own RSS downloader through /api/v2/rss/* β those routes exist in 4.4 (see API).
Where Hydranos differs from a plain qBittorrent RSS setup:
- A feed and its
.torrentfiles are fetched through the network path of the engine the torrent will go to (tunnel, interface or proxy), not through the host's default route. See Which way out a feed takes. - An article taken once (added, already present, or refused) is never taken again by any rule: it is remembered by its guid and its torrent link, so a feed that republishes the same link under a new guid does not add it twice. What was taken stays remembered when the feed is deleted.
- A refusal by the add pipeline is final; a network failure is retried. See What happens to an article.
Feeds, folders, articles, rules and the "taken" record are stored in hydra.db (tables rss_feeds, rss_folders, rss_articles, rss_rules, rss_taken) and survive a restart.
The RSS tab sits between Workflows and Trackers. It is loaded when you open it and after each action, not refreshed every second; after Refresh all it reloads once more 3 seconds later. Like other tabs, it can be hidden in Config βΊ Personalisation.
RSS feeds section:
| Element | What it does |
|---|---|
| Refresh all | Fetches every feed in the background (POST /api/rss/refresh). |
| + Feed | Opens the feed form: URL (placeholder https://tracker.example/rss?passkey=...), Name (Folder\Name groups feeds; empty = the host), Refresh every (minutes, minimum 5, prefilled with the default, 30), Save, Cancel. |
| Red banner |
RSS is switched off by [rss] enabled = false: feeds are kept, nothing is fetched or added. β shown when [rss] enabled = false. True for the automatic side only, see Known bugs. |
| Feed table |
Feed (path, masked URL, the feed's own title), State, Last refresh, Articles ({u} unread of {n}), Every ({m} min), and per row Articles, Refresh, Edit, Delete (deletes the feed and its articles after a confirmation). |
State reads, in this order: refreshing...; blocked by the kill switch (hover for the reason); the last error; not fetched yet; else OK with via β¦ (the network path used).
Articles opens the Articles of {name} panel: Download to category (a category picker, - none - by default), Mark all read, Close, and a table Date, Title (linked to the article's page when it has one), Size (blank when the feed does not say), State (new, read, or taken by {rule}: {outcome}), with a Download button per article (greyed out when the article has no torrent link). Download adds that article now, through the same path as a rule, into the category picked above; the result line says Added: {title} or Already in this client: {title}. The article is then recorded as taken by (manual), so no rule adds it later. The tab sends only the article and the category: no save path, never paused (the API takes both, see API).
Automatic download rules section: + New rule, then the rule table Enabled (a checkbox you can click), Name, Must contain (with not: β¦ under it for Must not contain), Category, Feeds, Last match, and per row Edit, Delete.
The rule form:
| Field | Rule document key | Notes |
|---|---|---|
| Name | (the rule's name) | Renaming in the form renames the rule, then saves it. |
Category β picks the engine and the folder
|
assignedCategory / torrentParams.category
|
- none - = no category: see Where an added torrent goes. |
Must contain (placeholder show 1080p|show 2160p) |
mustContain |
See Filters. |
Must not contain (placeholder cam|hdts) |
mustNotContain |
|
Episode filter β 1x1-10;12;20-; or S01E01-S01E10
|
episodeFilter |
|
Ignore later matches for β days, 0 = never
|
ignoreDays |
|
Save path β optional, else the category's
|
savePath / torrentParams.save_path
|
|
| Enabled (ticked by default) | enabled |
|
| Use regular expressions | useRegex |
|
| Smart episode filter | smartFilter |
|
| Add paused |
addPaused / torrentParams.stopped
|
|
| Feeds (one checkbox per feed) |
affectedFeeds (the feed URLs) |
A rule only sees the feeds ticked here. |
Buttons: Preview, Save, Cancel. Preview lists, feed by feed (50 titles at most per feed), the articles already fetched that the draft would match, and says {n} matching articles, {m} of them would be added (unread, not taken yet). Nothing is added. Preview ignores Enabled, and it judges the rule alone: an article another rule would take first is still counted.
The form has no field for the rule's priority or its tags; it keeps the stored priority, and loses the tags (see Known bugs).
The outcome of every rule (and of every manual Download) is written in the Workflows tab's Activity table: Workflow RSS: <rule name> (or RSS: (manual)), Action rss_add, Outcome applied, skipped (detail already in this client) or failed (with the reason). See Workflows.
-
URL:
http://orhttps://only (anything else: 400). The same URL cannot be added twice (409). -
Name and folders: a feed lives at a path like qBittorrent's,
Folder\Namewith a backslash. Missing folders are created. An empty name becomes the host of the URL (never the whole URL, which would show the passkey wherever the name is displayed), with(2),(3)β¦ if taken. A path that is already a feed or folder, or that would sit inside a feed, is refused (409). Folders are visible through the API and the shim; the tab shows each feed's full path. -
Interval: each feed has its own,
refresh_secs. A new feed gets[rss] refresh_minutes(default 30 minutes); the minimum is 5 minutes, and any smaller value is raised to it. A feed is due when its last fetch is older than its interval; a feed never fetched is due at once. - Fetch limits: 30-second timeout, 5 MiB per feed, the client's User-Agent. At most 4 feeds are fetched at the same time by the background worker; a feed already being fetched is not fetched twice.
-
Articles kept: the newest
[rss] max_articlesper feed (default 200, between 1 and 10 000), merged by guid at each fetch. Older ones are dropped. - Errors: a failed fetch (network, HTTP status, too large, unreadable XML, closed way out) does not raise an API error; it is stored as the feed's last error and shown in State. The articles already stored stay.
-
Changing the URL (Edit,
PATCH, orrss/setFeedURL) makes the rules that named the old URL follow the new one, and fetches the feed again at the next turn. - Deleting a feed deletes its articles, not the record of what was taken: re-adding the feed later does not re-add those torrents.
What an article is. Its identity is <guid> (<id> in Atom), else its torrent link, else its page link, else its title. Its torrent link is the enclosure of type application/x-bittorrent, else a magnet in <link>, else any other enclosure, else a magnetURI element, else <link>. Its size is the enclosure's length, else a torznab/newznab size attribute, contentLength or <size>; 0 = unknown. Its date is pubDate (published / updated in Atom); an article without a date gets the time it was first seen. The XML reader skips what it does not understand (DTDs, processing instructions, namespaces) rather than refusing the feed.
Secrets in URLs. Logs and error messages show feed and torrent URLs masked: query replaced by ?***, credentials removed, any path segment of 16 or more letters, digits, - or _ with at least one digit replaced by ***, a magnet reduced to its xt=. The tab shows the masked URL. The native API returns the URL in clear (url) and masked (url_masked): anyone with the API key can read your passkeys there.
A rule takes an article only if its title passes every filter. Case is ignored everywhere.
Must contain, without regular expressions:
-
|separates alternatives: one is enough. - Inside an alternative, spaces separate words that must all be in the title, in any order.
-
*(any run of characters) and?(one character) are wildcards. - Empty = every title. An empty alternative (
foo|) also matches every title, as in qBittorrent.
show 1080p|show 2160p takes a title that contains both show and 1080p, or both show and 2160p.
Must not contain: the same syntax; an article is rejected if any alternative matches. Empty alternatives are ignored here (cam| rejects only cam). There is no -word exclusion syntax: use Must not contain.
Use regular expressions: each of the two fields is one case-insensitive regular expression. A regular expression that does not compile is refused when the rule is saved (400), not silently ignored.
Episode filter, two syntaxes:
- qBittorrent's:
1x2;8-15;30-;= season 1, episode 2, episodes 8 to 15, and episode 30 onwards β the open range continues into the following seasons. Season 0 is refused; a reversed range (15-8) is ignored. - Season-episode ranges:
S01E01-S01E10;S02E05;S03E01-;β a range may cross seasons;;or,separate entries.
Titles are read as S01E05, s1.e5, S01 E05 or 1x05. A title with no episode number β a season pack, for instance β never matches a rule that has an episode filter. A filter that does not read is refused at save (400, with the expected forms in the message).
Smart episode filter: once the rule took an episode (key S01E05, or the date YYYY-MM-DD of a daily show), the same episode is not taken again, unless the new title contains REPACK or PROPER (always allowed; there is no setting for it). Taken episodes are kept in the rule's previouslyMatchedEpisodes.
Ignore later matches for N days: after the rule takes something, an article dated less than N days after that match is not taken. 0 = off. The date compared is the article's, so older articles found later do not wake the rule either.
Order between rules: rules are tried by priority (lower first; 0 by default), then by name. The first enabled rule that names the feed and matches the article takes it; the others never see that article.
The rule's Category decides, exactly as for any add (Categories and Routing):
- a category in
hoardmode β the hoard engine; any other category β the race engine; - the folder is the rule's Save path if set, else the category's save path;
- no category, or an unknown one β the race engine, and no folder: unless the rule has a Save path, every add is refused with
no save path: category "<name>" is unknown and no savepath was given(""with no category) (or, for a single-file.torrentwhen[daemon] create_torrent_folder = true,no save path: give one, or a category that has one) β and a refusal is final (the article is recorded as taken). Give every rule a category that has a save path, or a save path.
A .torrent also goes through [daemon] create_torrent_folder (single-file torrents in their own folder), the race disk admission check, the path claim and dedup, and fires the when a torrent is added workflows. A magnet goes through the magnet queue and is resolved later, like any magnet add. The rule's tags (torrentParams.tags, set through the API) are applied; Add paused adds it stopped.
Download order (file priorities, sequential, first/last piece) is not an RSS setting: it is set on the torrent after the add, see Adding Torrents and Existing Data (Download order).
flowchart TD
T["Worker turn: every 30 s, or at once when a feed is added or a rule changes"] --> EN{"[rss] enabled?"}
EN -- no --> OFF["Nothing fetched, no rule runs"]
EN -- yes --> DUE["Feeds due: never fetched, or interval passed; 4 at a time"]
MAN["Refresh, Refresh all, rss/refreshItem"] --> X
DUE --> X{"Way out of the feed's engine open?"}
X -- no --> E1["last error: not fetched, the way out is closed"]
X -- yes --> F["Fetch through that engine's tunnel, interface or proxy: 30 s, 5 MiB"]
F -- "network error, HTTP error, too large, not a feed" --> E2["last error set, stored articles kept"]
F -- ok --> M["Merge by guid, keep the newest max_articles"]
M --> EN2{"[rss] enabled?"}
EN2 -- no --> KEEP["Articles stored, no rule runs"]
EN2 -- yes --> A["Each unread article nothing took yet, oldest first"]
DUE -- "after the fetches, if a rule changed: every feed, no fetch" --> A
A --> R{"First enabled rule naming this feed that matches?"}
R -- none --> N["Left unread, offered again at later passes"]
R -- yes --> K{"Torrent link"}
K -- magnet --> MQ["Magnet queue"]
K -- "http or https" --> G["Fetch the .torrent through the way out of the rule's engine: 30 s, 10 MiB, must parse"]
K -- "none or other scheme" --> RF
G -- "way out closed, network error, not a torrent" --> TR["failed: stays unread and untaken, tried again after the next good fetch of this feed"]
G -- ok --> P["Add pipeline: category picks engine and folder, path claim, dedup, on-add workflows"]
MQ -- queued --> OK
MQ -- "already added" --> SK
MQ -- refused --> RF
P -- added --> OK["Taken (added), marked read; Activity: applied"]
P -- "already in this client" --> SK["Taken (already), marked read; Activity: skipped"]
P -- refused --> RF["Taken (refused: reason), marked read; Activity: failed; final"]
| Outcome | Taken record (taken.outcome) |
Article | Activity | Retried? |
|---|---|---|---|---|
| Added | added |
marked read | applied |
β |
| The torrent is already in this client | already |
marked read |
skipped, already in this client
|
β |
| Refused by the add pipeline (no save path, race disk full, path claimed, no torrent link, an unaddable linkβ¦) | refused: <reason> |
marked read |
failed, the reason |
No, never by a rule. Download in the Articles panel still adds it by hand. |
| The engine's way out is closed, a network error, or the answer is not a torrent | none | stays unread |
failed, reason + ; tried again at the next pass
|
Yes, after the next successful fetch of that feed |
After an added or already outcome, the rule's Last match is set to now and, with the smart filter on, the episode is recorded. A successful fetch always runs the rules on that feed, even if it brought nothing new: that is how a transient failure gets its second try. Saving or toggling a rule makes the next worker turn offer every feed's unread articles to the rules again, without fetching. Only one pass runs at a time, so two fetches ending together cannot take the same article twice.
Download in the Articles panel skips the rules, the enabled check and the "taken" check: it adds the article even if a rule took or refused it before, then records it as taken by (manual).
A tracker feed carries your passkey, and the tracker sees the address the feed is fetched from. So a feed is fetched by the way out of the engine its torrents will go to:
- the engine of the first enabled rule (by priority, then name) that names the feed β through that rule's category β else the engine an add without a category goes to (race);
- that engine's WireGuard tunnel, bound interface or SOCKS5 proxy, as
[daemon] egress = "engine:<id>"would resolve it; - then that engine's
announce_proxyif it has one (the route its announces take), else, when the route has no proxy yet,TYPHON_ANNOUNCE_PROXYif set; - an engine this node does not run: direct when the kill switch is disarmed, refused when it is armed.
A .torrent link is fetched by the way out of the engine its rule adds to (for a manual Download: the engine of the category picked). A feed shared by rules of two engines is fetched by the first rule's way out.
If the kill switch keeps that engine off the network, nothing is fetched: the feed's state is blocked by the kill switch and its last error reads not fetched: the way out of engine <id> is closed (β¦). The State column's via β¦ line and the API's engine / via / blocked fields tell you which path was used. Details: VPN and Egress, Managed WireGuard.
[rss] is optional and is not in the shipped default.toml: without it, the defaults below apply. Add it to the configuration file and restart Hydranos.
[rss]
enabled = true # false: the background worker fetches nothing and rules add nothing
max_articles = 200 # articles kept per feed, the newest (1 to 10000)
refresh_minutes = 30 # interval given to a NEW feed; minimum 5| Key | Default | Effect |
|---|---|---|
enabled |
true |
false stops the background worker and the rules. Feeds, articles and rules are kept. The manual Refresh and Download still work (see below). |
max_articles |
200 |
Articles kept per feed after each fetch, newest first. Clamped to 1β10 000. |
refresh_minutes |
30 |
The interval of feeds created afterwards (UI default, POST /api/rss/feeds without refresh_secs, rss/addFeed without refreshInterval). Existing feeds keep their own refresh_secs. Below 5 β 5. |
Not configurable: the 30-second worker tick, 4 parallel fetches, 30-second timeout, 5 MiB per feed, 10 MiB per .torrent. qBittorrent's RSS preferences (rss_refresh_interval, rss_processing_enabled, rss_auto_downloading_enabled, rss_smart_episode_filters, β¦) are not part of the shim's app/preferences and are not read by app/setPreferences: [rss] is the only switch.
All routes need the API key (X-Api-Key header or apikey parameter) or a logged-in session, like the rest of the API (Security and Access).
| Route | Method | What it does |
|---|---|---|
/api/rss |
GET | Everything the tab shows: enabled, max_articles, default_refresh_secs, min_refresh_secs, feeds, folders, rules. |
/api/rss/feeds |
GET, POST | List feeds; add one with {"url","path" (or "name"),"refresh_secs"} β 201. A feed: id, uid, path, name, url, url_masked, title, refresh_secs, last_refresh, next_refresh, last_error, loading, articles, unread, engine, via, blocked. |
/api/rss/feeds/:id |
PATCH, DELETE | Change path (or name), url, refresh_secs; delete the feed and its articles. |
/api/rss/feeds/:id/refresh |
POST | Fetch now; answers when done, with the feed (last_error says how it went). |
/api/rss/refresh |
POST | Fetch every feed in the background: 202 {"refreshing": n}. |
/api/rss/feeds/:id/articles |
GET | Newest first (?unread=1 for unread only): guid, title, torrent_url, link, description, author, category, date, size, read, taken ({"rule","at","info_hash","outcome"} or null). |
/api/rss/feeds/:id/read |
POST |
{"guid"} marks one article read, {} all. |
/api/rss/feeds/:id/download |
POST | Add one article by hand: {"guid","category","save_path","paused"} β {"info_hash","already"}. 400 refused by the add (reason given), 502 the .torrent could not be fetched. |
/api/rss/folders |
POST |
{"path"} β 201. |
/api/rss/rules |
GET | Every rule: qBittorrent's document plus name, in priority-then-name order. |
/api/rss/rules/:name |
PUT, DELETE | Create or replace (body = the rule document); delete. |
/api/rss/rules/:name/rename |
POST |
{"name"} (or new_name). 409 if taken. |
/api/rss/rules/:name/matching |
GET | What a saved rule would take: {"count","would_add","feeds":[{"id","path","name","articles"}]}. Nothing is added. |
/api/rss/preview |
POST | The same for a draft (body = a rule document). |
Errors are {"error": "β¦"} with 400 (bad value), 404 (unknown feed, folder or rule), 409 (conflict) or 500 (store).
The document is qBittorrent's (rss/setRule), so a rule exported from qBittorrent can be sent as it is. Fields kept and written back: enabled (default true), priority (default 0, lower first), useRegex, mustContain, mustNotContain, episodeFilter, affectedFeeds (feed URLs), ignoreDays, lastMatch (RFC 2822 date), smartFilter, previouslyMatchedEpisodes, and the add options in both of qBittorrent's spellings: 4.x's top-level assignedCategory, savePath, addPaused, and 4.6+/5.x's torrentParams {category, save_path, tags, stopped (or paused)}. Both spellings are written back; torrentParams wins when both are given.
- A text, boolean or list field of the wrong type (
enabled,useRegex,smartFilter,mustContain,mustNotContain,episodeFilter,affectedFeeds,previouslyMatchedEpisodes,assignedCategory,savePath) is refused (400).priority,ignoreDays,lastMatch,addPausedand thetorrentParamsfields of the wrong type are read as their default (0 / not set), without an error. - A save without
lastMatchorpreviouslyMatchedEpisodeskeeps the stored values, so an editor that does not show them does not resetignoreDaysand the smart filter. - Any other qBittorrent field is not kept:
torrentContentLayoutis written back asnull,torrentParams.skip_checkingasfalse, and othertorrentParamsoptions (content layout, automatic management, limitsβ¦) are dropped.
# Add a feed
curl -s -H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://tracker.example.org/rss?passkey=YOUR_PASSKEY","path":"TV\\Tracker","refresh_secs":900}' \
http://localhost:8199/api/rss/feeds
# Create a rule with tags (the web form cannot set tags)
curl -s -X PUT -H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"enabled":true,"mustContain":"show 1080p","mustNotContain":"cam|hdts",
"episodeFilter":"S02E01-","smartFilter":true,
"affectedFeeds":["https://tracker.example.org/rss?passkey=YOUR_PASSKEY"],
"torrentParams":{"category":"tv","tags":["rss"]}}' \
http://localhost:8199/api/rss/rules/Show%201080pThese 13 routes of qBittorrent's RSS API exist, with the same parameters (query string or form body):
| Route | Parameters | Notes |
|---|---|---|
rss/addFolder |
path |
|
rss/addFeed |
url, path, refreshInterval
|
refreshInterval in seconds; minimum 300. |
rss/setFeedURL |
path, url
|
Rules follow the new URL. |
rss/removeItem |
path |
A folder is removed with everything under it. |
rss/moveItem |
itemPath, destPath
|
|
rss/refreshItem |
itemPath |
A feed, or every feed under a folder; background. |
rss/markAsRead |
itemPath, articleId
|
Without articleId: everything under the path. |
rss/items |
withData |
The folder tree; each feed uid, url, and with withData=true: title, lastBuildDate, isLoading, hasError, refreshInterval, articles (id, date, title, author, category, description, torrentURL, link, isRead). |
rss/setRule |
ruleName, ruleDef (JSON) |
|
rss/renameRule |
ruleName, newRuleName
|
|
rss/removeRule |
ruleName |
|
rss/rules |
β | Rules keyed by name. |
rss/matchingArticles |
ruleName |
{"<feed name>": [titles]}; {} for an unknown rule. |
Writes are POST; items, rules and matchingArticles answer any method. A missing parameter β 400 Missing parameter: X; a malformed value (bad URL, ruleDef not JSON, bad regex) β 400; any other refusal β 409 with the reason in plain text. Shim details: qBittorrent Shim and Automation.
curl -s -H "X-Api-Key: YOUR_API_KEY" http://localhost:8199/api/v2/rss/setRule \
--data-urlencode 'ruleName=Show 1080p' \
--data-urlencode 'ruleDef={"enabled":true,"mustContain":"show 1080p","affectedFeeds":["https://tracker.example.org/rss?passkey=YOUR_PASSKEY"],"assignedCategory":"tv"}'Known bug in 4.4: saving a rule from the web form looks like it changes only what the form shows β it erases the rule's tags (
torrentParams.tags, set through the API or a qBittorrent client), because the form never sendstorrentParamsβ set tags withPUT /api/rss/rules/:nameorrss/setRule, and do not edit those rules in the form afterwards (the Enabled checkbox in the rule table is safe: it sends the whole document back).
Known bug in 4.4:
[rss] enabled = falseand its banner (nothing is fetched or added) look like a full stop β they stop only the background worker and the rules: Refresh / Refresh all (andPOST /api/rss/feeds/:id/refresh,/api/rss/refresh,rss/refreshItem) still fetch the feed, and Download (POST /api/rss/feeds/:id/download) still adds β delete the feeds if nothing at all may reach the tracker.
Not implemented in 4.4: the rule form has no priority and no tags field, and the Articles panel's Download has no save path and no paused option β use the API (
priorityandtorrentParams.tagsin the rule document;save_pathandpausedon/api/rss/feeds/:id/download).
Not implemented in 4.4: RSS in the MCP server and in the qBittorrent import β no MCP tool reads or edits feeds or rules (MCP Server), and the import wizard brings torrents, not qBittorrent's feeds or rules (Migrating from qBittorrent) β re-create them:
rss/setRuleaccepts qBittorrent's rule JSON as is.
Accepted but ignored in 4.4: qBittorrent's RSS preferences (
rss_refresh_interval,rss_processing_enabled,rss_auto_downloading_enabled,rss_smart_episode_filters, β¦) and the rule fieldstorrentContentLayout,skip_checkingand the othertorrentParamsoptions β they are not stored β use[rss]and the fields listed in The rule document.
Also by design in 4.4:
-
[rss]is not in the shippeddefault.toml; it appears only once you add it. - A refusal by the add pipeline (no save path, race disk fullβ¦) is final for the rules; re-add such an article with Download.
-
REPACK/PROPERalways get past the smart filter; episode keys are the fixed forms above (no custom episode patterns). - A season pack (no episode number in the title) never matches a rule with an episode filter.
- Some RSS tab errors (Refresh, Refresh all, Delete, Mark all read, a rule's Enabled checkbox) and the delete confirmations use the browser's own
alert()/confirm()dialogs, unlike the rest of the interface; the feed and rule forms, Preview and Download show their errors inline.
Adding Torrents and Existing Data (the add pipeline, download order) Β· Categories and Routing Β· Workflows (Activity) Β· VPN and Egress Β· Managed WireGuard Β· qBittorrent Shim and Automation Β· REST API Β· Configuration: Daemon, Auth and Global Sections Β· Known Limitations