1
0
Fork 0

Adding upstream version 1.0.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-05 07:43:11 +01:00
parent 5cd8ebe7c9
commit 70c11d34fc
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
55 changed files with 6853 additions and 0 deletions

6
feedgen/ext/__init__.py Normal file
View file

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
"""
===========
feedgen.ext
===========
"""

44
feedgen/ext/base.py Normal file
View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
'''
feedgen.ext.base
~~~~~~~~~~~~~~~~
Basic FeedGenerator extension which does nothing but provides all necessary
methods.
:copyright: 2013, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details.
'''
class BaseExtension(object):
'''Basic FeedGenerator extension.
'''
def extend_ns(self):
'''Returns a dict that will be used in the namespace map for the feed.
'''
return dict()
def extend_rss(self, feed):
'''Extend a RSS feed xml structure containing all previously set
fields.
:param feed: The feed xml root element.
:returns: The feed root element.
'''
return feed
def extend_atom(self, feed):
'''Extend an ATOM feed xml structure containing all previously set
fields.
:param feed: The feed xml root element.
:returns: The feed root element.
'''
return feed
class BaseEntryExtension(BaseExtension):
'''Basic FeedEntry extension.
'''

407
feedgen/ext/dc.py Normal file
View file

@ -0,0 +1,407 @@
# -*- coding: utf-8 -*-
'''
feedgen.ext.dc
~~~~~~~~~~~~~~~~~~~
Extends the FeedGenerator to add Dubline Core Elements to the feeds.
Descriptions partly taken from
http://dublincore.org/documents/dcmi-terms/#elements-coverage
:copyright: 2013-2017, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details.
'''
from feedgen.ext.base import BaseExtension
from feedgen.util import xml_elem
class DcBaseExtension(BaseExtension):
'''Dublin Core Elements extension for podcasts.
'''
def __init__(self):
# http://dublincore.org/documents/usageguide/elements.shtml
# http://dublincore.org/documents/dces/
# http://dublincore.org/documents/dcmi-terms/
self._dcelem_contributor = None
self._dcelem_coverage = None
self._dcelem_creator = None
self._dcelem_date = None
self._dcelem_description = None
self._dcelem_format = None
self._dcelem_identifier = None
self._dcelem_language = None
self._dcelem_publisher = None
self._dcelem_relation = None
self._dcelem_rights = None
self._dcelem_source = None
self._dcelem_subject = None
self._dcelem_title = None
self._dcelem_type = None
def extend_ns(self):
return {'dc': 'http://purl.org/dc/elements/1.1/'}
def _extend_xml(self, xml_element):
'''Extend xml_element with set DC fields.
:param xml_element: etree element
'''
DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/'
for elem in ['contributor', 'coverage', 'creator', 'date',
'description', 'language', 'publisher', 'relation',
'rights', 'source', 'subject', 'title', 'type', 'format',
'identifier']:
if hasattr(self, '_dcelem_%s' % elem):
for val in getattr(self, '_dcelem_%s' % elem) or []:
node = xml_elem('{%s}%s' % (DCELEMENTS_NS, elem),
xml_element)
node.text = val
def extend_atom(self, atom_feed):
'''Extend an Atom feed with the set DC fields.
:param atom_feed: The feed root element
:returns: The feed root element
'''
self._extend_xml(atom_feed)
return atom_feed
def extend_rss(self, rss_feed):
'''Extend a RSS feed with the set DC fields.
:param rss_feed: The feed root element
:returns: The feed root element.
'''
channel = rss_feed[0]
self._extend_xml(channel)
return rss_feed
def dc_contributor(self, contributor=None, replace=False):
'''Get or set the dc:contributor which is an entity responsible for
making contributions to the resource.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-contributor
:param contributor: Contributor or list of contributors.
:param replace: Replace already set contributors (default: False).
:returns: List of contributors.
'''
if contributor is not None:
if not isinstance(contributor, list):
contributor = [contributor]
if replace or not self._dcelem_contributor:
self._dcelem_contributor = []
self._dcelem_contributor += contributor
return self._dcelem_contributor
def dc_coverage(self, coverage=None, replace=True):
'''Get or set the dc:coverage which indicated the spatial or temporal
topic of the resource, the spatial applicability of the resource, or
the jurisdiction under which the resource is relevant.
Spatial topic and spatial applicability may be a named place or a
location specified by its geographic coordinates. Temporal topic may be
a named period, date, or date range. A jurisdiction may be a named
administrative entity or a geographic place to which the resource
applies. Recommended best practice is to use a controlled vocabulary
such as the Thesaurus of Geographic Names [TGN]. Where appropriate,
named places or time periods can be used in preference to numeric
identifiers such as sets of coordinates or date ranges.
References:
[TGN] http://www.getty.edu/research/tools/vocabulary/tgn/index.html
:param coverage: Coverage of the feed.
:param replace: Replace already set coverage (default: True).
:returns: Coverage of the feed.
'''
if coverage is not None:
if not isinstance(coverage, list):
coverage = [coverage]
if replace or not self._dcelem_coverage:
self._dcelem_coverage = []
self._dcelem_coverage = coverage
return self._dcelem_coverage
def dc_creator(self, creator=None, replace=False):
'''Get or set the dc:creator which is an entity primarily responsible
for making the resource.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-creator
:param creator: Creator or list of creators.
:param replace: Replace already set creators (default: False).
:returns: List of creators.
'''
if creator is not None:
if not isinstance(creator, list):
creator = [creator]
if replace or not self._dcelem_creator:
self._dcelem_creator = []
self._dcelem_creator += creator
return self._dcelem_creator
def dc_date(self, date=None, replace=True):
'''Get or set the dc:date which describes a point or period of time
associated with an event in the lifecycle of the resource.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-date
:param date: Date or list of dates.
:param replace: Replace already set dates (default: True).
:returns: List of dates.
'''
if date is not None:
if not isinstance(date, list):
date = [date]
if replace or not self._dcelem_date:
self._dcelem_date = []
self._dcelem_date += date
return self._dcelem_date
def dc_description(self, description=None, replace=True):
'''Get or set the dc:description which is an account of the resource.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-description
:param description: Description or list of descriptions.
:param replace: Replace already set descriptions (default: True).
:returns: List of descriptions.
'''
if description is not None:
if not isinstance(description, list):
description = [description]
if replace or not self._dcelem_description:
self._dcelem_description = []
self._dcelem_description += description
return self._dcelem_description
def dc_format(self, format=None, replace=True):
'''Get or set the dc:format which describes the file format, physical
medium, or dimensions of the resource.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-format
:param format: Format of the resource or list of formats.
:param replace: Replace already set format (default: True).
:returns: Format of the resource.
'''
if format is not None:
if not isinstance(format, list):
format = [format]
if replace or not self._dcelem_format:
self._dcelem_format = []
self._dcelem_format += format
return self._dcelem_format
def dc_identifier(self, identifier=None, replace=True):
'''Get or set the dc:identifier which should be an unambiguous
reference to the resource within a given context.
For more inidentifierion see:
http://dublincore.org/documents/dcmi-terms/#elements-identifier
:param identifier: Identifier of the resource or list of identifiers.
:param replace: Replace already set identifier (default: True).
:returns: Identifiers of the resource.
'''
if identifier is not None:
if not isinstance(identifier, list):
identifier = [identifier]
if replace or not self._dcelem_identifier:
self._dcelem_identifier = []
self._dcelem_identifier += identifier
return self._dcelem_identifier
def dc_language(self, language=None, replace=True):
'''Get or set the dc:language which describes a language of the
resource.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-language
:param language: Language or list of languages.
:param replace: Replace already set languages (default: True).
:returns: List of languages.
'''
if language is not None:
if not isinstance(language, list):
language = [language]
if replace or not self._dcelem_language:
self._dcelem_language = []
self._dcelem_language += language
return self._dcelem_language
def dc_publisher(self, publisher=None, replace=False):
'''Get or set the dc:publisher which is an entity responsible for
making the resource available.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-publisher
:param publisher: Publisher or list of publishers.
:param replace: Replace already set publishers (default: False).
:returns: List of publishers.
'''
if publisher is not None:
if not isinstance(publisher, list):
publisher = [publisher]
if replace or not self._dcelem_publisher:
self._dcelem_publisher = []
self._dcelem_publisher += publisher
return self._dcelem_publisher
def dc_relation(self, relation=None, replace=False):
'''Get or set the dc:relation which describes a related resource.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-relation
:param relation: Relation or list of relations.
:param replace: Replace already set relations (default: False).
:returns: List of relations.
'''
if relation is not None:
if not isinstance(relation, list):
relation = [relation]
if replace or not self._dcelem_relation:
self._dcelem_relation = []
self._dcelem_relation += relation
return self._dcelem_relation
def dc_rights(self, rights=None, replace=False):
'''Get or set the dc:rights which may contain information about rights
held in and over the resource.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-rights
:param rights: Rights information or list of rights information.
:param replace: Replace already set rights (default: False).
:returns: List of rights information.
'''
if rights is not None:
if not isinstance(rights, list):
rights = [rights]
if replace or not self._dcelem_rights:
self._dcelem_rights = []
self._dcelem_rights += rights
return self._dcelem_rights
def dc_source(self, source=None, replace=False):
'''Get or set the dc:source which is a related resource from which the
described resource is derived.
The described resource may be derived from the related resource in
whole or in part. Recommended best practice is to identify the related
resource by means of a string conforming to a formal identification
system.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-source
:param source: Source or list of sources.
:param replace: Replace already set sources (default: False).
:returns: List of sources.
'''
if source is not None:
if not isinstance(source, list):
source = [source]
if replace or not self._dcelem_source:
self._dcelem_source = []
self._dcelem_source += source
return self._dcelem_source
def dc_subject(self, subject=None, replace=False):
'''Get or set the dc:subject which describes the topic of the resource.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-subject
:param subject: Subject or list of subjects.
:param replace: Replace already set subjects (default: False).
:returns: List of subjects.
'''
if subject is not None:
if not isinstance(subject, list):
subject = [subject]
if replace or not self._dcelem_subject:
self._dcelem_subject = []
self._dcelem_subject += subject
return self._dcelem_subject
def dc_title(self, title=None, replace=True):
'''Get or set the dc:title which is a name given to the resource.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-title
:param title: Title or list of titles.
:param replace: Replace already set titles (default: False).
:returns: List of titles.
'''
if title is not None:
if not isinstance(title, list):
title = [title]
if replace or not self._dcelem_title:
self._dcelem_title = []
self._dcelem_title += title
return self._dcelem_title
def dc_type(self, type=None, replace=False):
'''Get or set the dc:type which describes the nature or genre of the
resource.
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-type
:param type: Type or list of types.
:param replace: Replace already set types (default: False).
:returns: List of types.
'''
if type is not None:
if not isinstance(type, list):
type = [type]
if replace or not self._dcelem_type:
self._dcelem_type = []
self._dcelem_type += type
return self._dcelem_type
class DcExtension(DcBaseExtension):
'''Dublin Core Elements extension for podcasts.
'''
class DcEntryExtension(DcBaseExtension):
'''Dublin Core Elements extension for podcasts.
'''
def extend_atom(self, entry):
'''Add dc elements to an atom item. Alters the item itself.
:param entry: An atom entry element.
:returns: The entry element.
'''
self._extend_xml(entry)
return entry
def extend_rss(self, item):
'''Add dc elements to a RSS item. Alters the item itself.
:param item: A RSS item element.
:returns: The item element.
'''
self._extend_xml(item)
return item

