Merge branch 'develop' of https://github.com/mopidy/mopidy into feature/extra_tags

This commit is contained in:
Lasse Bigum 2013-11-02 23:39:19 +01:00
commit e12a75c880
15 changed files with 205 additions and 59 deletions

View File

@ -4,7 +4,7 @@ install:
- "wget -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -"
- "sudo wget -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list"
- "sudo apt-get update || true"
- "sudo apt-get install $(apt-cache depends mopidy | awk '$2 !~ /mopidy/ {print $2}')"
- "sudo apt-get install $(apt-cache depends mopidy | awk '$2 !~ /mopidy|python:any/ {print $2}')"
- "pip install coveralls flake8"
before_script:

View File

@ -4,6 +4,27 @@ Changelog
This changelog is used to track all major changes to Mopidy.
v0.17.0 (UNRELEASED)
====================
**Core**
- The search field ``track`` has been renamed to ``track_name`` to avoid
confusion with ``track_no``.
**Local backend**
- Fix search filtering by track number.
- When scanning, we no longer default the album artist to be the same as the
track artist. Album artist is now only populated if the scanned file got an
explicit album artist set.
**MPD frontend**
- Add support for ``list "albumartist" ...``.
v0.16.0 (2013-10-27)
====================

View File

@ -21,6 +21,12 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
self._tag_cache_file = self.backend.config['local']['tag_cache_file']
self.refresh()
def _convert_to_int(self, string):
try:
return int(string)
except ValueError:
return object()
def refresh(self, uri=None):
logger.debug(
'Loading local tracks from %s using %s',
@ -61,12 +67,12 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
# FIXME this is bound to be slow for large libraries
for value in values:
if field == 'track_no':
q = value
q = self._convert_to_int(value)
else:
q = value.strip()
uri_filter = lambda t: q == t.uri
track_filter = lambda t: q == t.name
track_name_filter = lambda t: q == t.name
album_filter = lambda t: q == getattr(t, 'album', Album()).name
artist_filter = lambda t: filter(
lambda a: q == a.name, t.artists)
@ -84,7 +90,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
date_filter = lambda t: q == t.date
any_filter = lambda t: (
uri_filter(t) or
track_filter(t) or
track_name_filter(t) or
album_filter(t) or
artist_filter(t) or
albumartist_filter(t) or
@ -96,8 +102,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
if field == 'uri':
result_tracks = filter(uri_filter, result_tracks)
elif field == 'track':
result_tracks = filter(track_filter, result_tracks)
elif field == 'track_name':
result_tracks = filter(track_name_filter, result_tracks)
elif field == 'album':
result_tracks = filter(album_filter, result_tracks)
elif field == 'artist':
@ -135,12 +141,12 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
# FIXME this is bound to be slow for large libraries
for value in values:
if field == 'track_no':
q = value
q = self._convert_to_int(value)
else:
q = value.strip().lower()
uri_filter = lambda t: q in t.uri.lower()
track_filter = lambda t: q in t.name.lower()
track_name_filter = lambda t: q in t.name.lower()
album_filter = lambda t: q in getattr(
t, 'album', Album()).name.lower()
artist_filter = lambda t: filter(
@ -159,7 +165,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
date_filter = lambda t: t.date and t.date.startswith(q)
any_filter = lambda t: (
uri_filter(t) or
track_filter(t) or
track_name_filter(t) or
album_filter(t) or
artist_filter(t) or
albumartist_filter(t) or
@ -171,8 +177,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
if field == 'uri':
result_tracks = filter(uri_filter, result_tracks)
elif field == 'track':
result_tracks = filter(track_filter, result_tracks)
elif field == 'track_name':
result_tracks = filter(track_name_filter, result_tracks)
elif field == 'album':
result_tracks = filter(album_filter, result_tracks)
elif field == 'artist':

View File

@ -120,7 +120,6 @@ def _convert_mpd_data(data, tracks):
if 'artist' in data:
artist_kwargs['name'] = data['artist']
albumartist_kwargs['name'] = data['artist']
if 'albumartist' in data:
albumartist_kwargs['name'] = data['albumartist']

View File

@ -131,8 +131,8 @@ def findadd(context, mpd_query):
@handle_request(
r'^list "?(?P<field>([Aa]rtist|[Aa]lbum|[Cc]omposer|[Dd]ate|[Gg]enre|'
r'[Pp]erformer))"?'
r'^list "?(?P<field>([Aa]rtist|[Aa]lbumartist|[Aa]lbum|[Cc]omposer|'
r'[Dd]ate|[Gg]enre|[Pp]erformer))"?'
r'( (?P<mpd_query>.*))?$')
def list_(context, field, mpd_query=None):
"""
@ -141,7 +141,7 @@ def list_(context, field, mpd_query=None):
``list {TYPE} [ARTIST]``
Lists all tags of the specified type. ``TYPE`` should be ``album``,
``artist``, ``date``, or ``genre``.
``artist``, ``albumartist``, ``date``, or ``genre``.
``ARTIST`` is an optional parameter when type is ``album``,
``date``, or ``genre``. This filters the result list by an artist.
@ -223,6 +223,8 @@ def list_(context, field, mpd_query=None):
return
if field == 'artist':
return _list_artist(context, query)
if field == 'albumartist':
return _list_albumartist(context, query)
elif field == 'album':
return _list_album(context, query)
elif field == 'composer':
@ -245,6 +247,17 @@ def _list_artist(context, query):
return artists
def _list_albumartist(context, query):
albumartists = set()
results = context.core.library.find_exact(**query).get()
for track in _get_tracks(results):
if track.album:
for artist in track.album.artists:
if artist.name:
albumartists.add(('AlbumArtist', artist.name))
return albumartists
def _list_album(context, query):
albums = set()
results = context.core.library.find_exact(**query).get()

View File

@ -47,9 +47,6 @@ def track_to_mpd_format(track, position=None):
track.track_no, track.album.num_tracks)))
else:
result.append(('Track', track.track_no))
if track.album is not None and track.album.artists:
artists = artists_to_mpd_format(track.album.artists)
result.append(('AlbumArtist', artists))
if position is not None and tlid is not None:
result.append(('Pos', position))
result.append(('Id', tlid))
@ -58,6 +55,8 @@ def track_to_mpd_format(track, position=None):
# FIXME don't use first and best artist?
# FIXME don't duplicate following code?
if track.album is not None and track.album.artists:
artists = artists_to_mpd_format(track.album.artists)
result.append(('AlbumArtist', artists))
artists = filter(
lambda a: a.musicbrainz_id is not None, track.album.artists)
if artists:
@ -262,7 +261,7 @@ def query_from_mpd_search_format(mpd_query):
m = MPD_SEARCH_QUERY_PART_RE.match(query_part)
field = m.groupdict()['field'].lower()
if field == 'title':
field = 'track'
field = 'track_name'
elif field == 'track':
field = 'track_no'
elif field in ('file', 'filename'):

View File

@ -41,7 +41,7 @@ def _format_dependency(dep_info):
lines.append('%s: not found' % dep_info['name'])
else:
if 'path' in dep_info:
source = ' from %s' % os.path.dirname(dep_info['path'])
source = ' from %s' % dep_info['path']
else:
source = ''
lines.append('%s: %s%s' % (
@ -75,7 +75,7 @@ def python_info():
'name': 'Python',
'version': '%s %s' % (
platform.python_implementation(), platform.python_version()),
'path': platform.__file__,
'path': os.path.dirname(platform.__file__),
}
@ -127,7 +127,7 @@ def gstreamer_info():
return {
'name': 'GStreamer',
'version': '.'.join(map(str, gst.get_gst_version())),
'path': gst.__file__,
'path': os.path.dirname(gst.__file__),
'other': '\n'.join(other),
}

View File

@ -45,10 +45,14 @@ class LocalLibraryProviderTest(unittest.TestCase):
artists=[artists[3]], album=albums[2],
date='2003', length=4000, track_no=3),
Track(
uri='local:track:path4', name='track4', genre='genre1',
uri='local:track:path4', name='track4',
artists=[artists[2]], album=albums[3],
date='2004', length=60000, track_no=4),
Track(
uri='local:track:path5', name='track6', genre='genre1',
album=albums[3], length=4000, composers=[artists[4]]),
Track(
uri='local:track:path5', name='track5', genre='genre2',
uri='local:track:path6', name='track6', genre='genre2',
album=albums[3], length=4000, performers=[artists[5]]),
]
@ -111,12 +115,15 @@ class LocalLibraryProviderTest(unittest.TestCase):
self.assertEqual(tracks, [])
def test_find_exact_no_hits(self):
result = self.library.find_exact(track=['unknown track'])
result = self.library.find_exact(track_name=['unknown track'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(artist=['unknown artist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(albumartist=['unknown albumartist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(composer=['unknown composer'])
self.assertEqual(list(result[0].tracks), [])
@ -132,7 +139,10 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.find_exact(genre=['unknown genre'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(track_no=[9])
result = self.library.find_exact(track_no=['9'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(track_no=['no_match'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(uri=['fake uri'])
@ -151,10 +161,10 @@ class LocalLibraryProviderTest(unittest.TestCase):
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_track(self):
result = self.library.find_exact(track=['track1'])
result = self.library.find_exact(track_name=['track1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(track=['track2'])
result = self.library.find_exact(track_name=['track2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_artist(self):
@ -164,6 +174,9 @@ class LocalLibraryProviderTest(unittest.TestCase):
result = self.library.find_exact(artist=['artist2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
result = self.library.find_exact(artist=['artist3'])
self.assertEqual(list(result[0].tracks), self.tracks[3:4])
def test_find_exact_composer(self):
result = self.library.find_exact(composer=['artist5'])
self.assertEqual(list(result[0].tracks), self.tracks[3:4])
@ -193,10 +206,10 @@ class LocalLibraryProviderTest(unittest.TestCase):
self.assertEqual(list(result[0].tracks), [self.tracks[2]])
def test_find_exact_track_no(self):
result = self.library.find_exact(track_no=[1])
result = self.library.find_exact(track_no=['1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(track_no=[2])
result = self.library.find_exact(track_no=['2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_genre(self):
@ -237,7 +250,8 @@ class LocalLibraryProviderTest(unittest.TestCase):
# Matches on track album artists
result = self.library.find_exact(any=['artist3'])
self.assertEqual(list(result[0].tracks), self.tracks[2:3])
self.assertEqual(
list(result[0].tracks), [self.tracks[3], self.tracks[2]])
# Matches on track composer
result = self.library.find_exact(any=['artist5'])
@ -270,19 +284,22 @@ class LocalLibraryProviderTest(unittest.TestCase):
test = lambda: self.library.find_exact(artist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(albumartist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(track_name=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(composer=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(performer=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(track=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(album=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(track_no=[])
test = lambda: self.library.find_exact(track_no=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(genre=[''])
@ -295,22 +312,28 @@ class LocalLibraryProviderTest(unittest.TestCase):
self.assertRaises(LookupError, test)
def test_search_no_hits(self):
result = self.library.search(track=['unknown track'])
result = self.library.search(track_name=['unknown track'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(artist=['unknown artist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(albumartist=['unknown albumartist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(composer=['unknown composer'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(performer=['unknown performer'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(album=['unknown artist'])
result = self.library.search(album=['unknown album'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(track_no=[9])
result = self.library.search(track_no=['9'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(track_no=['no_match'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(genre=['unknown genre'])
@ -333,10 +356,10 @@ class LocalLibraryProviderTest(unittest.TestCase):
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_track(self):
result = self.library.search(track=['Rack1'])
result = self.library.search(track_name=['Rack1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(track=['Rack2'])
result = self.library.search(track_name=['Rack2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_artist(self):
@ -395,10 +418,10 @@ class LocalLibraryProviderTest(unittest.TestCase):
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_track_no(self):
result = self.library.search(track_no=[1])
result = self.library.search(track_no=['1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(track_no=[2])
result = self.library.search(track_no=['2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_any(self):
@ -427,7 +450,8 @@ class LocalLibraryProviderTest(unittest.TestCase):
# Matches on track album artists
result = self.library.search(any=['Tist3'])
self.assertEqual(list(result[0].tracks), self.tracks[2:3])
self.assertEqual(
list(result[0].tracks), [self.tracks[3], self.tracks[2]])
# Matches on track genre
result = self.library.search(any=['Enre1'])
@ -448,13 +472,16 @@ class LocalLibraryProviderTest(unittest.TestCase):
test = lambda: self.library.search(artist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(albumartist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(composer=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(performer=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(track=[''])
test = lambda: self.library.search(track_name=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(album=[''])

View File

@ -93,28 +93,30 @@ class URItoM3UTest(unittest.TestCase):
expected_artists = [Artist(name='name')]
expected_albums = [
Album(name='albumname', artists=expected_artists, num_tracks=2)]
Album(name='albumname', artists=expected_artists, num_tracks=2),
Album(name='albumname', num_tracks=2),
]
expected_tracks = []
def generate_track(path, ident):
def generate_track(path, ident, album_id):
uri = 'local:track:%s' % path
track = Track(
uri=uri, name='trackname', artists=expected_artists,
album=expected_albums[0], track_no=1, date='2006', length=4000,
album=expected_albums[album_id], track_no=1, date='2006', length=4000,
last_modified=1272319626)
expected_tracks.append(track)
generate_track('song1.mp3', 6)
generate_track('song2.mp3', 7)
generate_track('song3.mp3', 8)
generate_track('subdir1/song4.mp3', 2)
generate_track('subdir1/song5.mp3', 3)
generate_track('subdir2/song6.mp3', 4)
generate_track('subdir2/song7.mp3', 5)
generate_track('subdir1/subsubdir/song8.mp3', 0)
generate_track('subdir1/subsubdir/song9.mp3', 1)
generate_track('song1.mp3', 6, 0)
generate_track('song2.mp3', 7, 0)
generate_track('song3.mp3', 8, 1)
generate_track('subdir1/song4.mp3', 2, 0)
generate_track('subdir1/song5.mp3', 3, 0)
generate_track('subdir2/song6.mp3', 4, 1)
generate_track('subdir2/song7.mp3', 5, 1)
generate_track('subdir1/subsubdir/song8.mp3', 0, 0)
generate_track('subdir1/subsubdir/song9.mp3', 1, 1)
class MPDTagCacheToTracksTest(unittest.TestCase):

View File

@ -11,6 +11,7 @@ key: song8.mp3
file: subdir1/subsubdir/song8.mp3
Time: 4
Artist: name
AlbumArtist: name
Title: trackname
Album: albumname
Track: 1/2
@ -32,6 +33,7 @@ key: song4.mp3
file: subdir1/song4.mp3
Time: 4
Artist: name
AlbumArtist: name
Title: trackname
Album: albumname
Track: 1/2
@ -41,6 +43,7 @@ key: song5.mp3
file: subdir1/song5.mp3
Time: 4
Artist: name
AlbumArtist: name
Title: trackname
Album: albumname
Track: 1/2
@ -76,6 +79,7 @@ key: song1.mp3
file: /song1.mp3
Time: 4
Artist: name
AlbumArtist: name
Title: trackname
Album: albumname
Track: 1/2
@ -85,6 +89,7 @@ key: song2.mp3
file: /song2.mp3
Time: 4
Artist: name
AlbumArtist: name
Title: trackname
Album: albumname
Track: 1/2

View File

@ -6,6 +6,7 @@ songList begin
key: key1
file: /path1
Artist: artist1
AlbumArtist: artist1
Title: track1
Album: album1
Date: 2001-02-03
@ -14,6 +15,7 @@ Time: 4
key: key2
file: /path2
Artist: artist2
AlbumArtist: artist2
Title: track2
Album: album2
Date: 2002
@ -30,13 +32,21 @@ Track: 3
Time: 4
key: key4
file: /path4
Artist: artist3
Title: track4
Album: album4
Date: 2004
Track: 4
Time: 60
key: key5
file: /path5
Composer: artist5
Title: track4
Album: album4
Genre: genre1
Time: 4
key: key5
file: /path5
key: key6
file: /path6
Performer: artist6
Title: track5
Album: album4

View File

@ -7,6 +7,7 @@ key: song1.mp3
file: /song1.mp3
Time: 4
Artist: name
AlbumArtist: name
Title: trackname
Album: albumname
Track: 1/2

View File

@ -7,6 +7,7 @@ key: song1.mp3
file: /song1.mp3
Time: 4
Artist: æøå
AlbumArtist: æøå
Title: æøå
Album: æøå
mtime: 1272319626

View File

@ -398,6 +398,66 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
self.assertNotInResponse('Artist: ')
self.assertInResponse('OK')
### Albumartist
def test_list_albumartist_with_quotes(self):
self.sendRequest('list "albumartist"')
self.assertInResponse('OK')
def test_list_albumartist_without_quotes(self):
self.sendRequest('list albumartist')
self.assertInResponse('OK')
def test_list_albumartist_without_quotes_and_capitalized(self):
self.sendRequest('list Albumartist')
self.assertInResponse('OK')
def test_list_albumartist_with_query_of_one_token(self):
self.sendRequest('list "albumartist" "anartist"')
self.assertEqualResponse(
'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_albumartist_with_unknown_field_in_query_returns_ack(self):
self.sendRequest('list "albumartist" "foo" "bar"')
self.assertEqualResponse('ACK [2@0] {list} not able to parse args')
def test_list_albumartist_by_artist(self):
self.sendRequest('list "albumartist" "artist" "anartist"')
self.assertInResponse('OK')
def test_list_albumartist_by_album(self):
self.sendRequest('list "albumartist" "album" "analbum"')
self.assertInResponse('OK')
def test_list_albumartist_by_full_date(self):
self.sendRequest('list "albumartist" "date" "2001-01-01"')
self.assertInResponse('OK')
def test_list_albumartist_by_year(self):
self.sendRequest('list "albumartist" "date" "2001"')
self.assertInResponse('OK')
def test_list_albumartist_by_genre(self):
self.sendRequest('list "albumartist" "genre" "agenre"')
self.assertInResponse('OK')
def test_list_albumartist_by_artist_and_album(self):
self.sendRequest(
'list "albumartist" "artist" "anartist" "album" "analbum"')
self.assertInResponse('OK')
def test_list_albumartist_without_filter_value(self):
self.sendRequest('list "albumartist" "artist" ""')
self.assertInResponse('OK')
def test_list_albumartist_should_not_return_artists_without_names(self):
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(album=Album(artists=[Artist(name='')]))])
self.sendRequest('list "albumartist"')
self.assertNotInResponse('Artist: ')
self.assertInResponse('OK')
### Album
def test_list_album_with_quotes(self):

View File

@ -20,7 +20,7 @@ class DepsTest(unittest.TestCase):
lambda: dict(name='Platform', version='Loonix 4.0.1'),
lambda: dict(
name='Pykka', version='1.1',
path='/foo/bar/baz.py', other='Quux'),
path='/foo/bar', other='Quux'),
lambda: dict(name='Foo'),
lambda: dict(name='Mopidy', version='0.13', dependencies=[
dict(name='pylast', version='0.5', dependencies=[
@ -58,6 +58,7 @@ class DepsTest(unittest.TestCase):
self.assertIn(platform.python_implementation(), result['version'])
self.assertIn(platform.python_version(), result['version'])
self.assertIn('python', result['path'])
self.assertNotIn('platform.py', result['path'])
def test_gstreamer_info(self):
result = deps.gstreamer_info()
@ -66,6 +67,7 @@ class DepsTest(unittest.TestCase):
self.assertEquals(
'.'.join(map(str, gst.get_gst_version())), result['version'])
self.assertIn('gst', result['path'])
self.assertNotIn('__init__.py', result['path'])
self.assertIn('Python wrapper: gst-python', result['other'])
self.assertIn(
'.'.join(map(str, gst.get_pygst_version())), result['other'])