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""" + + + + Sample Title + file:///tmp/foo + + + Example title + file:///tmp/bar + + + Other title + file:///tmp/baz + + + +""" + + +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)