21
feedgen/ext/geo.py Normal file
View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
'''
feedgen.ext.geo
~~~~~~~~~~~~~~~~~~~
Extends the FeedGenerator to produce Simple GeoRSS feeds.
:copyright: 2017, Bob Breznak <bob.breznak@gmail.com>
:license: FreeBSD and LGPL, see license.* for more details.
'''
from feedgen.ext.base import BaseExtension
class GeoExtension(BaseExtension):
'''FeedGenerator extension for Simple GeoRSS.
'''
def extend_ns(self):
return {'georss': 'http://www.georss.org/georss'}

329
feedgen/ext/geo_entry.py Normal file
View file

@ -0,0 +1,329 @@
# -*- coding: utf-8 -*-
'''
feedgen.ext.geo_entry
~~~~~~~~~~~~~~~~~~~
Extends the FeedGenerator to produce Simple GeoRSS feeds.
:copyright: 2017, Bob Breznak <bob.breznak@gmail.com>
:license: FreeBSD and LGPL, see license.* for more details.
'''
import numbers
import warnings
from feedgen.ext.base import BaseEntryExtension
from feedgen.util import xml_elem
class GeoRSSPolygonInteriorWarning(Warning):
"""
Simple placeholder for warning about ignored polygon interiors.
Stores the original geom on a ``geom`` attribute (if required warnings are
raised as errors).
"""
def __init__(self, geom, *args, **kwargs):
self.geom = geom
super(GeoRSSPolygonInteriorWarning, self).__init__(*args, **kwargs)
def __str__(self):
return '{:d} interiors of polygon ignored'.format(
len(self.geom.__geo_interface__['coordinates']) - 1
) # ignore exterior in count
class GeoRSSGeometryError(ValueError):
"""
Subclass of ValueError for a GeoRSS geometry error
Only some geometries are supported in Simple GeoRSS, so if not raise an
error. Offending geometry is stored on the ``geom`` attribute.
"""
def __init__(self, geom, *args, **kwargs):
self.geom = geom
super(GeoRSSGeometryError, self).__init__(*args, **kwargs)
def __str__(self):
msg = "Geometry of type '{}' not in Point, Linestring or Polygon"
return msg.format(
self.geom.__geo_interface__['type']
)
class GeoEntryExtension(BaseEntryExtension):
'''FeedEntry extension for Simple GeoRSS.
'''
def __init__(self):
'''Simple GeoRSS tag'''
# geometries
self.__point = None
self.__line = None
self.__polygon = None
self.__box = None
# additional properties
self.__featuretypetag = None
self.__relationshiptag = None
self.__featurename = None
# elevation
self.__elev = None
self.__floor = None
# radius
self.__radius = None
def extend_file(self, entry):
'''Add additional fields to an RSS item.
:param feed: The RSS item XML element to use.
'''
GEO_NS = 'http://www.georss.org/georss'
if self.__point:
point = xml_elem('{%s}point' % GEO_NS, entry)
point.text = self.__point
if self.__line:
line = xml_elem('{%s}line' % GEO_NS, entry)
line.text = self.__line
if self.__polygon:
polygon = xml_elem('{%s}polygon' % GEO_NS, entry)
polygon.text = self.__polygon
if self.__box:
box = xml_elem('{%s}box' % GEO_NS, entry)
box.text = self.__box
if self.__featuretypetag:
featuretypetag = xml_elem('{%s}featuretypetag' % GEO_NS, entry)
featuretypetag.text = self.__featuretypetag
if self.__relationshiptag:
relationshiptag = xml_elem('{%s}relationshiptag' % GEO_NS, entry)
relationshiptag.text = self.__relationshiptag
if self.__featurename:
featurename = xml_elem('{%s}featurename' % GEO_NS, entry)
featurename.text = self.__featurename
if self.__elev:
elevation = xml_elem('{%s}elev' % GEO_NS, entry)
elevation.text = str(self.__elev)
if self.__floor:
floor = xml_elem('{%s}floor' % GEO_NS, entry)
floor.text = str(self.__floor)
if self.__radius:
radius = xml_elem('{%s}radius' % GEO_NS, entry)
radius.text = str(self.__radius)
return entry
def extend_rss(self, entry):
return self.extend_file(entry)
def extend_atom(self, entry):
return self.extend_file(entry)
def point(self, point=None):
'''Get or set the georss:point of the entry.
:param point: The GeoRSS formatted point (i.e. "42.36 -71.05")
:returns: The current georss:point of the entry.
'''
if point is not None:
self.__point = point
return self.__point
def line(self, line=None):
'''Get or set the georss:line of the entry
:param point: The GeoRSS formatted line (i.e. "45.256 -110.45 46.46
-109.48 43.84 -109.86")
:return: The current georss:line of the entry
'''
if line is not None:
self.__line = line
return self.__line
def polygon(self, polygon=None):
'''Get or set the georss:polygon of the entry
:param polygon: The GeoRSS formatted polygon (i.e. "45.256 -110.45
46.46 -109.48 43.84 -109.86 45.256 -110.45")
:return: The current georss:polygon of the entry
'''
if polygon is not None:
self.__polygon = polygon
return self.__polygon
def box(self, box=None):
'''
Get or set the georss:box of the entry
:param box: The GeoRSS formatted box (i.e. "42.943 -71.032 43.039
-69.856")
:return: The current georss:box of the entry
'''
if box is not None:
self.__box = box
return self.__box
def featuretypetag(self, featuretypetag=None):
'''
Get or set the georss:featuretypetag of the entry
:param featuretypetag: The GeoRSS feaaturertyptag (e.g. "city")
:return: The current georss:featurertypetag
'''
if featuretypetag is not None:
self.__featuretypetag = featuretypetag
return self.__featuretypetag
def relationshiptag(self, relationshiptag=None):
'''
Get or set the georss:relationshiptag of the entry
:param relationshiptag: The GeoRSS relationshiptag (e.g.
"is-centred-at")
:return: the current georss:relationshiptag
'''
if relationshiptag is not None:
self.__relationshiptag = relationshiptag
return self.__relationshiptag
def featurename(self, featurename=None):
'''
Get or set the georss:featurename of the entry
:param featuretypetag: The GeoRSS featurename (e.g. "Footscray")
:return: the current georss:featurename
'''
if featurename is not None:
self.__featurename = featurename
return self.__featurename
def elev(self, elev=None):
'''
Get or set the georss:elev of the entry
:param elev: The GeoRSS elevation (e.g. 100.3)
:type elev: numbers.Number
:return: the current georss:elev
'''
if elev is not None:
if not isinstance(elev, numbers.Number):
raise ValueError("elev tag must be numeric: {}".format(elev))
self.__elev = elev
return self.__elev
def floor(self, floor=None):
'''
Get or set the georss:floor of the entry
:param floor: The GeoRSS floor (e.g. 4)
:type floor: int
:return: the current georss:floor
'''
if floor is not None:
if not isinstance(floor, int):
raise ValueError("floor tag must be int: {}".format(floor))
self.__floor = floor
return self.__floor
def radius(self, radius=None):
'''
Get or set the georss:radius of the entry
:param radius: The GeoRSS radius (e.g. 100.3)
:type radius: numbers.Number
:return: the current georss:radius
'''
if radius is not None:
if not isinstance(radius, numbers.Number):
raise ValueError(
"radius tag must be numeric: {}".format(radius)
)
self.__radius = radius
return self.__radius
def geom_from_geo_interface(self, geom):
'''
Generate a georss geometry from some Python object with a
``__geo_interface__`` property (see the `geo_interface specification by
Sean Gillies`_geointerface )
Note only a subset of GeoJSON (see `geojson.org`_geojson ) can be
easily converted to GeoRSS:
- Point
- LineString
- Polygon (if there are holes / donuts in the polygons a warning will
be generated
Other GeoJson types will raise a ``ValueError``.
.. note:: The geometry is assumed to be x, y as longitude, latitude in
the WGS84 projection.
.. _geointerface: https://gist.github.com/sgillies/2217756
.. _geojson: https://geojson.org/
:param geom: Geometry object with a __geo_interface__ property
:return: the formatted GeoRSS geometry
'''
geojson = geom.__geo_interface__
if geojson['type'] not in ('Point', 'LineString', 'Polygon'):
raise GeoRSSGeometryError(geom)
if geojson['type'] == 'Point':
coords = '{:f} {:f}'.format(
geojson['coordinates'][1], # latitude is y
geojson['coordinates'][0]
)
return self.point(coords)
elif geojson['type'] == 'LineString':
coords = ' '.join(
'{:f} {:f}'.format(vertex[1], vertex[0])
for vertex in
geojson['coordinates']
)
return self.line(coords)
elif geojson['type'] == 'Polygon':
if len(geojson['coordinates']) > 1:
warnings.warn(GeoRSSPolygonInteriorWarning(geom))
coords = ' '.join(
'{:f} {:f}'.format(vertex[1], vertex[0])
for vertex in
geojson['coordinates'][0]
)
return self.polygon(coords)

