Django Feed Reader
django-feed-reader is a reusable Django app for subscribing to, fetching, and storing RSS, Atom, and JSON Feed sources.
It is designed as a backend library, not a complete reader application. It gives you Django models, polling utilities, admin integration, and read/unread helpers so you can build your own UI, APIs, and workflows on top.
What it provides
- RSS, Atom, and JSON Feed parsing
- Feed storage in Django models
- Automatic polling intervals based on feed activity
- Feed entry and enclosure persistence
- Single-user and multi-user read/unread tracking
- Optional raw JSON storage for uncommon feed attributes
- Optional Cloudflare workarounds via Dripfeed or a worker URL
- Django admin registrations for the core models
Requirements
- Python 3
- Django 3.2+
Installation
Install the package:
pip install django-feed-reader
Add feeds to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"feeds",
]
Run migrations:
python manage.py migrate
Set at least the polite-identification settings in your Django settings:
FEEDS_USER_AGENT = "ExampleReader/1.0"
FEEDS_SERVER = "https://example.com"
FEEDS_USER_AGENT is sent on outbound feed requests. FEEDS_SERVER is included in that user agent string so feed owners can identify your service.
Quick start
Create a feed source:
from feeds.models import Source
source = Source.objects.create(
feed_url="https://example.com/feed.xml",
)
Fetch it immediately:
from feeds.utils import read_feed
read_feed(source)
source.refresh_from_db()
Inspect the results:
source.name
source.description
source.posts.count()
source.posts.order_by("-created")[:10]
Core models
Source
Represents a single feed subscription.
Useful fields include:
feed_url: the URL fetched by the pollersite_url: the feed's corresponding website, when availablename: feed titledescription: feed description / summaryimage_url: feed icon or imagelast_result: human-readable result of the last fetchstatus_code: last HTTP status code seeninterval: next polling interval in minuteslive: whether the source should still be actively polled
Useful helpers include:
unread_countget_unread_posts()get_paginated_posts()mark_read()
Post
Represents one item / entry from a feed.
Important fields include:
titlebodylinkauthorcreatedguidimage_url
Enclosure
Represents media associated with a post, such as podcast audio or image attachments.
Useful helpers include:
is_imageis_audiois_video
Subscription
Represents a user following a Source.
Use Subscription when you need per-user read/unread tracking or folder-like grouping of sources.
Public utility functions
The main public API lives in feeds.utils.
read_feed(source)
Fetches one Source, parses it, and persists any changes.
Use this when:
- you want to fetch a feed immediately after creating it
- you are debugging a specific feed
- you need one-off refresh behavior
update_feeds(max_feeds=3)
Polls all due Source rows, ordered by due_poll, up to max_feeds.
Use this from cron, Celery, or another scheduled task runner.
test_feed(source, cache=False)
Performs a simple reachability test for a specific feed URL without going through the full persistence flow.
Subscription helpers
get_subscription_list_for_user(user)get_unread_subscription_list_for_user(user)
These help build folder trees and unread views for multi-user applications.
Polling behavior
The library automatically adjusts polling frequency based on whether a feed changes.
- Fastest poll frequency: 1 hour
- Slowest poll frequency: 24 hours
Feeds that change frequently are polled more often. Feeds that remain unchanged are polled less often.
The typical pattern is to run the poller every 5 to 10 minutes and let the library decide which sources are actually due.
Using the management command
The app includes:
python manage.py refreshfeeds
That command calls update_feeds(30).
Using Celery
from celery import shared_task
from feeds.utils import update_feeds
@shared_task
def refresh_feed_batch():
update_feeds(30)
Read/unread tracking
There are two supported patterns.
Single-user installations
If your project is effectively for one user, you can use the helper methods directly on Source.
source.unread_count
source.get_unread_posts()
source.mark_read()
Multi-user installations
If multiple users can follow the same feed, create Subscription rows.
from django.contrib.auth import get_user_model
from feeds.models import Source, Subscription
User = get_user_model()
user = User.objects.get(username="alice")
source = Source.objects.get(feed_url="https://example.com/feed.xml")
subscription = Subscription.objects.create(
user=user,
source=source,
name=source.display_name,
)
You can also create folder-like subscriptions by setting source=None and using parent relationships.
Settings
Required or strongly recommended
FEEDS_USER_AGENT- Example:
"ExampleReader/1.0" FEEDS_SERVER- Example:
"https://example.com"
If FEEDS_SERVER is not set, the library will derive a default from ALLOWED_HOSTS where possible.
Optional
FEEDS_VERIFY_HTTPS(default:True)-
Set to
Falseonly if you deliberately want to allow invalid HTTPS certificates. -
FEEDS_KEEP_OLD_ENCLOSURES(default:False) -
If a feed changes enclosure URLs over time, keep the old ones and mark them with
is_current=False. -
FEEDS_SAVE_JSON(default:False) - Store raw feed/parser data in the
jsonfields onSourceandPost. - Useful when you want access to custom or uncommon feed attributes.
-
Increases database usage.
-
FEEDS_DRIPFEED_KEY(default: unset) -
If present, Cloudflare-blocked feeds can be retried via Dripfeed.
-
FEEDS_CLOUDFLARE_WORKER(default: unset) - Optional alternate fetch endpoint used for Cloudflare-blocked feeds.
Cloudflare support
When a feed responds in a way that looks like Cloudflare protection, the library can:
- mark the source as Cloudflare-protected
- retry through Dripfeed if
FEEDS_DRIPFEED_KEYis configured - use
FEEDS_CLOUDFLARE_WORKERif configured
What this library does not do
- It does not ship a reader UI
- It does not define URLs or views for your application
- It does not download enclosure files for you
- It does not provide a complete end-user feed reader product
Development
From a checkout:
pip install -e ".[test]"
pytest
Notes for integrators
refreshfeedsis intentionally small and fixed toupdate_feeds(30). If you need different batching behavior, callupdate_feeds()from your own scheduler.- Redirect targets are validated before being followed or persisted.
- The app stores feed metadata and parsed content, but you remain responsible for presentation and any downstream content policies in your own application.
Documentation
Full documentation is available at django-feed-reader.readthedocs.io.