_images/logo.svg

Django Apple Podcast

PyPI version Build status

Django Apple Podcast is a Django podcast application optimized for Apple Podcasts. Formerly Django iTunes Podcast.

An online demo also exists.

Install

$ pipenv install django-applepodcast

Add to settings.py.

INSTALLED_APPS = [
    # ...
    'podcast',
]

Add to urls.py.

from django.urls import include, path

urlpatterns = [
    # ...
    path('podcast/', include('podcast.urls')),
]

Migrate the database.

$ pipenv run python manage.py migrate

Load the fixtures.

$ pipenv run python manage.py loaddata podcast_category.json

Usage

Run the local server.

$ pipenv run python manage.py runserver

Visit either the show view or the admin.

Contents

Install

Install with Pipenv.

$ pipenv install django django-applepodcast

After creating a project, add podcast to INSTALLED_APPS in settings.py.

INSTALLED_APPS = [
    # ...
    'podcast',
]

Because the app is primarily model driven, you will want to expose the URL of the show’s feed for submission to Apple Podcasts. Add the URL conf to urls.py.

from django.urls import include, path

urlpatterns = [
    # ...
    path('podcast/', include('podcast.urls')),
]

If you’re on Django 1.11, 1.10, or 1.9, use the older, regex-based syntax instead.

from django.conf.urls import include, url

urlpatterns = [
    # ...
    url(r'^podcast/', include('podcast.urls')),
]

If you’re on Django 1.8, you will additionally need to add the namespace keyword argument to the include() method manually because the convenient app_name attribute in urls.py wasn’t added until Django 1.9.

from django.conf.urls import include, url

urlpatterns = [
    # ...
    url(r'^podcast/', include('podcast.urls', namespace='podcast')),
]

Add the models to your project by migrating the database.

$ pipenv run python manage.py migrate

Add the default Apple Podcasts categories by loading the fixtures.

$ pipenv run python manage.py loaddata podcast_category.json

Usage

Structure

The best way to understand the app is to simply browse the online demo or locally after adding a show and a few episodes in the admin. But for reference, assuming you chose podcast as the base of your URL conf, the structure is:

URL URL name View Model Template Context Absolute URLs
/podcast/ podcast:show_detail podcast.views.ShowDetailView Show podcast/show_detail.html show, episode_list {{ show.get_absolute_url }}
/podcast/feed/ podcast:show_feed podcast.views.ShowFeed Show     {{ show.get_absolute_feed_url }}
/podcast/<episode-slug>/ podcast:episode_detail podcast.views.EpisodeDetailView Episode podcast/episode_detail.html episode {{ episode.get_absolute_url }}
/podcast/<episode-slug>/download/ podcast:episode_download podcast.views.EpisodeDownloadView Episode     {{ episode.get_absolute_download_url }}

And if you chose podcasts and set PODCAST_SINGULAR = False to display multiple shows:

URL URL name View Model Template Context Absolute URLs
/podcasts/ podcast:show_list podcast.views.ShowListView Show podcast/show_list.html show_list  
/podcasts/<show-slug>/ podcast:show_detail podcast.views.ShowDetailView Show podcast/show_detail.html show, episode_list {{ show.get_absolute_url }}
/podcasts/<show-slug>/feed/ podcast:show_feed podcast.views.ShowFeed Show     {{ show.get_absolute_feed_url }}
/podcasts/<show-slug>/<episode-slug>/ podcast:episode_detail podcast.views.EpisodeDetailView Episode podcast/episode_detail.html episode {{ episode.get_absolute_url }}
/podcasts/<show-slug>/<episode-slug>/download/ podcast:episode_download podcast.views.EpisodeDownloadView Episode     {{ episode.get_absolute_download_url }}

Enclosures

The show feed is a subclass of Django’s Rss201rev2Feed feed generator class, which prohibits the use of more than one enclosure. Although Django’s syndication documentation doesn’t directly state it, the use of multiple enclosures are meant for only subclasses of Django’s Atom1Feed feed generator class.

The RSS Advisory Board states:

Support for the enclosure element in RSS software varies significantly because of disagreement over whether the specification permits more than one enclosure per item. Although the author intended to permit no more than one enclosure in each item, this limit is not explicit in the specification.

For best support in the widest number of aggregators, an item should not contain more than one enclosure.

Therefore, enclosures are modeled as OneToOneField s off of episodes, limiting episodes to one and only one enclosure.

