BPD now reports empty path components as '(unknown)'

--HG--
extra : convert_revision : svn%3A41726ec3-264d-0410-9c23-a9f1637257cc/trunk%40201
This commit is contained in:
adrian.sampson 2009-04-12 01:48:37 +00:00
parent 0b309edd27
commit 4b7119855e
3 changed files with 76 additions and 35 deletions

View file

@ -696,7 +696,7 @@ class Library(object):
artist name or an arbitrary query. artist name or an arbitrary query.
""" """
query = self._get_query(query) query = self._get_query(query)
if artist: if artist is not None:
# "Add" the artist to the query. # "Add" the artist to the query.
query = AndQuery((query, MatchQuery('artist', artist))) query = AndQuery((query, MatchQuery('artist', artist)))
where, subvals = query.clause() where, subvals = query.clause()
@ -712,11 +712,11 @@ class Library(object):
albums appropriately. albums appropriately.
""" """
queries = [self._get_query(query)] queries = [self._get_query(query)]
if artist: if artist is not None:
queries.append(MatchQuery('artist', artist)) queries.append(MatchQuery('artist', artist))
if album: if album is not None:
queries.append(MatchQuery('album', album)) queries.append(MatchQuery('album', album))
if title: if title is not None:
queries.append(MatchQuery('title', title)) queries.append(MatchQuery('title', title))
super_query = AndQuery(queries) super_query = AndQuery(queries)
where, subvals = super_query.clause() where, subvals = super_query.clause()

View file

