diff --git a/mopidy/audio/playlists.py b/mopidy/audio/playlists.py
index 6f4fdd46..096348a0 100644
--- a/mopidy/audio/playlists.py
+++ b/mopidy/audio/playlists.py
@@ -21,7 +21,7 @@ def detect_m3u_header(typefind):
def detect_pls_header(typefind):
- return typefind.peek(0, 11) == b'[playlist]\n'
+ return typefind.peek(0, 11).lower() == b'[playlist]\n'
def detect_xspf_header(typefind):
@@ -30,7 +30,8 @@ def detect_xspf_header(typefind):
return False
try:
- for event, element in elementtree.iterparse(data, events=('start',)):
+ data = io.BytesIO(data)
+ for event, element in elementtree.iterparse(data, events=(b'start',)):
return element.tag.lower() == '{http://xspf.org/ns/0/}playlist'
except elementtree.ParseError:
pass
@@ -43,7 +44,8 @@ def detect_asx_header(typefind):
return False
try:
- for event, element in elementtree.iterparse(data, events=('start',)):
+ data = io.BytesIO(data)
+ for event, element in elementtree.iterparse(data, events=(b'start',)):
return element.tag.lower() == 'asx'
except elementtree.ParseError:
pass
@@ -52,24 +54,38 @@ def detect_asx_header(typefind):
def parse_m3u(data):
# TODO: convert non URIs to file URIs.
+ found_header = False
for line in data.readlines():
+ if found_header or line.startswith('#EXTM3U'):
+ found_header = True
+ else:
+ continue
if not line.startswith('#') and line.strip():
- yield line
+ yield line.strip()
def parse_pls(data):
- # TODO: error handling of bad playlists.
# TODO: convert non URIs to file URIs.
- cp = configparser.RawConfigParser()
- cp.readfp(data)
- for i in xrange(1, cp.getint('playlist', 'numberofentries')):
- yield cp.get('playlist', 'file%d' % i)
+ try:
+ cp = configparser.RawConfigParser()
+ cp.readfp(data)
+ except configparser.Error:
+ return
+
+ for section in cp.sections():
+ if section.lower() != 'playlist':
+ continue
+ for i in xrange(cp.getint(section, 'numberofentries')):
+ yield cp.get(section, 'file%d' % (i+1))
def parse_xspf(data):
# TODO: handle parser errors
- for event, element in elementtree.iterparse(data):
- element.tag = element.tag.lower() # normalize
+ try:
+ for event, element in elementtree.iterparse(data):
+ element.tag = element.tag.lower() # normalize
+ except elementtree.ParseError:
+ return
ns = 'http://xspf.org/ns/0/'
for track in element.iterfind('{%s}tracklist/{%s}track' % (ns, ns)):
@@ -78,8 +94,11 @@ def parse_xspf(data):
def parse_asx(data):
# TODO: handle parser errors
- for event, element in elementtree.iterparse(data):
- element.tag = element.tag.lower() # normalize
+ try:
+ for event, element in elementtree.iterparse(data):
+ element.tag = element.tag.lower() # normalize
+ except elementtree.ParseError:
+ return
for ref in element.findall('entry/ref'):
yield ref.get('href', '').strip()
diff --git a/tests/audio/playlists_test.py b/tests/audio/playlists_test.py
new file mode 100644
index 00000000..9f28527e
--- /dev/null
+++ b/tests/audio/playlists_test.py
@@ -0,0 +1,127 @@
+#encoding: utf-8
+
+from __future__ import unicode_literals
+
+import io
+import unittest
+
+from mopidy.audio import playlists
+
+class TypeFind(object):
+ def __init__(self, data):
+ self.data = data
+
+ def peek(self, start, end):
+ return self.data[start:end]
+
+
+BAD = b'foobarbaz'
+
+M3U = b"""#EXTM3U
+#EXTINF:123, Sample artist - Sample title
+file:///tmp/foo
+#EXTINF:321,Example Artist - Example title
+file:///tmp/bar
+#EXTINF:213,Some Artist - Other title
+file:///tmp/baz
+"""
+
+PLS = b"""[Playlist]
+NumberOfEntries=3
+File1=file:///tmp/foo
+Title1=Sample Title
+Length1=123
+File2=file:///tmp/bar
+Title2=Example title
+Length2=321
+File3=file:///tmp/baz
+Title3=Other title
+Length3=213
+Version=2
+"""
+
+ASX = b"""
+ Example
+
+ Sample Title
+
+
+
+ Example title
+
+
+
+ Other title
+
+
+
+"""
+
+XSPF = b"""
+
+
+
+
+
+
+
+"""
+
+
+class BasePlaylistTest(object):
+ valid = None
+ invalid = None
+ detect = None
+ parse = None
+
+ def test_detect_valid_header(self):
+ self.assertTrue(self.detect(TypeFind(self.valid)))
+
+ def test_detect_invalid_header(self):
+ self.assertFalse(self.detect(TypeFind(self.invalid)))
+
+ def test_parse_valid_playlist(self):
+ uris = list(self.parse(io.BytesIO(self.valid)))
+ expected = [b'file:///tmp/foo', b'file:///tmp/bar', b'file:///tmp/baz']
+ self.assertEqual(uris, expected)
+
+ def test_parse_invalid_playlist(self):
+ uris = list(self.parse(io.BytesIO(self.invalid)))
+ self.assertEqual(uris, [])
+
+
+class M3uPlaylistTest(BasePlaylistTest, unittest.TestCase):
+ valid = M3U
+ invalid = BAD
+ detect = staticmethod(playlists.detect_m3u_header)
+ parse = staticmethod(playlists.parse_m3u)
+
+
+class PlsPlaylistTest(BasePlaylistTest, unittest.TestCase):
+ valid = PLS
+ invalid = BAD
+ detect = staticmethod(playlists.detect_pls_header)
+ parse = staticmethod(playlists.parse_pls)
+
+
+class AsxPlsPlaylistTest(BasePlaylistTest, unittest.TestCase):
+ valid = ASX
+ invalid = BAD
+ detect = staticmethod(playlists.detect_asx_header)
+ parse = staticmethod(playlists.parse_asx)
+
+
+class XspfPlaylistTest(BasePlaylistTest, unittest.TestCase):
+ valid = XSPF
+ invalid = BAD
+ detect = staticmethod(playlists.detect_xspf_header)
+ parse = staticmethod(playlists.parse_xspf)