<<

PodGen Documentation Release 1.1.0

Lars Kiesow and Thorben Dahl

Mar 06, 2020

Contents

1 Contents 3 1.1 Background...... 3 1.1.1 Philosophy...... 3 1.1.2 Scope...... 4 1.1.3 Why the fork?...... 4 1.1.4 Roadmap...... 6 1.1.5 License...... 6 1.2 Usage Guide...... 6 1.2.1 Installation...... 6 1.2.2 ...... 7 1.2.3 Episodes...... 10 1.2.4 RSS...... 15 1.2.5 Full example...... 15 1.3 Advanced Topics...... 16 1.3.1 Using PubSubHubbub...... 16 1.3.2 Adding new tags...... 19 1.4 Contributing...... 22 1.4.1 Setting up...... 22 1.4.2 Testing...... 22 1.4.3 Values...... 23 1.4.4 The Workflow...... 23 1.5 API Documentation...... 23 1.5.1 podgen....... 24 1.5.2 podgen.Episode...... 33 1.5.3 podgen.Person...... 37 1.5.4 podgen.Media...... 38 1.5.5 podgen.Category...... 42 1.5.6 podgen.warnings...... 43 1.5.7 podgen.util...... 44

2 External Resources 45

Index 47

i ii PodGen Documentation, Release 1.1.0