Apple Podcasts does not host enclosure files; it is the responsibility of the developer to host them. Because an enclosure’s file is a FileField, files are uploaded to your MEDIA_ROOT setting. If you haven’t already, your urls.py should include patterns for interfacing with files in local development.

from django.conf import settings
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path

urlpatterns = [
    # ...
    path('podcast/', include('podcast.urls')),
]

# Static/media for local development
if getattr(settings, 'DEBUG', False):
    urlpatterns += staticfiles_urlpatterns()
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Although file management in production is out of scope of this documentation, consider using Amazon Web Service’s S3 (Simple Storage Service) to host files and Django Storages and Boto (and, if using Python 3, Boto 3), to interface with them.

The code repository of the online demo is worth looking at for a complete implementation, especially the settings file if you expect to use AWS’s S3.

Protocols

Depending on the operating system and/or web browser, it’s possible to immediately subscribe a user to a podcast in iTunes or the iOS Podcasts app by using itpc:// or feed:// protcol-based URLs rather than the standard http:// or https://.

The app comes with the {% show_url %} template tag to help create these URLs.

{% load podcast_tags %}
{% show_url protocol='itpc' url=show.get_absolute_feed_url %}">

Result:

itpc://127.0.0.1:8000/podcast/feed/

Beware that these URLs are purely interaction based; you would not be required to submit the show feed to Apple Podcasts, but you would also not be able to track users’ behavior in Podcasts Connect. For this reason, you’re probably better off in a traditional submission to Apple Podcasts, saving your new URL in the Show model, and using the show.apple variable in your template.

{{ show.apple }}

Submission

The show feed URL is:

/podcast/feed/

If you have multiple shows, each respective show feed URL is:

/podcasts/<show-slug>/feed/

Submit the show feed to Podcasts Connect.

Badges

After Apple Podcasts approves your podcast, feel free to use the “Listen on Apple Podcasts” badge or icon, the U.S. versions of which are included as minified static files. You can also download them from the Apple Podcasts Identity Guidelines. The SVGs were minified with SVGO.

Badge
_images/badge.svg
{% load i18n static %}
<img src="{% static 'podcast/img/badge.svg' %}" alt="{% trans 'Listen on Apple Podcasts' %}">
Icon
_images/icon.svg
{% load i18n static %}
<img src="{% static 'podcast/img/icon.svg' %}" alt="{% trans 'Listen on Apple Podcasts' %}">

Load the white or black icons similarly.

{% load i18n static %}
<img src="{% static 'podcast/img/icon-white.svg' %}" alt="{% trans 'Listen on Apple Podcasts' %}">
{% load i18n static %}
<img src="{% static 'podcast/img/icon-black.svg' %}" alt="{% trans 'Listen on Apple Podcasts' %}">

Settings

The app offers several settings. By default, they are:

PODCAST_SINGULAR = True

PODCAST_ID = 1

PODCAST_EPISODE_LIMIT = None

PODCAST_NO_ARTWORK = 'podcast/img/no_artwork.png'

PODCAST_PAGINATE_BY = 10

PODCAST_ALLOWED_TAGS = ['p', 'ol', 'ul', 'li', 'a', 'i', 'em', 'b', 'strong']

PODCAST_SINGULAR

A boolean indicating display of multiple podcast shows.

The app displays a single show by default. If you would like to display multiple shows, set the PODCAST_SINGULAR variable in settings.py.

PODCAST_SINGULAR = False

If you have multiple shows, you might want to edit the URL pattern in urls.py from podcast/ to podcasts/, although the difference is purely cosmetic.

from django.urls import include, path

urlpatterns = [
    # ...
    path('podcasts/', include('podcast.urls')),
]

PODCAST_ID

An integer indicating the primary key of the show to display; used when PODCAST_SINGULAR is True. Concept modeled after the SITE_ID setting used in the Sites application.

The app displays the first show by default.

PODCAST_EPISODE_LIMIT

An integer indicating the number of episodes to display in a show feed or None to display all episodes. Formerly the limit was an arbitrary 50 episodes.

The app displays all episodes in a show feed by default.

PODCAST_NO_ARTWORK

A string indicating the path to an image used when artwork is lacking; used for shows, episodes, and video enclosures.

Although the path can be customized in the setting, you’re probably better off overriding the image look up at the project level; that is, creating a new image at myproject/static/podcast/img/no_artwork.png.

PODCAST_PAGINATE_BY

An integer indicating how many items to display in a list view or in a detail view of related objects; used for shows and episodes.

