2013-05-03 17:03:22 +02:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
'''
|
2013-05-04 22:54:43 +02:00
|
|
|
|
feedgen.podcast_entry
|
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~
|
2013-05-03 17:03:22 +02:00
|
|
|
|
|
2013-05-04 22:54:43 +02:00
|
|
|
|
Extends the feedgen to produce podcasts.
|
2013-05-03 17:03:22 +02:00
|
|
|
|
|
|
|
|
|
:copyright: 2013, Lars Kiesow <lkiesow@uos.de>
|
|
|
|
|
|
2013-05-03 17:13:08 +02:00
|
|
|
|
:license: FreeBSD and LGPL, see license.* for more details.
|
2013-05-03 17:03:22 +02:00
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
from lxml import etree
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
import dateutil.parser
|
|
|
|
|
import dateutil.tz
|
2013-05-04 22:54:43 +02:00
|
|
|
|
from feedgen.entry import FeedEntry
|
|
|
|
|
from feedgen.util import ensure_format
|
2013-05-03 17:03:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PodcastEntry(FeedEntry):
|
2013-05-04 20:08:08 +02:00
|
|
|
|
'''FeedEntry extension for podcasts.
|
|
|
|
|
'''
|
2013-05-03 17:03:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## ITunes tags
|
|
|
|
|
# http://www.apple.com/itunes/podcasts/specs.html#rss
|
|
|
|
|
__itunes_author = None
|
|
|
|
|
__itunes_block = None
|
|
|
|
|
__itunes_image = None
|
|
|
|
|
__itunes_duration = None
|
|
|
|
|
__itunes_explicit = None
|
|
|
|
|
__itunes_is_closed_captioned = None
|
|
|
|
|
__itunes_order = None
|
|
|
|
|
__itunes_subtitle = None
|
|
|
|
|
__itunes_summary = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rss_entry(self, feed):
|
|
|
|
|
'''Insert an RSS item into a existing XML structure. Normally you
|
|
|
|
|
would pass the channel node of an RSS feed XML to this function.
|
|
|
|
|
|
|
|
|
|
:param feed: The XML element to use as parent node for the item.
|
|
|
|
|
'''
|
|
|
|
|
entry = super(PodcastEntry,self).rss_entry(feed)
|
|
|
|
|
ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
|
|
|
|
|
|
|
|
|
|
if self.__itunes_author:
|
|
|
|
|
author = etree.SubElement(entry, '{%s}author' % ITUNES_NS)
|
|
|
|
|
author.text = self.__itunes_author
|
|
|
|
|
|
|
|
|
|
if not self.__itunes_block is None:
|
|
|
|
|
block = etree.SubElement(entry, '{%s}block' % ITUNES_NS)
|
|
|
|
|
block.text = 'yes' if self.__itunes_block else 'no'
|
|
|
|
|
|
|
|
|
|
if self.__itunes_image:
|
|
|
|
|
image = etree.SubElement(entry, '{%s}image' % ITUNES_NS)
|
|
|
|
|
image.text = self.__itunes_image
|
|
|
|
|
|
|
|
|
|
if self.__itunes_duration:
|
|
|
|
|
duration = etree.SubElement(entry, '{%s}duration' % ITUNES_NS)
|
|
|
|
|
duration.text = self.__itunes_duration
|
|
|
|
|
|
|
|
|
|
if self.__itunes_explicit in ('yes', 'no', 'clean'):
|
|
|
|
|
explicit = etree.SubElement(entry, '{%s}explicit' % ITUNES_NS)
|
|
|
|
|
explicit.text = self.__itunes_explicit
|
|
|
|
|
|
|
|
|
|
if not self.__itunes_is_closed_captioned is None:
|
|
|
|
|
is_closed_captioned = etree.SubElement(entry, '{%s}isClosedCaptioned' % ITUNES_NS)
|
|
|
|
|
is_closed_captioned.text = 'yes' if self.__itunes_is_closed_captioned else 'no'
|
|
|
|
|
|
|
|
|
|
if not self.__itunes_order is None and self.__itunes_order >= 0:
|
|
|
|
|
order = etree.SubElement(entry, '{%s}order' % ITUNES_NS)
|
|
|
|
|
order.text = str(self.__itunes_order)
|
|
|
|
|
|
|
|
|
|
if self.__itunes_subtitle:
|
|
|
|
|
subtitle = etree.SubElement(entry, '{%s}subtitle' % ITUNES_NS)
|
|
|
|
|
subtitle.text = self.__itunes_subtitle
|
|
|
|
|
|
|
|
|
|
if self.__itunes_summary:
|
|
|
|
|
summary = etree.SubElement(entry, '{%s}summary' % ITUNES_NS)
|
|
|
|
|
summary.text = self.__itunes_summary
|
|
|
|
|
return entry
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def itunes_author(self, itunes_author=None):
|
|
|
|
|
'''Get or set the itunes:author of the podcast episode. The content of
|
|
|
|
|
this tag is shown in the Artist column in iTunes. If the tag is not
|
|
|
|
|
present, iTunes uses the contents of the <author> tag. If <itunes:author>
|
|
|
|
|
is not present at the feed level, iTunes will use the contents of
|
|
|
|
|
<managingEditor>.
|
|
|
|
|
|
|
|
|
|
:param itunes_author: The author of the podcast.
|
|
|
|
|
:returns: The author of the podcast.
|
|
|
|
|
'''
|
|
|
|
|
if not itunes_author is None:
|
|
|
|
|
self.__itunes_author = itunes_author
|
|
|
|
|
return self.__itunes_author
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def itunes_block(self, itunes_block=None):
|
|
|
|
|
'''Get or set the ITunes block attribute. Use this to prevent episodes
|
|
|
|
|
from appearing in the iTunes podcast directory.
|
|
|
|
|
|
|
|
|
|
:param itunes_block: Block podcast episodes.
|
|
|
|
|
:returns: If the podcast episode is blocked.
|
|
|
|
|
'''
|
|
|
|
|
if not itunes_block is None:
|
|
|
|
|
self.__itunes_block = itunes_block
|
|
|
|
|
return self.__itunes_block
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def itunes_image(self, itunes_image=None):
|
|
|
|
|
'''Get or set the image for the podcast episode. This tag specifies the
|
|
|
|
|
artwork for your podcast. Put the URL to the image in the href attribute.
|
|
|
|
|
iTunes prefers square .jpg images that are at least 1400x1400 pixels,
|
|
|
|
|
which is different from what is specified for the standard RSS image tag.
|
|
|
|
|
In order for a podcast to be eligible for an iTunes Store feature, the
|
|
|
|
|
accompanying image must be 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". If the
|
|
|
|
|
<itunes:image> tag is not present, iTunes will use the contents of the
|
|
|
|
|
RSS image tag.
|
|
|
|
|
|
|
|
|
|
If you change your podcast’s image, also change the file’s name. iTunes
|
|
|
|
|
may not change the image if it checks your feed and the image URL is the
|
|
|
|
|
same. The server hosting your cover art image must allow HTTP head
|
|
|
|
|
requests for iTS to be able to automatically update your cover art.
|
|
|
|
|
|
|
|
|
|
:param itunes_image: Image of the podcast.
|
|
|
|
|
:returns: Image of the podcast.
|
|
|
|
|
'''
|
|
|
|
|
if not itunes_image is None:
|
|
|
|
|
if not ( itunes_image.endswith('.jpg') or itunes_image.endswith('.png') ):
|
|
|
|
|
ValueError('Image file must be png or jpg')
|
|
|
|
|
self.__itunes_image = itunes_image
|
|
|
|
|
return self.__itunes_image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def itunes_duration(self, itunes_duration=None):
|
|
|
|
|
'''Get or set the duration of the podcast episode. The content of this
|
|
|
|
|
tag is shown in the Time column in iTunes.
|
|
|
|
|
|
|
|
|
|
The tag can be formatted HH:MM:SS, H:MM:SS, MM:SS, or M:SS (H = hours,
|
|
|
|
|
M = minutes, S = seconds). If an integer is provided (no colon present),
|
|
|
|
|
the value is assumed to be in seconds. If one colon is present, the
|
|
|
|
|
number to the left is assumed to be minutes, and the number to the right
|
|
|
|
|
is assumed to be seconds. If more than two colons are present, the
|
|
|
|
|
numbers farthest to the right are ignored.
|
|
|
|
|
|
|
|
|
|
:param itunes_duration: Duration of the podcast episode.
|
|
|
|
|
:returns: Duration of the podcast episode.
|
|
|
|
|
'''
|
|
|
|
|
if not itunes_duration is None:
|
|
|
|
|
itunes_duration = str(itunes_duration)
|
|
|
|
|
if len(itunes_duration.split(':')) > 3 or \
|
|
|
|
|
itunes_duration.lstrip('0123456789:') != '':
|
|
|
|
|
ValueError('Invalid duration format')
|
|
|
|
|
self.__itunes_duration = itunes_duration
|
|
|
|
|
return self.itunes_duration
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def itunes_explicit(self, itunes_explicit=None):
|
|
|
|
|
'''Get or the the itunes:explicit value of the podcast episode. This tag
|
|
|
|
|
should be used to indicate whether your podcast episode contains explicit
|
|
|
|
|
material. The three values for this tag are "yes", "no", and "clean".
|
|
|
|
|
|
|
|
|
|
If you populate this tag with "yes", an "explicit" parental advisory
|
|
|
|
|
graphic will appear next to your podcast artwork on the iTunes Store and
|
|
|
|
|
in the Name column in iTunes. If the value is "clean", 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. If the explicit tag is present and has any other value
|
|
|
|
|
(e.g., "no"), you see no indicator — blank is the default advisory type.
|
|
|
|
|
|
|
|
|
|
:param itunes_explicit: If the podcast episode contains explicit material.
|
|
|
|
|
:returns: If the podcast episode contains explicit material.
|
|
|
|
|
'''
|
|
|
|
|
if not itunes_explicit is None:
|
|
|
|
|
if not itunes_explicit in ('', 'yes', 'no', 'clean'):
|
|
|
|
|
raise ValueError('Invalid value for explicit tag')
|
|
|
|
|
self.__itunes_explicit = itunes_explicit
|
|
|
|
|
return self.__itunes_explicit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def itunes_is_closed_captioned(self, itunes_is_closed_captioned=None):
|
|
|
|
|
'''Get or set the is_closed_captioned value of the podcast episode. This
|
|
|
|
|
tag should be used if your podcast includes a video episode with embedded
|
|
|
|
|
closed captioning support. The two values for this tag are "yes" and
|
|
|
|
|
"no”.
|
|
|
|
|
|
|
|
|
|
:param is_closed_captioned: If the episode has closed captioning support.
|
|
|
|
|
:returns: If the episode has closed captioning support.
|
|
|
|
|
'''
|
|
|
|
|
if not itunes_is_closed_captioned is None:
|
|
|
|
|
self.__itunes_is_closed_captioned = itunes_is_closed_captioned in ('yes', True)
|
|
|
|
|
return self.__itunes_is_closed_captioned
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def itunes_order(self, itunes_order=None):
|
|
|
|
|
'''Get or set the itunes:order value of the podcast episode. This tag can
|
|
|
|
|
be used to override the default ordering of episodes on the store.
|
|
|
|
|
|
|
|
|
|
This tag is used at an <item> level by populating with the number value
|
|
|
|
|
in which you would like the episode to appear on the store. For example,
|
|
|
|
|
if you would like an <item> to appear as the first episode in the
|
|
|
|
|
podcast, you would populate the <itunes:order> tag with “1”. If
|
|
|
|
|
conflicting order values are present in multiple episodes, the store will
|
|
|
|
|
use default ordering (pubDate).
|
|
|
|
|
|
|
|
|
|
To remove the order from the episode set the order to a value below zero.
|
|
|
|
|
|
|
|
|
|
:param itunes_order: The order of the episode.
|
|
|
|
|
:returns: The order of the episode.
|
|
|
|
|
'''
|
|
|
|
|
if not itunes_order is None:
|
|
|
|
|
self.__itunes_order = int(itunes_order)
|
|
|
|
|
return self.__itunes_order
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def itunes_subtitle(self, itunes_subtitle=None):
|
|
|
|
|
'''Get or set the itunes:subtitle value for the podcast episode. The
|
|
|
|
|
contents of this tag are shown in the Description column in iTunes. The
|
|
|
|
|
subtitle displays best if it is only a few words long.
|
|
|
|
|
|
|
|
|
|
:param itunes_subtitle: Subtitle of the podcast episode.
|
|
|
|
|
:returns: Subtitle of the podcast episode.
|
|
|
|
|
'''
|
|
|
|
|
if not itunes_subtitle is None:
|
|
|
|
|
self.__itunes_subtitle = itunes_subtitle
|
|
|
|
|
return self.__itunes_subtitle
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def itunes_summary(self, itunes_summary=None):
|
|
|
|
|
'''Get or set the itunes:summary value for the podcast episode. The
|
|
|
|
|
contents of this tag are shown in a separate window that appears when the
|
|
|
|
|
"circled i" in the Description column is clicked. It also appears on the
|
|
|
|
|
iTunes page for your podcast. This field can be up to 4000 characters. If
|
|
|
|
|
<itunes:summary> is not included, the contents of the <description> tag
|
|
|
|
|
are used.
|
|
|
|
|
|
|
|
|
|
:param itunes_summary: Summary of the podcast episode.
|
|
|
|
|
:returns: Summary of the podcast episode.
|
|
|
|
|
'''
|
|
|
|
|
if not itunes_summary is None:
|
|
|
|
|
self.__itunes_summary = itunes_summary
|
|
|
|
|
return self.__itunes_summary
|