{"id":13523543,"url":"https://github.com/cristoper/feedmixer","last_synced_at":"2026-01-17T03:03:36.220Z","repository":{"id":49591831,"uuid":"88230593","full_name":"cristoper/feedmixer","owner":"cristoper","description":"A self-hosted API to fetch and mix entries from Atom and RSS feeds (returns Atom, RSS, or JSON)","archived":false,"fork":false,"pushed_at":"2025-09-02T17:06:22.000Z","size":314,"stargazers_count":215,"open_issues_count":4,"forks_count":13,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-09-02T19:04:49.787Z","etag":null,"topics":["api-server","atom","atom-feed","feed","python","rss","self-hosted"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"wtfpl","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cristoper.png","metadata":{"files":{"readme":"README.rst","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2017-04-14T03:45:38.000Z","updated_at":"2025-09-02T17:06:26.000Z","dependencies_parsed_at":"2023-10-03T15:01:56.750Z","dependency_job_id":"4fa1b977-05eb-4790-9e79-aae965e71c63","html_url":"https://github.com/cristoper/feedmixer","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/cristoper/feedmixer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cristoper%2Ffeedmixer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cristoper%2Ffeedmixer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cristoper%2Ffeedmixer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cristoper%2Ffeedmixer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cristoper","download_url":"https://codeload.github.com/cristoper/feedmixer/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cristoper%2Ffeedmixer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28492597,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T02:39:23.645Z","status":"ssl_error","status_checked_at":"2026-01-17T02:34:19.649Z","response_time":85,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-server","atom","atom-feed","feed","python","rss","self-hosted"],"created_at":"2024-08-01T06:01:01.125Z","updated_at":"2026-01-17T03:03:36.206Z","avatar_url":"https://github.com/cristoper.png","language":"Python","funding_links":["https://www.paypal.me/cristoper/5"],"categories":["Tools for Self-Hosting","Software","Python","Automation"],"sub_categories":["Automation"],"readme":"FeedMixer\n=========\nFeedMixer is a little web service (Python3/WSGI) which takes a list of feed\nURLs and  combines them into a single (Atom, RSS, or JSON) feed. Useful for\npersonal news aggregators, \"planet\"-like websites, etc.\n\nStatus\n------\n\nChangelog\n~~~~~~~~~\n\n- v2.5.1_ Fix regression that was preventing wsgi app from starting\n- v2.5.0_ Change logging from file to stderr. Configuration is now handled by environment variables: `FM_LOG_LEVEL \u003c#logging\u003e`_ for log level, `FM_ALLOW_CORS \u003c#cors\u003e`_ for CORS headers, `FM_TIMEOUT \u003c#timeout\u003e`_ for request timeouts, and `FM_CACHE_SIZE \u003c#cache-size\u003e`_ for the number of feed parse results to cache in memory.\n- v2.4.1_ Fix bug where RSS dates were potentially sorted incorrectly (d685db15_)\n- v2.4.0_ Migrate from pipenv to uv and update dependencies. `feedgenerator` now produces slightly different output including JSONFeed 1.1.\n- v2.3.2_ Update dependencies to use upstream feedparser now that the fix for `this bug \u003chttps://github.com/kurtmckee/feedparser/pull/260\u003e`_ has been merged.\n- v2.3.1_ More consistent builds: update dependencies in Pipfile.lock (which also seems to work better with newer versions of pipenv) and pin Dockerfile base image to specific hash\n- v2.3.0_ Replace on-disk cache with in-memory cache. This simplifies application code and administration (don't have to worry about pruning the cache database)\n- v2.2.0_ Fix handling of RSS feeds with missing pubDates so that they sort to the bottom instead of throwing an exception during sorting\n- v2.1.0_ Fix handling of RSS enclosures and Atom links so that they are included in output (important if you're trying to aggregate podcasts or similar)\n- v2.0.0_ The JSON output now conforms to `JSON Feed version 1`_. This breaks any client which depends on the previous ad-hoc JSON format. That legacy format will continue to be maintained in the `v1 branch`_, so any clients which don't want to update to the JSON Feed format should depend on that branch.\n\n- v1.0.0_ Stable API. I'm using it in production for small personal \"planet\"-like feed aggregators.\n\n.. _v2.5.1: https://github.com/cristoper/feedmixer/tree/v2.5.1\n.. _v2.5.0: https://github.com/cristoper/feedmixer/tree/v2.5.0\n.. _v2.4.1: https://github.com/cristoper/feedmixer/tree/v2.4.1\n.. _v2.4.0: https://github.com/cristoper/feedmixer/tree/v2.4.0\n.. _v2.3.2: https://github.com/cristoper/feedmixer/tree/v2.3.2\n.. _v2.3.1: https://github.com/cristoper/feedmixer/tree/v2.3.1\n.. _v2.3.0: https://github.com/cristoper/feedmixer/tree/v2.3.0\n.. _v2.2.0: https://github.com/cristoper/feedmixer/tree/v2.2.0\n.. _v2.1.0: https://github.com/cristoper/feedmixer/tree/v2.1.0\n.. _v2.0.0: https://github.com/cristoper/feedmixer/tree/v2.0.0\n.. _`JSON FEED version 1`: https://jsonfeed.org/\n.. _`v1 branch`: https://github.com/cristoper/feedmixer/tree/v1\n.. _v1.0.0: https://github.com/cristoper/feedmixer/tree/v1.0.0\n.. _d685db15: https://github.com/cristoper/feedmixer/commit/d685db15ab82d5c4268240aef7eedae051d7a2db\n\n\nAPI\n---\nFeedMixer exposes three endpoints:\n\n- /atom\n- /rss\n- /json\n\nWhen sent a GET request they return an Atom, an RSS 2.0, or a JSON feed, respectively. The query string of the GET request can contain these fields:\n\nf\n    A url-encoded URL of a feed (any version of Atom or RSS). To include multiple feeds, simply include multiple `f` fields.\n\nn\n    The number of entries to keep from each field (pass 0 to keep all entries, which is the default if no `n` field is provided).\n\nfull\n    If set to anything, prefer the full entry `content`; if absent, prefer the shorter entry `summary`.\n\nAn OpenAPI specification is available in `openapi.yaml`_\n\n.. _openapi.yaml: openapi.yaml\n\nFeatures\n--------\n\nAPI\n~~~\n\n- Combine several feeds (just about any version of Atom and RSS should work) into a single feed\n- Optionally return only the `n` most recent items from each input feed\n- Control whether the output feed contains only the summary or the entire content of the input feed items\n- Parser results are memoized so that repeated requests for the same feed can\n  be returned without re-parsing.\n\nIncluded WSGI app\n~~~~~~~~~~~~~~~~~\n- The provided `feedmixer_wsgi.py` application uses a session that caches HTTP\n  responses so that repeatedly fetching the same sets of feeds can usually be\n  responded to quickly by the FeedMixer service.\n\n  The `FeedMixer` object can be passed a custom `requests.session` object used\n  to make HTTP requests, which allows flexible customization in how requests\n  are made if you need that. \n\nNon-features\n------------\nFeedMixer does not (yet?) do any resource restriction itself:\n\n- Authorization\n- Rate limiting\n\nTo protect your installation either configure a front-end http proxy to take\ncare of your required restrictions (Nginx is a good choice), or/and use\nsuitable WSGI middleware.\n\n\nInstallation\n------------\n\n#. Clone this repository:\n   ``$ git clone https://github.com/cristoper/feedmixer.git``\n#. ``$ cd feedmixer``\n#. Recommended: use uv_ to create a virtualenv and install dependencies:\n   ``$ uv venv``\n   ``$ . .venv/bin/activate``\n   ``$ uv sync``\n\nThe project consists of three modules:\n\n- ``feedmixer.py`` - contains the core logic\n- ``feedmixer_api.py`` - contains the Falcon_-based API. Call ``wsgi_app()`` to\n  get a WSGI-compliant object to host.\n- ``feedmixer_wsgi.py`` - contains an actual WSGI application which can be used\n  as-is or as a starting point to create your own custom FeedMixer service.\n\n.. _falcon: https://falconframework.org/\n.. _gunicorn: http://gunicorn.org/\n.. _`virtual environment`: https://virtualenv.pypa.io/en/stable/\n.. _uv: https://github.com/astral-sh/uv\n\nRun Locally\n~~~~~~~~~~~\n\nThe feedmixer_wsgi module instantiates the feedmixer WSGI object (sets up\nlogging to stderr) as both `api` and `application` (default names used by\ncommon WSGI servers). To start the service with gunicorn_, for example, clone\nthe repository and in the root directory run::\n\n$ uv venv\n$ . .venv/bin/activate\n$ uv sync\n$ uv pip install gunicorn\n$ gunicorn feedmixer_wsgi\n\nAs an example, assuming an instance of the FeedMixer app is running on the localhost on port 8000, let's fetch the newest entry each from the following Atom and RSS feeds:\n\n- https://catswhisker.xyz/shaarli/?do=atom\n- https://hnrss.org/newest\n\nThe constructed URL to GET is:\n\n``http://localhost:8000/atom?f=https://catswhisker.xyz/shaarli/?do=atom\u0026f=https://hnrss.org/newest\u0026n=1``\n\nEntering it into a browser will return an Atom feed with two entries. To GET it from a client programatically, remember to URL-encode the `f` fields::\n\n$ curl 'localhost:8000/atom?f=https%3A%2F%2Fcatswhisker.xyz%2Fshaarli%2F%3Fdo%3Datom\u0026f=https%3A%2F%2Fhnrss.org%2Fnewest\u0026n=1'\n\n`HTTPie \u003chttps://httpie.org/\u003e`_ is a nice command-line http client that makes testing RESTful services more pleasant::\n\n$ pip3 install httpie\n$ http localhost:8000/json f==http://hnrss.org/newest f==http://catswhisker.xyz/atom.xml n==1\n\nYou should see some JSONFeed output (since we are requesting from the `/json` endpoint):\n\n.. code-block:: json\n  \n   HTTP/1.1 200 OK\n   Connection: close\n   Date: Thu, 23 Jan 2020 03:53:45 GMT\n   Server: gunicorn/20.0.4\n   content-length: 1296\n   content-type: application/json\n\n   {\n     \"version\": \"https://jsonfeed.org/version/1\", \n     \"title\": \"FeedMixer feed\", \n     \"home_page_url\": \"http://localhost:8000/json?f=http%3A%2F%2Fhnrss.org%2Fnewest\u0026f=https%3A%2F%2Fcatswhisker.xyz%2Fatom.xml\u0026n=1\", \n     \"description\": \"json feed created by FeedMixer.\", \n     \"items\": [\n       {\n         \"title\": \"Kyrsten Sinema, the Only Anti-Net Neutrality Dem, Linked to Comcast Super Pac\", \n         \"content_html\": \"\u003cp\u003eArticle URL: \u003ca href=\\\"https://prospect.org/politics/kyrsten-sinema-anti-net-neutrality-super-pac-comcast-lobbyist/\\\"\u003ehttps://prospect.org/politics/kyrsten-sinema-anti-net-neutrality-super-pac-comcast-lobbyist/\u003c/a\u003e\u003c/p\u003e\\n\u003cp\u003eComments URL: \u003ca href=\\\"https://news.ycombinator.com/item?id=22124592\\\"\u003ehttps://news.ycombinator.com/item?id=22124592\u003c/a\u003e\u003c/p\u003e\\n\u003cp\u003ePoints: 1\u003c/p\u003e\\n\u003cp\u003e# Comments: 0\u003c/p\u003e\", \n         \"url\": \"https://prospect.org/politics/kyrsten-sinema-anti-net-neutrality-super-pac-comcast-lobbyist/\", \n         \"id\": \"https://news.ycombinator.com/item?id=22124592\", \n         \"author\": {\n           \"name\": \"joeyespo\"\n         }, \n         \"date_published\": \"2020-01-23T03:32:19Z\", \n         \"date_modified\": \"2020-01-23T03:32:19Z\"\n       }, \n       {\n         \"title\": \"FO Roundup December 2019\", \n         \"content_html\": \"I've started knitting again.\", \n         \"url\": \"http://catswhisker.xyz/log/2019/12/3/fo_december/\", \n         \"id\": \"tag:catswhisker.xyz,2019-12-04:/log/2019/12/3/fo_december/\", \n         \"author\": {\n           \"name\": \"A. Cynic\", \n           \"url\": \"http://catswhisker.xyz/about/\"\n         }, \n         \"date_published\": \"2019-12-04T04:48:59Z\", \n         \"date_modified\": \"2019-12-04T04:48:59Z\"\n       }\n     ]\n   }\n\nDeploy\n~~~~~~\n\nDeploy FeedMixer using any WSGI-compliant server (uswgi, gunicorn, mod_wsgi,\n...). For a production deployment, put an asynchronous http proxy (like Nginx)\nin front of FeedMixer to protect it from too many and slow connections (as well\nas to provide SSL termination, additional caching, authoriziation, etc., as\nrequired)\n\nRefer to the documentation of the server of your choice.\n\nApache\n````````\nFor notes on deploying behind Apache, see `apache.rst`_ (from html docs: `apache.html`_)\n\n.. _apache.rst: doc/apache.rst\n.. _apache.html: apache.html\n\nDocker\n~~~~~~\n\nAn alternative to using a virtualenv for both building and deploying is to run\nFeedMixer in a Docker container. The included Dockerfile will produce an image\nwhich runs FeedMixer using gunicorn.\n\nBuild the image from the feedmixer directory::\n\n$ docker build . -t feedmixer\n\nRun it in the foreground::\n\n$ docker run -p --rm 8000:8000 feedmixer\n\nYou can set configuration environment variables using the ``-e`` flag:\n\n.. code-block:: bash\n\n   $ docker run --rm -p 8000:8000 -e FM_LOG_LEVEL=DEBUG -e FM_ALLOW_CORS=1 -e FM_TIMEOUT=20 feedmixer\n\nNow from another terminal you should be able to connect to FeedMixer on\nlocalhost port 8000 just as in the example above.\n\nThe Dockerfile is based on alpine linux and produces an image that is about\n60MB.\n\nIf you have issue building the docker image, you can try the included\nDebian-based Dockerfile (which produces an image about twice the size of the\nalpine Dockerfile):\n\n$ docker build . -t feedmixer-debian -f Dockerfile-debian\n$ docker run --rm -p 8000:8000 feedmixer-debian\n\nConfigure\n---------\n\nCORS\n~~~~\n\nIn order to access the `feedmixer` server from a JavaScript application, both\nthe API must be accessible from the same origin (protocol, domain, or port) as\nthe web client or browsers will not allow the connection to be made.\n\nTo get around this, the `feedmixer` server can set CORS headers to allow\nconnections from different origins. To enable this for development, set the\n``FM_ALLOW_CORS`` environment variable to any non-empty value when launching\nthe server.\n\nFor example:\n\n.. code-block:: bash\n\n   $ FM_ALLOW_CORS=1 gunicorn feedmixer_wsgi\n\nWhen this variable is set, the API will include the\n``Access-Control-Allow-Origin: *`` header in all responses, allowing the frontend\nto make requests from any origin.\n\nIn a production environment, it is recommended to configure a front-end reverse\nproxy like Nginx to set the appropriate CORS headers instead of relying on this\nenvironment variable. This provides more control and keeps application\nconfiguration separate from deployment-specific concerns.\n\n\nLogging\n~~~~~~~\n\nThe log level can be configured via the ``FM_LOG_LEVEL`` environment variable.\nValid values are ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``. The\ndefault is ``INFO``.\n\nFor example:\n\n.. code-block:: bash\n\n   $ FM_LOG_LEVEL=DEBUG gunicorn feedmixer_wsgi\n\n\nRequest Timeout\n~~~~~~~~~~~~~~~\n\nThe http timeout for fetching remote feeds can be configured with the ``FM_TIMEOUT``\nenvironment variable. The value is in seconds, and the default is ``30``.\n\n.. code-block:: bash\n\n   $ FM_TIMEOUT=12 gunicorn feedmixer_wsgi\n\n\nCache Size\n~~~~~~~~~~\n\nThe maximum number of parsed feeds to keep in the in-memory cache can be\nconfigured with the ``FM_CACHE_SIZE`` environment variable. The value is an\ninteger, and the default is ``128``.\n\n.. code-block:: bash\n\n   $ FM_CACHE_SIZE=256 gunicorn feedmixer_wsgi\n\n\nTroubleshooting\n---------------\n\nUsing the provided `feedmixer_wsgi.py` application, information and errors are\nlogged to `stderr`.\n\nAny errors encountered in fetching and parsing remote feeds are reported in a\ncustom HTTP header called `X-fm-errors`.\n\nHacking\n-------\n\nFirst install as per instructions above.\n\nDocumentation\n~~~~~~~~~~~~~\n\nOther than this README, the documentation is in the docstrings. To build a\npretty version (HTML) using Sphinx:\n\n1. Install Sphinx dependencies: ``$ uv pip install -r doc/requirements.txt``\n2. Change to `doc/` directory: ``$ cd doc``\n3. Build: ``$ make html``\n4. View: ``$ x-www-browser _build/html/index.html``\n\nTests\n~~~~~\n\nTests are in the `test` directory and Python will find and run them with::\n\n$ python3 -m unittest\n\nTypechecking\n~~~~~~~~~~~~\n\nTo check types using mypy_::\n\n$ MYPYPATH=stub/ mypy --ignore-missing-imports -p feedmixer\n\nNot everything is stubbed out, but can be useful for catching bugs after changing `feedparser.py`\n\n.. _mypy: http://mypy-lang.org/\n\n\nGet help\n--------\n\nFeel free to open an issue on Github for help: https://github.com/cristoper/feedmixer/issues\n\n\nSupport the project\n-------------------\n\nIf this package was useful to you, please consider supporting my work on this\nand other open-source projects by making a small (like a tip) one-time\ndonation: `donate via PayPal \u003chttps://www.paypal.me/cristoper/5\u003e`_\n\nIf you're looking to contract a Python developer, I might be able to help.\nContact me at chris@onpc.xyz\n\n\nLicense\n-------\n\nThe project is licensed under the WTFPL_ license, without warranty of any kind.\n\n.. _WTFPL: http://www.wtfpl.net/about/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcristoper%2Ffeedmixer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcristoper%2Ffeedmixer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcristoper%2Ffeedmixer/lists"}