PODCAST_ALLOWED_TAGS

A list indicating which HTML tags are allowed for display in output; used for show summaries and episode notes. The database can store HTML, but tags not specified in the list are stripped out, and the remaining output is wrapped in <![CDATA[...]]> tags. Uses the Bleach Python package.

The list includes HTML tags specified in the iOS 11 Apple Podcasts update by default. The list also includes the conspicuously absent <li> and <strong> tags, which appear to be an oversight on Apple’s part because <ol>, <ul>, and <b> are present in the original list. Note that the specification confusingly specifies a subset of fewer tags: <p>, <ol>, <ul>, and <a>.

Feed

Sample

The show feed is optimized for submission to the Apple Podcasts by adding additional Apple Podcast-specific tags.

The following is the direct output of a show feed following the RSS feed sample in the Podcasts Connect documentation as closely as possible.

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>All About Everything</title>
        <link>http://testserver/podcast/</link>
        <description>All About Everything is a show about everything. Each week we dive into any subject known to man and talk about it as much as we can. Look for our podcast in the Podcasts app or in the iTunes Store</description>
        <atom:link rel="self" href="http://testserver/podcast/feed/" />
        <language>en-us</language>
        <lastBuildDate>Thu, 10 Mar 2017 22:15:00 +0000</lastBuildDate>
        <copyright>&#x2117; &amp; &#xA9; 2017 John Doe &amp; Family</copyright>
        <itunes:subtitle><![CDATA[A show about everything]]></itunes:subtitle>
        <itunes:summary><![CDATA[All About Everything is a show about everything. Each week we dive into any subject known to man and talk about it as much as we can. Look for our podcast in the Podcasts app or in the iTunes Store]]></itunes:summary>
        <itunes:author>John Doe</itunes:author>
        <itunes:owner>
            <itunes:name>John Doe</itunes:name>
            <itunes:email>john.doe@example.com</itunes:email>
        </itunes:owner>
        <itunes:image href="http://testserver/podcast/tests/static/everything/AllAboutEverything.jpg" />
        <itunes:category text="Arts">
            <itunes:category text="Food" />
        </itunes:category>
        <itunes:category text="TV &amp; Film" />
        <itunes:category text="Technology">
            <itunes:category text="Gadgets" />
        </itunes:category>
        <itunes:explicit>no</itunes:explicit>
        <item>
            <title>Red,Whine, &amp; Blue</title>
            <link>http://testserver/podcast/red-whine-blue/</link>
            <description>This week we talk about surviving in a Red state if you are a Blue person. Or vice versa.</description>
            <pubDate>Thu, 10 Mar 2016 22:15:00 +0000</pubDate>
            <guid>http://testserver/podcast/tests/static/everything/AllAboutEverythingEpisode4.mp3</guid>
            <enclosure length="75232" type="audio/mpeg" url="http://testserver/podcast/tests/static/everything/AllAboutEverythingEpisode4.mp3" />
            <itunes:subtitle><![CDATA[Red + Blue != Purple]]></itunes:subtitle>
            <itunes:summary><![CDATA[This week we talk about surviving in a Red state if you are a Blue person. Or vice versa.]]></itunes:summary>
            <itunes:author>Various</itunes:author>
            <itunes:image href="http://testserver/podcast/tests/static/everything/AllAboutEverything/Episode4.jpg" />
            <itunes:explicit>no</itunes:explicit>
            <itunes:duration>00:03</itunes:duration>
        </item>
        <item>
            <title>The Best Chili</title>
            <link>http://testserver/podcast/best-chili/</link>
            <description>This week we talk about the best Chili in the world. Which chili is better?</description>
            <pubDate>Thu, 10 Mar 2016 09:00:00 +0000</pubDate>
            <guid>http://testserver/podcast/tests/static/everything/AllAboutEverythingEpisode2.m4v</guid>
            <enclosure length="25725" type="video/x-m4v" url="http://testserver/podcast/tests/static/everything/AllAboutEverythingEpisode2.m4v" />
            <itunes:subtitle><![CDATA[Jane and Eric]]></itunes:subtitle>
            <itunes:summary><![CDATA[This week we talk about the best Chili in the world. Which chili is better?]]></itunes:summary>
            <itunes:author>Jane Doe</itunes:author>
            <itunes:image href="http://testserver/podcast/tests/static/everything/AllAboutEverything/Episode3.jpg" />
            <itunes:explicit>no</itunes:explicit>
            <itunes:duration>00:02</itunes:duration>
            <itunes:isClosedCaptioned>yes</itunes:isClosedCaptioned>
        </item>
        <item>
            <title>Socket Wrench Shootout</title>
            <link>http://testserver/podcast/socket-wrench-shootout/</link>
            <description>This week we talk about metric vs. Old English socket wrenches. Which one is better? Do you really need both? Get all of your answers here.</description>
            <pubDate>Wed, 09 Mar 2016 18:00:00 +0000</pubDate>
            <guid>http://testserver/podcast/tests/static/everything/AllAboutEverythingEpisode2.mp4</guid>
            <enclosure length="28355" type="video/mp4" url="http://testserver/podcast/tests/static/everything/AllAboutEverythingEpisode2.mp4" />
            <itunes:subtitle><![CDATA[Comparing socket wrenches is fun!]]></itunes:subtitle>
            <itunes:summary><![CDATA[This week we talk about metric vs. Old English socket wrenches. Which one is better? Do you really need both? Get all of your answers here.]]></itunes:summary>
            <itunes:author>Jane Doe</itunes:author>
            <itunes:image href="http://testserver/podcast/tests/static/everything/AllAboutEverything/Episode2.jpg" />
            <itunes:explicit>no</itunes:explicit>
            <itunes:duration>00:03</itunes:duration>
        </item>
        <item>
            <title>Shake Shake Shake Your Spices</title>
            <link>http://testserver/podcast/shake-shake-shake-your-spices/</link>
            <description>This week we talk about &lt;a href="https://itunes/apple.com/us/book/antique-trader-salt-pepper/id429691295?mt=11"&gt;salt and pepper shakers&lt;/a&gt;, comparing and contrasting pour rates, construction materials, and overall aesthetics. Come and join the party!</description>
            <pubDate>Tue, 08 Mar 2016 12:00:00 +0000</pubDate>
            <guid>http://testserver/podcast/tests/static/everything/AllAboutEverythingEpisode3.m4a</guid>
            <enclosure length="46862" type="audio/x-m4a" url="http://testserver/podcast/tests/static/everything/AllAboutEverythingEpisode3.m4a" />
            <itunes:subtitle><![CDATA[A short primer on table spices]]></itunes:subtitle>
            <itunes:summary><![CDATA[This week we talk about <a href="https://itunes/apple.com/us/book/antique-trader-salt-pepper/id429691295?mt=11">salt and pepper shakers</a>, comparing and contrasting pour rates, construction materials, and overall aesthetics. Come and join the party!]]></itunes:summary>
            <itunes:author>John Doe</itunes:author>
            <itunes:image href="http://testserver/podcast/tests/static/everything/AllAboutEverything/Episode1.jpg" />
            <itunes:explicit>no</itunes:explicit>
            <itunes:duration>00:03</itunes:duration>
        </item>
    </channel>
