Skip to content

Commit 29f1552

Browse files
authored
Merge pull request #42 from tobinus/notify-pubsubhubbub-#21
Add methods for notifying hubs, closes #21
2 parents f1b498d + 3242d55 commit 29f1552

File tree

6 files changed

+401
-12
lines changed

6 files changed

+401
-12
lines changed

doc/extending.rst renamed to doc/advanced/extending.rst

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ Adding new tags
44
Are there XML elements you want to use that aren't supported by PodGen? If so,
55
you should be able to add them in using inheritance.
66

7-
.. warning::
8-
9-
This is an advanced topic.
10-
117
.. note::
128

139
There hasn't been a focus on making it easy to extend PodGen.

doc/advanced/index.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Advanced Topics
2+
===============
3+
4+
.. toctree::
5+
:maxdepth: 1
6+
7+
pubsubhubbub
8+
extending

doc/advanced/pubsubhubbub.rst

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
Using PubSubHubbub
2+
==================
3+
4+
PubSubHubbub is a free and open protocol for pushing updates to clients
5+
when there's new content available in the feed, as opposed to the traditional
6+
polling clients do.
7+
8+
Read about `what PubSubHubbub is`_ before you continue.
9+
10+
.. _what PubSubHubbub is: https://en.wikipedia.org/wiki/PubSubHubbub
11+
12+
.. note::
13+
14+
While the protocol supports having multiple PubSubHubbub hubs for a single
15+
Podcast, there is no support for this in PodGen at the moment.
16+
17+
--------------------------------------------------------------------------------
18+
19+
.. contents::
20+
:backlinks: none
21+
22+
23+
Step 1: Set feed_url
24+
--------------------
25+
26+
First, you must ensure that the :class:`.Podcast` object has the
27+
:attr:`~.Podcast.feed_url` attribute set to the URL at which the feed is
28+
accessible.
29+
30+
::
31+
32+
# Assume p is a Podcast object
33+
p.feed_url = "https://example.com/feeds/examplefeed.rss"
34+
35+
Step 2: Decide on a hub
36+
-----------------------
37+
38+
The `Wikipedia article`_ mentions a few options you can use (called Community
39+
Hosted hub providers). Alternatively, you can set up and host your own server
40+
using one of the implementations found at the `official PubSubHubbub project
41+
page`_.
42+
43+
.. _Wikipedia article: https://en.wikipedia.org/wiki/PubSubHubbub#Usage
44+
.. _official PubSubHubbub project page: https://github.com/pubsubhubbub
45+
46+
Step 3: Set pubsubhubbub
47+
------------------------
48+
49+
The Podcast must contain information about which hub to use. You do this by
50+
setting :attr:`~.Podcast.pubsubhubbub` to the URL which the hub is available at.
51+
52+
::
53+
54+
p.pubsubhubbub = "https://pubsubhubbub.example.com/"
55+
56+
Step 4: Set HTTP Link Header
57+
----------------------------
58+
59+
In addition to embedding the PubSubHubbub hub URL and the feed's URL in the
60+
RSS itself, you should use the
61+
`Link header`_ in the HTTP response that is sent with this feed,
62+
duplicating the link to the PubSubHubbub and the feed. Example of
63+
what it might look like:
64+
65+
.. code-block:: none
66+
67+
Link: <https://link.to.pubsubhubbub.example.org/>; rel="hub",
68+
<https://example.org/podcast.rss>; rel="self"
69+
70+
How you can achieve this varies from framework to framework. Here is an example
71+
using Flask (assuming the code is inside a view function)::
72+
73+
from flask import make_response
74+
from podgen import Podcast
75+
# ...
76+
@app.route("/<feedname>") # Just as an example
77+
def show_feed(feedname):
78+
p = Podcast()
79+
# ...
80+
# This is the relevant part:
81+
response = make_response(str(p))
82+
response.headers.add("Link", "<%s>" % p.pubsubhubbub, rel="hub")
83+
response.headers.add("Link", "<%s>" % p.feed_url, rel="self")
84+
return response
85+
86+
This is necessary for compatibility with the different versions of
87+
PubSubHubbub. The `latest version of the standard`_ specifically says
88+
that publishers MUST use the Link header. If you're unable to do this, you
89+
can try publishing the feed without; most clients and hubs should manage
90+
just fine.
91+
92+
.. _Link header: https://tools.ietf.org/html/rfc5988#page-6
93+
.. _latest version of the standard: http://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#rfc.section.4
94+
95+
Step 5: Publish the changes
96+
---------------------------
97+
98+
Ensure the changes above are published before proceeding. That is, if a client
99+
downloads the feed, it should receive the Link headers and the pubsubhubbub
100+
and feed_url contents.
101+
102+
Step 6: Notify the hub of new episodes
103+
--------------------------------------
104+
105+
.. note::
106+
107+
PodGen does not contain any logic for figuring out whether a Podcast has
108+
changed or not. You must do that part yourself.
109+
110+
PodGen has two convenience methods that you can use to notify the hub you chose
111+
of any additions made to the feed. The way this works, is that you say to the
112+
hub "Hey, we've made additions to this feed", and the hub looks at the feed and
113+
determines what is new, and sends the new episode(s) to any subscribed clients.
114+
115+
There are three pre-requisites for using those methods:
116+
117+
#. The `Requests`_ module has been installed.
118+
#. The :class:`.Podcast` object must have :attr:`~.Podcast.pubsubhubbub` and
119+
:attr:`~.Podcast.feed_url` set.
120+
#. The new episodes will be included in the feed if someone requests the feed
121+
at the moment the methods are called.
122+
123+
* If this isn't true, the hub will always be lagging one change behind!
124+
125+
.. _Requests: http://docs.python-requests.org
126+
127+
One of the methods work best when only one feed has changed. The other one can
128+
handle both the case where one feed has changed, and the case where multiple
129+
feeds have changed.
130+
131+
.. autosummary::
132+
podgen.Podcast.notify_hub
133+
podgen.Podcast.notify_multiple
134+
135+
Example where one Podcast has changed::
136+
137+
import requests
138+
from podgen import Podcast
139+
# ...
140+
p.notify_hub(requests)
141+
142+
Example where multiple Podcasts have changed::
143+
144+
import requests
145+
from podgen import Podcast
146+
# ...
147+
changed_podcasts = [
148+
# ... multiple Podcast objects here
149+
]
150+
Podcast.notify_multiple(requests, changed_podcasts)
151+
152+
Always use the latter form when multiple Podcasts have changed; you'll save
153+
lots of time since only one request needs to be made per hub.

