Skip to content

Commit

Permalink
Fix case-sensitive opml feed parser
Browse files Browse the repository at this point in the history
The OPML spec seems to be ambiguous on the subject of case-sensitivity.
As a result, OPML feeds that are exported from podcatchers may have
uppercase attributes (i.e., `URL` vs `url`). This patch will find
attributes of `xml.etree.ElementTree.Element` objects in a
case-insensitive way which side-steps the problem.
  • Loading branch information
thcipriani committed Jan 6, 2018
1 parent f126d6e commit d8b5e02
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 12 deletions.
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/'

0 comments on commit d8b5e02

Please sign in to comment.