</rss>

Sample differences

Although every effort was made to recreate the RSS feed sample on Podcasts Connnect as closely as possible, the limitations of the way in which Django creates feeds and the occassional stray error in the feed sample itself required small changes:

  • The RssFeed class in Django’s deep syndication class hierarchy adds an <atom:link> to the <channel> element that would require a significant code duplication and rewrite to eliminate. It does not affect Apple Podcasts compatibility and thus remains in the show feed.
  • The <atom:link> previously mentioned can only exist in a correponding XML namespace; i.e. the attribute xmlns:atom="http://www.w3.org/2005/Atom" in the <rss> element. The attribute could be easily removed, but would prevent the feed from achieving XML validation. The Atom XML namespace thus remains in the show feed.
  • The RssFeed class adds a <lastBuildDate> to the <channel> element that corresponds to the <pubDate> of the latest <item>. Due to Django’s deep syndication class hierarchy, it remains in the show feed.
  • In the RSS feed sample, the <copyright> element contains a year of 2014. The sample is replaced with the current year, at the time of this writing, 2017.
  • In the RSS feed sample, <itunes:summary> tag in the “Shake Shake Shake Your Spices” episode has an errant space in its <![CDATA[...]]> tag. The sample displays <![CDATA[...]] >. The show feed removes the errant space.
  • In the RSS feed sample, the domain in URLs is www.example.com or example.com. Django’s testing framework uses the server name testserver. The feed test replaces www.example.com with testserver.
  • In the RSS feed sample, the absolute URL of the show is /podcasts/everything/index.html. In the interest of clean URLs, the feed test removes index.html.
  • In the RSS feed sample, only instances of <itunes:summary> or <itunes:subtitle> that have HTML contain <![CDATA[...]]> tags to escape the HTML. Rather than conditionally insert <![CDATA[...]]> tags, they are inserted in all instances of <itunes:summary> and <itunes:subtitle>.
  • In the RSS feed sample, the enclosure url of an <item> is often different from the <guid>, e.g. http://example.com/podcasts/everything/AllAboutEverythingEpisode3.m4a vs. http://example.com/podcasts/archive/aae20140615.m4a. The <guid> of an <item> is normalized to return the enclosure URL and eliminate a competing, arbitrary URL.
  • In the RSS feed sample, the (fake) enclosure files have accompanying fake values in <itunes:duration> elements. The app automatically reads the duration of media files using the Python Mutagen package, and their durations are not subject to manual editing.
  • In the RSS feed sample, the enclosure length of an <item> is similarly determined by automatically reading the enclosure size of the file.
  • In the RSS feed sample, the paths to media files were changed to reflect a more typical Django file path.
  • In the RSS feed sample, <item> elements omit <link> and <description> elements. While technically valid, Django encourages its use by automatically querying for an item’s absolute URL, and thus each item’s <link> and <description> are preserved.
  • In the RSS feed sample, the <item> elements contain <pubDate> values whose time zones are inconsistent: GMT (which is obsolete), EST, -0700, and +3000 (which should be +0300). Because Django defines TIME_ZONE at the project level in settings, it’s impossible to display datetimes in the show feed with different time zones. For example, given a datetime 2016-03-11T01:15:00+0300 (which might be, say, 'Europe/Moscow'), a setting of TIME_ZONE = 'UTC' would ultimately result in a display of Thu, 10 Mar 2016 22:15:00 +0000, that is, moving three hours backward to achieve UTC, which would be around 10 p.m. the prior evening. All values of <pubDate> elements have been converted to their UTC-time zone equivalents.
  • In the RSS feed sample, elements that correspond to boolean values are inconsistently capitalized. The values of <itunes:explicit> elements are no, but the value of <itunes:isClosedCaptioned> is Yes. The sample was changed to yes.
  • In the RSS feed, the episode “Shake Shake Shake Your Spices” has a <description> and <itunes:summary> element whose value contains a malformed URL, i.e. https://itunes/apple.com. The error has been preserved.
  • In the RSS feed, the episode “Red,Whine, & Blue” is missing a space after the first comma. The error has been preserved.
  • The show feed and RSS feed sample only compare semantic differences, i.e. parsed content, and not syntax differences, i.e. various orderings of elements, capitialization, orderings of attributes, and spaces, etc. Django’s assertXMLEqual is used to assert equality.