183
feedgen/ext/media.py Normal file
View file

@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
'''
feedgen.ext.media
~~~~~~~~~~~~~~~~~
Extends the feedgen to produce media tags.
:copyright: 2013-2017, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details.
'''
from feedgen.ext.base import BaseEntryExtension, BaseExtension
from feedgen.util import ensure_format, xml_elem
MEDIA_NS = 'http://search.yahoo.com/mrss/'
class MediaExtension(BaseExtension):
'''FeedGenerator extension for torrent feeds.
'''
def extend_ns(self):
return {'media': MEDIA_NS}
class MediaEntryExtension(BaseEntryExtension):
'''FeedEntry extension for media tags.
'''
def __init__(self):
self.__media_content = []
self.__media_thumbnail = []
def extend_atom(self, entry):
'''Add additional fields to an RSS item.
:param feed: The RSS item XML element to use.
'''
groups = {None: entry}
for media_content in self.__media_content:
# Define current media:group
group = groups.get(media_content.get('group'))
if group is None:
group = xml_elem('{%s}group' % MEDIA_NS, entry)
groups[media_content.get('group')] = group
# Add content
content = xml_elem('{%s}content' % MEDIA_NS, group)
for attr in ('url', 'fileSize', 'type', 'medium', 'isDefault',
'expression', 'bitrate', 'framerate', 'samplingrate',
'channels', 'duration', 'height', 'width', 'lang'):
if media_content.get(attr):
content.set(attr, media_content[attr])
for media_thumbnail in self.__media_thumbnail:
# Define current media:group
group = groups.get(media_thumbnail.get('group'))
if group is None:
group = xml_elem('{%s}group' % MEDIA_NS, entry)
groups[media_thumbnail.get('group')] = group
# Add thumbnails
thumbnail = xml_elem('{%s}thumbnail' % MEDIA_NS, group)
for attr in ('url', 'height', 'width', 'time'):
if media_thumbnail.get(attr):
thumbnail.set(attr, media_thumbnail[attr])
return entry
def extend_rss(self, item):
return self.extend_atom(item)
def content(self, content=None, replace=False, group='default', **kwargs):
'''Get or set media:content data.
This method can be called with:
- the fields of a media:content as keyword arguments
- the fields of a media:content as a dictionary
- a list of dictionaries containing the media:content fields
<media:content> is a sub-element of either <item> or <media:group>.
Media objects that are not the same content should not be included in
the same <media:group> element. The sequence of these items implies
the order of presentation. While many of the attributes appear to be
audio/video specific, this element can be used to publish any type
of media. It contains 14 attributes, most of which are optional.
media:content has the following fields:
- *url* should specify the direct URL to the media object.
- *fileSize* number of bytes of the media object.
- *type* standard MIME type of the object.
- *medium* type of object (image | audio | video | document |
executable).
- *isDefault* determines if this is the default object.
- *expression* determines if the object is a sample or the full version
of the object, or even if it is a continuous stream (sample | full |
nonstop).
- *bitrate* kilobits per second rate of media.
- *framerate* number of frames per second for the media object.
- *samplingrate* number of samples per second taken to create the media
object. It is expressed in thousands of samples per second (kHz).
- *channels* number of audio channels in the media object.
- *duration* number of seconds the media object plays.
- *height* height of the media object.
- *width* width of the media object.
- *lang* is the primary language encapsulated in the media object.
:param content: Dictionary or list of dictionaries with content data.
:param replace: Add or replace old data.
:param group: Media group to put this content in.
:returns: The media content tag.
'''
# Handle kwargs
if content is None and kwargs:
content = kwargs
# Handle new data
if content is not None:
# Reset data if we want to replace them
if replace or self.__media_content is None:
self.__media_content = []
# Ensure list
if not isinstance(content, list):
content = [content]
# define media group
for c in content:
c['group'] = c.get('group', group)
self.__media_content += ensure_format(
content,
set(['url', 'fileSize', 'type', 'medium', 'isDefault',
'expression', 'bitrate', 'framerate', 'samplingrate',
'channels', 'duration', 'height', 'width', 'lang',
'group']),
set(['url', 'group']))
return self.__media_content
def thumbnail(self, thumbnail=None, replace=False, group='default',
**kwargs):
'''Get or set media:thumbnail data.
This method can be called with:
- the fields of a media:content as keyword arguments
- the fields of a media:content as a dictionary
- a list of dictionaries containing the media:content fields
Allows particular images to be used as representative images for
the media object. If multiple thumbnails are included, and time
coding is not at play, it is assumed that the images are in order
of importance. It has one required attribute and three optional
attributes.
media:thumbnail has the following fields:
- *url* should specify the direct URL to the media object.
- *height* height of the media object.
- *width* width of the media object.
- *time* specifies the time offset in relation to the media object.
:param thumbnail: Dictionary or list of dictionaries with thumbnail
data.
:param replace: Add or replace old data.
:param group: Media group to put this content in.
:returns: The media thumbnail tag.
'''
# Handle kwargs
if thumbnail is None and kwargs:
thumbnail = kwargs
# Handle new data
if thumbnail is not None:
# Reset data if we want to replace them
if replace or self.__media_thumbnail is None:
self.__media_thumbnail = []
# Ensure list
if not isinstance(thumbnail, list):
thumbnail = [thumbnail]
# Define media group
for t in thumbnail:
t['group'] = t.get('group', group)
self.__media_thumbnail += ensure_format(
thumbnail,
set(['url', 'height', 'width', 'time', 'group']),
set(['url', 'group']))
return self.__media_thumbnail

