Podcast Feeds in Django

I've been talking to Terry Johal about getting our podcast up and running again. Naturally my action plan was to rebuild the website.

It was original built in CakePHP but has a few little bugs I've never got around to fixing so I thought I'd just rebuild it in Django. It being Django, I'd say it's taken me about 6 hours, the majority of which was spent building the Podcast feed which I thought I'd go over here.

Firstly, credit where credit is due, I'm borrowing heavily from this Django Snippit but I thought it might be helpful for me to go over some of the 'gotcha's I ran into'.

So firstly I wanted to build a simple RSS feed which is super simple with Django:

class RssFeed(Feed):
    title = "Still Angry"
    link = "http://stillangry.com/"
    author_name = "Still Angry"
    author_email = "hello@stillangry.com"
    author_link = "http://stillangry.com/"
    categories = ["News & Politics"]
    feed_copyright = "Creative Commons Attribution 4.0 International"
    description = "Still Angry is an ongoing conversation between Hammy " +\
                  "Goonan and Terry Johal. They might not quite be the " +\
                  "activists they used to be, but they are certainly Still " +\

    def items(self):
        return Post.objects.order_by('-pubdate').all()[:20]

    def item_title(self, item):
        return item.title

    def item_description(self, item):
        return item.content

    def item_link(self, item):
        return "http://stillangry.com/" + item.slug + '/'

    def item_author_name(self, item):
        return "{} {}".format(item.user.first_name, item.user.last_name)

    def item_pubdate(self, item):
        return item.pubdate

There's no big surprises there and you can checkout the documentation here.

Then, given it's so easy, I extended this class to make an Atom feed as well (why not!):

class AtomFeed(RssFeed):
    feed_type = Atom1Feed
    subtitle = RssFeed.description

But then we just need to add a handfull of additional fields here and we've got a podcast feed. So, in accordance with the don't repeat yourself (DRY) principal, I just extended the AtomFeed to create the podcast feed.

Before we do that though, we need to create our own 'feed type' which essentially gets injected into the Podcast object:

class iTunesFeed(Rss201rev2Feed):
    def rss_attributes(self):
        return {
            "version": self._version,
            "xmlns:atom": "http://www.w3.org/2005/Atom",
            'xmlns:itunes': u'http://www.itunes.com/dtds/podcast-1.0.dtd'

    def add_root_elements(self, handler):
        handler.addQuickElement('itunes:subtitle', self.feed['subtitle'])
        handler.addQuickElement('itunes:author', self.feed['author_name'])
        handler.addQuickElement('itunes:summary', self.feed['description'])
        handler.startElement("itunes:owner", {})
        handler.addQuickElement('itunes:name', self.feed['iTunes_name'])
        handler.addQuickElement('itunes:email', self.feed['iTunes_email'])
        handler.addQuickElement('itunes:image', self.feed['iTunes_image_url'])

    def add_item_elements(self, handler, item):
        super().add_item_elements(handler, item)
        handler.addQuickElement(u'itunes:summary', item['summary'])
        handler.addQuickElement(u'itunes:duration', item['duration'])
        handler.addQuickElement(u'itunes:explicit', item['explicit'])

It should be pretty self explanatory what the above is doing. rss_attributes() changes the top level elements that are returned by the feed to announce that it is a podcast feed.

add_root_elements() adds fields to the <feed> node to provide some iTunes-specific fields. Remember to call it's parent method so that the standard fields are also called.

Ditto with add_item_elements() which adds <item> level nodes for each individual podcast.

Finally we have the podcast feed class itself (which is what is called in the url definitions):

class PodcastFeed(AtomFeed):
    iTunes_explicit = 'explicit'
    iTunes_name = "Still Angry"
    iTunes_email = "hello@stillangry.com"
    iTunes_image_url = "http://stillangry.com/static/images/stillangry.jpg"
    feed_type = iTunesFeed

    def items(self):
        return Podcast.objects.order_by('-pubdate').all()

    def feed_extra_kwargs(self, obj):
        return {
            'iTunes_name': self.iTunes_name,
            'iTunes_email': self.iTunes_email,
            'iTunes_image_url': self.iTunes_image_url,
            'iTunes_explicit': self.iTunes_explicit,
            'iTunes_category': 'News &amp; Politics'

    def item_extra_kwargs(self, item):
        return {
            'summary': item.summary,
            'duration': item.duration,
            'explicit': 'explicit' if item.explicit else 'no',

    def item_enclosure_url(self, item):
        return "http://stillangry.com/download/" + str(item.filepath)

    def item_enclosure_length(self, item):
        return item.duration

    def item_enclosure_mime_type(self, item):
        return 'audio/mp3'

I've altered the items() method so that a Podcast query object is returned instead of Post. Podcast extends Post so all the podcasts are included in the post query object but not the other way around.

It should be pretty obvious what feed_extra_kwargs() and item_extra_kwargs() do, but they are quite important and initially tripped me up with a bit of a cryptic error message. Without it the feed_extra_kwargs method self.feed dictionary won't have the additional fields in the iTunesFeed. The same goes with item_extra_kwargs() populating the item variable.

It's as simple as that! Keep an eye out for a new podcast in the next feed weeks (I hope). I'll put an update here if and when that happens.