@ -134,22 +134,30 @@ class BPDClose(Exception):
# directory structure required by the MPD protocol to browse music in # directory structure required by the MPD protocol to browse music in
# the library. # the library.
def seq_to_path(seq): def seq_to_path(seq, placeholder=''):
"""Encodes a sequence of strings as a path-like string. The """Encodes a sequence of strings as a path-like string. The
sequence can be recovered exactly using path_to_list. sequence can be recovered exactly using path_to_list. If
`placeholder` is provided, it is used in place of empty path
components.
""" """
out = [] out = []
for s in seq: for s in seq:
out.append(s.replace('\\', '\\\\') # preserve backslashes if placeholder and s == '':
.replace('_', '\\_') # preserve _s out.append(placeholder)
.replace('/', '_') # hide /s as _s else:
) out.append(s.replace('\\', '\\\\') # preserve backslashes
.replace('_', '\\_') # preserve _s
.replace('/', '_') # hide /s as _s
)
return '/'.join(out) return '/'.join(out)
def path_to_list(path): def path_to_list(path, placeholder=''):
"""Takes a path-like string (probably encoded by seq_to_path) and """Takes a path-like string (probably encoded by seq_to_path) and
returns the list of strings it represents. returns the list of strings it represents. If `placeholder` is
provided, it is interpreted to represent an empty path component.
Also, when given a `placeholder`, this function ignores empty
path components.
""" """
def repl(m): def repl(m):
# This function maps "escaped" characters to original # This function maps "escaped" characters to original
@ -159,8 +167,24 @@ def path_to_list(path):
'\\_': '_', '\\_': '_',
'_': '/', '_': '/',
}[m.group(0)] }[m.group(0)]
return [re.sub(r'\\\\|\\_|_', repl, component) components = [re.sub(r'\\\\|\\_|_', repl, component)
for component in path.split('/')] for component in path.split('/')]
if placeholder:
new_components = []
for c in components:
if c == '':
# Drop empty path components.
continue
if c == placeholder:
new_components.append('')
else:
new_components.append(c)
components = new_components
return components
PATH_PH = '(unknown)'
# Generic server infrastructure, implementing the basic protocol. # Generic server infrastructure, implementing the basic protocol.
@ -730,7 +754,7 @@ class Server(BaseServer):
def _item_path(self, item): def _item_path(self, item):
"""Returns the item's "virtual path.""" """Returns the item's "virtual path."""
return seq_to_path((item.artist, item.album, item.title)) return seq_to_path((item.artist, item.album, item.title), PATH_PH)
def _item_info(self, item): def _item_info(self, item):
info_lines = ['file: ' + self._item_path(item), info_lines = ['file: ' + self._item_path(item),
@ -770,25 +794,27 @@ class Server(BaseServer):
""" """
if len(path) >= 1 and path[0] == '/': # Remove leading slash. if len(path) >= 1 and path[0] == '/': # Remove leading slash.
path = path[1:] path = path[1:]
items = path_to_list(path) items = path_to_list(path, PATH_PH)
artist, album, track = None, None, None dirs = [None, None, None]
if items: artist = items.pop(0) for i in range(len(dirs)):
if items: album = items.pop(0) if items:
if items: track = items.pop(0) # Take a directory if it exists. Otherwise, leave as "none".
return artist, album, track # This way, we ensure that we always return 3 elements.
dirs[i] = items.pop(0)
return dirs
def cmd_lsinfo(self, conn, path="/"): def cmd_lsinfo(self, conn, path="/"):
"""Sends info on all the items in the path.""" """Sends info on all the items in the path."""
artist, album, track = self._parse_path(path) artist, album, track = self._parse_path(path)
if not artist: # List all artists. if artist is None: # List all artists.
for artist in self.lib.artists(): for artist in self.lib.artists():
conn.send('directory: ' + seq_to_path((artist,))) conn.send('directory: ' + seq_to_path((artist,), PATH_PH))
elif not album: # List all albums for an artist. elif album is None: # List all albums for an artist.
for album in self.lib.albums(artist): for album in self.lib.albums(artist):
conn.send('directory: ' + seq_to_path(album)) conn.send('directory: ' + seq_to_path(album, PATH_PH))
elif not track: # List all tracks on an album. elif track is None: # List all tracks on an album.
for item in self.lib.items(artist, album): for item in self.lib.items(artist, album):
conn.send(*self._item_info(item)) conn.send(*self._item_info(item))
else: # List a track. This isn't a directory. else: # List a track. This isn't a directory.
@ -808,7 +834,7 @@ class Server(BaseServer):
# albums # albums
if not album: if not album:
for a in self.lib.albums(artist or None): for a in self.lib.albums(artist or None):
conn.send('directory: ' + seq_to_path(a)) conn.send('directory: ' + seq_to_path(a, PATH_PH))
# tracks # tracks
items = self.lib.items(artist or None, album or None) items = self.lib.items(artist or None, album or None)
@ -831,7 +857,7 @@ class Server(BaseServer):
def _get_by_path(self, path): def _get_by_path(self, path):
"""Helper function returning the item at a given path.""" """Helper function returning the item at a given path."""
artist, album, track = path_to_list(path) artist, album, track = path_to_list(path, PATH_PH)
it = self.lib.items(artist, album, track) it = self.lib.items(artist, album, track)
try: try:
return it.next() return it.next()

View file

@ -23,13 +23,6 @@ sys.path.append('..')
from beets.player import bpd from beets.player import bpd
class FauxPathTest(unittest.TestCase): class FauxPathTest(unittest.TestCase):
# The current encoding actually cannot distinguish between ['']
# and []. This doesn't cause a bug because we never use empty
# sequences, but it might be nice to fix someday.
#def test_empty_seq_preserved(self):
# seq = []
# self.assertEqual(bpd.path_to_list(bpd.seq_to_path(seq)), seq)
def test_single_element_preserved(self): def test_single_element_preserved(self):
seq = ['hello'] seq = ['hello']
@ -72,7 +65,29 @@ class FauxPathTest(unittest.TestCase):
with_slashes = bpd.seq_to_path(['good/day', 'sir']) with_slashes = bpd.seq_to_path(['good/day', 'sir'])
self.assertEqual(no_slashes.count('/'), with_slashes.count('/')) self.assertEqual(no_slashes.count('/'), with_slashes.count('/'))
def test_empty_seq_preserved_with_placeholder(self):
seq = []
self.assertEqual(bpd.path_to_list(bpd.seq_to_path(seq, 'PH'), 'PH'),
seq)
def test_empty_strings_preserved_with_placeholder(self):
seq = ['hello', '', 'sup']
self.assertEqual(bpd.path_to_list(bpd.seq_to_path(seq, 'PH'), 'PH'),
seq)
def test_empty_strings_only_preserved_with_placeholder(self):
seq = ['', '', '']
self.assertEqual(bpd.path_to_list(bpd.seq_to_path(seq, 'PH'), 'PH'),
seq)
def test_placeholder_does_replace(self):
seq = ['hello', '', 'sup']
self.assertFalse('//' in bpd.seq_to_path(seq, 'PH'))
# Note that the path encodes doesn't currently try to distinguish
# between the placeholder and strings identical to the placeholder.
# This might be a nice feature but is not currently essential.
def suite(): def suite():
return unittest.TestLoader().loadTestsFromName(__name__) return unittest.TestLoader().loadTestsFromName(__name__)