(https://travis-ci.org/tobinus/python-podgen) (http://podgen.readthedocs.io/en/latest/?badge=latest) Are you looking for a clean and simple library which helps you generate podcast RSS feeds from your Python code? Here is how you do that with PodGen: from podgen import Podcast, Episode, Media # Create the Podcast p= Podcast( name="Animals Alphabetically", description="Every Tuesday, biologist John Doe and wildlife" "photographer Foo Bar introduce you to a new animal.", ="http://example.org/animals-alphabetically", explicit=False, ) # Add some episodes p.episodes+=[ Episode( title="Aardvark", media=Media("http://example.org/files/aardvark.mp3", 11932295), summary="With an English name adapted directly from Afrikaans" '-- literally meaning"earth pig" -- this fascinating' "animal has both circular teeth and a knack for" "digging.", ), Episode( title="Alpaca", media=Media("http://example.org/files/alpaca.mp3", 15363464), summary="Thousands of years ago, alpacas were already" "domesticated and bred to produce the best fibers." "Case in point: we have found clothing made from" "alpaca fiber that is 2000 years old. How is this" "possible, and what makes it different from llamas?", ), ] # Generate the RSS feed =p.rss_str()

You don’t need to read the RSS specification, write XML by hand or wrap your head around ambiguous, undocumented APIs. PodGen incorporates the industry’s best practices and lets you focus on collecting the necessary metadata and publishing the podcast. PodGen is compatible with Python 2.7 and 3.4+.

Warning: As of March 6th 2020 (v1.1.0), PodGen does not support the additions and changes made by Apple to their podcast standards since 2016, with the exception of the 2019 categories. This includes the ability to mark episodes with episode and season number, and the ability to mark the podcast as “”. It is a goal to implement those changes in a new release. Please refer to the Roadmap.

Contents 1 PodGen Documentation, Release 1.1.0

2 Contents CHAPTER 1

Contents

1.1 Background

Learn about the “why” and “how” of the PodGen project itself.

1.1.1 Philosophy

This project is heavily inspired by the “for humans” approach of the Requests (https://requests.readthedocs.io) library, which features an API that is designed to give the developer a great user experience. This is done by finding a suitable scope and abstraction level, and designing the API so it supports the developer’s vocabulary and their mental model of how the domain works. For example, instead of using the names of XML tags like “:image”, a more relevant name, here simply “image”, is used. Another example is the duration of a podcast episode. In XML terms, this is put into an “itunes:duration” which exists outside of the “enclosure” tag, which holds the filename and file size. In PodGen, the filename, file size, file type and audio duration are all placed together in a Media instance, since they are all related to the media itself. The goal has been to “hide” the messy details of the XML and provide an API on top which uses words that you recognize and use daily when working with podcasts. To be specific, PodGen aims to follow the same PEP 20 (https://www.python.org/dev/peps/pep-0020/) idioms as Re- quests (https://requests.readthedocs.io/en/master/user/intro/#philosophy): 1. Beautiful is better than ugly. 2. Explicit is better than implicit. 3. Simple is better than complex. 4. Complex is better than complicated. 5. Readability counts. To enable this, the project focuses on one task alone: making it easy to generate a podcast.

3 PodGen Documentation, Release 1.1.0

1.1.2 Scope

This library does NOT help you publish a podcast, or manage the metadata of your podcasts. It’s just a tool that accepts information about your podcast and outputs an RSS feed which you can then publish however you want. Both the process of getting information about your podcast, and publishing it needs to be done by you. Even then, it will save you from hammering your head over confusing and undocumented APIs and conflicting views on how different RSS elements should be used. It also saves you from reading the RSS specification, the RSS Best Practices and the documentation for iTunes’ Podcast Connect. Here is an example of how PodGen fits into your code: 1. A request comes to your webserver (using e.g. Flask (https://palletsprojects.com/p/flask/)) 2. A podcast router starts to handle the request. 3. The database is queried for information about the requested podcast. 4. The data retrieved from the database is “translated” into the language of PodGen, using its Podcast, Episode, People and Media classes. 5. The RSS document is generated by PodGen and saved to a variable. 6. The generated RSS document is made into a response and sent to the . PodGen is geared towards developers who aren’t super familiar with RSS and XML. If you know ex- actly how you want the XML to look, then you’re better off using a template engine like Jinja2 (even if friends don’t let friends touch XML bare-handed) or an XML processor like the built-in Python El- ementTree API (https://docs.python.org/3/library/xml.etree.elementtree.html#module-.etree.ElementTree). If you just want an easy way to create and manage your podcasts, check out systems like Podcast Generator (http://www.podcastgenerator.net/).

1.1.3 Why the fork?

This project is a fork of python-feedgen (https://github.com/lkiesow/python-feedgen) which cuts away everything that doesn’t serve the goal of making it easy and simple to generate podcasts from a Python program. Thus, this project includes only a subset of the features of python-feedgen (https://github.com/lkiesow/python-feedgen). And I don’t think anyone in their right mind would accept a pull request which removes 70% of the features ;-) Among other things, support for and Dublin Core is removed, and the remaining code is almost entirely rewritten. A more detailed reasoning follows. Read it if you’re interested, but feel free to skip to the Usage Guide.

Inspiration

The python-feedgen (https://github.com/lkiesow/python-feedgen) project is alright for creating general RSS and ATOM feeds, especially in situations where you’d like to serve the same content in those two formats. However, I wanted to create podcasts, and found myself struggling with getting the library to do what I wanted to do, and I frequently found myself looking at the source to understand what was going on. Perhaps the biggest problem is the awkwardness that stems from enabling RSS and ATOM feeds through the same API. In case you don’t know, ATOM is a competitor to RSS, and has many more capabilities than RSS. However, it is not used for podcasting. The result of mixing both ATOM and RSS include methods that will map an ATOM value to its closest sibling in RSS, some in logical ways (like the ATOM method rights setting the value of the RSS property copyright) and some differ in subtle ways (like using (ATOM) logo versus (RSS) image). Other methods are more complex ( link). They’re all confusing, though, since changing one property automatically changes another implicitly. They also cause bugs, since it is so difficult to wrap your head around how one interact with another. This is the inspiration for forking python-feedgen (https://github.com/lkiesow/python-feedgen) and rewrite the API, without mixing the different standards.

4 Chapter 1. Contents PodGen Documentation, Release 1.1.0

Futhermore, python-feedgen (https://github.com/lkiesow/python-feedgen) gives you a one-to-one mapping to the re- sulting XML elements. This means that you must learn the RSS and podcast standards, which include many legacy elements you don’t really need. For example, the original RSS spec includes support for an image, but that image is required to be less than 144 pixels wide (88 pixels being the default) and 400 pixels high (remember, this was year 2000). Itunes can’t have any of that (understandably so), so they added their own itunes:image tag, which has its own set of requirements (images can be no smaller than 1400x1400px!). I believe the API should help guide the users by hiding the legacy image tag, and you as a user shouldn’t need to know all this. You just need to know that the image must be larger than 1400x1400 pixels, not the history behind everything. Forking a project gives you a lot of freedom, since you don’t have to support any old behaviour. It would be difficult to make these changes upstream, since many of the problems are inherent to the scope and purpose of the library itself, and changing that is difficult and not always desirable.

Summary of changes

If you’ve used python-feedgen (https://github.com/lkiesow/python-feedgen) and want to move over to Pod- Gen, you might as well be moving to a completely different library. Everything has been renamed, some attributes expect bool (https://docs.python.org/3/library/functions.html#bool) where they earlier expected str (https://docs.python.org/3/library/stdtypes.html#str), and so on – you’ll have to forget whatever you’ve learnt about the library. Hopefully, the simple API should ease the pain of switching, and make the resulting code easier to main- tain. The following list is not exhaustive. • The module is renamed from feedgen to podgen. • FeedGenerator is renamed to Podcast and FeedItem is renamed to Episode. • All classes are available at package level, so you no longer need to import them from the module they reside in. For example, podgen.Podcast and podgen.Episode. • Support for ATOM is removed. • Stop using getter and setter methods and start using attributes. – Compound values (like managing_editor or media) expect objects now, like Person and Media. • Remove support for some uncommon, obsolete or difficult to use elements: – ttl – category – image – itunes:summary – rating – textInput • Rename the remaining properties so their names don’t necessarily match the RSS elements they map to. Instead, the names should be descriptive and easy to understand. • Podcast.explicit is now required, and is bool (https://docs.python.org/3/library/functions.html#bool). • Add shorthand for generating the RSS: Just try to converting your Podcast object to str (https://docs.python.org/3/library/stdtypes.html#str)! • Expand the documentation. • Move away from the extension framework, and rely on class inheritance instead.

1.1. Background 5 PodGen Documentation, Release 1.1.0

1.1.4 Roadmap

When PodGen reaches a certain point where it has all the features it needs, while still being simple to use, it would not be necessary with further updates. . . had it not been for changes to PodGen’s dependencies and changes to the overall podcast standards. Luckily, the applications that read podcasts tend to be backwards compatible, in order to support all the old podcasts that are out there. The current plan for PodGen updates is as follows: • New minor version: Support for the new Apple Podcast specifications, as much as is possible without breaking backwards compatibility. • Deprecation warnings for properties and such which will be removed in the next major version. • New major version: Support for the new Apple Podcast specifications which could not be included earlier due to backwards compatibility, or which have new names or behaviours to simplify the API. Removal of deprecated features. • New minor versions: Removal of support for Python releases that have passed End of Life (https://devguide.python.org/#status-of-python-branches) (Python 2.7 after January 1st 2020, Python 3.4), al- lowing for simplifications and use of new features in the code base. Other code and documentation improve- ments should hopefully stop PodGen from becoming stale and burdensome to maintain. • Better ways of adding support for RSS features which PodGen itself does not support. Unlike other libraries with evolving demands, PodGen is expected to stay relatively stable with the occasional update. Changes to the Apple Podcast specifications do not occur particularly often, so PodGen won’t need to change often either. Since PodGen is an open source project and its maintainer has a limited amount of time to spare, updates may be sporadic. Despite this, please don’t hesitate to report issues or feature requests if there is something that does not work or you feel should be included. The project is used by the Student Radio of Trondheim and will be kept up-to-date with their requirements, though they are not primarily a podcast producer.

1.1.5 License

PodGen is licensed under the terms of both the FreeBSD license and the LGPLv3+. Choose the one which is more convenient for you. For more details, have a look at li- cense.bsd (https://github.com/tobinus/python-podgen/blob/master/license.bsd) and license.lgpl (https://github.com/tobinus/python-podgen/blob/master/license.lgpl).

1.2 Usage Guide

1.2.1 Installation

PodGen can be used on any system (if not: file a bug report!), and officially supports Python 2.7 and 3.4, 3.5, 3.6 and 3.7. Use pip (https://pypi.python.org/pypi):

$ pip install podgen

Remember to use a virtual environment (http://docs.python-guide.org/en/latest/dev/virtualenvs/)!

6 Chapter 1. Contents PodGen Documentation, Release 1.1.0

Note: One of the dependencies of PodGen, lxml (https://lxml.de/), stopped supporting Python 3.4 in version 4.4.0. If you are installing PodGen using Python 3.4, you should select a compatible version of lxml by running e.g.: pip install'lxml<4.4.0'

The step “Running setup.py install for lxml” will take several minutes and requires installation of building tools (https://lxml.de/installation.html), since the lxml version does not include pre-built binaries.

1.2.2 Podcasts

In PodGen, the term podcast refers to the show which listeners can subscribe to, which consists of individual episodes. Therefore, the Podcast class will be the first thing you start with.

Creating a new instance

You can start with a blank podcast by invoking the Podcast constructor with no arguments, like this: from podgen import Podcast p= Podcast()

Mandatory attributes

There are four attributes which must be set before you can generate your podcast. They are mandatory because Apple’s podcast directory will not accept podcasts without this information. If you try to generate the podcast without setting all of the mandatory attributes, you will get an error. The mandatory attributes are: p.name="My Example Podcast" p.description="Lorem ipsum dolor sit amet, consectetur adipiscing elit." p.website="https://example.org" p.explicit= False

They’re mostly self explanatory, but you can read more about them if you’d like: • name • description • website • explicit

Image

A podcast’s image is worth special attention: p.image="https://example.com/static/example_podcast.png"

Podcast.image The URL of the artwork for this podcast. iTunes prefers square images that are at least 1400x1400 pixels. Podcasts with an image smaller than this are not eligible to be featured on the iTunes Store.

1.2. Usage Guide 7 PodGen Documentation, Release 1.1.0

iTunes supports images in JPEG and PNG formats with an RGB color space (CMYK is not supported). The URL must end in “.jpg” or “.png”; if they don’t, a NotSupportedByItunesWarning will be issued. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS itunes:image

Note: If you change your podcast’s image, you must also change the file’s name; iTunes doesn’t check the image to see if it has changed. Additionally, the hosting your cover art image must allow HTTP HEAD requests (most servers support this).

Even though the image technically is optional, you won’t reach people without it.

Optional attributes

There are plenty of other attributes that can be used with podgen.Podcast:

Commonly used p.copyright="2016 Example Radio" p.language="en-US" p.authors= [Person("John Doe","[email protected]")] p.feed_url="https://example.com/feeds/podcast.rss" # URL of this feed p.category= Category("Music","Music History") p.owner=p.authors[0] p.xslt="https://example.com/feed/stylesheet.xsl" # URL of XSLT stylesheet

Read more: • copyright • language • authors • feed_url • category • owner • xslt

Less commonly used

Some of those are obscure while some of them are often times not needed. Others again have very reasonable defaults.

# RSS Cloud enables podcatchers to subscribe to notifications when there's # a new episode ready, however it's not used much. p.cloud=("server.example.com", 80,"/rpc","cloud.notify","xml-rpc") import datetime # pytz is a dependency of this library, and makes it easy to deal with (continues on next page)

8 Chapter 1. Contents PodGen Documentation, Release 1.1.0

(continued from previous page) # timezones. Generally, all dates must be timezone aware. import pytz # last_updated is datetime when the feed was last refreshed. If you don't # set it, the current date and time will be used instead when the feed is # generated, which is generally what you want. Nevertheless, you can # set your own date: p.last_updated= datetime.datetime(2016,5, 18,0,0, tzinfo=pytz.utc))

# publication_date is when the contents of this feed last were published. # If you don't set it, the date of the most recent Episode is used. Again, # this is generally what you want, but you can override it: p.publication_date= datetime.datetime(2016,5, 17, 15, 32,tzinfo=pytz.utc))

# Set of days on which podcatchers won't need to refresh the feed. # Not implemented widely. p.skip_days={"Friday","Saturday","Sunday"}

# Set of hours on which podcatchers won't need to refresh the feed. # Not implemented widely. p.skip_hours= set(range(8)) p.skip_hours|= set(range(16, 24))

# Person to contact regarding technical aspects of the feed. p.web_master= Person( None,"[email protected]")

# Identify the software which generates the feed (defaults to python-podgen) p.set_generator("ExamplePodcastProgram",(1,0,0)) # (you can also set the generator string directly) p.generator="ExamplePodcastProgram v1.0.0 (with help from python-feedgen)"

# !!! Be very careful about using the following attributes !!!

# Tell iTunes that this feed has moved somewhere else. p.new_feed_url="https://podcast.example.com/example"

# Tell iTunes that this feed will never be updated again. p.complete= True

# Tell iTunes that you'd rather not have this feed appear on iTunes. p.withhold_from_itunes= True

Read more: • cloud • last_updated • publication_date • skip_days • skip_hours • web_master • set_generator() • new_feed_url • complete

1.2. Usage Guide 9 PodGen Documentation, Release 1.1.0

• withhold_from_itunes

Shortcut for filling in data

Instead of creating a new Podcast object in one statement, and populating it with data one statement at a time afterwards, you can create a new Podcast object and fill it with data in one statement. Simply use the attribute name as keyword arguments to the constructor:

import podgen p= podgen.Podcast( =, =, ... )

Using this technique, you can define the Podcast as part of a list comprehension, dictionaries and so on. Take a look at the API Documentation for Podcast for a practical example.

1.2.3 Episodes

Once you have created and populated a Podcast, you probably want to add some episodes to it. To add episodes to a feed, you need to create new podgen.Episode objects and append them to the list of episodes in the Podcast. That is pretty straight-forward:

from podgen import Podcast, Episode # Create the podcast (see the previous section) p= Podcast() # Create new episode my_episode= Episode() # Add it to the podcast p.episodes.append(my_episode)

There is a convenience method called Podcast.add_episode which optionally creates a new instance of Episode, adds it to the podcast and returns it, allowing you to assign it to a variable:

from podgen import Podcast p= Podcast() my_episode=p.add_episode()

If you prefer to use the constructor, there’s nothing wrong with that:

from podgen import Podcast, Episode p= Podcast() my_episode=p.add_episode(Episode())

The advantage of using the latter form is that you can pass data to the constructor.

Filling with data

There is only one rule for episodes: they must have either a title or a summary, or both. Additionally, you can opt to have a long summary, as well as a short subtitle:

10 Chapter 1. Contents PodGen Documentation, Release 1.1.0

my_episode.title="S01E10: The Best Example of them All" my_episode.subtitle="We found the greatest example!" my_episode.summary="In this week's episode, we have found the"+\ "greatest example of them all." my_episode.long_summary="In this week's episode, we went out on a"+\ "search to find the greatest example of them"+\ "all.
Today's intro music:"+\ "Example Song"

Read more: • title • subtitle • summary • long_summary

Enclosing media

Of course, this isn’t much of a podcast if we don’t have any media attached to it! from datetime import timedelta from podgen import Media my_episode.media= Media("http://example.com/podcast/s01e10.mp3", size=17475653, type="audio/mpeg", # Optional, can be determined # from the duration=timedelta(hours=1, minutes=2, seconds=36) )

The media’s attributes (and the arguments to the constructor) are:

At- Description tribute url The URL at which this media file is accessible. size The size of the media file as bytes, given either as int (https://docs.python.org/3/library/functions.html#int) or a str (https://docs.python.org/3/library/stdtypes.html#str) which will be parsed. type The media file’s MIME type (https://en.wikipedia.org/wiki/Media_type). durationHow long the media file lasts, given as a datetime.timedelta (https://docs.python.org/3/library/datetime.html#datetime.timedelta)

You can leave out some of these:

Attribute Effect if left out url Mandatory. size Can be 0, but do so only if you cannot determine its size (for example if it’s a stream). type Can be left out if the URL has a recognized file extensions. In that case, the type will be determined from the URL’s file extension. duration Can be left out since it is optional. It will stay as None (https://docs.python.org/3/library/constants.html#None).

1.2. Usage Guide 11 PodGen Documentation, Release 1.1.0

Warning: Remember to encode special characters in your ! For example, say you have a file named library-pod-#023-future.mp3, which you host at http://podcast. example.org/episodes. You might try to use the URL http://podcast.example. org/episodes/library-pod-#023-future.mp3. This, however, will not work, since the hash (#) has a special meaning in URLs. Instead, you should use urllib.parse.quote() (https://docs.python.org/3/library/urllib.parse.html#urllib.parse.quote) in Python3, or urllib.quote() in Python2, to escape the special characters in the filename in the URL. The correct URL would then become http://podcast.example.org/episodes/library-pod-%23023-future.mp3.

Populating size and type from server

By using the special factory Media.create_from_server_response you can gather missing information by asking the server at which the file is hosted:

my_episode.media= Media.create_from_server_response( "http://example.com/podcast/s01e10.mp3", duration=timedelta(hours=1, minutes=2, seconds=36) )

Here’s the effect of leaving out the fields:

Attribute Effect if left out url Mandatory. size Will be populated using the Content-Length header. type Will be populated using the Content-Type header. duration Will not be populated by data from the server; will stay None (https://docs.python.org/3/library/constants.html#None).

Populating duration from server

Determining duration requires that the media file is downloaded to the local machine, and is therefore not done unless you specifically ask for it. If you don’t have the media file locally, you can populate the duration field by using Media.fetch_duration():

my_episode.media.fetch_duration()

If you do happen to have the media file in your file system, you can use it to populate the duration attribute by calling Media.populate_duration_from():

filename="/home/example/Music/podcast/s01e10.mp3" my_episode.media.populate_duration_from(filename)

Note: Even though you technically can have file names which don’t end in their actual file extension, iTunes will use the file extension to determine what type of file it is, without even asking the server. You must therefore make sure your media files have the correct file extension. If you don’t care about compatibility with iTunes, you can provide the MIME type yourself to fix any errors you receive about this.

12 Chapter 1. Contents PodGen Documentation, Release 1.1.0

This also applies to the tool used to determine a file’s duration, which uses the file’s file extension to determine its type.

Read more about: • podgen.Episode.media (the attribute) • podgen.Media (the class which you use as value)

Identifying the episode

Every episode is identified by a globally unique identifier (GUID). By default, this id is set to be the same as the URL of the media (see above) when the feed is generated. That is, given the example above, the id of my_episode would be http://example.com/podcast/s01e10.mp3.

Warning: An episode’s ID should never change. Therefore, if you don’t set id, the media URL must never change either.

Read more about the id attribute.

Episode’s publication date

An episode’s publication date indicates when the episode first went live. It is used to indicate how old the episode is, and a client may say an episode is from “1 hour ago”, “yesterday”, “last week” and so on. You should therefore make sure that it matches the exact time that the episode went live, or else your listeners will get a new episode which appears to have existed for longer than it has.

Note: It is generally a bad idea to use the media file’s modification date as the publication date. If you make your episodes some time in advance, your listeners will suddenly get an “old” episode in their feed! my_episode.publication_date= datetime.datetime(2016,5, 18, 10,0, tzinfo=pytz.utc)

Read more about the publication_date attribute.

The Link

If you’re publishing articles along with your podcast episodes, you should link to the relevant article. Examples can be linking to the sound on SoundCloud or the post on your website. Usually, your listeners expect to find the entirety of the summary by following the link. my_episode.link="http://example.com/article/2016/05/18/Best-example"

Note: If you don’t have anything to link to, then that’s fine as well. No link is better than a disappointing link.

Read more about the link attribute.

1.2. Usage Guide 13 PodGen Documentation, Release 1.1.0

The Authors

Normally, the attributes Podcast.authors and Podcast.web_master (if set) are used to determine the au- thors of an episode. Thus, if all your episodes have the same authors, you should just set it at the podcast level. If an episode’s list of authors differs from the podcast’s, though, you can override it like this: my_episode.authors= [Person("Joe Bob")]

You can even have multiple authors: my_episode.authors= [Person("Joe Bob"), Person("Alice Bob")]

Read more about an episode's authors.

Less used attributes

# Not actually implemented by iTunes; the Podcast's image is used. my_episode.image="http://example.com/static/best-example.png"

# Set it to override the Podcast's explicit attribute for this episode only. my_episode.explicit= False

# Tell iTunes that the enclosed video is closed captioned. my_episode.is_closed_captioned= False

# Tell iTunes that this episode should be the first episode on the store # page. my_episode.position=1

# Careful! This will hide this episode from the iTunes store page. my_episode.withhold_from_itunes= True

More details: • image • explicit • is_closed_captioned • position • withhold_from_itunes

Shortcut for filling in data

Instead of assigning those values one at a time, you can assign them all in one go in the constructor – just like you can with Podcast. Just use the attribute name as the keyword:

Episode( =, =, ... )

See also the example in the API Documentation.

14 Chapter 1. Contents PodGen Documentation, Release 1.1.0

1.2.4 RSS

Once you’ve added all the information and episodes, you’re ready to take the final step:

rssfeed=p.rss_str() # Print to stdout, just as an example print(rssfeed)

If you’re okay with the default parameters of podgen.Podcast.rss_str(), you can use a shortcut by converting your Podcast to str (https://docs.python.org/3/library/stdtypes.html#str):

rssfeed= str(p) print(rssfeed) # Or let print convert to str for you print(p)

rss_str([minimize, encoding, xml_declaration]) Generate an RSS feed and return the feed XML as string.

You may also write the feed to a file directly, using podgen.Podcast.rss_file():

p.rss_file('rss.xml', minimize=True)

rss_file(filename[, minimize, encoding, . . . ]) Generate an RSS feed and write the resulting XML to a file.

Note: If there are any mandatory attributes that aren’t set, you’ll get errors when generating the RSS.

Note: Generating the RSS is not completely free. Save the result to a variable once instead of generating the same RSS over and over.

1.2.5 Full example

This example is located at podgen/__main__.py in the package, and is run as part of the testing routines.

1 def main():

2 """Create an example podcast and print it or save it to a file."""

3 # There must be exactly one argument, and it is must end with rss

4 if len(sys.argv) !=2 or not (

5 sys.argv[1].endswith('rss')):

6 # Invalid usage, print help message

7 # print_enc is just a custom function which functions like print,

8 # except it deals with byte arrays properly.

9 print_enc ('Usage: %s ( .rss | rss )'%\

10 'python -m podgen')

11 print_enc ('')

12 print_enc (' rss -- Generate RSS test output and print it to

˓→stdout.')

13 print_enc (' .rss -- Generate RSS test teed and write it to file.

˓→rss.') (continues on next page)

1.2. Usage Guide 15 PodGen Documentation, Release 1.1.0

(continued from previous page)

14 print_enc ('')

15 exit()

16

17 # Remember what type of feed the user wants

18 arg= sys.argv[1]

19

20 from podgen import Podcast, Person, Media, Category, htmlencode

21 # Initialize the feed

22 p= Podcast()

23 p.name='Testfeed'

24 p.authors.append(Person("Lars Kiesow","[email protected]"))

25 p.website='http://example.com'

26 p.copyright='cc-by'

27 p.description='This is a cool feed!'

28 p.language='de'

29 p.feed_url='http://example.com/feeds/myfeed.rss'

30 p.category= Category('Leisure','Aviation')

31 p.explicit= False

32 p.complete= False

33 p.new_feed_url='http://example.com/new-feed.rss'

34 p.owner= Person('John Doe','[email protected]')

35 p.xslt="http://example.com/stylesheet.xsl"

36

37 e1=p.add_episode()

38 e1.id='http://lernfunk.de/_MEDIAID_123#1'

39 e1.title='First Element'

40 e1.summary= htmlencode('''Lorem ipsum dolor sit amet, consectetur adipiscing

˓→elit. Tamen

41 aberramus a proposito, et, ne longius, prorsus, inquam, Piso, si ista

42 mala sunt, placet. Aut etiam, ut vestitum, sic sententiam habeas aliam

43 domesticam, aliam forensem, ut in fronte ostentatio sit, intus veritas

44 occultetur? Cum id fugiunt, re eadem defendunt, quae Peripatetici,

45 verba <3.''')

46 e1.link='http://example.com'

47 e1.authors= [Person('Lars Kiesow','[email protected]')]

48 e1.publication_date= datetime.datetime(2014,5, 17, 13, 37, 10, tzinfo=pytz.utc)

49 e1.media= Media("http://example.com/episodes/loremipsum.mp3", 454599964,

50 duration=

51 datetime.timedelta(hours=1, minutes=32, seconds=19))

52

53 # Should we just print out, or write to file?

54 if arg =='rss':

55 # Print

56 print_enc(p.rss_str())

57 elif arg.endswith('rss'):

58 # Write to file

59 p.rss_file(arg, minimize=True)

1.3 Advanced Topics

1.3.1 Using PubSubHubbub

PubSubHubbub is a free and open protocol for pushing updates to clients when there’s new content available in the feed, as opposed to the traditional polling clients do.

16 Chapter 1. Contents PodGen Documentation, Release 1.1.0

Read about what PubSubHubbub is (https://en.wikipedia.org/wiki/PubSubHubbub) before you continue.

Note: While the protocol supports having multiple PubSubHubbub hubs for a single Podcast, there is no support for this in PodGen at the moment.

Warning: Read through the whole guide at least once before you start implementing this. Specifically, you must not set the pubsubhubbub attribute if you haven’t got a way to notify hubs of new episodes.

Contents

• Using PubSubHubbub – Step 1: Set feed_url – Step 2: Decide on a hub – Step 3: Set pubsubhubbub – Step 4: Set HTTP Link Header – Step 5: Notify the hub of new episodes

Step 1: Set feed_url

First, you must ensure that the Podcast object has the feed_url attribute set to the URL at which the feed is accessible.

# Assume p is a Podcast object p.feed_url="https://example.com/feeds/examplefeed.rss"

Step 2: Decide on a hub

The Wikipedia article (https://en.wikipedia.org/wiki/PubSubHubbub#Usage) mentions a few options you can use (called Community Hosted hub providers). Alternatively, you can set up and host your own server using one of the open source alternatives, like for instance Switchboard (https://github.com/aaronpk/Switchboard).

Step 3: Set pubsubhubbub

The Podcast must contain information about which hub to use. You do this by setting pubsubhubbub to the URL which the hub is available at. p.pubsubhubbub="https://pubsubhubbub.example.com/"

Step 4: Set HTTP Link Header

In addition to embedding the PubSubHubbub hub URL and the feed’s URL in the RSS itself, you should use the Link header (https://tools.ietf.org/html/rfc5988#page-6) in the HTTP response that is sent with this feed, duplicating the link to the PubSubHubbub and the feed. Example of what it might look like:

1.3. Advanced Topics 17 PodGen Documentation, Release 1.1.0

Link: ; rel="hub", ; rel="self"

How you can achieve this varies from framework to framework. Here is an example using Flask (http://flask.pocoo.org/) (assuming the code is inside a view function):

from flask import make_response from podgen import Podcast # ... @app.route("/") # Just as an example def show_feed(feedname): p= Podcast() # ... # This is the relevant part: response= make_response(str(p)) response.headers.add("Link","< %s>"%p.pubsubhubbub, rel="hub") response.headers.add("Link","< %s>"%p.feed_url, rel="self") return response

This is necessary for compatibility with the different versions of PubSubHubbub. The latest version of the standard (http://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#rfc.section.4) specifically says that pub- lishers MUST use the Link header. If you’re unable to do this, you can try publishing the feed without; most clients and hubs should manage just fine.

Step 5: Notify the hub of new episodes

Warning: The hub won’t know that you’ve published new episodes unless you tell it about it. If you don’t do this, the hub will assume there is no new content, and clients which trust the hub to inform them of new episodes will think there is no new content either. Don’t set the pubsubhubbub field if you haven’t set this up yet.

Different hubs have different ways of notifying it of new episodes. That’s why you must notify the hubs yourself; supporting all hubs is out of scope for PodGen. If you use the Google PubSubHubbub (https://pubsubhubbub.appspot.com/) or the Superfeedr hub (https://pubsubhubbub.superfeedr.com/), there is a pip package called PubSubHubbub_Publisher (https://pypi.python.org/pypi/PubSubHubbub_Publisher) which provides this functionality for you. Example:

from pubsubhubbub_publish import publish, PublishError from podgen import Podcast # ... try: publish(p.pubsubhubbub, p.feed_url) except PublishError as e: # Handle error

In all other cases, you’re encouraged to use Requests (http://docs.python-requests.org) to make the necessary POST request (http://docs.python-requests.org/en/master/user/quickstart/#make-a-request) (if no publisher package is avail- able).

Note: If you have changes in multiple feeds, you can usually send just one single notification to the hub with all the feeds’ URLs included. It is worth researching, as it can save both you and the hub a lot of time.

18 Chapter 1. Contents PodGen Documentation, Release 1.1.0

1.3.2 Adding new tags

Are there XML elements you want to use that aren’t supported by PodGen? If so, you should be able to add them in using inheritance.

Note: There hasn’t been a focus on making it easy to extend PodGen. Future versions may provide better support for this.

Note: Feel free to add a feature request to GitHub Issues (https://github.com/tobinus/python-podgen/issues) if you think PodGen should support a certain element out of the box.

Quick How-to

1. Create new class that extends Podcast. 2. Add the new attribute. 3. Override _create_rss(), call super()._create_rss(), add the new element to its result and return the new tree. You can do the same with Episode, if you replace _create_rss() with rss_entry() above. There are plenty of small quirks you have to keep in mind. You are strongly encouraged to read the example below.

Using namespaces

If you’ll use RSS elements from another namespace, you must make sure you update the _nsmap attribute of Podcast (you cannot define new namespaces from an episode!). It is a dictionary with the prefix as key and the URI for that namespace as value. To use a namespace, you must put the URI inside curly braces, with the tag name following right after (outside the braces). For example:

"{%s}link"% self._nsmap['atom'] # This will render as atom:link

The lxml API documentation (http://lxml.de/api/index.html) is a pain to read, so just look at the source code for PodGen (https://github.com/tobinus/python-podgen/blob/master/podgen/podcast.py) and the example below.

Example: Adding a ttl element

The examples here assume version 3 of Python is used. ttl is an RSS element and stands for “time to live”, and can only be an integer which indicates how many min- utes the podcatcher can rely on its copy of the feed before refreshing (or something like that). There is confusion as to what it is supposed to mean (max refresh frequency? min refresh frequency?), which is why it is not in- cluded in PodGen. If you use it, you should treat it as the recommended update period (source: RSS Best Practices (http://www.rssboard.org/rss-profile#element-channel-ttl)).

Using traditional inheritance

1.3. Advanced Topics 19 PodGen Documentation, Release 1.1.0

# The module used to create the XML tree and generate the XML from lxml import etree

# The class we will extend from podgen import Podcast class PodcastWithTtl(Podcast): """This is an extension of Podcast, which supports ttl.

You gain access to ttl by creating a new instance of this class instead of Podcast. """ def __init__(self, *args, **kwargs): # Initialize the ttl value self.__ttl= None

# Call Podcast's constructor (this will set ttl using setattr if # given as argument to the constructor, hence why self.__ttl is # defined before we do this) super().__init__(*args, **kwargs)

# If we were to use another namespace, we would add this here: # self._nsmap['prefix'] = "URI"

@property def ttl(self): """Your suggestion for how many minutes podcatchers should wait before refreshing the feed.

ttl stands for "time to live".

:type: :obj:`int` :RSS: ttl """ # By using @property and @ttl.setter, we encapsulate the ttl field # so that we can check the value that is assigned to it. # If you don't need this, you could just rename self.__ttl to # self.ttl and remove those two methods. return self.__ttl

@ttl.setter def ttl(self, ttl): # Try to convert to int try: ttl_int= int(ttl) except ValueError: raise TypeError("ttl expects an integer, got %s"% ttl) # Is this negative? if ttl_int<0: raise ValueError("Negative ttl values aren't accepted, got %s" % ttl_int) # All checks passed self.__ttl= ttl_int

def _create_rss(self): # Let Podcast generate the lxml etree (adding the standard elements)

(continues on next page)

20 Chapter 1. Contents PodGen Documentation, Release 1.1.0

(continued from previous page) rss= super()._create_rss() # We must get the channel element, since we want to add subelements # to it. channel= rss.find("channel") # Only add the ttl element if it has been populated. if self.__ttl is not None: # First create our new subelement of channel. ttl= etree.SubElement(channel,'ttl') # If we were to use another namespace, we would instead do this: # ttl = etree.SubElement(channel, # '{%s}ttl' % self._nsmap['prefix'])

# Then, fill it with the ttl value ttl.text= str(self.__ttl)

# Return the new etree, now with ttl return rss

# How to use the new class (normally, you would put this somewhere else) if __name__ =='__main__': myPodcast= PodcastWithTtl(name="Test", website="http://example.org", explicit=False, description="Testing ttl") myPodcast.ttl= 90 # or set ttl=90 in the constructor print(myPodcast)

Using mixins

To use mixins, you cannot make the class with the ttl functionality inherit Podcast. Instead, it must inherit nothing. Other than that, the code will be the same, so it doesn’t make sense to repeat it here. class TtlMixin(object): # ...

# How to use the new mixin class PodcastWithTtl(TtlMixin, Podcast): def __init__(*args, **kwargs): super().__init__(*args, **kwargs) myPodcast= PodcastWithTtl(name="Test", website="http://example.org", explicit=False, description="Testing ttl") myPodcast.ttl= 90 print(myPodcast)

Note the order of the mixins in the class declaration. You should read it as the path Python takes when looking for a method. First Python checks PodcastWithTtl, then TtlMixin and finally Podcast. This is also the order the methods are called when chained together using super() (https://docs.python.org/3/library/functions.html#super). If you had Podcast first, Podcast._create_rss() method would be run first, and since it never calls super(). _create_rss(), the TtlMixin’s _create_rss would never be run. Therefore, you should always have Podcast last in that list.

Which approach is best?

The advantage of mixins isn’t really displayed here, but it will become apparent as you add more and more extensions. Say you define 5 different mixins, which all add exactly one more element to Podcast. If you used traditional

1.3. Advanced Topics 21 PodGen Documentation, Release 1.1.0

inheritance, you would have to make sure each of those 5 subclasses made up a tree. That is, class 1 would inherit Podcast. Class 2 would have to inherit class 1, class 3 would have to inherit class 2 and so on. If two of the classes had the same superclass, you could get screwed. By using mixins, you can put them together however you want. Perhaps for one podcast you only need ttl, while for another podcast you want to use the textInput element in addition to ttl, and another podcast requires the textInput element together with the comments element. Using traditional inheritance, you would have to duplicate code for textInput in two classes. Not so with mixins:

class PodcastWithTtl(TtlMixin, Podcast): def __init__(*args, **kwargs): super().__init__(*args, **kwargs) class PodcastWithTtlAndTextInput(TtlMixin, TextInputMixin, Podcast): def __init__(*args, **kwargs): super().__init__(*args, **kwargs) class PodcastWithTextInputAndComments(TextInputMixin, CommentsMixin, Podcast): def __init__(*args, **kwargs): super().__init__(*args, **kwargs)

If the list of elements you want to use varies between different podcasts, mixins are the way to go. On the other hand, mixins are overkill if you are okay with one giant class with all the elements you need.

1.4 Contributing

1.4.1 Setting up

To install the dependencies, run:

$ pip install -r requirements.txt

while you have a virtual environment (http://docs.python-guide.org/en/latest/dev/virtualenvs/) activated. You are recommended to use pyenv (https://github.com/yyuu/pyenv) to handle virtual environments and Python ver- sions. That way, you can easily test and debug problems that are specific to one version of Python.

1.4.2 Testing

You can perform an integration test by running podgen/__main__.py:

$ python -m podgen

When working on this project, you should run the unit tests as well as the integration test, like this:

$ make test

The unit tests reside in podgen/tests and are written using the unittest (https://docs.python.org/3/library/unittest.html#module-unittest) module.

22 Chapter 1. Contents PodGen Documentation, Release 1.1.0

1.4.3 Values

Read Philosophy, Scope and Why the fork? for a run-down on what values/principles lay the foundation for this project. In short, it is important to keep the API as simple as possible. You must also write unittests as you code, ideally using test-driven development (that is, write a test, observe that the test fails, write code so the test works, observe that the test succeeds, write a new test and so on). That way, you know that the tests actually contribute and you get to think about how the API will look before you tackle the problem head-on. Make sure you update podgen/__main__.py so it still works, and use your new functionality there if it makes sense. You must also make sure you update any relevant documentation. Remember that the documentation includes lots of examples and also describes the API independently from docstring comments in the code itself. Pull requests in which the unittests and documentation are NOT up to date with the code will NOT be accepted. Lastly, a single commit shouldn’t include more changes than it needs. It’s better to do a big change in small steps, each of which is one commit. Explain the impact of your changes in the commit message.

1.4.4 The Workflow

1. Check out waffle.io (https://waffle.io/tobinus/python-podgen) or GitHub Issues (https://github.com/tobinus/python-podgen/issues). • Find the issue you wish to work on. • Add your issue if it’s not already there. • Discuss the issue and get feedback on your proposed solution. Don’t waste time on a solution that might not be accepted! 2. Work on the issue in a separate branch which follows the name scheme tobinus/ python-podgen#- in your own fork. To be honest, I don’t know if Waffle.io will notice that, but it doesn’t hurt to try, I guess! You might want to read up on Waffle.io’s recommended workflow (https://github.com/waffleio/waffle.io/wiki/Recommended-Workflow- Using-Pull-Requests-&-Automatic-Work-Tracking). 3. Push the branch. 4. Do the work. 5. When you’re done and you’ve updated the documentation and tests (see above), create a pull request which references the issue. 6. Wait for me or some other team member to review the pull request. Keep an eye on your inbox or your GitHub notifications, since we may have some objections or feedback that you must take into consideration. It’d be a shame if your work never led to anything because you didn’t notice a comment! 7. Consider making the same changes to python-feedgen (https://github.com/lkiesow/python-feedgen) as well.

1.5 API Documentation

podgen.Podcast(**kwargs) Class representing one podcast feed. podgen.Episode(**kwargs) Class representing an episode in a podcast. Continued on next page

1.5. API Documentation 23 PodGen Documentation, Release 1.1.0

Table 3 – continued from previous page podgen.Person([name, ]) Data-oriented class representing a single person or en- tity. podgen.Media(url[, size, type, duration, . . . ]) Data-oriented class representing a pointer to a media file. podgen.Category(category[, subcategory]) Immutable class representing an Apple Podcasts cate- gory. podgen.warnings podgen.warnings podgen.util podgen.util

1.5.1 podgen.Podcast class podgen.Podcast(**kwargs) Class representing one podcast feed. The following attributes are mandatory: • name • website • description • explicit All attributes can be assigned None (https://docs.python.org/3/library/constants.html#None) in addition to the types specified below. Types etc. are checked during assignment, to help you discover errors earlier. Duck typing is employed wherever a class in podgen is expected. There is a shortcut you can use when creating new Podcast objects, that lets you populate the attributes using the constructor. Use keyword arguments with the attribute name as keyword and the desired value as value. As an example:

>>> import podgen >>> # The following... >>> p= Podcast() >>> p.name="The Test Podcast" >>> p.website="http://example.com" >>> # ...is the same as this: >>> p= Podcast( ... name="The Test Podcast", ... website="http://example.com", ... )

Of course, you can do this for as many (or few) attributes as you like, and you can still set the attributes afterwards, like always. Raises TypeError if you use a keyword which isn’t recognized as an attribute. ValueError if you use a value which isn’t compatible with the attribute (just like when you assign it manually). add_episode(new_episode=None) Shorthand method which adds a new episode to the feed, creating an object if it’s not provided, and returns it. This is the easiest way to add episodes to a podcast. Parameters new_episode – Episode object to add. A new instance of episode_class is used if new_episode is omitted. Returns Episode object created or passed to this function. Example:

24 Chapter 1. Contents PodGen Documentation, Release 1.1.0

... >>> episode1=p.add_episode() >>> episode1.title='First episode' >>> # You may also provide an episode object yourself: >>> another_episode=p.add_episode(podgen.Episode()) >>> another_episode.title='My second episode'

Internally, this method creates a new instance of episode_class, which means you can change what type of objects are created by changing episode_class. apply_episode_order() Make sure that the episodes appear on iTunes in the exact order they have in episodes. This will set each Episode.position so it matches the episode’s position in Podcast.episodes. If you’re using some Episode objects in multiple podcast feeds and you don’t use this method with every feed, you might want to call Podcast.clear_episode_order() after generating this feed’s RSS so an episode’s position in this feed won’t affect its position in the other feeds. authors List of Person that are responsible for this podcast’s editorial content. Any value you assign to authors will be automatically converted to a list, but only if it’s iterable (like tuple, set and so on). It is an error to assign a single Person object to this attribute:

>>> # This results in an error >>> p.authors= Person("John Doe","[email protected]") TypeError: Only iterable types can be assigned to authors, ... >>> # This is the correct way: >>> p.authors= [Person("John Doe","[email protected]")]

The authors don’t need to have both name and email set. The names are shown under the podcast’s title on iTunes. The initial value is an empty list, so you can use the list methods right away. Example:

>>> # This attribute is just a list - you can for example append: >>> p.authors.append(Person("John Doe","[email protected]")) >>> # Or they can be given as new list (overriding earlier authors) >>> p.authors= [Person("John Doe","[email protected]"), ... Person("Mary Sue","[email protected]")]

Type list (https://docs.python.org/3/library/stdtypes.html#list) of podgen.Person RSS managingEditor or dc:creator, and itunes:author

category The iTunes category, which appears in the category column and in iTunes Store listings. Type podgen.Category RSS itunes:category clear_episode_order() Reset Episode.position for every single episode. Use this if you want to reuse an Episode object in another feed, and don’t want its position in this feed to affect where it appears in the other feed. This is not needed if you’ll call Podcast. apply_episode_order() on the other feed, though.

1.5. API Documentation 25 PodGen Documentation, Release 1.1.0

cloud The cloud data of the feed, as a 5-tuple. It specifies a web service that supports the (somewhat dated) rssCloud interface, which can be implemented in HTTP-POST, XML-RPC or SOAP 1.1. The tuple should look like this: (domain, port, path, registerProcedure, protocol). Domain The domain where the webservice can be found. Port The port the webservice listens to. Path The path of the webservice. RegisterProcedure The procedure to call. Protocol Can be either “HTTP-POST”, “xml-rpc” or “soap”. Example:

p.cloud=("podcast.example.org", 80,"/rpc","cloud.notify", "xml-rpc")

Type tuple (https://docs.python.org/3/library/stdtypes.html#tuple) with (str (https://docs.python.org/3/library/stdtypes.html#str), int (https://docs.python.org/3/library/functions.html#int), str (https://docs.python.org/3/library/stdtypes.html#str), str (https://docs.python.org/3/library/stdtypes.html#str), str (https://docs.python.org/3/library/stdtypes.html#str)) RSS cloud

Tip: PubSubHubbub is a competitor to rssCloud, and is the preferred choice if you’re looking to set up a new service of this kind.

complete Whether this podcast is completed or not. If you set this to True, you are indicating that no more episodes will be added to the podcast. If you let this be None or False, you are indicating that new episodes may be posted. Type bool (https://docs.python.org/3/library/functions.html#bool) RSS itunes:complete

Warning: Setting this to True is the same as promising you’ll never ever release a new episode. Do NOT set this to True as long as there’s any chance AT ALL that a new episode will be released someday.

copyright = None The copyright notice for content in this podcast. This should be human-readable. For example, “Copyright 2016 Example Radio”. Note that even if you leave out the copyright notice, your content is still protected by copyright (unless anything else is indicated), since you do not need a copyright statement for something to be protected by copyright. If you intend to put the podcast in public domain or license it under a license, you should say so in the copyright notice. Type str (https://docs.python.org/3/library/stdtypes.html#str)

26 Chapter 1. Contents PodGen Documentation, Release 1.1.0

RSS copyright description = None The description of the podcast, which is a phrase or sentence describing it to potential new subscribers. It is mandatory for RSS feeds, and is shown under the podcast’s name on the iTunes store page. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS description episode_class Class used to represent episodes. This is used by add_episode() when creating new episode objects, and you, too, may use it when creating episodes. By default, this property points to Episode. When assigning a new class to episode_class, you must make sure that the new value (1) is a class and not an instance, and (2) that it is a subclass of Episode (or is Episode itself). Example of use:

>>> # Create new podcast >>> from podgen import Podcast, Episode >>> p= Podcast()

>>> # Normal way of creating new episodes >>> episode1= Episode() >>> p.episodes.append(episode1)

>>> # Or use add_episode (and thus episode_class indirectly) >>> episode2=p.add_episode()

>>> # Or use episode_class directly >>> episode3=p.episode_class() >>> p.episodes.append(episode3)

>>> # Say you want to use AlternateEpisode class instead of Episode >>> from mymodule import AlternateEpisode >>> p.episode_class= AlternateEpisode

>>> episode4=p.add_episode() >>> episode4.title("This is an instance of AlternateEpisode!")

Type class which extends podgen.Episode

episodes List of Episode objects that are part of this podcast. See add_episode() for an easy way to create new episodes and assign them to this podcast in one call. Type list (https://docs.python.org/3/library/stdtypes.html#list) of podgen.Episode RSS item elements explicit = None Whether this podcast may be inappropriate for children or not. This is one of the mandatory attributes, and can seen as the default for episodes. Individual episodes can be marked as explicit or clean independently from the podcast.

1.5. API Documentation 27 PodGen Documentation, Release 1.1.0

If you set this to True, an “explicit” parental advisory graphic will appear next to your podcast artwork on the iTunes Store and in the Name column in iTunes. If it is set to False, the parental advisory type is considered Clean, meaning that no explicit language or adult content is included anywhere in the episodes, and a “clean” graphic will appear. Type bool (https://docs.python.org/3/library/functions.html#bool) RSS itunes:explicit feed_url The URL which this feed is available at. Identifying a feed’s URL within the feed makes it more portable, self-contained, and easier to cache. You should therefore set this attribute if you’re able to. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS atom:link with rel="self" generator = None A string identifying the software that generated this RSS feed. Defaults to a string identifying PodGen. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS generator See also:

The set_generator() method A convenient way to set the generator value and include version and url.

image The URL of the artwork for this podcast. iTunes prefers square images that are at least 1400x1400 pixels. Podcasts with an image smaller than this are not eligible to be featured on the iTunes Store. iTunes supports images in JPEG and PNG formats with an RGB color space (CMYK is not supported). The URL must end in “.jpg” or “.png”; if they don’t, a NotSupportedByItunesWarning will be issued. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS itunes:image

Note: If you change your podcast’s image, you must also change the file’s name; iTunes doesn’t check the image to see if it has changed. Additionally, the server hosting your cover art image must allow HTTP HEAD requests (most servers support this).

language = None The language of the podcast. This allows aggregators to group all Italian language podcasts, for example, on a single page. It must be a two-letter code, as found in ISO639-1, with the possibility of specifying subcodes (eg. en-US for American English). See http://www.rssboard.org/rss-language-codes and http://www.loc.gov/ standards/iso639-2/php/code_list.php Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS language

28 Chapter 1. Contents PodGen Documentation, Release 1.1.0

last_updated The last time the feed was generated. It defaults to the time and date at which the RSS is generated, if set to None (https://docs.python.org/3/library/constants.html#None). The default should be sufficient for most, if not all, use cases. The value can either be a string, which will automatically be parsed into a datetime. datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime) object when assigned, or a datetime.datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime) object. In any case, the time and date must be timezone aware. Set this to False to leave out this element instead of using the default. Type datetime.datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime), str (https://docs.python.org/3/library/stdtypes.html#str) (will be converted to and stored as datetime.datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime)), None (https://docs.python.org/3/library/constants.html#None) for default or False (https://docs.python.org/3/library/constants.html#False) to leave out. RSS lastBuildDate name = None The name of the podcast as a str (https://docs.python.org/3/library/stdtypes.html#str). It should be a human readable title. Often the same as the title of the associated website. This is mandatory and must not be blank. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS title new_feed_url = None When set, tell iTunes that your feed has moved to this URL. After adding this attribute, you should maintain the old feed for 48 hours before retiring it. At that point, iTunes will have updated the directory with the new feed URL. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS itunes:new-feed-url

Warning: iTunes supports this mechanic of changing your feed’s location. However, you cannot assume the same of everyone else who has subscribed to this podcast. Therefore, you should NEVER stop supporting an old location for your podcast. Instead, you should create HTTP redirects so those with the old address are redirected to your new address, and keep those redirects up for all eternity.

Warning: Make sure the new URL you set is correct, or else you’re making people switch to a URL that doesn’t work!

owner The Person who owns this podcast. iTunes will use this person’s name and email address for all corre- spondence related to this podcast. It will not be publicly displayed, but it’s still publicly available in the RSS source. Both the name and email are required. Type podgen.Person RSS itunes:owner

1.5. API Documentation 29 PodGen Documentation, Release 1.1.0

publication_date The publication date for the content in this podcast. You probably want to use the default value. Default value If this is None (https://docs.python.org/3/library/constants.html#None) when the feed is generated, the publication date of the episode with the latest publication date (which may be in the future) is used. If there are no episodes, the publication date is omitted from the feed. If you set this to a str (https://docs.python.org/3/library/stdtypes.html#str), it will be parsed and made into a datetime.datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime) object when assigned. You may also set it to a datetime.datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime) object directly. In any case, the time and date must be timezone aware. If you want to forcefully omit the publication date from the feed, set this to False. Type datetime.datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime), str (https://docs.python.org/3/library/stdtypes.html#str) (will be converted to and stored as datetime.datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime)), None (https://docs.python.org/3/library/constants.html#None) for default or False (https://docs.python.org/3/library/constants.html#False) to leave out. RSS pubDate pubsubhubbub = None The URL at which the PubSubHubbub (https://en.wikipedia.org/wiki/PubSubHubbub) hub can be found. Podcatchers can tell the hub that they want to be notified when a new episode is released. This way, they don’t need to check for new episodes every few hours; instead, the episodes arrive at their doorstep as soon as they’re published, through a notification sent by the hub. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS atom:link with rel="hub"

Warning: Do NOT set this attribute if you haven’t set up mechanics for notifying the hub of new episodes. Doing so could make it appear to your listeners like there is no new content for this feed. See the guide.

See also:

The guide on how to use PubSubHubbub A step-for-step guide with examples.

rss_file(filename, minimize=False, encoding=’UTF-8’, xml_declaration=True) Generate an RSS feed and write the resulting XML to a file.

Note: If atomicity is needed, then you are expected to provide that yourself. That means that you should write the feed to a temporary file which you rename to the final name afterwards; renaming is an atomic operation on Unix(like) systems.

Note: File-like objects given to this method will not be closed.

Parameters

30 Chapter 1. Contents PodGen Documentation, Release 1.1.0

• filename (str (https://docs.python.org/3/library/stdtypes.html#str) or fd) – Name of file to write, or a file-like object (accepting string/unicode, not bytes). • minimize (bool (https://docs.python.org/3/library/functions.html#bool)) – Set to True to disable splitting the feed into multiple lines and adding properly indentation, saving bytes at the cost of readability (default: False). • encoding (str (https://docs.python.org/3/library/stdtypes.html#str)) – Encoding used in the XML file (default: UTF-8). • xml_declaration (bool (https://docs.python.org/3/library/functions.html#bool)) – Whether an XML declaration should be added to the output (default: True). Returns Nothing.

rss_str(minimize=False, encoding=’UTF-8’, xml_declaration=True) Generate an RSS feed and return the feed XML as string. Parameters • minimize (bool (https://docs.python.org/3/library/functions.html#bool)) – Set to True to disable splitting the feed into multiple lines and adding properly indentation, saving bytes at the cost of readability (default: False). • encoding (str (https://docs.python.org/3/library/stdtypes.html#str)) – Encoding used in the XML declaration (default: UTF-8). • xml_declaration (bool (https://docs.python.org/3/library/functions.html#bool)) – Whether an XML declaration should be added to the output (default: True). Returns The generated RSS feed as a str (https://docs.python.org/3/library/stdtypes.html#str) (unicode in 2.7) set_generator(generator=None, version=None, uri=None, exclude_podgen=False) Set the generator of the feed, formatted nicely, which identifies the software used to generate the feed. Parameters • generator (str (https://docs.python.org/3/library/stdtypes.html#str)) – Software used to create the feed. • version (tuple (https://docs.python.org/3/library/stdtypes.html#tuple) of int (https://docs.python.org/3/library/functions.html#int)) – (Optional) Version of the software, as a tuple. • uri (str (https://docs.python.org/3/library/stdtypes.html#str)) – (Optional) The soft- ware’s website. • exclude_podgen (bool (https://docs.python.org/3/library/functions.html#bool)) – (Optional) Set to True if you don’t want PodGen to be mentioned (e.g., “My Program (using PodGen 1.0.0)”) See also:

The attribute generator Lets you access and set the generator string yourself, without any formatting help.

skip_days Set of days in which podcatchers don’t need to refresh this feed. This isn’t widely supported by podcatchers.

1.5. API Documentation 31 PodGen Documentation, Release 1.1.0

The days are represented using strings of their English names, like “Monday” or “wednesday”. The day names are automatically capitalized when the set is assigned to skip_days, but subsequent changes to the set “in place” are only checked and capitalized when the RSS feed is generated. For example, to stop refreshing the feed in the weekend:

>>> from podgen import Podcast >>> p= Podcast() >>> p.skip_days={"Friday","Saturday","sUnDaY"} >>> p.skip_days {"Saturday", "Friday", "Sunday"}

Type set (https://docs.python.org/3/library/stdtypes.html#set) of str (https://docs.python.org/3/library/stdtypes.html#str) RSS skipDays

skip_hours Set of hours of the day in which podcatchers don’t need to refresh this feed. This isn’t widely supported by podcatchers. The hours are represented as integer values from 0 to 23. Note that while the content of the set is checked when it is first assigned to skip_hours, further changes to the set “in place” will not be checked before you generate the RSS. For example, to stop refreshing the feed between 18 and 7:

>>> from podgen import Podcast >>> p= Podcast() >>> p.skip_hours= set(range(18, 24)) >>> p.skip_hours {18, 19, 20, 21, 22, 23} >>> p.skip_hours|= set(range(8)) >>> p.skip_hours {0, 1, 2, 3, 4, 5, 6, 7, 18, 19, 20, 21, 22, 23}

Type set (https://docs.python.org/3/library/stdtypes.html#set) of int (https://docs.python.org/3/library/functions.html#int) RSS skipHours

subtitle = None The subtitle for your podcast, shown mainly as a very short description on iTunes. The subtitle displays best if it is only a few words long, like a short slogan. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS itunes:subtitle web_master The Person responsible for technical issues relating to the feed. Type podgen.Person RSS webMaster website = None The absolute URL of this podcast’s website. This is one of the mandatory attributes.

32 Chapter 1. Contents PodGen Documentation, Release 1.1.0

Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS link withhold_from_itunes = None Prevent the entire podcast from appearing in the iTunes podcast directory. Note that this will affect more than iTunes, since most podcatchers use the iTunes catalogue to implement the search feature. Listeners will still be able to subscribe by adding the feed’s address manually. If you don’t intend to submit this podcast to iTunes, you can set this to True as a way of giving iTunes the middle finger, and perhaps more importantly, preventing others from submitting it as well. Set it to True to withhold the entire podcast from iTunes. It is set to False by default, of course. Type bool (https://docs.python.org/3/library/functions.html#bool) RSS itunes:block xslt = None Absolute URL to the XSLT file which web browsers should use with this feed. XSLT (https://en.wikipedia.org/wiki/XSLT) stands for Extensible Stylesheet Language Transformations and can be regarded as a template language made for transforming XML into XHTML (among other things). You can use it to avoid giving users an ugly XML listing when trying to subscribe to your podcast; this technique is in fact employed by most podcast publishers today. In a web browser, it looks like a web page, and to the podcatchers, it looks like a normal podcast feed. To put it another way, the very same URL can be used as an information web page about the podcast as well as the URL you subscribe to in podcatchers. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS Processor instruction right after the xml declaration called xml-stylesheet, with type set to text/xsl and href set to this attribute.

1.5.2 podgen.Episode class podgen.Episode(**kwargs) Class representing an episode in a podcast. Corresponds to an RSS Item. When creating a new Episode, you can populate any attribute using keyword arguments. Use the attribute’s name on the left side of the equals sign and its value on the right side. Here’s an example:

>>> # This... >>> ep= Episode() >>> ep.title="Exploring the RTS genre" >>> ep.summary="Tory and I talk about a genre of games we've"+\ ... "never dared try out before now..." >>> # ...is equal to this: >>> ep= Episode( ... title="Exploring the RTS genre", ... summary="Tory and I talk about a genre of games we've" ... "never dared try out before now..." ... )

Raises TypeError if you try to set an attribute which doesn’t exist, ValueError if you set an attribute to an invalid value.

You must have filled in either title or summary before the RSS can be generated.

1.5. API Documentation 33 PodGen Documentation, Release 1.1.0

To add an episode to a podcast:

>>> import podgen >>> p= podgen.Podcast() >>> episode= podgen.Episode() >>> p.episodes.append(episode)

You may also replace the last two lines with a shortcut:

>>> episode=p.add_episode(podgen.Episode())

See also:

Episodes A friendlier introduction to episodes.

authors List of Person that contributed to this episode. The authors don’t need to have both name and email set. They’re usually not displayed anywhere.

Note: You do not need to provide any authors for an episode if they’re identical to the podcast’s authors.

Any value you assign to authors will be automatically converted to a list, but only if it’s iterable (like tuple, set and so on). It is an error to assign a single Person object to this attribute:

>>> # This results in an error >>> ep.authors= Person("John Doe","[email protected]") TypeError: Only iterable types can be assigned to authors, ... >>> # This is the correct way: >>> ep.authors= [Person("John Doe","[email protected]")]

The initial value is an empty list, so you can use the list methods right away. Example:

>>> # This attribute is just a list - you can for example append: >>> ep.authors.append(Person("John Doe","[email protected]")) >>> # Or assign a new list (discarding earlier authors) >>> ep.authors= [Person("John Doe","[email protected]"), ... Person("Mary Sue","[email protected]")]

Type list (https://docs.python.org/3/library/stdtypes.html#list) of podgen.Person RSS author or dc:creator, and itunes:author

explicit Whether this podcast episode contains material which may be inappropriate for children. The value of the podcast’s explicit attribute is used by default, if this is kept as None. If you set this to True, an “explicit” parental advisory graphic will appear in the Name column in iTunes. If the value is False, the parental advisory type is considered Clean, meaning that no explicit language or adult content is included anywhere in this episode, and a “clean” graphic will appear. Type bool (https://docs.python.org/3/library/functions.html#bool) RSS itunes:explicit

34 Chapter 1. Contents PodGen Documentation, Release 1.1.0

id = None This episode’s globally unique identifier. If not present, the URL of the enclosed media is used. This is usually the best way to go, as long as the media URL doesn’t change. Set the id to boolean False if you don’t want to associate any id to this episode. It is important that an episode keeps the same ID until the end of time, since the ID is used by clients to identify which episodes have been listened to, which episodes are new, and so on. Changing the ID causes the same consequences as deleting the existing episode and adding a new, identical episode. Note that this is a GLOBALLY unique identifier. Thus, not only must it be unique in this podcast, it must not be the same ID as any other episode for any podcast out there. To ensure this, you should use a domain which you own (for example, use something like http://example.org/podcast/episode1 if you own example.org). Type str (https://docs.python.org/3/library/stdtypes.html#str), None (https://docs.python.org/3/library/constants.html#None) to use default or False (https://docs.python.org/3/library/constants.html#False) to leave out. RSS guid image The podcast episode’s image, overriding the podcast’s image. This attribute specifies the absolute URL to the artwork for your podcast. iTunes prefers square images that are at least 1400x1400 pixels. iTunes supports images in JPEG and PNG formats with an RGB color space (CMYK is not supported). The URL must end in “.jpg” or “.png”; a NotSupportedByItunesWarning will be issued if it doesn’t. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS itunes:image

Note: If you change an episode’s image, you should also change the file’s name; iTunes doesn’t check the actual file to see if it’s changed. Additionally, the server hosting your cover art image must allow HTTP HEAD requests.

Warning: Almost no podcatchers support this. iTunes supports it only if you embed the cover in the media file (the same way you would embed an album cover), and recommends that you use Garageband’s feature. The podcast’s image is used if this isn’t supported.

is_closed_captioned = None Whether this podcast includes a video episode with embedded closed captioning (https://en.wikipedia.org/wiki/Closed_captioning) support. Defaults to False. Type bool (https://docs.python.org/3/library/functions.html#bool) RSS itunes:isClosedCaptioned link = None The link to the full version of this episode’s summary. Remember to start the link with the scheme, e.g. https://. Type str (https://docs.python.org/3/library/stdtypes.html#str)

1.5. API Documentation 35 PodGen Documentation, Release 1.1.0

RSS link long_summary = None A long (read: full) summary, which supplements the shorter summary. Like summary, this must be compatible with XHTML parsers; use podgen.htmlencode() if this isn’t HTML. This attribute should be seen as a full, longer variation of summary if summary exists. Even then, the long_summary should be independent from summary, in that you only need to read one of them. This means you may have to repeat the first sentences. Type str (https://docs.python.org/3/library/stdtypes.html#str) which can be parsed as XHTML. RSS content:encoded or description media Get or set the Media object that is attached to this episode. Note that if id is not set, the media’s URL is used as the id. If you rely on this, you should make sure the URL never changes, since changing the id messes up with clients (they will think this episode is new again, even if the user has listened to it already). Therefore, you should only rely on this behaviour if you own the domain which the episodes reside on. If you don’t, then you must set id to an appropriate value manually. Type podgen.Media RSS enclosure and itunes:duration position A custom position for this episode on the iTunes store page. If you would like this episode to appear first, set it to 1. If you want it second, set it to 2, and so on. If multiple episodes share the same position, they will be sorted by their publication date. To remove the order from the episode, set the position back to None (https://docs.python.org/3/library/constants.html#None). Type int (https://docs.python.org/3/library/functions.html#int) RSS itunes:order publication_date The time and date this episode was first published. The value can be a str (https://docs.python.org/3/library/stdtypes.html#str), which will be parsed and made into a datetime.datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime) object when assigned. You may also assign a datetime.datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime) object directly. In both cases, you must ensure that the value includes timezone information. Type str (https://docs.python.org/3/library/stdtypes.html#str) (will be converted to and stored as datetime.datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime)) or datetime. datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime). RSS pubDate

Note: Don’t use the media file’s modification date as the publication date, unless they’re the same. It looks very odd when an episode suddenly pops up in the feed, but it claims to be several hours old!

rss_entry() Create an RSS item using lxml’s etree and return it.

36 Chapter 1. Contents PodGen Documentation, Release 1.1.0

This is primarily used by podgen.Podcast when generating the podcast’s RSS feed. Returns etree.Element(‘item’) subtitle = None A short subtitle. This is shown in the Description column in iTunes. The subtitle displays best if it is only a few words long. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS itunes:subtitle summary = None The summary of this episode, in a format that can be parsed by XHTML parsers. If your summary isn’t fit to be parsed as XHTML, you can use podgen.htmlencode() to fix the text, like this:

>>> ep.summary= podgen.htmlencode("We spread lots of love <3") >>> ep.summary We spread lots of love <3

In iTunes, the summary is shown in a separate window that appears when the “circled i” in the Description column is clicked. This field can be up to 4000 characters in length. See also Episode.subtitle and Episode.long_summary. Type str (https://docs.python.org/3/library/stdtypes.html#str) which can be parsed as XHTML. RSS description title = None This episode’s human-readable title. Title is mandatory and should not be blank. Type str (https://docs.python.org/3/library/stdtypes.html#str) RSS title withhold_from_itunes Prevent this episode from appearing in the iTunes podcast directory. Note that the episode can still be found by inspecting the XML, so it is still public. One use case would be if you knew that this episode would get you kicked out from iTunes, should it make it there. In such cases, you can set withhold_from_itunes to True so this episode isn’t published on iTunes, allowing you to publish it to everyone else while keeping your podcast on iTunes. This attribute defaults to False, of course. Type bool (https://docs.python.org/3/library/functions.html#bool) RSS itunes:block

1.5.3 podgen.Person class podgen.Person(name=None, email=None) Data-oriented class representing a single person or entity. A Person can represent both real persons and less personal entities like organizations. Example:

>>> p.authors= [Person("Example Radio","[email protected]")]

1.5. API Documentation 37 PodGen Documentation, Release 1.1.0

Note: At any time, one of name or email must be present. Both cannot be None or empty at the same time.

Warning: Any names and email addresses you put into a Person object will eventually be included and published together with the feed. If you want to keep a name or email address private, then you must make sure it isn’t used in a Person object (or to be precise: that the Person object with the name or email address isn’t used in any Podcast or Episode.)

Example of use:

>>> from podgen import Person >>> Person("John Doe") Person(name=John Doe, email=None) >>> Person(email="[email protected]") Person(name=None, [email protected]) >>> Person() ValueError: You must provide either a name or an email address.

email This person’s public email address. Type str (https://docs.python.org/3/library/stdtypes.html#str) name This person’s name. Type str (https://docs.python.org/3/library/stdtypes.html#str)

1.5.4 podgen.Media class podgen.Media(url, size=0, type=None, duration=None, requests_session=None) Data-oriented class representing a pointer to a media file. A media file can be a sound file (most typical), video file or a document. You should provide the absolute URL at which this media can be found, and the media’s file size in bytes. Optionally, you can provide the type of media (expressed using MIME types). When not given in the constructor, it will be found automatically by looking at the url’s file extension. If the url’s file extension isn’t supported by iTunes, you will get an error if you don’t supply the type. You are also highly encouraged to provide the duration of the media.

Note: iTunes is lazy and will just look at the URL to figure out if a file is of a supported file type. You must therefore ensure your URL ends with a supported file extension.

Note: A warning called NotSupportedByItunesWarning will be issued if your URL or type isn’t compatible with iTunes. See the Python documentation for more details on warnings (https://docs.python.org/3/library/warnings.html#module-warnings).

Media types supported by iTunes: • Audio

38 Chapter 1. Contents PodGen Documentation, Release 1.1.0

– M4A – MP3 • Video – MOV – MP4 – M4V • Document – PDF – EPUB All attributes will always have a value, except size which can be 0 if the size cannot be determined by any means (eg. if it’s a stream) and duration which is optional (but recommended). See also:

Enclosing media for a more gentle introduction.

classmethod create_from_server_response(url, size=None, type=None, duration=None, requests_=None) Create new Media object, with size and/or type fetched from the server when not given. See Media.fetch_duration() for a (slow!) way to fill in the duration as well. Example (assuming the server responds with Content-Length: 252345991 and Content-Type: au- dio/mpeg):

>>> from podgen import Media >>> # Assume an episode is hosted at example.com >>>m= Media.create_from_server_response( ..."http://example.com/episodes/ep1.mp3") >>>m Media(url=http://example.com/episodes/ep1.mp3, size=252345991, type=audio/mpeg, duration=None)

Parameters • url (str (https://docs.python.org/3/library/stdtypes.html#str)) – The URL at which the media can be accessed right now. • size (int (https://docs.python.org/3/library/functions.html#int) or None (https://docs.python.org/3/library/constants.html#None)) – Size of the file. Will be fetched from server if not given. • type (str (https://docs.python.org/3/library/stdtypes.html#str) or None (https://docs.python.org/3/library/constants.html#None)) – The of the file. Will be fetched from server if not given. • duration (datetime.timedelta (https://docs.python.org/3/library/datetime.html#datetime.timedelta) or None (https://docs.python.org/3/library/constants.html#None)) – The media’s duration. • requests (requests (https://requests.readthedocs.io/en/master/api/#module- requests) or requests.Session (https://requests.readthedocs.io/en/master/api/#requests.Session)) – Either the requests (http://docs.python-requests.org/en/master/) module itself, or a requests.Session (https://requests.readthedocs.io/en/master/api/#requests.Session) object. Defaults to a new Session (https://requests.readthedocs.io/en/master/api/#requests.Session).

1.5. API Documentation 39 PodGen Documentation, Release 1.1.0

Returns New instance of Media with url, size and type filled in. Raises The appropriate requests exceptions are thrown when networking errors occur. Run- timeError is thrown if some information isn’t given and isn’t found in the server’s response.

download(destination) Download the media file. This method will block until the file is downloaded in its entirety.

Note: The destination will not be populated atomically; if you need this, you must give provide a tempo- rary file as destination and rename the file yourself.

Parameters destination (fd or str (https://docs.python.org/3/library/stdtypes.html#str).) – Where to save the media file. Either a filename, or a file-like object. The file-like object will not be closed by PodGen.

duration The duration of the media file. Type datetime.timedelta (https://docs.python.org/3/library/datetime.html#datetime.timedelta) Raises TypeError (https://docs.python.org/3/library/exceptions.html#TypeError) if you try to assign anything other than datetime.timedelta (https://docs.python.org/3/library/datetime.html#datetime.timedelta) or None (https://docs.python.org/3/library/constants.html#None) to this attribute. Raises ValueError (https://docs.python.org/3/library/exceptions.html#ValueError) if a neg- ative timedelta value is given. duration_str duration, formatted as a string according to iTunes’ specs. That is, HH:MM:SS if it lasts more than an hour, or MM:SS if it lasts less than an hour. This is just an alternate, read-only view of duration. If duration is None (https://docs.python.org/3/library/constants.html#None), then this will be None (https://docs.python.org/3/library/constants.html#None) as well. Type str (https://docs.python.org/3/library/stdtypes.html#str) fetch_duration() Download Media.url locally and use it to populate Media.duration. Use this method when you don’t have the media file on the local file system. Use populate_duration_from() otherwise. This method will take quite some time, since the media file must be downloaded before it can be analyzed. file_extension The file extension of url. Read-only. Type str (https://docs.python.org/3/library/stdtypes.html#str) get_type(url) Guess the MIME type from the URL. This is used to fill in type when it is not given (and thus called implicitly by the constructor), but you can call it yourself. Example:

40 Chapter 1. Contents PodGen Documentation, Release 1.1.0

>>> from podgen import Media >>> m= Media("http://example.org/1.mp3", 136532744) >>> # The type was detected from the url: >>> m.type audio/mpeg >>> # Ops, I changed my mind... >>> m.url="https://example.org/1.m4a" >>> # As you can see, the type didn't change: >>> m.type audio/mpeg >>> # So update type yourself >>> m.type=m.get_type(m.url) >>> m.type audio/x-m4a

Parameters url (str (https://docs.python.org/3/library/stdtypes.html#str)) – The URL which should be used to guess the MIME type. Returns The guessed MIME type. Raises ValueError if the MIME type couldn’t be guessed from the URL.

populate_duration_from(filename) Populate Media.duration by analyzing the given file. Use this method when you have the media file on the local file system. Use Media. fetch_duration() if you need to download the file from the server. Parameters filename (str (https://docs.python.org/3/library/stdtypes.html#str)) – Path to the media file which shall be used to determine this media’s duration. The file extension must match its file type, since it is used to determine what type of media file it is. For a list of supported formats, see https://pypi.python.org/pypi/tinytag/ requests_session = None The requests.Session object which shall be used. Defaults to a new session with PodGen as User-Agent. This is used by the instance methods download() and fetch_duration(). create_from_server_response(), however, creates its own requests Session if not given as a parameter (since it is a static method). You can set this attribute manually to set your own User-Agent and benefit from Keep-Alive across differ- ent instances of Media. Type requests.Session (https://requests.readthedocs.io/en/master/api/#requests.Session) size The media’s file size in bytes. You can either provide the number of bytes as an int (https://docs.python.org/3/library/functions.html#int), or you can provide a human-readable str (https://docs.python.org/3/library/stdtypes.html#str) with a unit, like MB or GiB. An unknown size is represented as 0. This should ONLY be used in exceptional cases, where it is theoret- ically impossible to determine the file size (for example if it’s a stream). Setting the size to 0 will issue a UserWarning. Type str (https://docs.python.org/3/library/stdtypes.html#str) (which will be converted to and stored as int (https://docs.python.org/3/library/functions.html#int)) or int (https://docs.python.org/3/library/functions.html#int)

1.5. API Documentation 41 PodGen Documentation, Release 1.1.0

Note: If you provide a string, it will be translated to int when the assignment happens. Thus, on subse- quent accesses, you will get the resulting int, not the string you put in.

Note: The units are case-insensitive. This means that the B is always assumed to mean “bytes”, even if it is lowercase (b). Likewise, m is taken to mean mega, not milli.

type The MIME type of this media. See https://en.wikipedia.org/wiki/Media_type for an introduction. Type str (https://docs.python.org/3/library/stdtypes.html#str)

Note: If you leave out type when creating a new Media object, the type will be auto-detected from the url attribute. However, this won’t happen automatically other than during initialization. If you want to autodetect type when assigning a new value to url, you should use get_type().

url The URL at which this media is publicly accessible. Only absolute URLs are allowed, so make sure it starts with http:// or https://. The server should support HEAD-requests and byte-range requests. Ensure you quote parts of the URL that are not supposed to carry any special meaning to the browser, typically the name of your file. Common offenders include the slash character when not used to separate folders, the hash mark (#) and the question mark (?). Use urllib. parse.quote() (https://docs.python.org/3/library/urllib.parse.html#urllib.parse.quote) in Python3 and urllib.quote() in Python2. Type str (https://docs.python.org/3/library/stdtypes.html#str)

1.5.5 podgen.Category class podgen.Category(category, subcategory=None) Immutable class representing an Apple Podcasts category. By using this class, you can be sure that the chosen category is a valid category, that it is formatted correctly and you will be warned when using an old category. See https://help.apple.com/itc/podcasts_connect/#/itc9267a2f12 for an overview of the available categories and their subcategories. Changed in version 1.1.0: Updated to reflect the new categories (https://podnews.net/article/apple-changed- podcast-categories-2019) as of August 9th 2019 and yield a LegacyCategoryWarning when using one of the old categories.

Note: The categories are case-insensitive, and you may escape ampersands. The category and subcategory will end up properly capitalized and with unescaped ampersands.

Example:

42 Chapter 1. Contents PodGen Documentation, Release 1.1.0

>>> from podgen import Category >>> c= Category("Music") >>> c.category Music >>> c.subcategory None >>> >>> d= Category("games & hobbies","Video games") >>> d.category Games & Hobbies >>> d.subcategory Video Games

category The category represented by this object. Read-only. Type str (https://docs.python.org/3/library/stdtypes.html#str) subcategory The subcategory this object represents. Read-only. Type str (https://docs.python.org/3/library/stdtypes.html#str)

1.5.6 podgen.warnings

This file contains PodGen-specific warnings. They can be imported directly from podgen. copyright 2019, Thorben Dahl license FreeBSD and LGPL, see license.* for more details. exception podgen.warnings.LegacyCategoryWarning Indicates that the category created is an old category. It will still be accepted by Apple Podcasts, but it would be wise to use the new categories since they may have more relevant options for your podcast. See also:

What’s New: Enhanced Apple Podcasts Categories (https://itunespartner.apple.com/podcasts/whats-new/100002564) Consequences of using old categories. Podcasts Connect Help: Apple Podcasts categories (https://help.apple.com/itc/podcasts_connect/#/itc9267a2f12) Up-to-date list of available categories. Podnews: New and changed Apple Podcasts categories (https://podnews.net/article/apple-changed-podcast-categories-2019) List of changes between the old and the new categories. exception podgen.warnings.NotRecommendedWarning Warns against behaviour or usage which is usually discouraged. However, there may exist exceptions where there is no better way. exception podgen.warnings.NotSupportedByItunesWarning Indicates that PodGen is used in a way that may not be compatible with Apple Podcasts (previously known as iTunes). In some cases, this may be because PodGen has not been kept up-to-date with new features which Apple Pod- casts has added support for. Please add an issue if that is the case! exception podgen.warnings.PodgenWarning Superclass for all warnings defined by PodGen.

1.5. API Documentation 43 PodGen Documentation, Release 1.1.0

1.5.7 podgen.util

This file contains helper functions for the feed generator module. copyright 2013, Lars Kiesow and 2016, Thorben Dahl license FreeBSD and LGPL, see license.* for more details. podgen.util.ensure_format(val, allowed, required, allowed_values=None, defaults=None) Takes a dictionary or a list of dictionaries and check if all keys are in the set of allowed keys, if all required keys are present and if the values of a specific key are ok. Parameters • val – Dictionaries to check. • allowed – Set of allowed keys. • required – Set of required keys. • allowed_values – Dictionary with keys and sets of their allowed values. • defaults – Dictionary with default values. Returns List of checked dictionaries. podgen.util.formatRFC2822(d) Format a datetime according to RFC2822. This implementation exists as a workaround to ensure that the locale setting does not interfere with the time format. For example, day names might get translated to your local language, which would break with the standard. Parameters d (datetime.datetime (https://docs.python.org/3/library/datetime.html#datetime.datetime)) – Time and date you want to format according to RFC2822. Returns The datetime formatted according to the RFC2822. Return type str (https://docs.python.org/3/library/stdtypes.html#str) podgen.util.htmlencode(s) Encode the given string so its content won’t be confused as HTML markup. This function exists as a cross-version compatibility alias. podgen.util.listToHumanreadableStr(l) Create a human-readable string out of the given iterable. Example:

>>> from podgen.util import listToHumanreadableStr >>> listToHumanreadableStr([1,2,3]) 1, 2 and 3

The string (empty) is returned if the list is empty – it is assumed that you check whether the list is empty yourself.

44 Chapter 1. Contents CHAPTER 2

External Resources

• Changelog (https://github.com/tobinus/python-podgen/blob/master/CHANGELOG.md) • GitHub Repository (https://github.com/tobinus/python-podgen/tree/master) • Python Package Index (https://pypi.org/project/podgen/)

45 PodGen Documentation, Release 1.1.0

46 Chapter 2. External Resources Index

A G add_episode() (podgen.Podcast method), 24 generator (podgen.Podcast attribute), 28 apply_episode_order() (podgen.Podcast get_type() (podgen.Media method), 40 method), 25 authors (podgen.Episode attribute), 34 H authors (podgen.Podcast attribute), 25 htmlencode() (in module podgen.util), 44 C I Category (class in podgen), 42 id (podgen.Episode attribute), 34 category (podgen.Category attribute), 43 image (podgen.Episode attribute), 35 category (podgen.Podcast attribute), 25 image (podgen.Podcast attribute), 28 clear_episode_order() (podgen.Podcast is_closed_captioned (podgen.Episode attribute), method), 25 35 cloud (podgen.Podcast attribute), 26 complete (podgen.Podcast attribute), 26 L copyright (podgen.Podcast attribute), 26 language (podgen.Podcast attribute), 28 create_from_server_response() (pod- last_updated (podgen.Podcast attribute), 28 gen.Media class method), 39 LegacyCategoryWarning, 43 link (podgen.Episode attribute), 35 D listToHumanreadableStr() (in module pod- description (podgen.Podcast attribute), 27 gen.util), 44 download() (podgen.Media method), 40 long_summary (podgen.Episode attribute), 36 duration (podgen.Media attribute), 40 duration_str (podgen.Media attribute), 40 M Media (class in podgen), 38 E media (podgen.Episode attribute), 36 email (podgen.Person attribute), 38 ensure_format() (in module podgen.util), 44 N Episode (class in podgen), 33 name (podgen.Person attribute), 38 episode_class (podgen.Podcast attribute), 27 name (podgen.Podcast attribute), 29 episodes (podgen.Podcast attribute), 27 new_feed_url (podgen.Podcast attribute), 29 explicit (podgen.Episode attribute), 34 NotRecommendedWarning, 43 explicit (podgen.Podcast attribute), 27 NotSupportedByItunesWarning, 43 F O feed_url (podgen.Podcast attribute), 28 owner (podgen.Podcast attribute), 29 fetch_duration() (podgen.Media method), 40 file_extension (podgen.Media attribute), 40 P formatRFC2822() (in module podgen.util), 44 Person (class in podgen), 37

47 PodGen Documentation, Release 1.1.0

Podcast (class in podgen), 24 podgen.util (module), 43 podgen.warnings (module), 43 PodgenWarning, 43 populate_duration_from() (podgen.Media method), 41 position (podgen.Episode attribute), 36 publication_date (podgen.Episode attribute), 36 publication_date (podgen.Podcast attribute), 29 pubsubhubbub (podgen.Podcast attribute), 30 R requests_session (podgen.Media attribute), 41 rss_entry() (podgen.Episode method), 36 rss_file() (podgen.Podcast method), 30 rss_str() (podgen.Podcast method), 31 S set_generator() (podgen.Podcast method), 31 size (podgen.Media attribute), 41 skip_days (podgen.Podcast attribute), 31 skip_hours (podgen.Podcast attribute), 32 subcategory (podgen.Category attribute), 43 subtitle (podgen.Episode attribute), 37 subtitle (podgen.Podcast attribute), 32 summary (podgen.Episode attribute), 37 T title (podgen.Episode attribute), 37 type (podgen.Media attribute), 42 U url (podgen.Media attribute), 42 W web_master (podgen.Podcast attribute), 32 website (podgen.Podcast attribute), 32 withhold_from_itunes (podgen.Episode at- tribute), 37 withhold_from_itunes (podgen.Podcast at- tribute), 33 X xslt (podgen.Podcast attribute), 33

48 Index