doc/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@ User Guide
5757
user/basic_usage_guide/part_2
5858
user/basic_usage_guide/part_3
5959
user/example
60-
extending
60+
advanced/index
6161
contributing
6262
api

podgen/podcast.py

Lines changed: 121 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from podgen.compat import string_types
2424
import collections
2525
import inspect
26+
import warnings
2627

2728

2829
_feedgen_version = podgen.version.version_str
@@ -272,14 +273,12 @@ def __init__(self, **kwargs):
272273
273274
.. note::
274275
275-
You only need to worry about this attribute if you've `set up
276-
PubSubHubbub`_ for your podcast. PodGen does NOT include this
277-
functionality for you, and you must notify the hub of new content
278-
yourself.
276+
You only need to worry about this attribute if you've :doc:`set up
277+
PubSubHubbub </advanced/pubsubhubbub>` for your podcast.
279278
280279
.. note::
281280
282-
In addition to setting this attribute, you should set the
281+
In addition to setting this attribute, you must set the
283282
:attr:`.feed_url` to the canonical URL of this feed. That way, there
284283
is no confusion about which URL should be given to the PubSubHubbub
285284
by the podcatcher when subscribing.
@@ -300,9 +299,11 @@ def __init__(self, **kwargs):
300299
PubSubHubbub. The `latest version of the standard`_ specifically says
301300
that publishers MUST use the Link header.
302301
302+
.. seealso:
303+
The :doc:`guide on how to use PubSubHubbub </advanced/pubsubhubbub>`
304+
A more detailed walk-through on how to use PubSubHubbub.
305+
303306
.. _PubSubHubbub: https://en.wikipedia.org/wiki/PubSubHubbub
304-
.. _set up PubSubHubbub:
305-
https://indieweb.org/How_to_publish_and_consume_PubSubHubbub
306307
.. _Link header: https://tools.ietf.org/html/rfc5988#page-6
307308
.. _latest version of the standard: http://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#rfc.section.4
308309
"""
@@ -744,6 +745,119 @@ def clear_episode_order(self):
744745
for episode in self.episodes:
745746
episode.position = None
746747

748+
@classmethod
749+
def notify_multiple(cls, requests, feeds, timeout=10.0):
750+
"""Notify the PubSubHubbub hubs of additions in multiple Podcasts.
751+
752+
When using PubSubHubbub, you must notify the hub whenever a feed has
753+
new entries. Using this method, you can give a list of feeds
754+
which all have new content. This saves time compared to
755+
:meth:`~.Podcast.notify_hub` since they can be made into one request per
756+
hub, instead of having one request per feed per hub.
757+
758+
The Podcast objects don't need to use the same PubSubHubbub hub.
759+
760+
:param requests: The requests module, or a Session object.
761+
:type requests: requests or requests.Session
762+
:param feeds: List of Podcast objects which have new or changed content
763+
published.
764+
:type feeds: :obj:`list` of :class:`.Podcast`
765+
:param timeout: Number of seconds we can wait for the server to respond.
766+
Applies to each hub separately. Defaults to ten seconds.
767+
:type timeout: float
768+
:warnings: UserWarning for each feed that has no value for
769+
:attr:`~.Podcast.pubsubhubbub` and :attr:`~.Podcast.feed_url`.
770+
771+
.. note::
772+
773+
This method is blocking, and will return when the servers respond.
774+
775+
.. note::
776+
777+
For this to work for a given feed, it must have
778+
:attr:`~.Podcast.feed_url` and :attr:`~.Podcast.pubsubhubbub` set
779+
correctly.
780+
781+
.. seealso::
782+
783+
The instance method :meth:`~.Podcast.notify_hub`
784+
For notifying a single hub about a single feed
785+
786+
The :doc:`guide on using PubSubHubbub </advanced/pubsubhubbub>`
787+
For a step-for-step guide with examples.
788+
"""
789+
# Which hubs should be notified about what feeds?
790+
hubs = dict()
791+
for feed in feeds:
792+
if feed.pubsubhubbub:
793+
hubs.setdefault(feed.pubsubhubbub, []).append(feed)
794+
else:
795+
warnings.warn("Cannot notify feed %s: pubsubhubbub not set"
796+
% feed)
797+
798+
# We now have a dictionary which maps a hub to a list of its feeds
799+
for hub, hub_feeds in iteritems(hubs):
800+
# Create the POST parameters
801+
# Tell the PubSubHubbub we are notifying it of updates
802+
params = [("hub.mode", "publish")]
803+
# Tell the hub which feeds have been updated
804+
for feed in hub_feeds:
805+
if feed.feed_url:
806+
params.append(("hub.url", feed.feed_url))
807+
else:
808+
warnings.warn("Cannot notify feed %s: feed_url not set"
809+
% feed)
810+
# Do the notifying!
811+
if len(params) > 1:
812+
r = requests.post(hub, data=params, timeout=timeout)
813+
r.raise_for_status()
814+
else:
815+
# No feeds which we can notify this hub of...
816+
pass
817+
818+
def notify_hub(self, requests, timeout=5.0):
819+
"""Notify this podcast's PubSubHubbub hub about new episode(s).
820+
821+
When using PubSubHubbub, you must notify it whenever there's a new
822+
episode available. Use this method to do
823+
so, *after* the new episode is available -- the hub will check the feed
824+
to figure out what's new once you do so.
825+
826+
:param requests: The requests module, or a Session object.
827+
:type requests: requests or requests.Session
828+
:param timeout: Number of seconds to wait for the hub to respond.
829+
Defaults to five seconds.
830+
:type timeout: float
831+
832+
.. note::
833+
834+
This method is blocking, and will return when the server responds.
835+
836+
.. note::
837+
838+
For this to work, this feed must have
839+
:attr:`~.Podcast.feed_url` and :attr:`~.Podcast.pubsubhubbub` set
840+
correctly.
841+
842+
.. seealso::
843+
844+
The class method :meth:`~.Podcast.notify_multiple`
845+
For sending notifications about multiple feeds.
846+
847+
The :doc:`guide on using PubSubHubbub </advanced/pubsubhubbub>`
848+
For a step-for-step guide with examples.
849+
"""
850+
if not self.feed_url:
851+
raise RuntimeError("Cannot notify hub of this feed, since feed_url "
852+
"is not set.")
853+
elif not self.pubsubhubbub:
854+
raise RuntimeError("Cannot notify hub of this feed, since "
855+
"pubsubhubbub is not set.")
856+
else:
857+
self.notify_multiple(requests, [self], timeout=timeout)
858+
859+
860+
747861
@property
748862
def last_updated(self):
749863
"""The last time the feed was generated. It defaults to the time and

0 commit comments

Comments
 (0)