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 through validateFetchURL: must be http/https, must not point at a private/loopback address. The dialer in safeHTTPClient is a second layer that catches DNS-rebind tricks.
  • name — display label; doesn’t have to match the feed’s <title>.
  • interval_min — minutes between polls.
  • etag — last ETag header observed. Mosaic sends it back as If-None-Match on 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}
Poll now PollFeedNow(id) (Wails only)
Browse items GET /api/feeds/{id}/items

URL validation runs on both create and update; bad URLs return 400 with the validation error inline.

The Poll now action is a per-feed refresh icon in the RSS pane that triggers an immediate poll, bypassing the configured interval. It runs pollOne against the feed and persists the same LastPolled / ETag side effects as the periodic ticker would have.

Filters

A filter belongs to a feed and is {id, feed_id, regex, category_id, save_path, enabled}.

  • regex — Go’s regexp syntax (RE2). Empty string matches every item.
  • category_id — optional. When set, the auto-added torrent has its category set after add via SetTorrentCategory. The poller does not consult the category’s default_save_path to pick a save path.
  • save_path — optional explicit save path. The poller passes fil.SavePath directly to the engine. There is no automatic fallback to the category’s default_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

For each matching item, the poller looks for a torrent source in two passes:

  1. Magnet URI. Extracted via extractMagnet — checks <link>, each <enclosure> URL, and torznab/RSS extension elements (magnetURI, magneturl). Passed straight to AddMagnet.
  2. Direct .torrent URL. Extracted via extractTorrentURL — checks <link> and enclosure URLs whose type is application/x-bittorrent or whose URL contains .torrent. The poller then issues a 10 MiB-capped HTTP fetch through the same SSRF-validated client, and passes the bytes to AddTorrentBytes.

Magnet wins when both are present. Items with neither are marked seen and skipped on subsequent polls.

Each successfully added torrent emits on the next torrents:tick frame with the new row, so any connected SPA / API client sees it within ~1 second of the poll completing. Failures (bad URL, fetch error, engine refusal) are logged but don’t abort the poll — a single broken item won’t strand the rest of the feed.

Live feed browser

The RSS settings pane has an expandable items list per feed: click the disclosure triangle on a feed row to fetch the feed live (bypassing dedup) and see a list of every item with its title, publication date, and a per-item Add button. The Add button drops the torrent into the global default save path without authoring a regex filter — useful for cherry-picking from a feed you don’t want to fully automate.

There’s a “FOSS Torrents” preset in the empty state so the first interaction is one click away from a working public-domain feed.

The browser uses the same magnet-or-.torrent extraction as the periodic poller; an item with no resolvable source shows up with no Add button.

Browser API

GET  /api/feeds/{id}/items    → FeedItemDTO[]
POST /api/torrents/url        body: { "url": "magnet:...|https://...torrent", "save_path": "" }
// FeedItemDTO
{ guid: string; title: string; pub_date: string; torrent_url: string }

torrent_url is the magnet URI when present, otherwise the .torrent URL, otherwise empty.

Caveats

  • Per-feed dedup via a seenByID set. The poller tracks seen item IDs per feed (capped at rssSeenCap = 1000 in rss_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_min is a fixed period from last_polled. Polls don’t align to a wall-clock minute. Use Poll now for one-shot refreshes.
  • Fetch timeout. Each fetch is bounded by safeHTTPClient’s 30-second timeout. There is no body-size cap on the RSS XML path itself (the 50 MiB cap elsewhere is for blocklists). The .torrent URL fetch path does cap at 10 MiB — large .torrent files past that are truncated and refused by anacrolix.
  • Live feed browser bypasses dedup. The browser always re-fetches the feed when you expand it; if a feed publishes 500 items in one shot, expanding it will pull all 500. Collapse and re-expand to re-fetch.