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 :class:`~pod2gen.Episode` objects and append them to the list of episodes in the Podcast. That is pretty straight-forward:: from pod2gen 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 :meth:`Podcast.add_episode() ` which optionally creates a new instance of :class:`~pod2gen.Episode`, adds it to the podcast and returns it, allowing you to assign it to a variable:: from pod2gen import Podcast p = Podcast() my_episode = p.add_episode() If you prefer to use the constructor, there's nothing wrong with that:: from pod2gen 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:: 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: * :attr:`~pod2gen.Episode.title` * :attr:`~pod2gen.Episode.subtitle` * :attr:`~pod2gen.Episode.summary` * :attr:`~pod2gen.Episode.long_summary` .. _pod2gen.Media-guide: Enclosing media ^^^^^^^^^^^^^^^ Of course, this isn't much of a podcast if we don't have any :attr:`~pod2gen.Episode.media` attached to it! :: from datetime import timedelta from pod2gen import Media my_episode.media = Media("http://example.com/podcast/s01e10.mp3", size=17475653, type="audio/mpeg", # Optional, can be determined # from the url duration=timedelta(hours=1, minutes=2, seconds=36) ) The media's attributes (and the arguments to the constructor) are: ======================== ======================================================= Attribute Description ======================== ======================================================= :attr:`~.Media.url` The URL at which this media file is accessible. :attr:`~.Media.size` The size of the media file as bytes, given either as :obj:`int` or a :obj:`str` which will be parsed. :attr:`~.Media.type` The media file's `MIME type`_. :attr:`~.Media.duration` How long the media file lasts, given as a :class:`datetime.timedelta` ======================== ======================================================= You can leave out some of these: ======================== ======================================================= Attribute Effect if left out ======================== ======================================================= :attr:`~.Media.url` Mandatory. :attr:`~.Media.size` Can be 0, but do so only if you cannot determine its size (for example if it's a stream). :attr:`~.Media.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. :attr:`~.Media.duration` Can be left out since it is optional. It will stay as :obj:`None`. ======================== ======================================================= .. warning:: Remember to encode special characters in your URLs! 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 :func:`urllib.parse.quote`. The correct URL would then become ``http://podcast.example.org/episodes/library-pod-%23023-future.mp3``. This warning applies to all properties and attributes of pod2gen objects containing urls and links. Populating size and type from server ==================================== By using the special factory :meth:`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 ======================== ======================================================= :attr:`~.Media.url` Mandatory. :attr:`~.Media.size` Will be populated using the ``Content-Length`` header. :attr:`~.Media.type` Will be populated using the ``Content-Type`` header. :attr:`~.Media.duration` Will *not* be populated by data from the server; will stay :obj:`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 :attr:`~.Media.duration` field by using :meth:`.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 :attr:`~.Media.duration` attribute by calling :meth:`.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. 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: * :attr:`~.pod2gen.Episode.media` (the attribute) * :class:`~.pod2gen.Media` (the class which you use as value) .. _MIME type: https://en.wikipedia.org/wiki/Media_type Alternate enclosing media ^^^^^^^^^^^^^^^^^^^^^^^^^ Alternate enclosing media are meant to provide different versions of, or companion media to the main :attr:`Episode.media ` object. An alternate enclosing media could be an audio only version of a video podcast to allow apps to switch back and forth between audio/video, lower (or higher) bitrate versions for bandwidth constrained areas, alternative codecs for different device platforms, alternate URI schemes and download types such as IPFS or WebTorrent, commentary tracks or supporting source clips, etc... :: from pod2gen import AlternateMedia am = AlternateMedia( "audio/mp4", 43200000, bitrate=128000, height=1080, lang="en-US", title="Standard", rel="Off stage", codecs="mp4a.40.2", default=False, encryption="sri", signature="sha384-ExVqijgYHm15PqQqdXfW95x+Rs6C+d6E/ICxyQOeFevnxNLR/wtJNrNYTjIysUBo", ) my_episode.add_alternate_media(am) The AlternateMedia's attributes (and the arguments to the constructor) are: ==================================== ============================================================ Attribute Description ==================================== ============================================================ :attr:`~.AlternateMedia.type` The media file's `MIME type`_. :attr:`~.AlternateMedia.length` The length of the file in bytes as :obj:`int`. :attr:`~.AlternateMedia.bitrate` The Encoding bitrate of media asset as :obj:`float`. :attr:`~.AlternateMedia.height` Height of the media asset for video formats as :obj:`int`. :attr:`~.AlternateMedia.lang` An `IETF language tag (BCP 47) code `_ identifying the language of this media. :attr:`~.AlternateMedia.title` A human-readable string identifying the name of the media asset. :attr:`~.AlternateMedia.rel` A label to group several :attr:`~pod2gen.Episode.media` objects. :attr:`~.AlternateMedia.codecs` An `RFC 6381 string `_ specifying the codecs available in this media. :attr:`~.AlternateMedia.default` :obj:`bool` specifying whether or not the given media is the same as the one from the enclosure element and should be the preferred media element. :attr:`~.AlternateMedia.encryption` The type of method of verifying integrity of the media ``"sri"`` or ``"pgp-signature"``. :attr:`~.AlternateMedia.signature` The value of the media signature depenting on the selected :attr:`~.AlternateMedia.encryption`. ==================================== ============================================================ Only :attr:`~.AlternateMedia.type` and :attr:`~.AlternateMedia.length` are required, all other fields are optional and will be skipped if unspecified while generating the corresponding XML tags. Alternatively, it is possible to create a minimal :class:`~pod2gen.AlternateMedia` with the required constructor parameters and populate the optional fields later on.:: from pod2gen import AlternateMedia # Creating an AlternateMedia object with just a type and a length am = AlternateMedia("audio/mp4", 43200000) # Editing fields am.bitrate = 128000 am.height = 1080 am.lang = "en-US" am.title = "Standard" am.rel = "Off stage" am.codecs = "mp4a.40.2" am.default = False am.encryption = "sri" am.signature = "sha384-ExVqijgYHm15PqQqdXfW95x+Rs6C+d6E/ICxyQOeFevnxNLR/wtJNrNYTjIysUBo" An episode can have multiple alternative media groups. Each group is represented by an :class:`~.AlternateMedia` that contains several sources of the same media file. Each :class:`~.AlternateMedia` is stored in :attr:`Episode.alternate_media_list ` and can be tagged using the :attr:`~.AlternateMedia.rel` property. For example, an episode can have two :class:`~.AlternateMedia` items, one representing the audio only format of the episode with various sources and the other representing the video format of the episode with various alternate sources to the main media file of the episode.:: from pod2gen import AlternateMedia videos = AlternateMedia("video/x-msvideo", 432000000, rel="video format") audios = AlternateMedia(""audio/mp4, 432000000, rel="audio format") my_episode.add_alternate_media(videos) my_episode.add_alternate_media(audios) .. _pod2gen.Episode-alternateEnclosure-sources: Alternate enclosure sources =========================== :class:`~pod2gen.AlternateMedia` is just the container of various sources of the same file. It must contain at least one source otherwise no ```` tag will be generated. Sources are stored as a :obj:`dict` in :attr:`AlternateMedia.sources `. The key represents the uri where the media file resides while the value is just the contentType of the media file. The contentType is meant to be a string that declares the mime-type of the file. It is useful if the transport mechanism is different than the file being delivered, as is the case with a torrents. Since the contentType is optional, the value of each key/value pair can be ``None``. :: # Example of AlternateMedia.sources sources = { "https://example.com/file-0.mp3": None, "ipfs://QmdwGqd3d2gFPGeJNLLCshdiPert45fMu84552Y4XHTy4y": None, "https://example.com/file-0.torrent": "application/x-bittorrent", } For each source, a ```` will be generated within the parent ```` element. The recommended way of adding a new source to an :class:`~pod2gen.AlternateMedia` object is using :meth:`AlternateMedia.add_source() `. Editing directly :attr:`AlternateMedia.sources ` should be avoided and is against the philosophy of this project which is making sure that all fields and values are properly formatted. :: # The contentType is not specified and will be 'None' am.add_source("https://example.com/file-0.mp3") # The contentType here is "application/x-bittorrent" am.add_source("https://example.com/file-0.torrent", "application/x-bittorrent") Editing the given contentType after adding the source is also possible using :meth:`AlternateMedia.edit_source_content() ` :: # Setting the contentType from `None` to `"audio/mpeg"` am.edit_source_content("https://example.com/file-0.mp3", "audio/mpeg",) # Setting the contentType from `"application/x-bittorrent"` to `None` am.edit_source_content("https://example.com/file-0.torrent", None) Since sources are stored as a key/value pair in a :obj:`dict`, it is possible to delete a source using the uri with :meth:`AlternateMedia.delete_source() ` :: am.delete_source("https://example.com/file-0.mp3") am.delete_source("https://example.com/file-0.torrent") Read more about: * :class:`~pod2gen.AlternateMedia` * :attr:`AlternateMedia.sources ` * :meth:`AlternateMedia.add_source() ` * :meth:`AlternateMedia.delete_source() ` * :meth:`AlternateMedia.edit_source_content() ` .. warning:: Just like for :attr:`~.Media.url`, remember to encode special characters in your sources URLs with :func:`urllib.parse.quote`. .. _pod2gen.Episode-alternateEnclosure-integrity: Alternate enclosure integrity ============================= When :attr:`AlternateMedia.encryption ` and :attr:`AlternateMedia.signature ` are specified, a ```` tag is added to the parent ```` element. Please checkout `RSS Namespace Extension for Podcasting`_ for more details about the ```` tag. Read more about: * :attr:`AlternateMedia.encryption ` * :attr:`AlternateMedia.signature ` Future improvements ============================= Just like it is possible to create a :class:`~pod2gen.Media` from a url address using :meth:`Media.create_from_server_response() `, it should also be possible to create an :class:`~pod2gen.AlternateMedia` object from a list of urls pointing to the same media file. Fetching data from these urls like the :attr:`~.AlternateMedia.length` in bytes, the :attr:`~.AlternateMedia.bitrate` and other attributes should also be possible using pod2gen. Also in order to make sure that the :class:`~pod2gen.AlternateMedia` object is correctly populated, checking that all the various sources **do** point to the same media file will also be implemented. In order to generate the ```` tag, users will have to select a type of encryption (``"sri"`` or ``"pgp-signature"``) and calculates themselves the :attr:`~.AlternateMedia.signature` based on the selected :attr:`~.AlternateMedia.encryption`. Auto-calculation of :attr:`~.AlternateMedia.encryption` based on the urls of the sources will be implemented in future versions. 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 :attr:`Episode.id ` attribute. .. _pod2gen.Episode-organization: Organization of episodes ^^^^^^^^^^^^^^^^^^^^^^^^ By default, podcast applications will organize episodes by their publication date, with the most recent episode at top. In addition to this, many publishers number their episodes by including a number in the episode titles. Some also divide their episodes into seasons. Such titles may look like "S02E04 Example title", to take an example. Generally, podcast applications can provide a better presentation when the information is *structured*, rather than mangled together in the episode titles. Apple therefore introduced `new ways of specifying season and episode numbers`_ through separate fields in mid 2017. Unfortunately, `not all podcast applications have adopted the fields`_, but hopefully that will improve as more publishers use the new fields. The :attr:`~pod2gen.Episode.season` and :attr:`~pod2gen.Episode.episode_number` attributes are used to set this information:: my_episode.title = "Example title" my_episode.season = 2 my_episode.episode_number = 4 The ``episode_number`` attribute is mandatory for full episodes if the podcast is marked as serial. Otherwise, they are just nice to have. ``episode_number`` and ``season`` attributes are also used in the `RSS Namespace Extension for Podcasting`_ for generating the tags ```` and ```` in addition to itunes tags. It is also possible to set an :attr:`~pod2gen.Episode.episode_name` an a :attr:`~pod2gen.Episode.season_name` attributes as podcast apps and aggregators are encouraged to show these values instead of the purely numerical :attr:`~pod2gen.Episode.episode_number` and :attr:`~pod2gen.Episode.season`:: my_episode.season = 3 my_episode.season_name = "Volume 3 my_episode.episode_number = 4 my_episode.episode_name = "Chapter 4" Read more: * :attr:`~pod2gen.Episode.episode_number` * :attr:`~pod2gen.Episode.season` * :attr:`~pod2gen.Episode.episode_name` * :attr:`~pod2gen.Episode.season_name` .. _new ways of specifying season and episode numbers: https://podnews.net/article/episode-numbers-faq .. _not all podcast applications have adopted the fields: https://podnews.net/article/episode-number-support-in-podcast-apps 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=dateutil.tz.UTC) Read more about the :attr:`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 :attr:`~pod2gen.Episode.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 :attr:`link ` attribute. .. _pod2gen.Person-guide: The Authors and Persons of interest ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Normally, the attributes :attr:`Podcast.authors ` and :attr:`Podcast.web_master ` (if set) are used to determine the authors 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")] The `RSS Namespace Extension for Podcasting`_ has also a ```` tag definition. :attr:`Episode.persons ` is similar to :attr:`Podcast.persons `. It is possible to define authors of the episode using :meth:`Episode.add_person() ` :: my_episode.add_person(Person("Joe Bob", group="Writing", role="Author")) my_episode.add_person(Person("Alice Bob"group="Writing", role="Author")) In general :attr:`Episode.persons ` is a list of persons of interest to the episode and can represent other roles than ``"Author"`` like ``"Cover Art Designer"``, ``"Technical Director"`` etc... :: director = Person("Slim Beji", group="Audio Production", role="Technical Director") my_episode.add_person(director) designer = Person("Frank Drogba", group="Writing", role="Author") my_episode.add_person(designer) :attr:`Person.group ` and :attr:`Person.role ` are case insensitive. The full list of available :attr:`~.Person.group` and :attr:`~.Person.role` labels is defined by the `Podcast Taxonomy Project`_. Using an unknown :attr:`~.Person.group` or :attr:`~.Person.role` or an unknown group/role pairing to the `Podcast Taxonomy Project`_ is accepted, however :class:`~pod2gen.warnings.UnknownPersonGroup`, :class:`~pod2gen.warnings.UnknownPersonRole` and :class:`~pod2gen.warnings.UnknownPersonGroupRoleTuple` warnings will be respectively issued. Read more: * :class:`~pod2gen.Person` * :attr:`Episode.persons ` * :meth:`Episode.add_person() ` * :attr:`Person.group ` * :attr:`Person.role ` * :class:`~pod2gen.warnings.UnknownPersonGroup` * :class:`~pod2gen.warnings.UnknownPersonRole` * :class:`~pod2gen.warnings.UnknownPersonGroupRoleTuple` .. _`Podcast Taxonomy Project`: https://podcasttaxonomy.com/home Bonuses and Trailers ^^^^^^^^^^^^^^^^^^^^ Sometimes, you may have some bonus material that did not make it into the published episode, such as a 1-hour interview which was cut down to 10 minutes for the podcast, or funny outtakes. Or, you may want to generate some hype for an upcoming season of a podcast ahead of its first episode. Bonuses and trailers are added to the podcast the same way regular episodes are added, but with the :attr:`~pod2gen.Episode.episode_type` attribute set to a different value depending on if it is a bonus or a trailer. The following constants are used as values of ``episode_type``: * Bonus: ``EPISODE_TYPE_BONUS`` * Trailer: ``EPISODE_TYPE_TRAILER`` * Full/regular (default): ``EPISODE_TYPE_FULL`` The constants can be imported from ``pod2gen``. Here is an example:: from pod2gen import Podcast, EPISODE_TYPE_BONUS # Create the podcast my_podcast = Podcast() # Fill in the podcast details # ... # Create the ordinary episode my_episode = my_podcast.add_episode() my_episode.title = "The history of Acme Industries" my_episode.season = 1 my_episode.episode_number = 9 # Create the bonus episode associated with the ordinary episode above my_bonus = my_podcast.add_episode() my_bonus.title = "Full interview with John Doe about Acme Industries" my_bonus.episode_type = EPISODE_TYPE_BONUS my_bonus.season = 1 my_bonus.episode_number = 9 # ... :attr:`~pod2gen.Episode.episode_type` combines with :attr:`~pod2gen.Episode.season` and :attr:`~pod2gen.Episode.episode_number` to indicate what this is a bonus or trailer for. * If you specify an :attr:`episode_number `, optionally with a :attr:`season ` number if you divide episodes by season, it will be a bonus or trailer for that episode. You can see this in the example above. * If you specify only a :attr:`~pod2gen.Episode.season`, then it will be a bonus or trailer for that season. * If you specify none of those, it will be a bonus or trailer for the podcast itself. Podcastindex related attributes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The `RSS Namespace Extension for Podcasting`_ introduced several new XML tags related to the world of podcasting. These new attributes meant to synthesize the fragmented world of podcast namespaces. Location ======== The purpose of the :attr:`~pod2gen.Episode.location` attribute is to to describe the location of editorial focus for an episode's content (i.e. "what place is this episode about?"). :: from pod2gen import Location my_episode.location = Location( "Dreamworld (Queensland)", geo="geo:-27.86159,153.3169", osm="W43678282" ) Read more: * :class:`~pod2gen.Location` * :attr:`Episode.location ` License ======== The purpose of the :attr:`~pod2gen.Episode.license` attribute is to define a license that is applied to the audio/video content of the episode. :: from pod2gen import License my_episode.license = License( "my-podcast-license-v1", "https://example.org/mypodcastlicense/full.pdf" ) Read more: * :class:`~pod2gen.License` * :attr:`Episode.license ` Soundbite ========== :attr:`Episode.soundbites ` contains a list of :class:`~pod2gen.Soundbite` objects representing an episode soundbite. A :class:`~pod2gen.Soundbite` object is represented by a start_time and a duration and can also have a text. Use :meth:`Episode.add_soundbite() ` to add a Soundbite object. When generating the RSS feed, the soundbite will be extracted from the audio/video of the :attr:`Episode.media ` attribute starting from :attr:`Soundbite.start_time ` seconds and lasting :attr:`Soundbite.duration ` seconds. :: from pod2gen import Soundbite soundbite_1 = Soundbite(1234.5, 42.25, text="Why the Podcast Namespace Matters") my_episode.add_soundbite(soundbite_1) soundbite_2 = Soundbite(73.0, 60) my_episode.add_soundbite(soundbite_2) Read more: * :class:`~pod2gen.Soundbite` * :attr:`Episode.soundbites ` * :meth:`Episode.add_soundbite() ` Transcript ========== :attr:`Episode.transcripts ` contains a list of :class:`~pod2gen.Transcript` objects representing an episode transcript. Use :meth:`Episode.add_transcript() ` to add a transcript to the episode. :: from pod2gen import Transcript transcript = Transcript( "https://examples.com/transcript_sample.txt", "text/html", language="es", is_caption=True, ) my_episode.add_transcript(transcript) Read more: * :class:`~pod2gen.Transcript` * :attr:`Episode.transcripts ` * :meth:`Episode.add_transcript() ` Chapters ========== :attr:`Episode.chapters_json ` is a link to an external file containing chapter data for the episode. Check `example.json`_ for a real world example. :: my_episode.chapters_json = "https://example.com/episode1/chapters.json" Read more: * :attr:`Episode.chapters_json ` .. _`example.json`: https://github.com/Podcastindex-org/podcast-namespace/blob/main/chapters/example.json 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: * :attr:`~pod2gen.Episode.image` * :attr:`~pod2gen.Episode.explicit` * :attr:`~pod2gen.Episode.is_closed_captioned` * :attr:`~pod2gen.Episode.position` * :attr:`~pod2gen.Episode.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 :doc:`the API Documentation `. Podcast and Episode common attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :class:`~pod2gen.Podcast` and :class:`~pod2gen.Episode` can share the same attributes like :class:`~pod2gen.Location`, :class:`~pod2gen.License` etc... When the same attribute is defined for both the podcast and one of its episodes, like :class:`~pod2gen.Location` for example, the episode :attr:`Episode.location ` will be prioratized over the podcast :attr:`Podcast.location ` when reading the feed. .. _`RSS Namespace Extension for Podcasting`: https://podcastindex.org/namespace/1.0