RSS feed aggregator and merger built with FastAPI, SQLModel, and Jinja2.
- Multiple feed sources: RSS/Atom, HackerNews, Instagram, Facebook
- Merge multiple feeds into one
- Digest feeds with cron scheduling
- Media caching (images, videos)
- JSON configuration with validation
- Server-rendered RSS and HTML output
- Python 3.11+
- uv
By default, all times in the UI are displayed in America/New_York timezone. You can customize this by setting the DISPLAY_TIMEZONE environment variable to any valid IANA timezone identifier:
# Run with custom timezone
DISPLAY_TIMEZONE=Europe/London uv run uvicorn rss_glue.main:app --reload
# Or set it permanently in your environment
export DISPLAY_TIMEZONE=America/Los_Angeles
uv run uvicorn rss_glue.main:app --reloadTimes are always stored in UTC in the database and converted to your display timezone only when shown in the web interface.
# Install dependencies
uv sync
# Run the server
poe devThe server runs at http://localhost:8000 (and listens on all interfaces).
- Go to http://localhost:8000/config
- Add your feed configuration (see format below)
- Click "Save Configuration"
- Go to http://localhost:8000 to see your feeds
- Click "Update All Feeds" to fetch posts
- Click "HTML" or "RSS" next to any feed to view output
{
"cache_media": false,
"feeds": [
{
"id": "hn-rss",
"type": "rss",
"name": "Hacker News RSS",
"url": "https://news.ycombinator.com/rss",
"limit": 50,
"cache_media": true
},
{
"id": "hn-api",
"type": "hackernews",
"name": "HN Top Stories",
"story_type": "top",
"limit": 30
},
{
"id": "tech",
"type": "merge",
"name": "Tech Combined",
"sources": ["hn-rss", "hn-api"],
"limit": 100
},
{
"id": "weekly",
"type": "digest",
"name": "Weekly Digest",
"source": "tech",
"schedule": "0 0 * * 0",
"limit": 20
}
]
}RSS Glue can automatically update feeds using a background worker.
To run only the background worker with or without the web server:
poe worker
poe devworker-
Regular feeds: Update based on
cooldown_minutes- Set to 0 or omit for manual-only
- Example:
"cooldown_minutes": 60updates hourly
-
Digest feeds: Update based on cron
schedule- Example:
"schedule": "0 0 * * 0"runs weekly
- Example:
-
Dependencies: Source feeds update before merge/digest feeds
- Worker updates dependencies even if not individually due
- Ensures fresh data for merged/digest outputs
Per-feed cooldown:
{
"feeds": [
{
"id": "hn",
"type": "rss",
"url": "https://news.ycombinator.com/rss",
"cooldown_minutes": 30
}
]
}Global default cooldown:
{
"default_cooldown_minutes": 60,
"feeds": [...]
}Manual-only feed:
{
"id": "manual-feed",
"type": "rss",
"url": "...",
"cooldown_minutes": 0
}Fetch posts from any RSS or Atom feed URL.
{
"id": "example",
"type": "rss",
"name": "Example Feed",
"url": "https://example.com/feed.xml",
"limit": 50,
"cache_media": true
}| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier (alphanumeric, hyphens, underscores) |
type |
Yes | Must be "rss" |
name |
Yes | Display name |
url |
Yes | RSS/Atom feed URL |
limit |
No | Max posts to fetch (default: 50, max: 1000) |
cache_media |
No | Cache embedded images/videos (default: use global) |
Fetch stories directly from HackerNews API.
{
"id": "hn",
"type": "hackernews",
"name": "HN Top",
"story_type": "top",
"limit": 30
}| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier |
type |
Yes | Must be "hackernews" |
name |
Yes | Display name |
story_type |
No | "top", "new", or "best" (default: "top") |
limit |
No | Max stories to fetch (default: 30, max: 500) |
Fetch posts from Instagram Business/Creator accounts.
{
"id": "my-ig",
"type": "instagram",
"name": "My Instagram",
"user_id": "17841400000000000",
"access_token": "EAAG...",
"limit": 20
}| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier |
type |
Yes | Must be "instagram" |
name |
Yes | Display name |
user_id |
Yes | Instagram Business Account ID |
access_token |
Yes | Facebook Graph API access token |
limit |
No | Max posts to fetch (default: 20, max: 100) |
Setup:
- Create a Facebook App at developers.facebook.com
- Add Instagram Graph API product
- Connect your Instagram Business/Creator account
- Generate a long-lived access token
- Get your Instagram Business Account ID from the API
Fetch posts from Facebook Pages.
{
"id": "my-page",
"type": "facebook",
"name": "My FB Page",
"page_id": "123456789",
"access_token": "EAAG...",
"limit": 20
}| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier |
type |
Yes | Must be "facebook" |
name |
Yes | Display name |
page_id |
Yes | Facebook Page ID |
access_token |
Yes | Page Access Token |
limit |
No | Max posts to fetch (default: 20, max: 100) |
Setup:
- Create a Facebook App at developers.facebook.com
- Add Facebook Login product
- Request pages_read_engagement permission
- Generate a Page Access Token for the target page
Merge posts from multiple source feeds into one.
{
"id": "combined",
"type": "merge",
"name": "All Feeds",
"sources": ["feed1", "feed2", "feed3"],
"limit": 100
}| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier |
type |
Yes | Must be "merge" |
name |
Yes | Display name |
sources |
Yes | Array of feed IDs to merge |
limit |
No | Max posts in output (default: 100, max: 1000) |
Create periodic digest issues from a source feed.
{
"id": "weekly",
"type": "digest",
"name": "Weekly Digest",
"source": "combined",
"schedule": "0 0 * * 0",
"limit": 20
}| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier |
type |
Yes | Must be "digest" |
name |
Yes | Display name |
source |
Yes | Source feed ID |
schedule |
Yes | Cron expression (e.g., "0 0 * * 0" for weekly) |
limit |
No | Max posts per digest (default: 20, max: 1000) |
| Field | Default | Description |
|---|---|---|
cache_media |
false | Cache embedded media from all feeds |
default_cooldown_minutes |
15 | Default cooldown for automatic updates (used when feed doesn't specify) |
When enabled, media (images, videos) embedded in posts are downloaded and served locally.
- Enable globally: Set
"cache_media": trueat the root config level - Enable per-feed: Set
"cache_media": trueon individual RSS feeds - Per-feed setting overrides global setting
Cached media is stored in the media/ directory.
| Method | Path | Description |
|---|---|---|
| GET | / |
Feed list page |
| GET | /config |
Config editor page |
| POST | /config |
Save configuration |
| GET | /feed/{id}/html |
HTML preview for a feed |
| GET | /feed/{id}/rss |
RSS output for a feed |
| POST | /feed/{id}/update |
Update a single feed |
| POST | /update |
Update all feeds |
| GET | /media/{path} |
Serve cached media |
- Feed IDs must be unique
- Feed IDs must be alphanumeric with hyphens and underscores only
- Merge/digest feeds can only reference existing feed IDs
- Circular dependencies are not allowed
- Cron expressions must be valid
# Install dev dependencies
uv sync --all-extras
# Run tests
uv run pytestSQLite database stored in rss_glue.db. Tables are created automatically on first run.
To reset the database, delete the file:
rm rss_glue.db