15
15
import dateutil .parser
16
16
import dateutil .tz
17
17
from podgen .episode import Episode
18
- from podgen .util import ensure_format , formatRFC2822 , listToHumanreadableStr
18
+ from podgen .util import ensure_format , formatRFC2822 , listToHumanreadableStr , \
19
+ htmlencode
19
20
from podgen .person import Person
20
21
import podgen .version
21
22
import sys
@@ -306,6 +307,37 @@ def __init__(self, **kwargs):
306
307
.. _latest version of the standard: http://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#rfc.section.4
307
308
"""
308
309
310
+ self .xslt = None
311
+ """
312
+ Absolute URL to the XSLT file which web browsers should use with this
313
+ feed.
314
+
315
+ `XSLT`_ stands for Extensible Stylesheet Language Transformations and
316
+ can be regarded as a template language made for transforming XML into
317
+ XHTML (among other things). You can use it to avoid giving users an
318
+ ugly XML listing when trying to subscribe to your podcast; this
319
+ technique is in fact employed by most podcast publishers today.
320
+ In a web browser, it looks like a web page, and to the
321
+ podcatchers, it looks like a normal podcast feed. To put it another
322
+ way, the very same URL can be used as an information web page about the
323
+ podcast as well as the URL you subscribe to in podcatchers.
324
+
325
+ :type: :obj:`str`
326
+ :RSS: Processor instruction right after the xml declaration called
327
+ ``xml-stylesheet``, with type set to ``text/xsl`` and href set to
328
+ this attribute.
329
+
330
+ .. note::
331
+
332
+ Firefox will use its own stylesheet for RSS feeds, so you
333
+ must test using another browser and possibly a `simple web server`_
334
+ (``python -m http.server 8000 -b 127.0.0.1``).
335
+
336
+ .. _XSLT: https://en.wikipedia.org/wiki/XSLT
337
+ .. _simple web server:
338
+ https://docs.python.org/3/library/http.server.html
339
+ """
340
+
309
341
# Populate the podcast with the keyword arguments
310
342
for attribute , value in iteritems (kwargs ):
311
343
if hasattr (self , attribute ):
@@ -584,6 +616,34 @@ def _create_rss(self):
584
616
585
617
return feed
586
618
619
+ def _add_xslt_pi (self , rss , xml_declaration ):
620
+ """Add an XSLT processor instruction to the RSS string provided."""
621
+ # This is a hackish way of getting a processor instruction between
622
+ # the XML declaration and the RSS element; simply because lxml doesn't
623
+ # support processor instructions outside the root element. So we do
624
+ # a str.replace to replace the first newline with the processor
625
+ # instruction, since the XML declaration is followed by a newline.
626
+
627
+ # Get the processor instruction as a string
628
+ pi = self ._get_xslt_pi ()
629
+ if xml_declaration :
630
+ return rss .replace (
631
+ "\n " ,
632
+ '\n %s\n ' % pi ,
633
+ 1 )
634
+ else :
635
+ # No declaration, so just put it at the beginning (assuming the
636
+ # caller wants it there, why else would you set self.xslt?)
637
+ return pi + "\n " + rss
638
+
639
+ def _get_xslt_pi (self ):
640
+ htmlescaped_url = htmlencode (self .xslt )
641
+ quote_sanitized = htmlescaped_url .replace ('"' , '' ).replace ("\\ " , "" )
642
+ return etree .tostring (etree .ProcessingInstruction (
643
+ "xml-stylesheet" ,
644
+ 'type="text/xsl" href="' + quote_sanitized + '"' ,
645
+ ), encoding = str )
646
+
587
647
def __str__ (self ):
588
648
"""Print the podcast in RSS format, using the default options.
589
649
@@ -607,14 +667,28 @@ def rss_str(self, minimize=False, encoding='UTF-8',
607
667
:returns: The generated RSS feed as a :obj:`str`.
608
668
"""
609
669
feed = self ._create_rss ()
610
- return etree .tostring (feed , pretty_print = not minimize , encoding = encoding ,
670
+ rss = etree .tostring (feed , pretty_print = not minimize , encoding = encoding ,
611
671
xml_declaration = xml_declaration ).decode (encoding )
612
-
672
+ if self .xslt :
673
+ return self ._add_xslt_pi (rss , xml_declaration = xml_declaration )
674
+ else :
675
+ return rss
613
676
614
677
def rss_file (self , filename , minimize = False ,
615
678
encoding = 'UTF-8' , xml_declaration = True ):
616
679
"""Generate an RSS feed and write the resulting XML to a file.
617
680
681
+ .. note::
682
+
683
+ If atomicity is needed, then you are expected to provide that
684
+ yourself. That means that you should write the feed to a temporary
685
+ file which you rename to the final name afterwards; renaming is an
686
+ atomic operation on Unix(like) systems.
687
+
688
+ .. note::
689
+
690
+ File-like objects given to this method will not be closed.
691
+
618
692
:param filename: Name of file to write, or a file-like object, or a URL.
619
693
:type filename: str or fd
620
694
:param minimize: Set to True to disable splitting the feed into multiple
@@ -628,10 +702,20 @@ def rss_file(self, filename, minimize=False,
628
702
:type xml_declaration: bool
629
703
:returns: Nothing.
630
704
"""
631
- feed = self ._create_rss ()
632
- doc = etree .ElementTree (feed )
633
- doc .write (filename , pretty_print = not minimize , encoding = encoding ,
634
- xml_declaration = xml_declaration )
705
+ rss = self .rss_str (minimize = minimize , encoding = encoding ,
706
+ xml_declaration = xml_declaration )
707
+ # Have we got a filename, or a file-like object?
708
+ if isinstance (filename , string_types ):
709
+ # It is a string, assume it is filename
710
+ with open (filename , "w" ) as fd :
711
+ fd .write (rss )
712
+ elif hasattr (filename , "write" ):
713
+ # It is file-like enough to fool us
714
+ filename .write (rss )
715
+ else :
716
+ raise TypeError ("filename must either be a filename (str/unicode) "
717
+ "or a file-like object (with write method); "
718
+ "%s satisfies none of those conditions." % filename )
635
719
636
720
def apply_episode_order (self ):
637
721
"""Make sure that the episodes appear on iTunes in the exact order
0 commit comments