Enable Multiple media:content/media:group Elements
One item can have multiple media:content elements which may be located in multiple media:group element to indicate that content is the same but for the format. This patch adds the ability to add multiple content elements and define the group to which they go belong. If no group is specified, all elements are located in a `default` group. If the group is set to None, the content element is directly attached to the item element. Part of #58
This commit is contained in:
parent
a7ae36cb4f
commit
4970dab6d7
3 changed files with 84 additions and 90 deletions
|
@ -1,7 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
'''
|
'''
|
||||||
feedgen.ext.media
|
feedgen.ext.media
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Extends the feedgen to produce media tags.
|
Extends the feedgen to produce media tags.
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from feedgen.util import ensure_format
|
||||||
from feedgen.ext.base import BaseExtension, BaseEntryExtension
|
from feedgen.ext.base import BaseExtension, BaseEntryExtension
|
||||||
|
|
||||||
MEDIA_NS = 'http://search.yahoo.com/mrss/'
|
MEDIA_NS = 'http://search.yahoo.com/mrss/'
|
||||||
|
@ -38,39 +39,21 @@ class MediaEntryExtension(BaseEntryExtension):
|
||||||
:param feed: The RSS item XML element to use.
|
:param feed: The RSS item XML element to use.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
group = etree.SubElement(entry, '{%s}group' % MEDIA_NS)
|
groups = {None: entry}
|
||||||
if self.__media_content:
|
for media_content in self.__media_content:
|
||||||
|
# Define current media:group
|
||||||
|
group = groups.get(media_content.get('group'))
|
||||||
|
if group is None:
|
||||||
|
group = etree.SubElement(entry, '{%s}group' % MEDIA_NS)
|
||||||
|
groups[media_content.get('group')] = group
|
||||||
|
# Add content
|
||||||
content = etree.SubElement(group, '{%s}content' % MEDIA_NS)
|
content = etree.SubElement(group, '{%s}content' % MEDIA_NS)
|
||||||
if self.__media_content.get('url'):
|
for attr in ('url', 'fileSize', 'type', 'medium', 'isDefault',
|
||||||
content.set('url', self.__media_content.get('url'))
|
'expression', 'bitrate', 'framerate', 'samplingrate',
|
||||||
if self.__media_content.get('fileSize'):
|
'channels', 'duration', 'height', 'width', 'lang'):
|
||||||
content.set('fileSize', self.__media_content.get('fileSize'))
|
if media_content.get(attr):
|
||||||
if self.__media_content.get('type'):
|
content.set(attr, media_content[attr])
|
||||||
content.set('type', self.__media_content.get('type'))
|
|
||||||
if self.__media_content.get('medium'):
|
|
||||||
content.set('medium', self.__media_content.get('medium'))
|
|
||||||
if self.__media_content.get('isDefault'):
|
|
||||||
content.set('isDefault', self.__media_content.get('isDefault'))
|
|
||||||
if self.__media_content.get('expression'):
|
|
||||||
content.set(
|
|
||||||
'expression', self.__media_content.get('expression'))
|
|
||||||
if self.__media_content.get('bitrate'):
|
|
||||||
content.set('bitrate', self.__media_content.get('bitrate'))
|
|
||||||
if self.__media_content.get('framerate'):
|
|
||||||
content.set('framerate', self.__media_content.get('framerate'))
|
|
||||||
if self.__media_content.get('samplingrate'):
|
|
||||||
content.set('samplingrate',
|
|
||||||
self.__media_content.get('samplingrate'))
|
|
||||||
if self.__media_content.get('channels'):
|
|
||||||
content.set('channels', self.__media_content.get('channels'))
|
|
||||||
if self.__media_content.get('duration'):
|
|
||||||
content.set('duration', self.__media_content.get('duration'))
|
|
||||||
if self.__media_content.get('height'):
|
|
||||||
content.set('height', self.__media_content.get('height'))
|
|
||||||
if self.__media_content.get('width'):
|
|
||||||
content.set('width', self.__media_content.get('width'))
|
|
||||||
if self.__media_content.get('lang'):
|
|
||||||
content.set('lang', self.__media_content.get('lang'))
|
|
||||||
if self.__media_thumbnail:
|
if self.__media_thumbnail:
|
||||||
thumbnail = etree.SubElement(group, '{%s}thumbnail' % MEDIA_NS)
|
thumbnail = etree.SubElement(group, '{%s}thumbnail' % MEDIA_NS)
|
||||||
if self.__media_thumbnail.get('url'):
|
if self.__media_thumbnail.get('url'):
|
||||||
|
@ -87,69 +70,68 @@ class MediaEntryExtension(BaseEntryExtension):
|
||||||
def extend_rss(self, item):
|
def extend_rss(self, item):
|
||||||
return self.extend_atom(item)
|
return self.extend_atom(item)
|
||||||
|
|
||||||
def content(self, url=None, fileSize=None, type=None, medium=None,
|
def content(self, content=None, replace=False, group='default', **kwargs):
|
||||||
isDefault=None, expression=None, bitrate=None, framerate=None,
|
'''Get or set media:content data.
|
||||||
samplingrate=None, channels=None, duration=None, height=None,
|
|
||||||
width=None, lang=None):
|
This method can be called with:
|
||||||
'''<media:content> is a sub-element of either <item> or <media:group>.
|
- 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
|
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 same <media:group> element. The sequence of these items implies
|
||||||
the order of presentation. While many of the attributes appear to be
|
the order of presentation. While many of the attributes appear to be
|
||||||
audio/video specific, this element can be used to publish any type
|
audio/video specific, this element can be used to publish any type
|
||||||
of media. It contains 14 attributes, most of which are optional.
|
of media. It contains 14 attributes, most of which are optional.
|
||||||
|
|
||||||
:param url: should specify the direct URL to the media object.
|
media:content has the following fields:
|
||||||
:param fileSize: number of bytes of the media object.
|
- *url* should specify the direct URL to the media object.
|
||||||
:param type: standard MIME type of the object.
|
- *fileSize* number of bytes of the media object.
|
||||||
:param medium: type of object
|
- *type* standard MIME type of the object.
|
||||||
(image | audio | video | document | executable).
|
- *medium* type of object (image | audio | video | document |
|
||||||
:param isDefault: determines if this is the default object.
|
executable).
|
||||||
:param expression: determines if the object is a sample or the full
|
- *isDefault* determines if this is the default object.
|
||||||
version of the object, or even if it is a
|
- *expression* determines if the object is a sample or the full version
|
||||||
continuous stream (sample | full | nonstop).
|
of the object, or even if it is a continuous stream (sample | full |
|
||||||
:param bitrate: kilobits per second rate of media.
|
nonstop).
|
||||||
:param framerate: number of frames per second for the media object.
|
- *bitrate* kilobits per second rate of media.
|
||||||
:param samplingrate: number of samples per second taken to create the
|
- *framerate* number of frames per second for the media object.
|
||||||
media object. It is expressed in thousands of
|
- *samplingrate* number of samples per second taken to create the media
|
||||||
samples per second (kHz).
|
object. It is expressed in thousands of samples per second (kHz).
|
||||||
:param channels: number of audio channels in the media object.
|
- *channels* number of audio channels in the media object.
|
||||||
:param duration: number of seconds the media object plays.
|
- *duration* number of seconds the media object plays.
|
||||||
:param height: height of the media object.
|
- *height* height of the media object.
|
||||||
:param width: width of the media object.
|
- *width* width of the media object.
|
||||||
:param lang: is the primary language encapsulated in 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.
|
:returns: The media content tag.
|
||||||
'''
|
'''
|
||||||
|
# Handle kwargs
|
||||||
if url is not None:
|
if content is None and kwargs:
|
||||||
self.__media_content = {'url': url}
|
content = kwargs
|
||||||
if fileSize is not None:
|
# Handle new data
|
||||||
self.__media_content['fileSize'] = fileSize
|
if content is not None:
|
||||||
if type is not None:
|
# Reset data if we want to replace them
|
||||||
self.__media_content['type'] = type
|
if replace or self.__media_content is None:
|
||||||
if medium is not None:
|
self.__media_content = []
|
||||||
self.__media_content['medium'] = medium
|
# Ensure list
|
||||||
if isDefault is not None:
|
if not isinstance(content, list):
|
||||||
self.__media_content['isDefault'] = isDefault
|
content = [content]
|
||||||
if expression is not None:
|
# define media group
|
||||||
self.__media_content['expression'] = expression
|
for c in content:
|
||||||
if bitrate is not None:
|
c['group'] = c.get('group', group)
|
||||||
self.__media_content['bitrate'] = bitrate
|
self.__media_content += ensure_format(
|
||||||
if framerate is not None:
|
content,
|
||||||
self.__media_content['framerate'] = framerate
|
set(['url', 'fileSize', 'type', 'medium', 'isDefault',
|
||||||
if samplingrate is not None:
|
'expression', 'bitrate', 'framerate', 'samplingrate',
|
||||||
self.__media_content['samplingrate'] = samplingrate
|
'channels', 'duration', 'height', 'width', 'lang',
|
||||||
if channels is not None:
|
'group']),
|
||||||
self.__media_content['channels'] = channels
|
set(['url', 'group']))
|
||||||
if duration is not None:
|
|
||||||
self.__media_content['duration'] = duration
|
|
||||||
if height is not None:
|
|
||||||
self.__media_content['height'] = height
|
|
||||||
if width is not None:
|
|
||||||
self.__media_content['width'] = width
|
|
||||||
if lang is not None:
|
|
||||||
self.__media_content['lang'] = lang
|
|
||||||
|
|
||||||
return self.__media_content
|
return self.__media_content
|
||||||
|
|
||||||
def thumbnail(self, url=None, height=None, width=None, time=None):
|
def thumbnail(self, url=None, height=None, width=None, time=None):
|
||||||
|
|
|
@ -26,7 +26,7 @@ def ensure_format(val, allowed, required, allowed_values=None, defaults=None):
|
||||||
:returns: List of checked dictionaries.
|
:returns: List of checked dictionaries.
|
||||||
'''
|
'''
|
||||||
if not val:
|
if not val:
|
||||||
return None
|
return []
|
||||||
if allowed_values is None:
|
if allowed_values is None:
|
||||||
allowed_values = {}
|
allowed_values = {}
|
||||||
if defaults is None:
|
if defaults is None:
|
||||||
|
|
|
@ -212,6 +212,10 @@ class TestExtensionMedia(unittest.TestCase):
|
||||||
fe.title('title')
|
fe.title('title')
|
||||||
fe.content('content')
|
fe.content('content')
|
||||||
fe.media.content(url='file1.xy')
|
fe.media.content(url='file1.xy')
|
||||||
|
fe.media.content(url='file2.xy')
|
||||||
|
fe.media.content(url='file1.xy', group=2)
|
||||||
|
fe.media.content(url='file2.xy', group=2)
|
||||||
|
fe.media.content(url='file.xy', group=None)
|
||||||
|
|
||||||
ns = {'media': 'http://search.yahoo.com/mrss/',
|
ns = {'media': 'http://search.yahoo.com/mrss/',
|
||||||
'a': 'http://www.w3.org/2005/Atom'}
|
'a': 'http://www.w3.org/2005/Atom'}
|
||||||
|
@ -219,10 +223,18 @@ class TestExtensionMedia(unittest.TestCase):
|
||||||
root = etree.fromstring(self.fg.rss_str())
|
root = etree.fromstring(self.fg.rss_str())
|
||||||
url = root.xpath('/rss/channel/item/media:group/media:content[1]/@url',
|
url = root.xpath('/rss/channel/item/media:group/media:content[1]/@url',
|
||||||
namespaces=ns)
|
namespaces=ns)
|
||||||
assert url == ['file1.xy']
|
assert url == ['file1.xy', 'file1.xy']
|
||||||
|
|
||||||
|
# There is one without a group
|
||||||
|
url = root.xpath('/rss/channel/item/media:content[1]/@url',
|
||||||
|
namespaces=ns)
|
||||||
|
assert url == ['file.xy']
|
||||||
|
|
||||||
# Check that we have the item in the resulting Atom feed
|
# Check that we have the item in the resulting Atom feed
|
||||||
root = etree.fromstring(self.fg.atom_str())
|
root = etree.fromstring(self.fg.atom_str())
|
||||||
url = root.xpath('/a:feed/a:entry/media:group/media:content[1]/@url',
|
url = root.xpath('/a:feed/a:entry/media:group/media:content[1]/@url',
|
||||||
namespaces=ns)
|
namespaces=ns)
|
||||||
assert url == ['file1.xy']
|
assert url == ['file1.xy', 'file1.xy']
|
||||||
|
|
||||||
|
fe.media.content(content=[], replace=True)
|
||||||
|
assert fe.media.content() == []
|
||||||
|
|
Loading…
Reference in a new issue