diff --git a/feedgenerator/__main__.py b/feedgenerator/__main__.py index 888b09d..f30946e 100644 --- a/feedgenerator/__main__.py +++ b/feedgenerator/__main__.py @@ -10,13 +10,16 @@ ''' from feedgenerator.feed import FeedGenerator +from feedgenerator.podcast import PodcastGenerator import sys if __name__ == '__main__': if len(sys.argv) != 2 or not ( - sys.argv[1].endswith('rss') or sys.argv[1].endswith('atom') ): + sys.argv[1].endswith('rss') \ + or sys.argv[1].endswith('atom') \ + or sys.argv[1].endswith('podcast') ): print 'Usage: %s ( .atom | atom | .rss | rss )' % \ 'pythom -m feedgenerator' print '' @@ -29,7 +32,7 @@ if __name__ == '__main__': arg = sys.argv[1] - fg = FeedGenerator() + fg = PodcastGenerator() if arg.endswith('podcast') else FeedGenerator() fg.id('http://lernfunk.de/_MEDIAID_123') fg.title('Testfeed') fg.author( {'name':'Lars Kiesow','email':'lkiesow@uos.de'} ) @@ -60,6 +63,10 @@ if __name__ == '__main__': print fg.atom_str(pretty=True) elif arg == 'rss': print fg.rss_str(pretty=True) + elif arg == 'podcast': + fg.itunes_author('Lars Kiesow') + fg.itunes_category('Technology', 'Podcasting') + print fg.podcast_str(pretty=True) elif arg.endswith('atom'): fg.atom_file(arg) elif arg.endswith('rss'): diff --git a/feedgenerator/entry.py b/feedgenerator/entry.py index f2a122e..c11d586 100644 --- a/feedgenerator/entry.py +++ b/feedgenerator/entry.py @@ -16,7 +16,7 @@ import dateutil.tz from feedgenerator.util import ensure_format -class FeedEntry: +class FeedEntry(object): # ATOM # required diff --git a/feedgenerator/feed.py b/feedgenerator/feed.py index 423b1d0..e397a95 100644 --- a/feedgenerator/feed.py +++ b/feedgenerator/feed.py @@ -17,7 +17,7 @@ from feedgenerator.entry import FeedEntry from feedgenerator.util import ensure_format -class FeedGenerator: +class FeedGenerator(object): __feed_entries = [] @@ -69,7 +69,7 @@ class FeedGenerator: - def __create_atom(self): + def _create_atom(self): '''Create a ATOM feed xml structure containing all previously set fields. :returns: Tuple containing the feed root element and the element tree. @@ -176,7 +176,7 @@ class FeedGenerator: properly indented. :returns: String representation of the ATOM feed. ''' - feed, doc = self.__create_atom() + feed, doc = self._create_atom() return etree.tostring(feed, pretty_print=pretty) @@ -185,12 +185,12 @@ class FeedGenerator: :param filename: Name of file to write. ''' - feed, doc = self.__create_atom() + feed, doc = self._create_atom() with open(filename, 'w') as f: doc.write(f) - def __create_rss(self): + def _create_rss(self): '''Create an RSS feed xml structure containing all previously set fields. :returns: Tuple containing the feed root element and the element tree. @@ -317,7 +317,7 @@ class FeedGenerator: properly indented. :returns: String representation of the RSS feed. ''' - feed, doc = self.__create_rss() + feed, doc = self._create_rss() return etree.tostring(feed, pretty_print=pretty) @@ -326,7 +326,7 @@ class FeedGenerator: :param filename: Name of file to write. ''' - feed, doc = self.__create_rss() + feed, doc = self._create_rss() with open(filename, 'w') as f: doc.write(f) diff --git a/feedgenerator/podcast.py b/feedgenerator/podcast.py new file mode 100644 index 0000000..56c1365 --- /dev/null +++ b/feedgenerator/podcast.py @@ -0,0 +1,197 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +''' + feedgenerator.podcast + ~~~~~~~~~~~~~~~~~~~~~ + + Extends the feedgenerator to produce podcasts. + + :copyright: 2013, Lars Kiesow + + :license: FreeBSD and LGPL, see LICENSE for more details. +''' + +from lxml import etree +from datetime import datetime +import dateutil.parser +import dateutil.tz +from feedgenerator.feed import FeedGenerator +from feedgenerator.util import ensure_format + + +class PodcastGenerator(FeedGenerator): + + + ## ITunes tags + # http://www.apple.com/itunes/podcasts/specs.html#rss + __itunes_author = None + __itunes_block = None + __itunes_category = None + + + + + def __create_podcast(self): + '''Create an RSS feed xml structure containing all previously set fields. + + :returns: Tuple containing the feed root element and the element tree. + ''' + rss_feed, _ = super(PodcastGenerator,self)._create_rss() + # Replace the root element to add the itunes namespace + ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd' + feed = etree.Element('rss', version='2.0', + nsmap={ + 'atom' :'http://www.w3.org/2005/Atom', + 'itunes':ITUNES_NS} ) + feed[:] = rss_feed[:] + channel = feed[0] + doc = etree.ElementTree(feed) + + if self.__itunes_author: + author = etree.SubElement(channel, '{%s}author' % ITUNES_NS) + author.text = self.__itunes_author + + if not self.__itunes_block is None: + block = etree.SubElement(channel, '{%s}block' % ITUNES_NS) + block.text = 'yes' if self.__itunes_block else 'no' + + if self.__itunes_category: + category = etree.SubElement(channel, '{%s}category' % ITUNES_NS) + category.attrib['text'] = self.__itunes_category['cat'] + if self.__itunes_category.get('sub'): + subcategory = etree.SubElement(category, '{%s}category' % ITUNES_NS) + subcategory.attrib['text'] = self.__itunes_category['sub'] + + return feed, doc + + + def podcast_str(self, pretty=False): + '''Generates an RSS feed and returns the feed XML as string. + + :param pretty: If the feed should be split into multiple lines and + properly indented. + :returns: String representation of the RSS feed. + ''' + feed, doc = self.__create_podcast() + return etree.tostring(feed, pretty_print=pretty) + + + def podcast_file(self, filename): + '''Generates an RSS feed and write the resulting XML to a file. + + :param filename: Name of file to write. + ''' + feed, doc = self.__create_podcast() + with open(filename, 'w') as f: + doc.write(f) + + + def itunes_author(self, itunes_author=None): + '''Get or set the itunes:author. 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 tag. If is not present at the + feed level, iTunes will use the contents of . + + :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 the entire + podcast from appearing in the iTunes podcast directory. + + :param itunes_block: Block the podcast. + :returns: If the podcast is blocked. + ''' + if not itunes_block is None: + self.__itunes_block = itunes_block + return self.__itunes_block + + + def itunes_category(self, itunes_category=None, itunes_subcategory=None): + '''Get or set the ITunes category which appears in the category column + and in iTunes Store Browser. + + The (sub-)category has to be one from the values defined at + http://www.apple.com/itunes/podcasts/specs.html#categories + + :param itunes_category: Category of the podcast. + :param itunes_subcategory: Subcategory of the podcast. + :returns: Category data of the podcast. + ''' + if not itunes_category is None: + if not itunes_category in self._itunes_categories.keys(): + raise ValueError('Invalid category') + cat = {'cat':itunes_category} + if not itunes_subcategory is None: + if not itunes_subcategory in self._itunes_categories[itunes_category]: + raise ValueError('Invalid subcategory') + cat['sub'] = itunes_subcategory + self.__itunes_category = cat + return self.__itunes_category + + + def itunes_image(self, itunes_image=None): + '''Get or set the image for the podcast. 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 + 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 + + + + + + _itunes_categories = { + 'Arts': [ 'Design', 'Fashion & Beauty', 'Food', 'Literature', + 'Performing Arts', 'Visual Arts' ], + 'Business' : [ 'Business News', 'Careers', 'Investing', + 'Management & Marketing', 'Shopping' ], + 'Comedy' : [], + 'Education' : [ 'Education', 'Education Technology', + 'Higher Education', 'K-12', 'Language Courses', 'Training' ], + 'Games & Hobbies' : [ 'Automotive', 'Aviation', 'Hobbies', + 'Other Games', 'Video Games' ], + 'Government & Organizations' : [ 'Local', 'National', 'Non-Profit', + 'Regional' ], + 'Health' : [ 'Alternative Health', 'Fitness & Nutrition', 'Self-Help', + 'Sexuality' ], + 'Kids & Family' : [], + 'Music' : [], + 'News & Politics' : [], + 'Religion & Spirituality' : [ 'Buddhism', 'Christianity', 'Hinduism', + 'Islam', 'Judaism', 'Other', 'Spirituality' ], + 'Science & Medicine' : [ 'Medicine', 'Natural Sciences', + 'Social Sciences' ], + 'Society & Culture' : [ 'History', 'Personal Journals', 'Philosophy', + 'Places & Travel' ], + 'Sports & Recreation' : [ 'Amateur', 'College & High School', + 'Outdoor', 'Professional' ], + 'Technology' : [ 'Gadgets', 'Tech News', 'Podcasting', + 'Software How-To' ], + 'TV & Film' : [] + }