-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathpelican_podcast_feed.py
377 lines (329 loc) · 15.3 KB
/
pelican_podcast_feed.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
#!/usr/bin/env python
# encoding: utf-8
"""
iTunes Feed Generator for Pelican.
"""
from __future__ import unicode_literals
import six
from jinja2 import Markup
from pelican import signals
from pelican.writers import Writer
from pelican.generators import Generator
from pelican.utils import set_date_tzinfo
from feedgenerator import Rss201rev2Feed
from feedgenerator.django.utils.feedgenerator import rfc2822_date
# These are the attributes we want to pass to iTunes.
# TODO: provide a link to documentation about expected attributes.
ITEM_ELEMENTS = (
'title',
'itunes:author',
'itunes:subtitle',
'itunes:summary',
'itunes:image',
'enclosure',
'description',
'link',
'guid',
'pubDate',
'itunes:duration',
)
DEFAULT_ITEM_ELEMENTS = {}
for key in ITEM_ELEMENTS:
DEFAULT_ITEM_ELEMENTS[key] = None
class PodcastFeed(Rss201rev2Feed):
"""Helper class which generates the XML based in the global settings"""
def __init__(self, *args, **kwargs):
"""Nice method docstring goes here"""
super(PodcastFeed, self).__init__(*args, **kwargs)
def set_settings(self, settings):
"""Helper function which just receives the podcast settings.
:param settings: A dictionary with all the site settings.
"""
self.settings = settings
def rss_attributes(self):
"""Returns the podcast feed's attributes.
:return: A dictionary containing the feed's attributes.
"""
attrs = super(PodcastFeed, self).root_attributes()
attrs['xmlns:itunes'] = "http://www.itunes.com/dtds/podcast-1.0.dtd"
attrs['version'] = '2.0'
return attrs
def add_root_elements(self, handler):
"""Adds some basic but useful attributes for an iTunes feed.
:param handler: A SimplerXMLGenerator instance.
"""
super(PodcastFeed, self).add_root_elements(handler)
# Adds a language root tag. Ex:
# <language>en</language>
if 'PODCAST_FEED_LANGUAGE' in self.settings:
handler.addQuickElement(
'language', self.settings['PODCAST_FEED_LANGUAGE']
)
# Adds a copyright root tag. Ex:
# <copyright>℗ &© 2014 Hack 'n' Cast</copyright>
if 'PODCAST_FEED_COPYRIGHT' in self.settings:
handler.addQuickElement(
'copyright', self.settings['PODCAST_FEED_COPYRIGHT']
)
# Adds a explicit content root tag. Ex:
# <itunes:explicit>No</itunes:explicit>
if 'PODCAST_FEED_EXPLICIT' in self.settings:
handler.addQuickElement(
'itunes:explicit', self.settings['PODCAST_FEED_EXPLICIT']
)
# Adds a show subtitle root tag. Ex:
# <itunes:subtitle>The hacker podcast!</itunes:subtitle>
if 'PODCAST_FEED_SUBTITLE' in self.settings:
handler.addQuickElement(
'itunes:subtitle', self.settings['PODCAST_FEED_SUBTITLE']
)
# Adds a author root tag. Ex:
# <itunes:author>John Doe</itunes:author>
if 'PODCAST_FEED_AUTHOR' in self.settings:
handler.addQuickElement(
'itunes:author', self.settings['PODCAST_FEED_AUTHOR']
)
# Adds a podcast summary root tag. Ex:
# <itunes:summary>A podcast about... </itunes:summary>
if 'PODCAST_FEED_SUMMARY' in self.settings:
handler.addQuickElement(
'itunes:summary', self.settings['PODCAST_FEED_SUMMARY']
)
# Adds a podcast logo image root tag. Ex:
# <itunes:image href="http://example.com/logo.jpg" />
if 'PODCAST_FEED_IMAGE' in self.settings:
handler.addQuickElement(
'itunes:image', attrs={
'href': self.settings['PODCAST_FEED_IMAGE']
}
)
# Adds a feed owner root tag an some child tags. Ex:
# <itunes:owner>
# <itunes:name>John Doe</itunes:name>
# <itunes:email>[email protected]</itunes:email>
# </itunes:owner>
if ('PODCAST_FEED_OWNER_NAME' in self.settings and
'PODCAST_FEED_OWNER_EMAIL' in self.settings):
handler.startElement('itunes:owner', {})
handler.addQuickElement(
'itunes:name', self.settings['PODCAST_FEED_OWNER_NAME']
)
handler.addQuickElement(
'itunes:email', self.settings['PODCAST_FEED_OWNER_EMAIL']
)
handler.endElement('itunes:owner')
# Adds a show category root tag and some child tags. Ex:
# <itunes:category text="Technology">
# <itunes:category text="Gadgets"/>
# </itunes:category>
if 'PODCAST_FEED_CATEGORY' in self.settings:
categories = self.settings['PODCAST_FEED_CATEGORY']
if type(categories) in (list, tuple):
handler.startElement(
'itunes:category', attrs={'text': categories[0]}
)
handler.addQuickElement(
'itunes:category', attrs={'text': categories[1]}
)
handler.endElement('itunes:category')
else:
handler.addQuickElement(
'itunes:category', attrs={'text': categories}
)
def add_item_elements(self, handler, item):
"""Adds a new element to the iTunes feed, using information from
``item`` to populate it with relevant information about the article.
:param handler: A SimplerXMLGenerator instance
:param item: The dict generated by iTunesWriter._add_item_to_the_feed
"""
for key in DEFAULT_ITEM_ELEMENTS:
# empty attributes will be ignored.
if item[key] is None:
continue
if key == 'description':
content = item[key]
handler.startElement('description', {})
if not isinstance(content, six.text_type):
content = six.text_type(content, handler._encoding)
content = content.replace("<html><body>", "")
handler._write(content)
handler.endElement('description')
elif isinstance(item[key], six.text_type):
handler.addQuickElement(key, item[key])
elif type(item[key]) is dict:
handler.addQuickElement(key, attrs=item[key])
class iTunesWriter(Writer):
"""Writer class for our iTunes feed. This class is responsible for
invoking the PodcastFeed and writing the feed itself (using it's superclass
methods)."""
def __init__(self, *args, **kwargs):
"""Class initializer"""
super(iTunesWriter, self).__init__(*args, **kwargs)
def _create_new_feed(self, *args):
"""Helper function (called by the super class) which will initialize
the PodcastFeed object."""
if len(args) == 2:
# we are on pelican <2.7
feed_type, context = args
elif len(args) == 3:
# we are on Pelican >=2.7
feed_type, feed_title, context = args
else:
# this is not expected, let's provide a useful message
raise Exception(
'The Writer._create_new_feed signature has changed, check the '
'current Pelican source for the updated signature'
)
self.context = context
description = self.settings.get('PODCAST_FEED_SUMMARY', '')
title = (self.settings.get('PODCAST_FEED_TITLE', '') or
context['SITENAME'])
feed = PodcastFeed(
title=title,
link=("{0}/".format(self.site_url)),
feed_url=None,
description=description)
feed.set_settings(self.settings)
return feed
def _add_item_to_the_feed(self, feed, item):
"""Performs an 'in-place' update of existing 'published' articles
in ``feed`` by creating a new entry using the contents from the
``item`` being passed.
This method is invoked by pelican's core.
:param feed: A PodcastFeed instance.
:param item: An article (pelican's Article object).
"""
# Local copy of iTunes attributes to add to the feed.
items = DEFAULT_ITEM_ELEMENTS.copy()
# Link to the new article.
# http://example.com/episode-01
items['link'] = '{0}/{1}'.format(self.site_url, item.url)
# Title for the article.
# ex: <title>Episode Title</title>
items['title'] = Markup(item.title).striptags()
# Summary for the article. This can be obtained either from
# a ``:description:`` or a ``:summary:`` directive.
# ex: <itunes:summary>In this episode... </itunes:summary>
if hasattr(item, 'description'):
items['itunes:summary'] = item.description
else:
items['itunes:summary'] = Markup(item.summary).striptags()
items['description'] = "<![CDATA[{}]]>".format(
Markup(item.summary)
)
# Date the article was last modified.
# ex: <pubDate>Fri, 13 Jun 2014 04:59:00 -0300</pubDate>
items['pubDate'] = rfc2822_date(
set_date_tzinfo(
item.modified if hasattr(item, 'modified') else item.date,
self.settings.get('TIMEZONE', None))
)
# Name(s) for the article's author(s).
# ex: <itunes:author>John Doe</itunes:author>
if hasattr(item, 'author'):
items['itunes:author'] = item.author.name
# Subtitle for the article.
# ex: <itunes:subtitle>My episode subtitle</itunes:subtitle>
if hasattr(item, 'subtitle'):
items['itunes:subtitle'] = Markup(item.subtitle).striptags()
# Ex:
# <itunes:image href="http://example.com/Episodio1.jpg" />
if hasattr(item, 'image'):
items['itunes:image'] = {
'href': '{0}{1}'.format(self.site_url, item.image)}
# Information about the episode audio.
# ex: <enclosure url="http://example.com/episode.m4a"
# length="872731" type="audio/x-m4a" />
if hasattr(item, 'podcast'):
enclosure = {'url': item.podcast}
# Include the file size if available.
if hasattr(item, 'length'):
enclosure['length'] = item.length
# Include the audio mime type if available...
if hasattr(item, 'mimetype'):
enclosure['type'] = item.mimetype
else:
# ... or default to 'audio/mpeg'.
enclosure['type'] = 'audio/mpeg'
items['enclosure'] = enclosure
# Duration for the audio file.
# <itunes:duration>7:04</itunes:duration>
if hasattr(item, 'duration'):
items['itunes:duration'] = item.duration
# Unique identifier for the episode.
# Use a 'guid' if available...
# ex: <guid>http://example.com/aae20050615.m4a</guid>
if hasattr(item, 'guid'):
items['guid'] = item.guid
# ... else, use the article's link instead.
# ex: <guid>http://example.com/episode-01</guid>
else:
items['guid'] = items['link']
# Add the new article to the feed.
feed.add_item(**items)
class PodcastFeedGenerator(Generator):
"""Generates an iTunes content by inspecting all articles and invokes the
iTunesWriter object, which will write the itunes Feed."""
def __init__(self, *args, **kwargs):
"""Starts a brand new feed generator."""
super(PodcastFeedGenerator, self).__init__(*args, **kwargs)
# Initialize the number of episodes and where to save the feed.
self.episodes = []
self.podcast_episodes = {}
self.feed_path = self.settings.get('PODCAST_FEED_PATH', None)
self.podcasts = self.settings.get('PODCASTS',None)
if self.podcasts:
for show in self.podcasts:
self.podcast_episodes[show] = []
def generate_context(self):
"""Looks for all 'published' articles and add them to the episodes
list."""
if self.feed_path:
for article in self.context['articles']:
# Only 'published' articles with the 'podcast' metatag.
if (article.status.lower() == "published" and
hasattr(article, 'podcast')):
self.episodes.append(article)
if self.podcasts:
if hasattr(article, 'category'):
show = getattr(article, 'category').slug
# Only articles in the individual feed categories
if show in self.podcasts:
self.episodes.append(article)
self.podcast_episodes[show].append(article)
def generate_output(self, writer):
"""Write out the iTunes feed to a file.
:param writer: A ``Pelican Writer`` instance.
"""
if self.feed_path:
writer = iTunesWriter(self.output_path, self.settings)
writer.write_feed(self.episodes, self.context, self.feed_path)
if self.podcasts:
# Backup all of the global settings
self.original_settings = {}
self.original_settings['PODCAST_FEED_PATH'] = self.settings.get('PODCAST_FEED_PATH',None)
self.original_settings['PODCAST_FEED_TITLE'] = self.settings.get('PODCAST_FEED_TITLE',None)
self.original_settings['PODCAST_FEED_EXPLICIT'] = self.settings.get('PODCAST_FEED_EXPLICIT',None)
self.original_settings['PODCAST_FEED_LANGUAGE'] = self.settings.get('PODCAST_FEED_LANGUAGE',None)
self.original_settings['PODCAST_FEED_COPYRIGHT'] = self.settings.get('PODCAST_FEED_COPYRIGHT',None)
self.original_settings['PODCAST_FEED_SUBTITLE'] = self.settings.get('PODCAST_FEED_SUBTITLE',None)
self.original_settings['PODCAST_FEED_AUTHOR'] = self.settings.get('PODCAST_FEED_AUTHOR',None)
self.original_settings['PODCAST_FEED_SUMMARY'] = self.settings.get('PODCAST_FEED_SUMMARY',None)
self.original_settings['PODCAST_FEED_IMAGE'] = self.settings.get('PODCAST_FEED_IMAGE',None)
self.original_settings['PODCAST_FEED_OWNER_NAME'] = self.settings.get('PODCAST_FEED_OWNER_NAME',None)
self.original_settings['PODCAST_FEED_OWNER_EMAIL'] = self.settings.get('PODCAST_FEED_OWNER_EMAIL',None)
self.original_settings['PODCAST_FEED_CATEGORY'] = self.settings.get('PODCAST_FEED_CATEGORY',None)
for show in self.podcasts:
# Override the global settings with the per-show settings
self.settings.update(self.podcasts[show])
# Write out this podcast's feed
writer = iTunesWriter(self.output_path, self.settings)
writer.write_feed(self.podcast_episodes[show], self.context, self.podcasts[show]['PODCAST_FEED_PATH'])
# Restore the original global settings
self.settings.update(self.original_settings)
def get_generators(generators):
"""Module function invoked by the signal 'get_generators'."""
return PodcastFeedGenerator
def register():
"""Registers the module function `get_generators`."""
signals.get_generators.connect(get_generators)