RSS Feeds
Subscribe, regex-filter, auto-add. ETag-aware fetches and SSRF-validated URLs.
Mosaic polls RSS feeds at a configurable interval and auto-adds items matching a per-feed regex filter. The poller is implemented in backend/api/rss_poller.go (constructed with api.NewRSSPoller); the auto-add path goes through the same api.Service that the UI calls, so RSS-added torrents look identical to manually-added ones.
Feeds
A feed is {id, url, name, interval_min, etag, last_polled, enabled}.
url— fetched over HTTPS (HTTP also accepted). Validated throughvalidateFetchURL: must behttp/https, must not point at a private/loopback address. The dialer insafeHTTPClientis a second layer that catches DNS-rebind tricks. (safeHTTPClientitself does not impose a body cap; the 50 MiB body cap mentioned elsewhere is enforced byRefreshBlockliston the blocklist refresh path, not the RSS poller.)name— display label; doesn’t have to match the feed’s<title>.interval_min— minutes between polls.etag— lastETagheader observed. Mosaic sends it back asIf-None-Matchon the next poll, so unchanged feeds return 304 and don’t re-process.last_polled— unix seconds; updated on every successful fetch (304 included).enabled— disable to pause without deleting.
Operations
| Action | Endpoint |
|---|---|
| List | GET /api/feeds |
| Create | POST /api/feeds |
| Update | PUT /api/feeds |
| Delete | DELETE /api/feeds/{id} |
URL validation runs on both create and update; bad URLs return 400 with the validation error inline.
Filters
A filter belongs to a feed and is {id, feed_id, regex, category_id, save_path, enabled}.
regex— Go’sregexpsyntax (RE2). Empty string matches every item.category_id— optional. When set, the auto-added torrent has its category set after add viaSetTorrentCategory. The poller does not consult the category’sdefault_save_pathto pick a save path.save_path— optional explicit save path. The poller passesfil.SavePathdirectly toAddMagnet. There is no automatic fallback to the category’sdefault_save_path— set the save path on the filter itself if you want a category-specific destination.enabled— disable to pause without deleting.
The matcher tests the item’s <title> (most feeds put the descriptive name there). When at least one enabled filter on the feed matches, the item is added; if multiple match, the first one wins for category/save-path resolution.
Operations
| Action | Endpoint |
|---|---|
| List for a feed | GET /api/feeds/{feedID}/filters |
| Create | POST /api/filters |
| Update | PUT /api/filters |
| Delete | DELETE /api/filters/{id} |
Examples
// Auto-add every weekly Linux ISO release matching a name pattern,
// route into the "ISOs" category.
{
"feed_id": 1,
"regex": "(?i)(ubuntu|fedora|debian).*?(amd64|x86_64)\\.iso",
"category_id": 3,
"save_path": "",
"enabled": true
}
// Catch-all: anything from a personal feed goes to the global default.
{
"feed_id": 4,
"regex": "",
"category_id": null,
"save_path": "",
"enabled": true
}
What gets added
The poller extracts a magnet URI from each matching item via extractMagnet (looks at <enclosure>, <link>, and text-mines <description>). Only magnet URIs are accepted — .torrent URLs are not fetched by the poller; if a feed publishes only .torrent enclosures with no matching magnet, the items are skipped. Validated magnets are added the same way POST /api/torrents/magnet would. Failures are logged but don’t abort the poll — a single malformed item won’t strand the rest of the feed.
Each successfully added torrent emits the next torrents:tick frame with the new row, so any connected SPA / API client sees it within ~1 second of the poll completing.
Caveats
- Per-feed dedup via a
seenByIDset. The poller tracks seen item IDs per feed (capped atrssSeenCap = 1000inrss_poller.go); the second time a feed item with the same ID appears, it’s skipped before reaching the engine. Combined with anacrolix refusing duplicate info-hashes, this keeps re-poll noise down. Loose-pattern filters can still log noise on the first sighting of every match. - No cron-style scheduling.
interval_minis a fixed period fromlast_polled. Polls don’t align to a wall-clock minute. - Fetch timeout. Each fetch is bounded by
safeHTTPClient’s 30-second timeout. There is no body-size cap on the RSS path — feeds that publish multi-megabyte XML payloads will be fully buffered. (The 50 MiB body cap elsewhere in the codebase is onRefreshBlocklist, not RSS.)