Admin

The data in the screenshots of the admin mimics the RSS feed sample of the Podcasts Connect documentation.

App

_images/admin-app.png

Shows

List view
_images/admin-show-list.png
Change view
_images/admin-show-change.png

Episodes

List view
_images/admin-episode-list.png
Change view
_images/admin-episode-change.png

Enclosures

List view
_images/admin-enclosure-list.png
Change view
_images/admin-enclosure-change.png

Categories

List view
_images/admin-category-list.png
Change view
_images/admin-category-change.png

Speakers

List view
_images/admin-speaker-list.png
Change view
_images/admin-speaker-change.png

Documentation

Full documentation is available online.

However, you can also build the documentation from source.

Clone the code repository.

$ git clone git@github.com:richardcornish/django-applepodcast.git
$ cd django-applepodcast/

Install Sphinx, sphinx-autobuild, and sphinx_rtd_theme.

$ pipenv install sphinx sphinx-autobuild sphinx_rtd_theme --three

Create an HTML build.

$ (cd docs/ && make html)

Or use sphinx-autobuild to watch for live changes.

$ sphinx-autobuild docs/ docs/_build_html

Open 127.0.0.1:8000.

Tests

Continuous integration test results are available online.

However, you can also test the source code. Note that because the app stores media files, you will have to change to the directory above the source code directory before testing, typically site-packages.

$ cd ~/.virtualenvs/myenv/lib/python3.6/site-packages/
$ django-admin test podcast.tests --settings="podcast.tests.settings"

Creating test database for alias 'default'...
..........
----------------------------------------------------------------------
Ran 1 test in 0.119s

OK
Destroying test database for alias 'default'...

A bundled settings file allows you to test the code without even creating a Django project.

Indices and tables