388
feedgen/ext/podcast.py Normal file
View file

@ -0,0 +1,388 @@
# -*- coding: utf-8 -*-
'''
feedgen.ext.podcast
~~~~~~~~~~~~~~~~~~~
Extends the FeedGenerator to produce podcasts.
:copyright: 2013, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details.
'''
from feedgen.compat import string_types
from feedgen.ext.base import BaseExtension
from feedgen.util import ensure_format, xml_elem
class PodcastExtension(BaseExtension):
'''FeedGenerator extension for podcasts.
'''
def __init__(self):
# ITunes tags
# http://www.apple.com/itunes/podcasts/specs.html#rss
self.__itunes_author = None
self.__itunes_block = None
self.__itunes_category = None
self.__itunes_image = None
self.__itunes_explicit = None
self.__itunes_complete = None
self.__itunes_new_feed_url = None
self.__itunes_owner = None
self.__itunes_subtitle = None
self.__itunes_summary = None
self.__itunes_type = None
def extend_ns(self):
return {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'}
def extend_rss(self, rss_feed):
'''Extend an RSS feed root with set itunes fields.
:returns: The feed root element.
'''
ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
channel = rss_feed[0]
if self.__itunes_author:
author = xml_elem('{%s}author' % ITUNES_NS, channel)
author.text = self.__itunes_author
if self.__itunes_block is not None:
block = xml_elem('{%s}block' % ITUNES_NS, channel)
block.text = 'yes' if self.__itunes_block else 'no'
for c in self.__itunes_category or []:
if not c.get('cat'):
continue
category = channel.find(
'{%s}category[@text="%s"]' % (ITUNES_NS, c.get('cat')))
if category is None:
category = xml_elem('{%s}category' % ITUNES_NS, channel)
category.attrib['text'] = c.get('cat')
if c.get('sub'):
subcategory = xml_elem('{%s}category' % ITUNES_NS, category)
subcategory.attrib['text'] = c.get('sub')
if self.__itunes_image:
image = xml_elem('{%s}image' % ITUNES_NS, channel)
image.attrib['href'] = self.__itunes_image
if self.__itunes_explicit in ('yes', 'no', 'clean'):
explicit = xml_elem('{%s}explicit' % ITUNES_NS, channel)
explicit.text = self.__itunes_explicit
if self.__itunes_complete in ('yes', 'no'):
complete = xml_elem('{%s}complete' % ITUNES_NS, channel)
complete.text = self.__itunes_complete
if self.__itunes_new_feed_url:
new_feed_url = xml_elem('{%s}new-feed-url' % ITUNES_NS, channel)
new_feed_url.text = self.__itunes_new_feed_url
if self.__itunes_owner:
owner = xml_elem('{%s}owner' % ITUNES_NS, channel)
owner_name = xml_elem('{%s}name' % ITUNES_NS, owner)
owner_name.text = self.__itunes_owner.get('name')
owner_email = xml_elem('{%s}email' % ITUNES_NS, owner)
owner_email.text = self.__itunes_owner.get('email')
if self.__itunes_subtitle:
subtitle = xml_elem('{%s}subtitle' % ITUNES_NS, channel)
subtitle.text = self.__itunes_subtitle
if self.__itunes_summary:
summary = xml_elem('{%s}summary' % ITUNES_NS, channel)
summary.text = self.__itunes_summary
if self.__itunes_type in ('episodic', 'serial'):
type = xml_elem('{%s}type' % ITUNES_NS, channel)
type.text = self.__itunes_type
return rss_feed
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 <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 itunes_author is not 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 itunes_block is not None:
self.__itunes_block = itunes_block
return self.__itunes_block
def itunes_category(self, itunes_category=None, replace=False, **kwargs):
'''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
This method can be called with:
- the fields of an itunes_category as keyword arguments
- the fields of an itunes_category as a dictionary
- a list of dictionaries containing the itunes_category fields
An itunes_category has the following fields:
- *cat* name for a category.
- *sub* name for a subcategory, child of category
If a podcast has more than one subcategory from the same category, the
category is called more than once.
Likei the parameter::
[{"cat":"Arts","sub":"Design"},{"cat":"Arts","sub":"Food"}]
would become::
<itunes:category text="Arts">
<itunes:category text="Design"/>
<itunes:category text="Food"/>
</itunes:category>
:param itunes_category: Dictionary or list of dictionaries with
itunes_category data.
:param replace: Add or replace old data.
:returns: List of itunes_categories as dictionaries.
---
**Important note about deprecated parameter syntax:** Old version of
the feedgen did only support one category plus one subcategory which
would be passed to this ducntion as first two parameters. For
compatibility reasons, this still works but should not be used any may
be removed at any time.
'''
# Ensure old API still works for now. Note that the API is deprecated
# and this fallback may be removed at any time.
if isinstance(itunes_category, string_types):
itunes_category = {'cat': itunes_category}
if replace:
itunes_category['sub'] = replace
replace = True
if itunes_category is None and kwargs:
itunes_category = kwargs
if itunes_category is not None:
if replace or self.__itunes_category is None:
self.__itunes_category = []
self.__itunes_category += ensure_format(itunes_category,
set(['cat', 'sub']),
set(['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
<itunes:image> tag is not present, iTunes will use the contents of the
RSS image tag.
If you change your podcasts image, also change the files 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 itunes_image is not None:
if itunes_image.endswith('.jpg') or itunes_image.endswith('.png'):
self.__itunes_image = itunes_image
else:
ValueError('Image file must be png or jpg')
return self.__itunes_image
def itunes_explicit(self, itunes_explicit=None):
'''Get or the the itunes:explicit value of the podcast. This tag should
be used to indicate whether your podcast 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 contains explicit material.
:returns: If the podcast contains explicit material.
'''
if itunes_explicit is not None:
if itunes_explicit not in ('', 'yes', 'no', 'clean'):
raise ValueError('Invalid value for explicit tag')
self.__itunes_explicit = itunes_explicit
return self.__itunes_explicit
def itunes_complete(self, itunes_complete=None):
'''Get or set the itunes:complete value of the podcast. This tag can be
used to indicate the completion of a podcast.
If you populate this tag with "yes", you are indicating that no more
episodes will be added to the podcast. If the <itunes:complete> tag is
present and has any other value (e.g. no), it will have no effect on
the podcast.
:param itunes_complete: If the podcast is complete.
:returns: If the podcast is complete.
'''
if itunes_complete is not None:
if itunes_complete not in ('yes', 'no', '', True, False):
raise ValueError('Invalid value for complete tag')
if itunes_complete is True:
itunes_complete = 'yes'
if itunes_complete is False:
itunes_complete = 'no'
self.__itunes_complete = itunes_complete
return self.__itunes_complete
def itunes_new_feed_url(self, itunes_new_feed_url=None):
'''Get or set the new-feed-url property of the podcast. This tag allows
you to change the URL where the podcast feed is located
After adding the tag to your old feed, you should maintain the old feed
for 48 hours before retiring it. At that point, iTunes will have
updated the directory with the new feed URL.
:param itunes_new_feed_url: New feed URL.
:returns: New feed URL.
'''
if itunes_new_feed_url is not None:
self.__itunes_new_feed_url = itunes_new_feed_url
return self.__itunes_new_feed_url
def itunes_owner(self, name=None, email=None):
'''Get or set the itunes:owner of the podcast. This tag contains
information that will be used to contact the owner of the podcast for
communication specifically about the podcast. It will not be publicly
displayed.
:param itunes_owner: The owner of the feed.
:returns: Data of the owner of the feed.
'''
if name is not None:
if name and email:
self.__itunes_owner = {'name': name, 'email': email}
elif not name and not email:
self.__itunes_owner = None
else:
raise ValueError('Both name and email have to be set.')
return self.__itunes_owner
def itunes_subtitle(self, itunes_subtitle=None):
'''Get or set the itunes:subtitle value for the podcast. 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.
:returns: Subtitle of the podcast.
'''
if itunes_subtitle is not 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. 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.
:returns: Summary of the podcast.
'''
if itunes_summary is not None:
self.__itunes_summary = itunes_summary
return self.__itunes_summary
def itunes_type(self, itunes_type=None):
'''Get or set the itunes:type value of the podcast. This tag should
be used to indicate the type of your podcast.
The two values for this tag are "episodic" and "serial".
If your show is Serial you must use this tag.
Specify episodic when episodes are intended to be consumed without any
specific order. Apple Podcasts will present newest episodes first and
display the publish date (required) of each episode. If organized into
seasons, the newest season will be presented first - otherwise,
episodes will be grouped by year published, newest first.
Specify serial when episodes are intended to be consumed in sequential
order. Apple Podcasts will present the oldest episodes first and
display the episode numbers (required) of each episode. If organized
into seasons, the newest season will be presented first and
<itunes:episode> numbers must be given for each episode.
:param itunes_type: The type of the podcast
:returns: type of the pdocast.
'''
if itunes_type is not None:
if itunes_type not in ('episodic', 'serial'):
raise ValueError('Invalid value for type tag')
self.__itunes_type = itunes_type
return self.__itunes_type
_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': []}

View file

@ -0,0 +1,321 @@
# -*- coding: utf-8 -*-
'''
feedgen.ext.podcast_entry
~~~~~~~~~~~~~~~~~~~~~~~~~
Extends the feedgen to produce podcasts.
:copyright: 2013-2016, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details.
'''
from feedgen.ext.base import BaseEntryExtension
from feedgen.util import xml_elem
class PodcastEntryExtension(BaseEntryExtension):
'''FeedEntry extension for podcasts.
'''
def __init__(self):
# ITunes tags
# http://www.apple.com/itunes/podcasts/specs.html#rss
self.__itunes_author = None
self.__itunes_block = None
self.__itunes_image = None
self.__itunes_duration = None
self.__itunes_explicit = None
self.__itunes_is_closed_captioned = None
self.__itunes_order = None
self.__itunes_subtitle = None
self.__itunes_summary = None
self.__itunes_season = None
self.__itunes_episode = None
self.__itunes_title = None
self.__itunes_episode_type = None
def extend_rss(self, entry):
'''Add additional fields to an RSS item.
:param feed: The RSS item XML element to use.
'''
ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
if self.__itunes_author:
author = xml_elem('{%s}author' % ITUNES_NS, entry)
author.text = self.__itunes_author
if self.__itunes_block is not None:
block = xml_elem('{%s}block' % ITUNES_NS, entry)
block.text = 'yes' if self.__itunes_block else 'no'
if self.__itunes_image:
image = xml_elem('{%s}image' % ITUNES_NS, entry)
image.attrib['href'] = self.__itunes_image
if self.__itunes_duration:
duration = xml_elem('{%s}duration' % ITUNES_NS, entry)
duration.text = self.__itunes_duration
if self.__itunes_explicit in ('yes', 'no', 'clean'):
explicit = xml_elem('{%s}explicit' % ITUNES_NS, entry)
explicit.text = self.__itunes_explicit
if self.__itunes_is_closed_captioned is not None:
is_closed_captioned = xml_elem(
'{%s}isClosedCaptioned' % ITUNES_NS, entry)
if self.__itunes_is_closed_captioned:
is_closed_captioned.text = 'yes'
else:
is_closed_captioned.text = 'no'
if self.__itunes_order is not None and self.__itunes_order >= 0:
order = xml_elem('{%s}order' % ITUNES_NS, entry)
order.text = str(self.__itunes_order)
if self.__itunes_subtitle:
subtitle = xml_elem('{%s}subtitle' % ITUNES_NS, entry)
subtitle.text = self.__itunes_subtitle
if self.__itunes_summary:
summary = xml_elem('{%s}summary' % ITUNES_NS, entry)
summary.text = self.__itunes_summary
if self.__itunes_season:
season = xml_elem('{%s}season' % ITUNES_NS, entry)
season.text = str(self.__itunes_season)
if self.__itunes_episode:
episode = xml_elem('{%s}episode' % ITUNES_NS, entry)
episode.text = str(self.__itunes_episode)
if self.__itunes_title:
title = xml_elem('{%s}title' % ITUNES_NS, entry)
title.text = self.__itunes_title
if self.__itunes_episode_type in ('full', 'trailer', 'bonus'):
episode_type = xml_elem('{%s}episodeType' % ITUNES_NS, entry)
episode_type.text = self.__itunes_episode_type
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 itunes_author is not 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 itunes_block is not 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 podcasts image, also change the files 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 itunes_image is not None:
if itunes_image.endswith('.jpg') or itunes_image.endswith('.png'):
self.__itunes_image = itunes_image
else:
raise ValueError('Image file must be png or jpg')
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 itunes_duration is not None:
itunes_duration = str(itunes_duration)
if len(itunes_duration.split(':')) > 3 or \
itunes_duration.lstrip('0123456789:') != '':
raise 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 itunes_explicit is not None:
if itunes_explicit not 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 itunes_is_closed_captioned is not 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 itunes_order is not 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 itunes_subtitle is not 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 itunes_summary is not None:
self.__itunes_summary = itunes_summary
return self.__itunes_summary
def itunes_season(self, itunes_season=None):
'''Get or set the itunes:season value for the podcast episode.
:param itunes_season: Season number of the podcast epiosode.
:returns: Season number of the podcast episode.
'''
if itunes_season is not None:
self.__itunes_season = int(itunes_season)
return self.__itunes_season
def itunes_episode(self, itunes_episode=None):
'''Get or set the itunes:episode value for the podcast episode.
:param itunes_season: Episode number of the podcast epiosode.
:returns: Episode number of the podcast episode.
'''
if itunes_episode is not None:
self.__itunes_episode = int(itunes_episode)
return self.__itunes_episode
def itunes_title(self, itunes_title=None):
'''Get or set the itunes:title value for the podcast episode.
An episode title specific for Apple Podcasts. Dont specify the episode
number or season number in this tag. Also, dont repeat the title of
your show within your episode title.
:param itunes_title: Episode title specific for Apple Podcasts
:returns: Title specific for Apple Podcast
'''
if itunes_title is not None:
self.__itunes_title = itunes_title
return self.__itunes_title
def itunes_episode_type(self, itunes_episode_type=None):
'''Get or set the itunes:episodeType value of the item. This tag should
be used to indicate the episode type.
The three values for this tag are "full", "trailer" and "bonus".
If an episode is a trailer or bonus content, use this tag.
Specify full when you are submitting the complete content of your show.
Specify trailer when you are submitting a short, promotional piece of
content that represents a preview of your current show.
Specify bonus when you are submitting extra content for your show (for
example, behind the scenes information or interviews with the cast) or
cross-promotional content for another show.
:param itunes_episode_type: The episode type
:returns: type of the episode.
'''
if itunes_episode_type is not None:
if itunes_episode_type not in ('full', 'trailer', 'bonus'):
raise ValueError('Invalid value for episodeType tag')
self.__itunes_episode_type = itunes_episode_type
return self.__itunes_episode_type

View file

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 Kenichi Sato <ksato9700@gmail.com>
#
'''
Extends FeedGenerator to support Syndication module
See below for details
http://web.resource.org/rss/1.0/modules/syndication/
'''
from feedgen.ext.base import BaseExtension
from feedgen.util import xml_elem
SYNDICATION_NS = 'http://purl.org/rss/1.0/modules/syndication/'
PERIOD_TYPE = ('hourly', 'daily', 'weekly', 'monthly', 'yearly')
def _set_value(channel, name, value):
if value:
newelem = xml_elem('{%s}' % SYNDICATION_NS + name, channel)
newelem.text = value
class SyndicationExtension(BaseExtension):
def __init__(self):
self._update_period = None
self._update_freq = None
self._update_base = None
def extend_ns(self):
return {'sy': SYNDICATION_NS}
def extend_rss(self, rss_feed):
channel = rss_feed[0]
_set_value(channel, 'UpdatePeriod', self._update_period)
_set_value(channel, 'UpdateFrequency', str(self._update_freq))
_set_value(channel, 'UpdateBase', self._update_base)
def update_period(self, value):
if value not in PERIOD_TYPE:
raise ValueError('Invalid update period value')
self._update_period = value
return self._update_period
def update_frequency(self, value):
if type(value) is not int or value <= 0:
raise ValueError('Invalid update frequency value')
self._update_freq = value
return self._update_freq
def update_base(self, value):
# the value should be in W3CDTF format
self._update_base = value
return self._update_base
class SyndicationEntryExtension(BaseExtension):
pass

126
feedgen/ext/torrent.py Normal file
View file

@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
'''
feedgen.ext.torrent
~~~~~~~~~~~~~~~~~~~
Extends the FeedGenerator to produce torrent feeds.
:copyright: 2016, Raspbeguy <raspbeguy@hashtagueule.fr>
:license: FreeBSD and LGPL, see license.* for more details.
'''
from feedgen.ext.base import BaseEntryExtension, BaseExtension
from feedgen.util import xml_elem
TORRENT_NS = 'http://xmlns.ezrss.it/0.1/dtd/'
class TorrentExtension(BaseExtension):
'''FeedGenerator extension for torrent feeds.
'''
def extend_ns(self):
return {'torrent': TORRENT_NS}
class TorrentEntryExtension(BaseEntryExtension):
'''FeedEntry extension for torrent feeds
'''
def __init__(self):
self.__torrent_filename = None
self.__torrent_infohash = None
self.__torrent_contentlength = None
self.__torrent_seeds = None
self.__torrent_peers = None
self.__torrent_verified = None
def extend_rss(self, entry):
'''Add additional fields to an RSS item.
:param feed: The RSS item XML element to use.
'''
if self.__torrent_filename:
filename = xml_elem('{%s}filename' % TORRENT_NS, entry)
filename.text = self.__torrent_filename
if self.__torrent_contentlength:
contentlength = xml_elem('{%s}contentlength' % TORRENT_NS, entry)
contentlength.text = self.__torrent_contentlength
if self.__torrent_infohash:
infohash = xml_elem('{%s}infohash' % TORRENT_NS, entry)
infohash.text = self.__torrent_infohash
magnet = xml_elem('{%s}magneturi' % TORRENT_NS, entry)
magnet.text = 'magnet:?xt=urn:btih:' + self.__torrent_infohash
if self.__torrent_seeds:
seeds = xml_elem('{%s}seed' % TORRENT_NS, entry)
seeds.text = self.__torrent_seeds
if self.__torrent_peers:
peers = xml_elem('{%s}peers' % TORRENT_NS, entry)
peers.text = self.__torrent_peers
if self.__torrent_verified:
verified = xml_elem('{%s}verified' % TORRENT_NS, entry)
verified.text = self.__torrent_verified
def filename(self, torrent_filename=None):
'''Get or set the name of the torrent file.
:param torrent_filename: The name of the torrent file.
:returns: The name of the torrent file.
'''
if torrent_filename is not None:
self.__torrent_filename = torrent_filename
return self.__torrent_filename
def infohash(self, torrent_infohash=None):
'''Get or set the hash of the target file.
:param torrent_infohash: The target file hash.
:returns: The target hash file.
'''
if torrent_infohash is not None:
self.__torrent_infohash = torrent_infohash
return self.__torrent_infohash
def contentlength(self, torrent_contentlength=None):
'''Get or set the size of the target file.
:param torrent_contentlength: The target file size.
:returns: The target file size.
'''
if torrent_contentlength is not None:
self.__torrent_contentlength = torrent_contentlength
return self.__torrent_contentlength
def seeds(self, torrent_seeds=None):
'''Get or set the number of seeds.
:param torrent_seeds: The seeds number.
:returns: The seeds number.
'''
if torrent_seeds is not None:
self.__torrent_seeds = torrent_seeds
return self.__torrent_seeds
def peers(self, torrent_peers=None):
'''Get or set the number od peers
:param torrent_infohash: The peers number.
:returns: The peers number.
'''
if torrent_peers is not None:
self.__torrent_peers = torrent_peers
return self.__torrent_peers
def verified(self, torrent_verified=None):
'''Get or set the number of verified peers.
:param torrent_infohash: The verified peers number.
:returns: The verified peers number.
'''
if torrent_verified is not None:
self.__torrent_verified = torrent_verified
return self.__torrent_verified