Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix case-sensitive OPML feed parser #46

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/conf.py
Expand Up @@ -12,6 +12,7 @@ def get_version(filename):
metadata = dict(findall(r"__([a-z]+)__ = '([^']+)'", f.read()))
return metadata['version']


project = 'Mopidy-Podcast'
copyright = '2014-2016 Thomas Kemmer'
version = get_version(b'../mopidy_podcast/__init__.py')
Expand Down
43 changes: 31 additions & 12 deletions mopidy_podcast/feeds.py
Expand Up @@ -30,6 +30,24 @@ def parse(source):
raise TypeError('Not a recognized podcast feed: %s', url)


def get_attrib(element, attrib, default=None):
"""
Case-insensitive get for xml.etree.ElementTree.Element objects.

The OPML spec is ambiguous on the subject of case-sensitivity so we can't
assume keys will be lowercase.
"""
return element.get(
attrib,
element.get(
attrib.upper(),
element.get(
attrib.lower(), default
)
)
)


class PodcastFeed(object):

def __init__(self, url):
Expand Down Expand Up @@ -76,7 +94,7 @@ def __init__(self, url, root):
def getstreamuri(self, guid):
for item in self.__items:
if self.__guid(item) == guid:
return item.find('enclosure').get('url')
return get_attrib(item.find('enclosure'), 'url')
return None

def items(self, newest_first=False):
Expand Down Expand Up @@ -148,19 +166,20 @@ def __date(cls, etree):
def __genre(cls, etree):
elem = etree.find(cls.ITUNES_PREFIX + 'category')
if elem is not None:
return elem.get('text')
return get_attrib(elem, 'text')
else:
return None

@classmethod
def __guid(cls, etree):
return etree.findtext('guid') or etree.find('enclosure').get('url')
return (etree.findtext('guid') or
get_attrib(etree.find('enclosure'), 'url'))

@classmethod
def __image(cls, etree):
elem = etree.find(cls.ITUNES_PREFIX + 'image')
if elem is not None:
return models.Image(uri=elem.get('href'))
return models.Image(uri=get_attrib(elem, 'href'))
else:
return None

Expand Down Expand Up @@ -194,21 +213,21 @@ class OpmlFeed(PodcastFeed): # not really a "feed"

TYPES = {
'include': lambda e: models.Ref.directory(
name=e.get('text'),
uri=PodcastFeed.getfeeduri(e.get('url'))
name=get_attrib(e, 'text'),
uri=PodcastFeed.getfeeduri(get_attrib(e, 'url'))
),
'link': lambda e: models.Ref(
type=(
models.Ref.DIRECTORY
if e.get('url').endswith('.opml')
if get_attrib(e, 'url').endswith('.opml')
else models.Ref.ALBUM
),
name=e.get('text'),
uri=PodcastFeed.getfeeduri(e.get('url'))
name=get_attrib(e, 'text'),
uri=PodcastFeed.getfeeduri(get_attrib(e, 'url'))
),
'rss': lambda e: models.Ref.album(
name=e.get('title', e.get('text')),
uri=PodcastFeed.getfeeduri(e.get('xmlUrl'))
name=get_attrib(e, 'title', default=get_attrib(e, 'text')),
uri=PodcastFeed.getfeeduri(get_attrib(e, 'xmlUrl'))
)
}

Expand All @@ -219,7 +238,7 @@ def __init__(self, url, root):
def items(self, newest_first=None):
for e in self.__outlines:
try:
ref = self.TYPES[e.get('type').lower()]
ref = self.TYPES[get_attrib(e, 'type').lower()]
except KeyError:
pass
else:
Expand Down
21 changes: 21 additions & 0 deletions tests/test_feeds.py
Expand Up @@ -6,6 +6,11 @@

from mopidy_podcast import feeds

try:
import xml.etree.cElementTree as ElementTree
except ImportError:
import xml.etree.ElementTree as ElementTree


@pytest.mark.parametrize('filename,expected', [
('directory.xml', feeds.OpmlFeed),
Expand All @@ -17,3 +22,19 @@ def test_parse(abspath, filename, expected):
feed = feeds.parse(path)
assert isinstance(feed, expected)
assert feed.uri == uritools.uricompose('podcast+file', '', path)


def test_case_sensitive_parse():
xml = r'''<?xml version="1.0" encoding="utf-8" ?>
<opml version="1.1">
<head title="Podcasts">
<expansionState></expansionState>
</head>
<body>
<outline URL="http://example.com/" text="example" type="link" />
</body>
</opml>
'''
root = ElementTree.fromstring(xml)
feed = feeds.OpmlFeed('foo', root)
assert feed.items().next().uri == 'podcast+http://example.com/'