From ac2e413ec03c68b39819292a3ecf28dadd61d470 Mon Sep 17 00:00:00 2001 From: David Eisner Date: Fri, 1 Nov 2013 09:27:31 +0000 Subject: [PATCH 01/43] Advertise MPD with Avahi A rudimentary implementation to resolve #39, ignoring dbus errors (just restart), name collisions (choose a fresh name), etc. --- mopidy/frontends/mpd/__init__.py | 2 ++ mopidy/frontends/mpd/actor.py | 26 ++++++++++++++++++++ mopidy/frontends/mpd/ext.conf | 2 ++ mopidy/frontends/mpd/zeroconf.py | 42 ++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 mopidy/frontends/mpd/zeroconf.py diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 276be450..28f9c951 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -23,6 +23,8 @@ class Extension(ext.Extension): schema['password'] = config.Secret(optional=True) schema['max_connections'] = config.Integer(minimum=1) schema['connection_timeout'] = config.Integer(minimum=1) + schema['zeroconf_enabled'] = config.Boolean() + schema['zeroconf_name'] = config.String() return schema def validate_environment(self): diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index 4d983b73..b48b2b65 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -17,6 +17,9 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): super(MpdFrontend, self).__init__() hostname = network.format_hostname(config['mpd']['hostname']) port = config['mpd']['port'] + self.config = config + self.hostname = hostname + self.port = port try: network.Server( @@ -36,8 +39,31 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): logger.info('MPD server running at [%s]:%s', hostname, port) + def on_start(self): + try: + if self.config['mpd']['zeroconf_enabled']: + name = self.config['mpd']['zeroconf_name'] + import re + lo = re.search('(? Date: Sat, 2 Nov 2013 02:29:37 +0100 Subject: [PATCH 02/43] Add genre, composer, and performer tags and use them --- mopidy/backends/local/library.py | 32 +++++++ mopidy/backends/local/translator.py | 9 ++ mopidy/frontends/mpd/protocol/music_db.py | 48 ++++++++-- mopidy/frontends/mpd/translator.py | 35 ++++++- mopidy/models.py | 22 +++++ mopidy/scanner.py | 36 +++++--- tests/backends/local/library_test.py | 107 +++++++++++++++++++++- tests/data/library_tag_cache | 14 +++ tests/models_test.py | 13 ++- 9 files changed, 288 insertions(+), 28 deletions(-) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index 2ff0e6d1..dc699853 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -73,7 +73,14 @@ class LocalLibraryProvider(base.BaseLibraryProvider): albumartist_filter = lambda t: any([ q == a.name for a in getattr(t.album, 'artists', [])]) + composer_filter = lambda t: any([ + q == a.name + for a in getattr(t, 'composers', [])]) + performer_filter = lambda t: any([ + q == a.name + for a in getattr(t, 'performers', [])]) track_no_filter = lambda t: q == t.track_no + genre_filter = lambda t: t.genre and q == t.genre date_filter = lambda t: q == t.date any_filter = lambda t: ( uri_filter(t) or @@ -81,7 +88,10 @@ class LocalLibraryProvider(base.BaseLibraryProvider): album_filter(t) or artist_filter(t) or albumartist_filter(t) or + composer_filter(t) or + performer_filter(t) or track_no_filter(t) or + genre_filter(t) or date_filter(t)) if field == 'uri': @@ -94,8 +104,14 @@ class LocalLibraryProvider(base.BaseLibraryProvider): result_tracks = filter(artist_filter, result_tracks) elif field == 'albumartist': result_tracks = filter(albumartist_filter, result_tracks) + elif field == 'composer': + result_tracks = filter(composer_filter, result_tracks) + elif field == 'performer': + result_tracks = filter(performer_filter, result_tracks) elif field == 'track_no': result_tracks = filter(track_no_filter, result_tracks) + elif field == 'genre': + result_tracks = filter(genre_filter, result_tracks) elif field == 'date': result_tracks = filter(date_filter, result_tracks) elif field == 'any': @@ -132,7 +148,14 @@ class LocalLibraryProvider(base.BaseLibraryProvider): albumartist_filter = lambda t: any([ q in a.name.lower() for a in getattr(t.album, 'artists', [])]) + composer_filter = lambda t: any([ + q in a.name.lower() + for a in getattr(t, 'composers', [])]) + performer_filter = lambda t: any([ + q in a.name.lower() + for a in getattr(t, 'performers', [])]) track_no_filter = lambda t: q == t.track_no + genre_filter = lambda t: t.genre and q in t.genre.lower() date_filter = lambda t: t.date and t.date.startswith(q) any_filter = lambda t: ( uri_filter(t) or @@ -140,7 +163,10 @@ class LocalLibraryProvider(base.BaseLibraryProvider): album_filter(t) or artist_filter(t) or albumartist_filter(t) or + composer_filter(t) or + performer_filter(t) or track_no_filter(t) or + genre_filter(t) or date_filter(t)) if field == 'uri': @@ -153,8 +179,14 @@ class LocalLibraryProvider(base.BaseLibraryProvider): result_tracks = filter(artist_filter, result_tracks) elif field == 'albumartist': result_tracks = filter(albumartist_filter, result_tracks) + elif field == 'composer': + result_tracks = filter(composer_filter, result_tracks) + elif field == 'performer': + result_tracks = filter(performer_filter, result_tracks) elif field == 'track_no': result_tracks = filter(track_no_filter, result_tracks) + elif field == 'genre': + result_tracks = filter(genre_filter, result_tracks) elif field == 'date': result_tracks = filter(date_filter, result_tracks) elif field == 'any': diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 7cd46fbb..59b849ef 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -125,12 +125,21 @@ def _convert_mpd_data(data, tracks): if 'albumartist' in data: albumartist_kwargs['name'] = data['albumartist'] + if 'composer' in data: + track_kwargs['composers'] = [Artist(name=data['composer'])] + + if 'performer' in data: + track_kwargs['performers'] = [Artist(name=data['performer'])] + if 'album' in data: album_kwargs['name'] = data['album'] if 'title' in data: track_kwargs['name'] = data['title'] + if 'genre' in data: + track_kwargs['genre'] = data['genre'] + if 'date' in data: track_kwargs['date'] = data['date'] diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 6dd43d68..b35aff14 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -10,8 +10,9 @@ from mopidy.frontends.mpd.protocol import handle_request, stored_playlists QUERY_RE = ( - r'(?P("?([Aa]lbum|[Aa]rtist|[Aa]lbumartist|[Dd]ate|[Ff]ile|' - r'[Ff]ilename|[Tt]itle|[Tt]rack|[Aa]ny)"? "[^"]*"\s?)+)$') + r'(?P("?([Aa]lbum|[Aa]rtist|[Aa]lbumartist|[Cc]omment|' + r'[Cc]omposer|[Dd]ate|[Ff]ile|[Ff]ilename|[Gg]enre|[Pp]erformer|' + r'[Tt]itle|[Tt]rack|[Aa]ny)"? "[^"]*"\s?)+)$') def _get_field(field, search_results): @@ -100,9 +101,12 @@ def find(context, mpd_query): return results = context.core.library.find_exact(**query).get() result_tracks = [] - if 'artist' not in query and 'albumartist' not in query: + if ('artist' not in query and + 'albumartist' not in query and + 'composer' not in query and + 'performer' not in query): result_tracks += [_artist_as_track(a) for a in _get_artists(results)] - if 'album' not in query: + if 'album' not in query and 'genre' not in query: result_tracks += [_album_as_track(a) for a in _get_albums(results)] result_tracks += _get_tracks(results) return translator.tracks_to_mpd_format(result_tracks) @@ -127,7 +131,8 @@ def findadd(context, mpd_query): @handle_request( - r'^list "?(?P([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?' + r'^list "?(?P([Aa]rtist|[Aa]lbum|[Cc]omposer|[Dd]ate|[Gg]enre|' + r'[Pp]erformer))"?' r'( (?P.*))?$') def list_(context, field, mpd_query=None): """ @@ -220,10 +225,14 @@ def list_(context, field, mpd_query=None): return _list_artist(context, query) elif field == 'album': return _list_album(context, query) + elif field == 'composer': + return _list_composer(context, query) + elif field == 'performer': + return _list_performer(context, query) elif field == 'date': return _list_date(context, query) elif field == 'genre': - pass # TODO We don't have genre in our internal data structures yet + return _list_genre(context, query) def _list_artist(context, query): @@ -245,6 +254,24 @@ def _list_album(context, query): return albums +def _list_composer(context, query): + composers = set() + results = context.core.library.find_exact(**query).get() + for track in _get_tracks(results): + if track.composer and track.composer.name: + composers.add(('Composer', track.composer.name)) + return composers + + +def _list_performer(context, query): + performers = set() + results = context.core.library.find_exact(**query).get() + for track in _get_tracks(results): + if track.performer and track.performer.name: + performers.add(('Performer', track.performer.name)) + return performers + + def _list_date(context, query): dates = set() results = context.core.library.find_exact(**query).get() @@ -254,6 +281,15 @@ def _list_date(context, query): return dates +def _list_genre(context, query): + genres = set() + results = context.core.library.find_exact(**query).get() + for track in _get_tracks(results): + if track.genre: + genres.add(('Genre', track.genre)) + return genres + + @handle_request(r'^listall$') @handle_request(r'^listall "(?P[^"]+)"$') def listall(context, uri=None): diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 9b331395..a60dfd20 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -37,8 +37,11 @@ def track_to_mpd_format(track, position=None): ('Artist', artists_to_mpd_format(track.artists)), ('Title', track.name or ''), ('Album', track.album and track.album.name or ''), - ('Date', track.date or ''), ] + + if track.date: + result.append(('Date', track.date)) + if track.album is not None and track.album.num_tracks != 0: result.append(('Track', '%d/%d' % ( track.track_no, track.album.num_tracks))) @@ -64,14 +67,31 @@ def track_to_mpd_format(track, position=None): artists = filter(lambda a: a.musicbrainz_id is not None, track.artists) if artists: result.append(('MUSICBRAINZ_ARTISTID', artists[0].musicbrainz_id)) + + if track.composers: + result.append(('Composer', artists_to_mpd_format(track.composers))) + + if track.performers: + result.append(('Performer', artists_to_mpd_format(track.performers))) + + if track.genre: + result.append(('Genre', track.genre)) + + if track.disc_no: + result.append(('Disc', track.disc_no)) + + if track.comment: + result.append(('Comment', track.comment)) + if track.musicbrainz_id is not None: result.append(('MUSICBRAINZ_TRACKID', track.musicbrainz_id)) return result MPD_KEY_ORDER = ''' - key file Time Artist AlbumArtist Title Album Track Date MUSICBRAINZ_ALBUMID - MUSICBRAINZ_ALBUMARTISTID MUSICBRAINZ_ARTISTID MUSICBRAINZ_TRACKID mtime + key file Time Artist Album AlbumArtist Title Track Genre Date Composer + Performer Comment Disc MUSICBRAINZ_ALBUMID MUSICBRAINZ_ALBUMARTISTID + MUSICBRAINZ_ARTISTID MUSICBRAINZ_TRACKID mtime '''.split() @@ -166,7 +186,8 @@ def query_from_mpd_list_format(field, mpd_query): key = tokens[0].lower() value = tokens[1] tokens = tokens[2:] - if key not in ('artist', 'album', 'albumartist', 'date', 'genre'): + if key not in ('artist', 'album', 'albumartist', 'composer', + 'date', 'genre', 'performer'): raise MpdArgError('not able to parse args', command='list') if not value: raise ValueError @@ -189,9 +210,12 @@ MPD_SEARCH_QUERY_RE = re.compile(r""" [Aa]lbum | [Aa]rtist | [Aa]lbumartist + | [Cc]omposer | [Dd]ate | [Ff]ile | [Ff]ilename + | [Gg]enre + | [Pp]erformer | [Tt]itle | [Tt]rack | [Aa]ny @@ -208,9 +232,12 @@ MPD_SEARCH_QUERY_PART_RE = re.compile(r""" [Aa]lbum | [Aa]rtist | [Aa]lbumartist + | [Cc]omposer | [Dd]ate | [Ff]ile | [Ff]ilename + | [Gg]enre + | [Pp]erformer | [Tt]itle | [Tt]rack | [Aa]ny diff --git a/mopidy/models.py b/mopidy/models.py index 3fc92bb4..5ab2ed92 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -219,6 +219,12 @@ class Track(ImmutableObject): :type artists: list of :class:`Artist` :param album: track album :type album: :class:`Album` + :param composer: track composer + :type composer: string + :param performer: track performer + :type performer: string + :param genre: track genre + :type genre: string :param track_no: track number in album :type track_no: integer :param disc_no: disc number in album @@ -229,6 +235,8 @@ class Track(ImmutableObject): :type length: integer :param bitrate: bitrate in kbit/s :type bitrate: integer + :param comment: track comment + :type comment: string :param musicbrainz_id: MusicBrainz ID :type musicbrainz_id: string :param last_modified: Represents last modification time @@ -247,6 +255,15 @@ class Track(ImmutableObject): #: The track :class:`Album`. Read-only. album = None + #: A set of track composers. Read-only. + composers = frozenset() + + #: A set of track performers`. Read-only. + performers = frozenset() + + #: The track genre. Read-only. + genre = None + #: The track number in the album. Read-only. track_no = 0 @@ -262,6 +279,9 @@ class Track(ImmutableObject): #: The track's bitrate in kbit/s. Read-only. bitrate = None + #: The track comment. Read-only. + comment = None + #: The MusicBrainz ID of the track. Read-only. musicbrainz_id = None @@ -272,6 +292,8 @@ class Track(ImmutableObject): def __init__(self, *args, **kwargs): self.__dict__['artists'] = frozenset(kwargs.pop('artists', [])) + self.__dict__['composers'] = frozenset(kwargs.pop('composers', [])) + self.__dict__['performers'] = frozenset(kwargs.pop('performers', [])) super(Track, self).__init__(*args, **kwargs) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index dd21fdb4..9f4f36e4 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -139,6 +139,8 @@ def translator(data): albumartist_kwargs = {} album_kwargs = {} artist_kwargs = {} + composer_kwargs = {} + performer_kwargs = {} track_kwargs = {} def _retrieve(source_key, target_key, target): @@ -149,6 +151,22 @@ def translator(data): _retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs) _retrieve(gst.TAG_ALBUM_VOLUME_COUNT, 'num_discs', album_kwargs) _retrieve(gst.TAG_ARTIST, 'name', artist_kwargs) + _retrieve(gst.TAG_COMPOSER, 'name', composer_kwargs) + _retrieve(gst.TAG_PERFORMER, 'name', performer_kwargs) + _retrieve(gst.TAG_ALBUM_ARTIST, 'name', albumartist_kwargs) + _retrieve(gst.TAG_TITLE, 'name', track_kwargs) + _retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs) + _retrieve(gst.TAG_ALBUM_VOLUME_NUMBER, 'disc_no', track_kwargs) + _retrieve(gst.TAG_GENRE, 'genre', track_kwargs) + _retrieve(gst.TAG_BITRATE, 'bitrate', track_kwargs) + + # Following keys don't seem to have TAG_* constant. + _retrieve('comment', 'comment', track_kwargs) + _retrieve('musicbrainz-trackid', 'musicbrainz_id', track_kwargs) + _retrieve('musicbrainz-artistid', 'musicbrainz_id', artist_kwargs) + _retrieve('musicbrainz-albumid', 'musicbrainz_id', album_kwargs) + _retrieve( + 'musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs) if gst.TAG_DATE in data and data[gst.TAG_DATE]: date = data[gst.TAG_DATE] @@ -159,18 +177,6 @@ def translator(data): else: track_kwargs['date'] = date.isoformat() - _retrieve(gst.TAG_TITLE, 'name', track_kwargs) - _retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs) - _retrieve(gst.TAG_ALBUM_VOLUME_NUMBER, 'disc_no', track_kwargs) - - # Following keys don't seem to have TAG_* constant. - _retrieve('album-artist', 'name', albumartist_kwargs) - _retrieve('musicbrainz-trackid', 'musicbrainz_id', track_kwargs) - _retrieve('musicbrainz-artistid', 'musicbrainz_id', artist_kwargs) - _retrieve('musicbrainz-albumid', 'musicbrainz_id', album_kwargs) - _retrieve( - 'musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs) - if albumartist_kwargs: album_kwargs['artists'] = [Artist(**albumartist_kwargs)] @@ -180,6 +186,12 @@ def translator(data): track_kwargs['album'] = Album(**album_kwargs) track_kwargs['artists'] = [Artist(**artist_kwargs)] + if composer_kwargs: + track_kwargs['composers'] = [Artist(**composer_kwargs)] + + if performer_kwargs: + track_kwargs['performers'] = [Artist(**performer_kwargs)] + return Track(**track_kwargs) diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index 1cb07451..532008a6 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -20,12 +20,15 @@ class LocalLibraryProviderTest(unittest.TestCase): Artist(name='artist2'), Artist(name='artist3'), Artist(name='artist4'), + Artist(name='artist5'), + Artist(name='artist6'), ] albums = [ Album(name='album1', artists=[artists[0]]), Album(name='album2', artists=[artists[1]]), Album(name='album3', artists=[artists[2]]), + Album(name='album4'), ] tracks = [ @@ -41,6 +44,12 @@ class LocalLibraryProviderTest(unittest.TestCase): uri='local:track:path3', name='track3', artists=[artists[3]], album=albums[2], date='2003', length=4000, track_no=3), + Track( + uri='local:track:path4', name='track4', genre='genre1', + album=albums[3], length=4000, composers=[artists[4]]), + Track( + uri='local:track:path5', name='track5', genre='genre2', + album=albums[3], length=4000, performers=[artists[5]]), ] config = { @@ -108,12 +117,21 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(artist=['unknown artist']) self.assertEqual(list(result[0].tracks), []) - result = self.library.find_exact(album=['unknown artist']) + result = self.library.find_exact(composer=['unknown composer']) + self.assertEqual(list(result[0].tracks), []) + + result = self.library.find_exact(performer=['unknown performer']) + self.assertEqual(list(result[0].tracks), []) + + result = self.library.find_exact(album=['unknown album']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(date=['1990']) self.assertEqual(list(result[0].tracks), []) + result = self.library.find_exact(genre=['unknown genre']) + self.assertEqual(list(result[0].tracks), []) + result = self.library.find_exact(track_no=[9]) self.assertEqual(list(result[0].tracks), []) @@ -146,6 +164,14 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(artist=['artist2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) + def test_find_exact_composer(self): + result = self.library.find_exact(composer=['artist5']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + + def test_find_exact_performer(self): + result = self.library.find_exact(performer=['artist6']) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + def test_find_exact_album(self): result = self.library.find_exact(album=['album1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -173,6 +199,13 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(track_no=[2]) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) + def test_find_exact_genre(self): + result = self.library.find_exact(genre=['genre1']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + + result = self.library.find_exact(genre=['genre2']) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + def test_find_exact_date(self): result = self.library.find_exact(date=['2001']) self.assertEqual(list(result[0].tracks), []) @@ -206,6 +239,21 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(any=['artist3']) self.assertEqual(list(result[0].tracks), self.tracks[2:3]) + # Matches on track composer + result = self.library.find_exact(any=['artist5']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + + # Matches on track performer + result = self.library.find_exact(any=['artist6']) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + + # Matches on track genre + result = self.library.find_exact(any=['genre1']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + + result = self.library.find_exact(any=['genre2']) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + # Matches on track year result = self.library.find_exact(any=['2002']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) @@ -222,6 +270,12 @@ class LocalLibraryProviderTest(unittest.TestCase): test = lambda: self.library.find_exact(artist=['']) 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) @@ -231,6 +285,9 @@ class LocalLibraryProviderTest(unittest.TestCase): test = lambda: self.library.find_exact(track_no=[]) self.assertRaises(LookupError, test) + test = lambda: self.library.find_exact(genre=['']) + self.assertRaises(LookupError, test) + test = lambda: self.library.find_exact(date=['']) self.assertRaises(LookupError, test) @@ -244,12 +301,21 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.search(artist=['unknown artist']) 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']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(track_no=[9]) self.assertEqual(list(result[0].tracks), []) + result = self.library.search(genre=['unknown genre']) + self.assertEqual(list(result[0].tracks), []) + result = self.library.search(date=['unknown date']) self.assertEqual(list(result[0].tracks), []) @@ -293,6 +359,14 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.search(albumartist=['Tist3']) self.assertEqual(list(result[0].tracks), [self.tracks[2]]) + def test_search_composer(self): + result = self.library.search(composer=['Tist5']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + + def test_search_performer(self): + result = self.library.search(performer=['Tist6']) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + def test_search_album(self): result = self.library.search(album=['Bum1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -300,6 +374,13 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.search(album=['Bum2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) + def test_search_genre(self): + result = self.library.search(genre=['Enre1']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + + result = self.library.search(genre=['Enre2']) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + def test_search_date(self): result = self.library.search(date=['2001']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -325,6 +406,14 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.search(any=['Tist1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) + # Matches on track composer + result = self.library.search(any=['Tist5']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + + # Matches on track performer + result = self.library.search(any=['Tist6']) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + # Matches on track result = self.library.search(any=['Rack1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -340,6 +429,13 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.search(any=['Tist3']) self.assertEqual(list(result[0].tracks), self.tracks[2:3]) + # Matches on track genre + result = self.library.search(any=['Enre1']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + + result = self.library.search(any=['Enre2']) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + # Matches on URI result = self.library.search(any=['TH1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -352,12 +448,21 @@ class LocalLibraryProviderTest(unittest.TestCase): test = lambda: self.library.search(artist=['']) 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=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(album=['']) self.assertRaises(LookupError, test) + test = lambda: self.library.search(genre=['']) + self.assertRaises(LookupError, test) + test = lambda: self.library.search(date=['']) self.assertRaises(LookupError, test) diff --git a/tests/data/library_tag_cache b/tests/data/library_tag_cache index e9e87c1b..02cbf927 100644 --- a/tests/data/library_tag_cache +++ b/tests/data/library_tag_cache @@ -28,4 +28,18 @@ Album: album3 Date: 2003 Track: 3 Time: 4 +key: key4 +file: /path4 +Composer: artist5 +Title: track4 +Album: album4 +Genre: genre1 +Time: 4 +key: key5 +file: /path5 +Performer: artist6 +Title: track5 +Album: album4 +Genre: genre2 +Time: 4 songList end diff --git a/tests/models_test.py b/tests/models_test.py index afd1858b..9f43e624 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -450,12 +450,14 @@ class TrackTest(unittest.TestCase): def test_repr_without_artists(self): self.assertEquals( - "Track(artists=[], name=u'name', uri=u'uri')", + "Track(artists=[], composers=[], name=u'name', " + "performers=[], uri=u'uri')", repr(Track(uri='uri', name='name'))) def test_repr_with_artists(self): self.assertEquals( - "Track(artists=[Artist(name=u'foo')], name=u'name', uri=u'uri')", + "Track(artists=[Artist(name=u'foo')], composers=[], name=u'name', " + "performers=[], uri=u'uri')", repr(Track(uri='uri', name='name', artists=[Artist(name='foo')]))) def test_serialize_without_artists(self): @@ -670,7 +672,8 @@ class TlTrackTest(unittest.TestCase): def test_repr(self): self.assertEquals( - "TlTrack(tlid=123, track=Track(artists=[], uri=u'uri'))", + "TlTrack(tlid=123, track=Track(artists=[], composers=[], " + "performers=[], uri=u'uri'))", repr(TlTrack(tlid=123, track=Track(uri='uri')))) def test_serialize(self): @@ -804,8 +807,8 @@ class PlaylistTest(unittest.TestCase): def test_repr_with_tracks(self): self.assertEquals( - "Playlist(name=u'name', tracks=[Track(artists=[], name=u'foo')], " - "uri=u'uri')", + "Playlist(name=u'name', tracks=[Track(artists=[], composers=[], " + "name=u'foo', performers=[])], uri=u'uri')", repr(Playlist(uri='uri', name='name', tracks=[Track(name='foo')]))) def test_serialize_without_tracks(self): From 2bbd9003a217c2a365ddd74568c8824e814b68fa Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sat, 2 Nov 2013 03:07:13 +0100 Subject: [PATCH 03/43] Date is not mandatory AFAICT --- tests/frontends/mpd/protocol/status_test.py | 1 - tests/frontends/mpd/translator_test.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/frontends/mpd/protocol/status_test.py b/tests/frontends/mpd/protocol/status_test.py index 24f24ab2..bd75efb5 100644 --- a/tests/frontends/mpd/protocol/status_test.py +++ b/tests/frontends/mpd/protocol/status_test.py @@ -21,7 +21,6 @@ class StatusHandlerTest(protocol.BaseTestCase): self.assertInResponse('Title: ') self.assertInResponse('Album: ') self.assertInResponse('Track: 0') - self.assertInResponse('Date: ') self.assertInResponse('Pos: 0') self.assertInResponse('Id: 0') self.assertInResponse('OK') diff --git a/tests/frontends/mpd/translator_test.py b/tests/frontends/mpd/translator_test.py index 860e01e9..96d5f316 100644 --- a/tests/frontends/mpd/translator_test.py +++ b/tests/frontends/mpd/translator_test.py @@ -36,8 +36,7 @@ class TrackMpdFormatTest(unittest.TestCase): self.assertIn(('Title', ''), result) self.assertIn(('Album', ''), result) self.assertIn(('Track', 0), result) - self.assertIn(('Date', ''), result) - self.assertEqual(len(result), 7) + self.assertEqual(len(result), 6) def test_track_to_mpd_format_with_position(self): result = translator.track_to_mpd_format(Track(), position=1) From f90f5f608e74395d07c1039153cdf05809799c4e Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sat, 2 Nov 2013 23:44:33 +0100 Subject: [PATCH 04/43] Fix testcases after merging mopidy/develop --- tests/backends/local/library_test.py | 34 ++++++++++++++-------------- tests/data/library_tag_cache | 4 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index a676c810..d8085691 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -49,7 +49,7 @@ class LocalLibraryProviderTest(unittest.TestCase): artists=[artists[2]], album=albums[3], date='2004', length=60000, track_no=4), Track( - uri='local:track:path5', name='track6', genre='genre1', + uri='local:track:path5', name='track5', genre='genre1', album=albums[3], length=4000, composers=[artists[4]]), Track( uri='local:track:path6', name='track6', genre='genre2', @@ -179,11 +179,11 @@ class LocalLibraryProviderTest(unittest.TestCase): def test_find_exact_composer(self): result = self.library.find_exact(composer=['artist5']) - self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) def test_find_exact_performer(self): result = self.library.find_exact(performer=['artist6']) - self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + self.assertEqual(list(result[0].tracks), self.tracks[5:6]) def test_find_exact_album(self): result = self.library.find_exact(album=['album1']) @@ -214,10 +214,10 @@ class LocalLibraryProviderTest(unittest.TestCase): def test_find_exact_genre(self): result = self.library.find_exact(genre=['genre1']) - self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) result = self.library.find_exact(genre=['genre2']) - self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + self.assertEqual(list(result[0].tracks), self.tracks[5:6]) def test_find_exact_date(self): result = self.library.find_exact(date=['2001']) @@ -255,18 +255,18 @@ class LocalLibraryProviderTest(unittest.TestCase): # Matches on track composer result = self.library.find_exact(any=['artist5']) - self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) # Matches on track performer result = self.library.find_exact(any=['artist6']) - self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + self.assertEqual(list(result[0].tracks), self.tracks[5:6]) # Matches on track genre result = self.library.find_exact(any=['genre1']) - self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) result = self.library.find_exact(any=['genre2']) - self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + self.assertEqual(list(result[0].tracks), self.tracks[5:6]) # Matches on track year result = self.library.find_exact(any=['2002']) @@ -384,11 +384,11 @@ class LocalLibraryProviderTest(unittest.TestCase): def test_search_composer(self): result = self.library.search(composer=['Tist5']) - self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) def test_search_performer(self): result = self.library.search(performer=['Tist6']) - self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + self.assertEqual(list(result[0].tracks), self.tracks[5:6]) def test_search_album(self): result = self.library.search(album=['Bum1']) @@ -399,10 +399,10 @@ class LocalLibraryProviderTest(unittest.TestCase): def test_search_genre(self): result = self.library.search(genre=['Enre1']) - self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) result = self.library.search(genre=['Enre2']) - self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + self.assertEqual(list(result[0].tracks), self.tracks[5:6]) def test_search_date(self): result = self.library.search(date=['2001']) @@ -431,11 +431,11 @@ class LocalLibraryProviderTest(unittest.TestCase): # Matches on track composer result = self.library.search(any=['Tist5']) - self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) # Matches on track performer result = self.library.search(any=['Tist6']) - self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + self.assertEqual(list(result[0].tracks), self.tracks[5:6]) # Matches on track result = self.library.search(any=['Rack1']) @@ -455,10 +455,10 @@ class LocalLibraryProviderTest(unittest.TestCase): # Matches on track genre result = self.library.search(any=['Enre1']) - self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + self.assertEqual(list(result[0].tracks), self.tracks[4:5]) result = self.library.search(any=['Enre2']) - self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + self.assertEqual(list(result[0].tracks), self.tracks[5:6]) # Matches on URI result = self.library.search(any=['TH1']) diff --git a/tests/data/library_tag_cache b/tests/data/library_tag_cache index 0d57d0cf..7d23bddc 100644 --- a/tests/data/library_tag_cache +++ b/tests/data/library_tag_cache @@ -41,14 +41,14 @@ Time: 60 key: key5 file: /path5 Composer: artist5 -Title: track4 +Title: track5 Album: album4 Genre: genre1 Time: 4 key: key6 file: /path6 Performer: artist6 -Title: track5 +Title: track6 Album: album4 Genre: genre2 Time: 4 From a516d2051d5e16b6496b0ebd0690767bcb661a2d Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sun, 3 Nov 2013 01:01:42 +0100 Subject: [PATCH 05/43] Added a ton of extra tests and expanded a few to include new tags --- mopidy/backends/local/library.py | 12 +- mopidy/backends/local/translator.py | 3 + mopidy/frontends/mpd/protocol/music_db.py | 10 +- tests/backends/local/library_test.py | 60 +++++- tests/backends/local/translator_test.py | 4 +- tests/data/library_tag_cache | 1 + tests/data/utf8_tag_cache | 4 + tests/frontends/mpd/protocol/music_db_test.py | 197 ++++++++++++++++++ tests/frontends/mpd/translator_test.py | 12 +- 9 files changed, 290 insertions(+), 13 deletions(-) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index 6219b267..1ba8813e 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -88,6 +88,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider): track_no_filter = lambda t: q == t.track_no genre_filter = lambda t: t.genre and q == t.genre date_filter = lambda t: q == t.date + comment_filter = lambda t: q == t.comment any_filter = lambda t: ( uri_filter(t) or track_name_filter(t) or @@ -98,7 +99,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider): performer_filter(t) or track_no_filter(t) or genre_filter(t) or - date_filter(t)) + date_filter(t) or + comment_filter(t)) if field == 'uri': result_tracks = filter(uri_filter, result_tracks) @@ -120,6 +122,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider): result_tracks = filter(genre_filter, result_tracks) elif field == 'date': result_tracks = filter(date_filter, result_tracks) + elif field == 'comment': + result_tracks = filter(comment_filter, result_tracks) elif field == 'any': result_tracks = filter(any_filter, result_tracks) else: @@ -163,6 +167,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider): track_no_filter = lambda t: q == t.track_no genre_filter = lambda t: t.genre and q in t.genre.lower() date_filter = lambda t: t.date and t.date.startswith(q) + comment_filter = lambda t: t.comment and q in t.comment.lower() any_filter = lambda t: ( uri_filter(t) or track_name_filter(t) or @@ -173,7 +178,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider): performer_filter(t) or track_no_filter(t) or genre_filter(t) or - date_filter(t)) + date_filter(t) or + comment_filter(t)) if field == 'uri': result_tracks = filter(uri_filter, result_tracks) @@ -195,6 +201,8 @@ class LocalLibraryProvider(base.BaseLibraryProvider): result_tracks = filter(genre_filter, result_tracks) elif field == 'date': result_tracks = filter(date_filter, result_tracks) + elif field == 'comment': + result_tracks = filter(comment_filter, result_tracks) elif field == 'any': result_tracks = filter(any_filter, result_tracks) else: diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 2134d7d1..63c46d01 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -142,6 +142,9 @@ def _convert_mpd_data(data, tracks): if 'date' in data: track_kwargs['date'] = data['date'] + if 'comment' in data: + track_kwargs['comment'] = data['comment'] + if 'musicbrainz_trackid' in data: track_kwargs['musicbrainz_id'] = data['musicbrainz_trackid'] diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index dbdb8b69..0d84171c 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -271,8 +271,9 @@ def _list_composer(context, query): composers = set() results = context.core.library.find_exact(**query).get() for track in _get_tracks(results): - if track.composer and track.composer.name: - composers.add(('Composer', track.composer.name)) + for composer in track.composers: + if composer.name: + composers.add(('Composer', composer.name)) return composers @@ -280,8 +281,9 @@ def _list_performer(context, query): performers = set() results = context.core.library.find_exact(**query).get() for track in _get_tracks(results): - if track.performer and track.performer.name: - performers.add(('Performer', track.performer.name)) + for performer in track.performers: + if performer.name: + performers.add(('Performer', performer.name)) return performers diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index d8085691..39980fcc 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -47,7 +47,9 @@ class LocalLibraryProviderTest(unittest.TestCase): Track( uri='local:track:path4', name='track4', artists=[artists[2]], album=albums[3], - date='2004', length=60000, track_no=4), + date='2004', length=60000, track_no=4, + comment='Music server with support for MPD/HTTP clients ' + 'and Spotify streaming http://www.mopidy.com'), Track( uri='local:track:path5', name='track5', genre='genre1', album=albums[3], length=4000, composers=[artists[4]]), @@ -145,6 +147,9 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(track_no=['no_match']) self.assertEqual(list(result[0].tracks), []) + result = self.library.find_exact(comment=['fake comment']) + self.assertEqual(list(result[0].tracks), []) + result = self.library.find_exact(uri=['fake uri']) self.assertEqual(list(result[0].tracks), []) @@ -160,7 +165,7 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(uri=track_2_uri) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) - def test_find_exact_track(self): + def test_find_exact_track_name(self): result = self.library.find_exact(track_name=['track1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -181,10 +186,16 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(composer=['artist5']) self.assertEqual(list(result[0].tracks), self.tracks[4:5]) + result = self.library.find_exact(composer=['artist6']) + self.assertEqual(list(result[0].tracks), []) + def test_find_exact_performer(self): result = self.library.find_exact(performer=['artist6']) self.assertEqual(list(result[0].tracks), self.tracks[5:6]) + result = self.library.find_exact(performer=['artist5']) + self.assertEqual(list(result[0].tracks), []) + def test_find_exact_album(self): result = self.library.find_exact(album=['album1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -229,6 +240,16 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(date=['2002']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) + def test_find_exact_comment(self): + result = self.library.find_exact( + comment=['Music server with support for MPD/HTTP clients ' + 'and Spotify streaming http://www.mopidy.com']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + + result = self.library.find_exact( + comment=['Music server with support for MPD/HTTP clients']) + self.assertEqual(list(result[0].tracks), []) + def test_find_exact_any(self): # Matches on track artist result = self.library.find_exact(any=['artist1']) @@ -237,7 +258,7 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(any=['artist2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) - # Matches on track + # Matches on track name result = self.library.find_exact(any=['track1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -268,10 +289,16 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(any=['genre2']) self.assertEqual(list(result[0].tracks), self.tracks[5:6]) - # Matches on track year + # Matches on track date result = self.library.find_exact(any=['2002']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) + # Matches on track comment + result = self.library.find_exact( + any=['Music server with support for MPD/HTTP clients ' + 'and Spotify streaming http://www.mopidy.com']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + # Matches on URI result = self.library.find_exact(any=['local:track:path1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -308,6 +335,9 @@ class LocalLibraryProviderTest(unittest.TestCase): test = lambda: self.library.find_exact(date=['']) self.assertRaises(LookupError, test) + test = lambda: self.library.find_exact(comment=['']) + self.assertRaises(LookupError, test) + test = lambda: self.library.find_exact(any=['']) self.assertRaises(LookupError, test) @@ -342,6 +372,9 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.search(date=['unknown date']) self.assertEqual(list(result[0].tracks), []) + result = self.library.search(comment=['unknown comment']) + self.assertEqual(list(result[0].tracks), []) + result = self.library.search(uri=['unknown uri']) self.assertEqual(list(result[0].tracks), []) @@ -355,7 +388,7 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.search(uri=['TH2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) - def test_search_track(self): + def test_search_track_name(self): result = self.library.search(track_name=['Rack1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -424,6 +457,13 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.search(track_no=['2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) + def test_search_comment(self): + result = self.library.search(comment=['mopidy']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + + result = self.library.search(comment=['Potify']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + def test_search_any(self): # Matches on track artist result = self.library.search(any=['Tist1']) @@ -460,6 +500,13 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.search(any=['Enre2']) self.assertEqual(list(result[0].tracks), self.tracks[5:6]) + # Matches on track comment + result = self.library.search(any=['http']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + + result = self.library.search(any=['streaming']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + # Matches on URI result = self.library.search(any=['TH1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -493,6 +540,9 @@ class LocalLibraryProviderTest(unittest.TestCase): test = lambda: self.library.search(date=['']) self.assertRaises(LookupError, test) + test = lambda: self.library.search(comment=['']) + self.assertRaises(LookupError, test) + test = lambda: self.library.search(uri=['']) self.assertRaises(LookupError, test) diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index 07990e47..5623c787 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -147,7 +147,9 @@ class MPDTagCacheToTracksTest(unittest.TestCase): album = Album(name='æøå', artists=artists) track = Track( uri='local:track:song1.mp3', name='æøå', artists=artists, - album=album, length=4000, last_modified=1272319626) + composers=artists, performers=artists, genre='æøå', + album=album, length=4000, last_modified=1272319626, + comment='æøå&^`ൂ㔶') self.assertEqual(track, list(tracks)[0]) diff --git a/tests/data/library_tag_cache b/tests/data/library_tag_cache index 7d23bddc..fb89a26d 100644 --- a/tests/data/library_tag_cache +++ b/tests/data/library_tag_cache @@ -37,6 +37,7 @@ Title: track4 Album: album4 Date: 2004 Track: 4 +Comment: Music server with support for MPD/HTTP clients and Spotify streaming http://www.mopidy.com Time: 60 key: key5 file: /path5 diff --git a/tests/data/utf8_tag_cache b/tests/data/utf8_tag_cache index 6f6abe60..83fbcad4 100644 --- a/tests/data/utf8_tag_cache +++ b/tests/data/utf8_tag_cache @@ -8,7 +8,11 @@ file: /song1.mp3 Time: 4 Artist: æøå AlbumArtist: æøå +Composer: æøå +Performer: æøå Title: æøå Album: æøå +Genre: æøå +Comment: æøå&^`ൂ㔶 mtime: 1272319626 songList end diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index 60c30372..0f6bbf10 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -261,6 +261,22 @@ class MusicDatabaseFindTest(protocol.BaseTestCase): self.sendRequest('find albumartist "what"') self.assertInResponse('OK') + def test_find_composer(self): + self.sendRequest('find "composer" "what"') + self.assertInResponse('OK') + + def test_find_composer_without_quotes(self): + self.sendRequest('find composer "what"') + self.assertInResponse('OK') + + def test_find_performer(self): + self.sendRequest('find "performer" "what"') + self.assertInResponse('OK') + + def test_find_performer_without_quotes(self): + self.sendRequest('find performer "what"') + self.assertInResponse('OK') + def test_find_filename(self): self.sendRequest('find "filename" "afilename"') self.assertInResponse('OK') @@ -297,6 +313,14 @@ class MusicDatabaseFindTest(protocol.BaseTestCase): self.sendRequest('find "track" ""') self.assertInResponse('OK') + def test_find_genre(self): + self.sendRequest('find "genre" "what"') + self.assertInResponse('OK') + + def test_find_genre_without_quotes(self): + self.sendRequest('find genre "what"') + self.assertInResponse('OK') + def test_find_date(self): self.sendRequest('find "date" "2002-01-01"') self.assertInResponse('OK') @@ -456,6 +480,135 @@ class MusicDatabaseListTest(protocol.BaseTestCase): self.sendRequest('list "albumartist"') self.assertNotInResponse('Artist: ') + self.assertNotInResponse('Albumartist: ') + self.assertNotInResponse('Composer: ') + self.assertNotInResponse('Performer: ') + self.assertInResponse('OK') + + ### Composer + + def test_list_composer_with_quotes(self): + self.sendRequest('list "composer"') + self.assertInResponse('OK') + + def test_list_composer_without_quotes(self): + self.sendRequest('list composer') + self.assertInResponse('OK') + + def test_list_composer_without_quotes_and_capitalized(self): + self.sendRequest('list Composer') + self.assertInResponse('OK') + + def test_list_composer_with_query_of_one_token(self): + self.sendRequest('list "composer" "anartist"') + self.assertEqualResponse( + 'ACK [2@0] {list} should be "Album" for 3 arguments') + + def test_list_composer_with_unknown_field_in_query_returns_ack(self): + self.sendRequest('list "composer" "foo" "bar"') + self.assertEqualResponse('ACK [2@0] {list} not able to parse args') + + def test_list_composer_by_artist(self): + self.sendRequest('list "composer" "artist" "anartist"') + self.assertInResponse('OK') + + def test_list_composer_by_album(self): + self.sendRequest('list "composer" "album" "analbum"') + self.assertInResponse('OK') + + def test_list_composer_by_full_date(self): + self.sendRequest('list "composer" "date" "2001-01-01"') + self.assertInResponse('OK') + + def test_list_composer_by_year(self): + self.sendRequest('list "composer" "date" "2001"') + self.assertInResponse('OK') + + def test_list_composer_by_genre(self): + self.sendRequest('list "composer" "genre" "agenre"') + self.assertInResponse('OK') + + def test_list_composer_by_artist_and_album(self): + self.sendRequest( + 'list "composer" "artist" "anartist" "album" "analbum"') + self.assertInResponse('OK') + + def test_list_composer_without_filter_value(self): + self.sendRequest('list "composer" "artist" ""') + self.assertInResponse('OK') + + def test_list_composer_should_not_return_artists_without_names(self): + self.backend.library.dummy_find_exact_result = SearchResult( + tracks=[Track(composers=[Artist(name='')])]) + + self.sendRequest('list "composer"') + self.assertNotInResponse('Artist: ') + self.assertNotInResponse('Albumartist: ') + self.assertNotInResponse('Composer: ') + self.assertNotInResponse('Performer: ') + self.assertInResponse('OK') + + ### Performer + + def test_list_performer_with_quotes(self): + self.sendRequest('list "performer"') + self.assertInResponse('OK') + + def test_list_performer_without_quotes(self): + self.sendRequest('list performer') + self.assertInResponse('OK') + + def test_list_performer_without_quotes_and_capitalized(self): + self.sendRequest('list Albumartist') + self.assertInResponse('OK') + + def test_list_performer_with_query_of_one_token(self): + self.sendRequest('list "performer" "anartist"') + self.assertEqualResponse( + 'ACK [2@0] {list} should be "Album" for 3 arguments') + + def test_list_performer_with_unknown_field_in_query_returns_ack(self): + self.sendRequest('list "performer" "foo" "bar"') + self.assertEqualResponse('ACK [2@0] {list} not able to parse args') + + def test_list_performer_by_artist(self): + self.sendRequest('list "performer" "artist" "anartist"') + self.assertInResponse('OK') + + def test_list_performer_by_album(self): + self.sendRequest('list "performer" "album" "analbum"') + self.assertInResponse('OK') + + def test_list_performer_by_full_date(self): + self.sendRequest('list "performer" "date" "2001-01-01"') + self.assertInResponse('OK') + + def test_list_performer_by_year(self): + self.sendRequest('list "performer" "date" "2001"') + self.assertInResponse('OK') + + def test_list_performer_by_genre(self): + self.sendRequest('list "performer" "genre" "agenre"') + self.assertInResponse('OK') + + def test_list_performer_by_artist_and_album(self): + self.sendRequest( + 'list "performer" "artist" "anartist" "album" "analbum"') + self.assertInResponse('OK') + + def test_list_performer_without_filter_value(self): + self.sendRequest('list "performer" "artist" ""') + self.assertInResponse('OK') + + def test_list_performer_should_not_return_artists_without_names(self): + self.backend.library.dummy_find_exact_result = SearchResult( + tracks=[Track(performers=[Artist(name='')])]) + + self.sendRequest('list "performer"') + self.assertNotInResponse('Artist: ') + self.assertNotInResponse('Albumartist: ') + self.assertNotInResponse('Composer: ') + self.assertNotInResponse('Performer: ') self.assertInResponse('OK') ### Album @@ -492,6 +645,14 @@ class MusicDatabaseListTest(protocol.BaseTestCase): self.sendRequest('list "album" "albumartist" "anartist"') self.assertInResponse('OK') + def test_list_album_by_composer(self): + self.sendRequest('list "album" "composer" "anartist"') + self.assertInResponse('OK') + + def test_list_album_by_performer(self): + self.sendRequest('list "album" "performer" "anartist"') + self.assertInResponse('OK') + def test_list_album_by_full_date(self): self.sendRequest('list "album" "date" "2001-01-01"') self.assertInResponse('OK') @@ -679,6 +840,30 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase): self.sendRequest('search "albumartist" ""') self.assertInResponse('OK') + def test_search_composer(self): + self.sendRequest('search "composer" "acomposer"') + self.assertInResponse('OK') + + def test_search_composer_without_quotes(self): + self.sendRequest('search composer "acomposer"') + self.assertInResponse('OK') + + def test_search_composer_without_filter_value(self): + self.sendRequest('search "composer" ""') + self.assertInResponse('OK') + + def test_search_performer(self): + self.sendRequest('search "performer" "aperformer"') + self.assertInResponse('OK') + + def test_search_performer_without_quotes(self): + self.sendRequest('search performer "aperformer"') + self.assertInResponse('OK') + + def test_search_performer_without_filter_value(self): + self.sendRequest('search "performer" ""') + self.assertInResponse('OK') + def test_search_filename(self): self.sendRequest('search "filename" "afilename"') self.assertInResponse('OK') @@ -739,6 +924,18 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase): self.sendRequest('search "track" ""') self.assertInResponse('OK') + def test_search_genre(self): + self.sendRequest('search "genre" "agenre"') + self.assertInResponse('OK') + + def test_search_genre_without_quotes(self): + self.sendRequest('search genre "agenre"') + self.assertInResponse('OK') + + def test_search_genre_without_filter_value(self): + self.sendRequest('search "genre" ""') + self.assertInResponse('OK') + def test_search_date(self): self.sendRequest('search "date" "2002-01-01"') self.assertInResponse('OK') diff --git a/tests/frontends/mpd/translator_test.py b/tests/frontends/mpd/translator_test.py index 96d5f316..1b89c283 100644 --- a/tests/frontends/mpd/translator_test.py +++ b/tests/frontends/mpd/translator_test.py @@ -17,7 +17,12 @@ class TrackMpdFormatTest(unittest.TestCase): album=Album(name='an album', num_tracks=13, artists=[Artist(name='an other artist')]), track_no=7, + composers=[Artist(name='a composer')], + performers=[Artist(name='a performer')], + genre='a genre', date=datetime.date(1977, 1, 1), + disc_no='1', + comment='a comment', length=137000, ) @@ -61,11 +66,16 @@ class TrackMpdFormatTest(unittest.TestCase): self.assertIn(('Title', 'a name'), result) self.assertIn(('Album', 'an album'), result) self.assertIn(('AlbumArtist', 'an other artist'), result) + self.assertIn(('Composer', 'a composer'), result) + self.assertIn(('Performer', 'a performer'), result) + self.assertIn(('Genre', 'a genre'), result) self.assertIn(('Track', '7/13'), result) self.assertIn(('Date', datetime.date(1977, 1, 1)), result) + self.assertIn(('Disc', '1'), result) + self.assertIn(('Comment', 'a comment'), result) self.assertIn(('Pos', 9), result) self.assertIn(('Id', 122), result) - self.assertEqual(len(result), 10) + self.assertEqual(len(result), 15) def test_track_to_mpd_format_musicbrainz_trackid(self): track = self.track.copy(musicbrainz_id='foo') From 862b7019e06a1ec3c5d287a6bcc26bdfb39f2f9a Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sun, 3 Nov 2013 01:42:42 +0100 Subject: [PATCH 06/43] Add missing regexp --- mopidy/frontends/mpd/translator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index d6da15fa..1d0676bd 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -209,6 +209,7 @@ MPD_SEARCH_QUERY_RE = re.compile(r""" [Aa]lbum | [Aa]rtist | [Aa]lbumartist + | [Cc]omment | [Cc]omposer | [Dd]ate | [Ff]ile @@ -231,6 +232,7 @@ MPD_SEARCH_QUERY_PART_RE = re.compile(r""" [Aa]lbum | [Aa]rtist | [Aa]lbumartist + | [Cc]omment | [Cc]omposer | [Dd]ate | [Ff]ile From 9d7db4cf4f2305b123c96a47ccb1ddc231023b21 Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sun, 3 Nov 2013 01:52:34 +0100 Subject: [PATCH 07/43] Add frontend MPD tests for 'search comment "foo"' --- tests/frontends/mpd/protocol/music_db_test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index 0f6bbf10..3e0439e6 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -952,6 +952,18 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase): self.sendRequest('search "date" ""') self.assertInResponse('OK') + def test_search_comment(self): + self.sendRequest('search "comment" "acomment"') + self.assertInResponse('OK') + + def test_search_comment_without_quotes(self): + self.sendRequest('search comment "acomment"') + self.assertInResponse('OK') + + def test_search_comment_without_filter_value(self): + self.sendRequest('search "comment" ""') + self.assertInResponse('OK') + def test_search_else_should_fail(self): self.sendRequest('search "sometype" "something"') self.assertEqualResponse('ACK [2@0] {search} incorrect arguments') From 483ced3c16fce1372301685f3326d53be9bcaaae Mon Sep 17 00:00:00 2001 From: David Eisner Date: Tue, 5 Nov 2013 10:33:04 +0000 Subject: [PATCH 08/43] Avahi python compatibility fix --- mopidy/frontends/mpd/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index b48b2b65..f2e203c6 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -47,7 +47,7 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): lo = re.search('(? Date: Tue, 5 Nov 2013 10:37:54 +0000 Subject: [PATCH 09/43] Avahi wrapper moved to utils --- mopidy/frontends/mpd/actor.py | 2 +- mopidy/{frontends/mpd => utils}/zeroconf.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename mopidy/{frontends/mpd => utils}/zeroconf.py (100%) diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index f2e203c6..ab5bd35d 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -47,7 +47,7 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): lo = re.search('(? Date: Tue, 5 Nov 2013 11:06:51 +0000 Subject: [PATCH 10/43] Avahi constants named --- mopidy/utils/zeroconf.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/zeroconf.py b/mopidy/utils/zeroconf.py index db92877a..750f6731 100644 --- a/mopidy/utils/zeroconf.py +++ b/mopidy/utils/zeroconf.py @@ -2,6 +2,10 @@ import dbus __all__ = ["Zeroconf"] +avahi_IF_UNSPEC = -1 +avahi_PROTO_UNSPEC = -1 +avahi_PublishFlags_None = 0 + class Zeroconf: """A simple class to publish a network service with zeroconf using @@ -31,7 +35,8 @@ class Zeroconf: server.EntryGroupNew()), "org.freedesktop.Avahi.EntryGroup") - g.AddService(-1, -1, dbus.UInt32(0), + g.AddService(avahi_IF_UNSPEC, avahi_PROTO_UNSPEC, + dbus.UInt32(avahi_PublishFlags_None), self.name, self.stype, self.domain, self.host, dbus.UInt16(self.port), self.text) From c4281339b6401d5c83f6231b4b5ade3e8a2334cd Mon Sep 17 00:00:00 2001 From: David Eisner Date: Tue, 5 Nov 2013 11:07:35 +0000 Subject: [PATCH 11/43] Avahi hostname choice extracted for reuse --- mopidy/frontends/mpd/actor.py | 5 +---- mopidy/utils/zeroconf.py | 6 +++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index ab5bd35d..66218593 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -43,14 +43,11 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): try: if self.config['mpd']['zeroconf_enabled']: name = self.config['mpd']['zeroconf_name'] - import re - lo = re.search('(? Date: Tue, 5 Nov 2013 13:05:05 +0100 Subject: [PATCH 12/43] core: Letting filter() accept lists --- mopidy/core/tracklist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index dbc81945..9f7d40ee 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -318,10 +318,10 @@ class TracklistController(object): matches = self._tl_tracks for (key, value) in criteria.iteritems(): if key == 'tlid': - matches = filter(lambda ct: ct.tlid == value, matches) + matches = filter(lambda ct: value in ct.tlid, matches) else: matches = filter( - lambda ct: getattr(ct.track, key) == value, matches) + lambda ct: value in getattr(ct.track, key), matches) return matches def move(self, start, end, to_position): From 32b01f4e4aba65ef7e11fe27378e13d04d65f3d8 Mon Sep 17 00:00:00 2001 From: Javier Domingo Cansino Date: Tue, 5 Nov 2013 13:17:55 +0100 Subject: [PATCH 13/43] Adding a 'to list' conversion and correcting horrible mistake in comparison (put it reverse) --- mopidy/core/tracklist.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index 9f7d40ee..002b0d0d 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -317,11 +317,13 @@ class TracklistController(object): criteria = criteria or kwargs matches = self._tl_tracks for (key, value) in criteria.iteritems(): + if not type(value) is list: + value = [value] if key == 'tlid': - matches = filter(lambda ct: value in ct.tlid, matches) + matches = filter(lambda ct: ct.tlid in value, matches) else: matches = filter( - lambda ct: value in getattr(ct.track, key), matches) + lambda ct: getattr(ct.track, key) in value, matches) return matches def move(self, start, end, to_position): From 697bff81cdad59c7c51f54237f076d1d690ea737 Mon Sep 17 00:00:00 2001 From: David Eisner Date: Tue, 5 Nov 2013 16:23:58 +0000 Subject: [PATCH 14/43] Advertise HTTP with Avahi --- mopidy/frontends/http/__init__.py | 2 ++ mopidy/frontends/http/actor.py | 26 ++++++++++++++++++++++++-- mopidy/frontends/http/ext.conf | 2 ++ mopidy/frontends/mpd/actor.py | 6 +++--- mopidy/frontends/mpd/ext.conf | 2 +- mopidy/utils/zeroconf.py | 5 ++++- 6 files changed, 36 insertions(+), 7 deletions(-) diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index 6d84b25b..d5f8f1bc 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -21,6 +21,8 @@ class Extension(ext.Extension): schema['hostname'] = config.Hostname() schema['port'] = config.Port() schema['static_dir'] = config.Path(optional=True) + schema['zeroconf_enabled'] = config.Boolean() + schema['zeroconf_name'] = config.String() return schema def validate_environment(self): diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index 5e49d2cd..47602b33 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -28,10 +28,13 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): self._setup_logging(app) def _setup_server(self): + self.config_section = self.config['http'] + self.hostname = self.config_section['hostname'] + self.port = self.config_section['port'] cherrypy.config.update({ 'engine.autoreload_on': False, - 'server.socket_host': self.config['http']['hostname'], - 'server.socket_port': self.config['http']['port'], + 'server.socket_host': self.hostname, + 'server.socket_port': self.port, }) def _setup_websocket_plugin(self): @@ -87,11 +90,30 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): logger.debug('Starting HTTP server') cherrypy.engine.start() logger.info('HTTP server running at %s', cherrypy.server.base()) + try: + if self.config_section['zeroconf_enabled']: + name = self.config_section['zeroconf_name'] + + from mopidy.utils.zeroconf import Zeroconf + self.service = Zeroconf( + stype="_http._tcp", + name=name, port=self.port, host=self.hostname, + text=["path=/"]) + self.service.publish() + + logger.info('Registered with Avahi as %s', name) + except Exception as e: + logger.warning('Avahi registration failed (%s)', e) def on_stop(self): logger.debug('Stopping HTTP server') cherrypy.engine.exit() logger.info('Stopped HTTP server') + try: + if self.service: + self.service.unpublish() + except Exception as e: + logger.warning('Avahi unregistration failed (%s)', e) def on_event(self, name, **data): event = data diff --git a/mopidy/frontends/http/ext.conf b/mopidy/frontends/http/ext.conf index 04fb1aae..f3df5f1a 100644 --- a/mopidy/frontends/http/ext.conf +++ b/mopidy/frontends/http/ext.conf @@ -3,6 +3,8 @@ enabled = true hostname = 127.0.0.1 port = 6680 static_dir = +zeroconf_enabled = true +zeroconf_name = Mopidy [loglevels] cherrypy = warning diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index 66218593..b252ee2d 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -17,7 +17,7 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): super(MpdFrontend, self).__init__() hostname = network.format_hostname(config['mpd']['hostname']) port = config['mpd']['port'] - self.config = config + self.config_section = config['mpd'] self.hostname = hostname self.port = port @@ -41,8 +41,8 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): def on_start(self): try: - if self.config['mpd']['zeroconf_enabled']: - name = self.config['mpd']['zeroconf_name'] + if self.config_section['zeroconf_enabled']: + name = self.config_section['zeroconf_name'] from mopidy.utils.zeroconf import Zeroconf self.service = Zeroconf( diff --git a/mopidy/frontends/mpd/ext.conf b/mopidy/frontends/mpd/ext.conf index 667aff24..d51c04f6 100644 --- a/mopidy/frontends/mpd/ext.conf +++ b/mopidy/frontends/mpd/ext.conf @@ -6,4 +6,4 @@ password = max_connections = 20 connection_timeout = 60 zeroconf_enabled = true -zeroconf_name = Mopidy +zeroconf_name = Mopidy (MPD) diff --git a/mopidy/utils/zeroconf.py b/mopidy/utils/zeroconf.py index d77738b4..4e802f7c 100644 --- a/mopidy/utils/zeroconf.py +++ b/mopidy/utils/zeroconf.py @@ -14,7 +14,7 @@ class Zeroconf: """ def __init__(self, name, port, stype="_http._tcp", - domain="", host="", text=""): + domain="", host="", text=[]): self.name = name self.stype = stype self.domain = domain @@ -39,6 +39,9 @@ class Zeroconf: server.EntryGroupNew()), "org.freedesktop.Avahi.EntryGroup") + if self.text: + self.text = [[dbus.Byte(ord(c)) for c in s] for s in self.text] + g.AddService(avahi_IF_UNSPEC, avahi_PROTO_UNSPEC, dbus.UInt32(avahi_PublishFlags_None), self.name, self.stype, self.domain, self.host, From af6225538db0ce8d7d947c77e2bc18188c22ce09 Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Tue, 5 Nov 2013 23:17:15 +0100 Subject: [PATCH 15/43] Add more tests --- tests/scanner_test.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 1102c525..3c5485f8 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -24,6 +24,8 @@ class TranslatorTest(unittest.TestCase): 'album': 'albumname', 'track-number': 1, 'artist': 'name', + 'composer': 'composer', + 'performer': 'performer', 'album-artist': 'albumartistname', 'title': 'trackname', 'track-count': 2, @@ -31,7 +33,9 @@ class TranslatorTest(unittest.TestCase): 'album-disc-count': 3, 'date': FakeGstDate(2006, 1, 1,), 'container-format': 'ID3 tag', + 'genre': 'genre', 'duration': 4531, + 'comment': 'comment', 'musicbrainz-trackid': 'mbtrackid', 'musicbrainz-albumid': 'mbalbumid', 'musicbrainz-artistid': 'mbartistid', @@ -51,6 +55,16 @@ class TranslatorTest(unittest.TestCase): 'musicbrainz_id': 'mbartistid', } + self.composer = { + 'name': 'composer', + #'musicbrainz_id': 'mbcomposerid', + } + + self.performer = { + 'name': 'performer', + #'musicbrainz_id': 'mbperformerid', + } + self.albumartist = { 'name': 'albumartistname', 'musicbrainz_id': 'mbalbumartistid', @@ -60,8 +74,10 @@ class TranslatorTest(unittest.TestCase): 'uri': 'uri', 'name': 'trackname', 'date': '2006-01-01', + 'genre': 'genre', 'track_no': 1, 'disc_no': 2, + 'comment': 'comment', 'length': 4531, 'musicbrainz_id': 'mbtrackid', 'last_modified': 1234, @@ -72,6 +88,10 @@ class TranslatorTest(unittest.TestCase): self.album['artists'] = [Artist(**self.albumartist)] self.track['album'] = Album(**self.album) self.track['artists'] = [Artist(**self.artist)] + if self.composer: + self.track['composers'] = [Artist(**self.composer)] + if self.performer: + self.track['performers'] = [Artist(**self.performer)] return Track(**self.track) def check(self): @@ -117,6 +137,16 @@ class TranslatorTest(unittest.TestCase): del self.artist['name'] self.check() + def test_missing_composer_name(self): + del self.data['composer'] + del self.composer['name'] + self.check() + + def test_missing_performer_name(self): + del self.data['performer'] + del self.performer['name'] + self.check() + def test_missing_artist_musicbrainz_id(self): del self.data['musicbrainz-artistid'] del self.artist['musicbrainz_id'] @@ -132,6 +162,11 @@ class TranslatorTest(unittest.TestCase): del self.albumartist['musicbrainz_id'] self.check() + def test_missing_genre(self): + del self.data['genre'] + del self.track['genre'] + self.check() + def test_missing_date(self): del self.data['date'] del self.track['date'] @@ -142,6 +177,11 @@ class TranslatorTest(unittest.TestCase): del self.track['date'] self.check() + def test_missing_comment(self): + del self.data['comment'] + del self.track['comment'] + self.check() + class ScannerTest(unittest.TestCase): def setUp(self): From 8b5f30e5ffe736ef95e64a8a0ec5bd91883ed28c Mon Sep 17 00:00:00 2001 From: Javier Domingo Cansino Date: Thu, 7 Nov 2013 09:34:40 +0100 Subject: [PATCH 16/43] Adding test case --- tests/backends/local/tracklist_test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/backends/local/tracklist_test.py b/tests/backends/local/tracklist_test.py index 8310ce1a..21c06673 100644 --- a/tests/backends/local/tracklist_test.py +++ b/tests/backends/local/tracklist_test.py @@ -239,6 +239,18 @@ class LocalTracklistProviderTest(unittest.TestCase): def test_removing_from_empty_playlist_does_nothing(self): self.controller.remove(uri='/nonexistant') + @populate_tracklist + def test_remove_lists(self): + track1 = self.controller.tracks[1] + track2 = self.controller.tracks[2] + track3 = self.controller.tracks[3] + version = self.controller.version + self.controller.remove(uri=[track1.uri, track3.uri]) + self.assertLess(version, self.controller.version) + self.assertNotIn(track1, self.controller.tracks) + self.assertNotIn(track3, self.controller.tracks) + self.assertEqual(track2, self.controller.tracks[1]) + @populate_tracklist def test_shuffle(self): random.seed(1) From 6721a59b266b9f3209e792910255ba3565c07469 Mon Sep 17 00:00:00 2001 From: Javier Domingo Cansino Date: Thu, 7 Nov 2013 09:46:34 +0100 Subject: [PATCH 17/43] tests: Fixing self confusion mistake docs: Documenting tracklist's new filter() feature --- mopidy/core/tracklist.py | 16 ++++++++++++++++ tests/backends/local/tracklist_test.py | 10 +++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index 002b0d0d..5c2d91e0 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -298,18 +298,34 @@ class TracklistController(object): filter({'tlid': 7}) filter(tlid=7) + # Returns tracks with TLIDs 1, 2, 3 and 4 + filter({'tlid': [1, 2, 3, 4]}) + filter(tlid=[1, 2, 3, 4]) + # Returns track with ID 1 filter({'id': 1}) filter(id=1) + # Returns track with IDs 1 5 and 7 + filter({'id': [1, 5, 7]}) + filter(id=[1, 5, 7]) + # Returns track with URI 'xyz' filter({'uri': 'xyz'}) filter(uri='xyz') + # Returns track with URIs 'xyz' and 'abc' + filter({'uri': ['xyz', 'abc']}) + filter(uri=['xyz', 'abc']) + # Returns track with ID 1 and URI 'xyz' filter({'id': 1, 'uri': 'xyz'}) filter(id=1, uri='xyz') + # Returns track with IDs (1, 3 or 6) and URIs ('xyz' or 'abc') + filter({'id': [1, 3, 6], 'uri': ['xyz', 'abc']}) + filter(id=[1, 3, 6], uri=['xyz', 'abc']) + :param criteria: on or more criteria to match by :type criteria: dict :rtype: list of :class:`mopidy.models.TlTrack` diff --git a/tests/backends/local/tracklist_test.py b/tests/backends/local/tracklist_test.py index 21c06673..90eadeb9 100644 --- a/tests/backends/local/tracklist_test.py +++ b/tests/backends/local/tracklist_test.py @@ -241,15 +241,15 @@ class LocalTracklistProviderTest(unittest.TestCase): @populate_tracklist def test_remove_lists(self): + track0 = self.controller.tracks[0] track1 = self.controller.tracks[1] track2 = self.controller.tracks[2] - track3 = self.controller.tracks[3] version = self.controller.version - self.controller.remove(uri=[track1.uri, track3.uri]) + self.controller.remove(uri=[track0.uri, track2.uri]) self.assertLess(version, self.controller.version) - self.assertNotIn(track1, self.controller.tracks) - self.assertNotIn(track3, self.controller.tracks) - self.assertEqual(track2, self.controller.tracks[1]) + self.assertNotIn(track0, self.controller.tracks) + self.assertNotIn(track2, self.controller.tracks) + self.assertEqual(track1, self.controller.tracks[0]) @populate_tracklist def test_shuffle(self): From 45a38cdaf1510c1b17c8ecb1f830b9792de7b071 Mon Sep 17 00:00:00 2001 From: Javier Domingo Cansino Date: Thu, 7 Nov 2013 10:51:29 +0100 Subject: [PATCH 18/43] core: Changing input to accept also sets, as might be also used. --- mopidy/core/tracklist.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index 5c2d91e0..584a8bb9 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -290,7 +290,8 @@ class TracklistController(object): def filter(self, criteria=None, **kwargs): """ - Filter the tracklist by the given criterias. + Filter the tracklist by the given criterias. The value of the field to + check can be a list or a set. Examples:: @@ -333,7 +334,7 @@ class TracklistController(object): criteria = criteria or kwargs matches = self._tl_tracks for (key, value) in criteria.iteritems(): - if not type(value) is list: + if not type(value) in [list, set]: value = [value] if key == 'tlid': matches = filter(lambda ct: ct.tlid in value, matches) From 71bae709ef3143ff00f1b0f7b11385984bc11cc2 Mon Sep 17 00:00:00 2001 From: Javier Domingo Cansino Date: Thu, 7 Nov 2013 11:05:33 +0100 Subject: [PATCH 19/43] core: filter() to accept also tuples --- mopidy/core/tracklist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index 584a8bb9..012dd796 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -291,7 +291,7 @@ class TracklistController(object): def filter(self, criteria=None, **kwargs): """ Filter the tracklist by the given criterias. The value of the field to - check can be a list or a set. + check can be a list, a tuple or a set. Examples:: @@ -334,7 +334,7 @@ class TracklistController(object): criteria = criteria or kwargs matches = self._tl_tracks for (key, value) in criteria.iteritems(): - if not type(value) in [list, set]: + if not type(value) in [list, tuple, set]: value = [value] if key == 'tlid': matches = filter(lambda ct: ct.tlid in value, matches) From 03750a8bf22251ec96060b070e7bcf113b9f13f8 Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Fri, 8 Nov 2013 21:25:21 +0100 Subject: [PATCH 20/43] Fix scan with multiple track artists and add tests --- mopidy/audio/scan.py | 8 +++++++- tests/audio/scan_test.py | 24 ++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 82803379..1d5f85f6 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -144,6 +144,12 @@ def audio_data_to_track(data): track_kwargs['last_modified'] = int(data['mtime']) track_kwargs['length'] = data[gst.TAG_DURATION] // gst.MSECOND track_kwargs['album'] = Album(**album_kwargs) - track_kwargs['artists'] = [Artist(**artist_kwargs)] + + if ('name' in artist_kwargs and + type(artist_kwargs['name']) is list): + track_kwargs['artists'] = [Artist(**{'name': artist}) + for artist in artist_kwargs['name']] + else: + track_kwargs['artists'] = [Artist(**artist_kwargs)] return Track(**track_kwargs) diff --git a/tests/audio/scan_test.py b/tests/audio/scan_test.py index b53b0b57..6fda7f74 100644 --- a/tests/audio/scan_test.py +++ b/tests/audio/scan_test.py @@ -46,11 +46,18 @@ class TranslatorTest(unittest.TestCase): 'musicbrainz_id': 'mbalbumid', } - self.artist = { + self.artist_single = { 'name': 'name', 'musicbrainz_id': 'mbartistid', } + self.artist_multiple = { + 'name': ['name1', 'name2'], + 'musicbrainz_id': 'mbartistid', + } + + self.artist = self.artist_single + self.albumartist = { 'name': 'albumartistname', 'musicbrainz_id': 'mbalbumartistid', @@ -71,7 +78,14 @@ class TranslatorTest(unittest.TestCase): if self.albumartist: self.album['artists'] = [Artist(**self.albumartist)] self.track['album'] = Album(**self.album) - self.track['artists'] = [Artist(**self.artist)] + + if ('name' in self.artist and + type(self.artist['name']) is list): + self.track['artists'] = [Artist(**{'name': artist}) + for artist in self.artist['name']] + else: + self.track['artists'] = [Artist(**self.artist)] + return Track(**self.track) def check(self): @@ -122,6 +136,12 @@ class TranslatorTest(unittest.TestCase): del self.artist['musicbrainz_id'] self.check() + def test_multiple_track_artists(self): + self.data['artist'] = ['name1', 'name2'] + self.data['musicbrainz-artistid'] = 'mbartistid' + self.artist = self.artist_multiple + self.check() + def test_missing_album_artist(self): del self.data['album-artist'] del self.albumartist['name'] From 093c0400a9ff28d0fe6a3cadeb7b2bd283afc112 Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Fri, 8 Nov 2013 21:43:31 +0100 Subject: [PATCH 21/43] Fix review comments --- mopidy/audio/scan.py | 2 +- tests/audio/scan_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 1d5f85f6..52885000 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -147,7 +147,7 @@ def audio_data_to_track(data): if ('name' in artist_kwargs and type(artist_kwargs['name']) is list): - track_kwargs['artists'] = [Artist(**{'name': artist}) + track_kwargs['artists'] = [Artist(name=artist) for artist in artist_kwargs['name']] else: track_kwargs['artists'] = [Artist(**artist_kwargs)] diff --git a/tests/audio/scan_test.py b/tests/audio/scan_test.py index 6fda7f74..2a669bd6 100644 --- a/tests/audio/scan_test.py +++ b/tests/audio/scan_test.py @@ -81,7 +81,7 @@ class TranslatorTest(unittest.TestCase): if ('name' in self.artist and type(self.artist['name']) is list): - self.track['artists'] = [Artist(**{'name': artist}) + self.track['artists'] = [Artist(name=artist) for artist in self.artist['name']] else: self.track['artists'] = [Artist(**self.artist)] From c776565f656dd1a3a3812e9f8ddb388a1835e1c3 Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sat, 9 Nov 2013 01:22:27 +0100 Subject: [PATCH 22/43] Another round of review comments --- mopidy/audio/scan.py | 4 ++-- tests/audio/scan_test.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 52885000..071857df 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -145,8 +145,8 @@ def audio_data_to_track(data): track_kwargs['length'] = data[gst.TAG_DURATION] // gst.MSECOND track_kwargs['album'] = Album(**album_kwargs) - if ('name' in artist_kwargs and - type(artist_kwargs['name']) is list): + if ('name' in artist_kwargs + and not isinstance(artist_kwargs['name'], basestring)): track_kwargs['artists'] = [Artist(name=artist) for artist in artist_kwargs['name']] else: diff --git a/tests/audio/scan_test.py b/tests/audio/scan_test.py index 2a669bd6..efeb3877 100644 --- a/tests/audio/scan_test.py +++ b/tests/audio/scan_test.py @@ -79,8 +79,8 @@ class TranslatorTest(unittest.TestCase): self.album['artists'] = [Artist(**self.albumartist)] self.track['album'] = Album(**self.album) - if ('name' in self.artist and - type(self.artist['name']) is list): + if ('name' in self.artist + and not isinstance(self.artist['name'], basestring)): self.track['artists'] = [Artist(name=artist) for artist in self.artist['name']] else: From 2bd1f043ceacfcd951f4bdcb68a2a36207e7c4cc Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sat, 9 Nov 2013 03:01:53 +0100 Subject: [PATCH 23/43] Updated code with new multiple artist handling and added tests --- mopidy/audio/scan.py | 22 ++++++++++++++----- tests/audio/scan_test.py | 47 +++++++++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index b0ddc2af..8499f73c 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -146,16 +146,26 @@ def audio_data_to_track(data): if albumartist_kwargs: album_kwargs['artists'] = [Artist(**albumartist_kwargs)] - if composer_kwargs: - track_kwargs['composers'] = [Artist(**composer_kwargs)] - - if performer_kwargs: - track_kwargs['performers'] = [Artist(**performer_kwargs)] - track_kwargs['uri'] = data['uri'] track_kwargs['last_modified'] = int(data['mtime']) track_kwargs['length'] = data[gst.TAG_DURATION] // gst.MSECOND track_kwargs['album'] = Album(**album_kwargs) track_kwargs['artists'] = [Artist(**artist_kwargs)] + if ('name' in composer_kwargs + and not isinstance(composer_kwargs['name'], basestring)): + track_kwargs['composers'] = [Artist(name=artist) + for artist in composer_kwargs['name']] + else: + track_kwargs['composers'] = \ + [Artist(**composer_kwargs)] if composer_kwargs else '' + + if ('name' in performer_kwargs + and not isinstance(performer_kwargs['name'], basestring)): + track_kwargs['performers'] = [Artist(name=artist) + for artist in performer_kwargs['name']] + else: + track_kwargs['performers'] = \ + [Artist(**performer_kwargs)] if performer_kwargs else '' + return Track(**track_kwargs) diff --git a/tests/audio/scan_test.py b/tests/audio/scan_test.py index 3587ac1c..bea34d0b 100644 --- a/tests/audio/scan_test.py +++ b/tests/audio/scan_test.py @@ -55,14 +55,26 @@ class TranslatorTest(unittest.TestCase): 'musicbrainz_id': 'mbartistid', } - self.composer = { + self.composer_single = { 'name': 'composer', } - self.performer = { + self.composer_multiple = { + 'name': ['composer1', 'composer2'], + } + + self.composer = self.composer_single + + self.performer_single = { 'name': 'performer', } + self.performer_multiple = { + 'name': ['performer1', 'performer2'], + } + + self.performer = self.performer_single + self.albumartist = { 'name': 'albumartistname', 'musicbrainz_id': 'mbalbumartistid', @@ -86,10 +98,23 @@ class TranslatorTest(unittest.TestCase): self.album['artists'] = [Artist(**self.albumartist)] self.track['album'] = Album(**self.album) self.track['artists'] = [Artist(**self.artist)] - if self.composer: - self.track['composers'] = [Artist(**self.composer)] - if self.performer: - self.track['performers'] = [Artist(**self.performer)] + + if ('name' in self.composer + and not isinstance(self.composer['name'], basestring)): + self.track['composers'] = [Artist(name=artist) + for artist in self.composer['name']] + else: + self.track['composers'] = [Artist(**self.composer)] \ + if self.composer else '' + + if ('name' in self.performer + and not isinstance(self.performer['name'], basestring)): + self.track['performers'] = [Artist(name=artist) + for artist in self.performer['name']] + else: + self.track['performers'] = [Artist(**self.performer)] \ + if self.performer else '' + return Track(**self.track) def check(self): @@ -140,6 +165,16 @@ class TranslatorTest(unittest.TestCase): del self.composer['name'] self.check() + def test_multiple_track_composers(self): + self.data['composer'] = ['composer1', 'composer2'] + self.composer = self.composer_multiple + self.check() + + def test_multiple_track_performers(self): + self.data['performer'] = ['performer1', 'performer2'] + self.performer = self.performer_multiple + self.check() + def test_missing_performer_name(self): del self.data['performer'] del self.performer['name'] From deb442c54af4a5e30983e58e6b6e205b22ed8c25 Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sat, 9 Nov 2013 03:10:20 +0100 Subject: [PATCH 24/43] Update documentation --- docs/changelog.rst | 5 +++++ docs/ext/mpd.rst | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f9ccbb3e..189e06d3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,11 @@ v0.17.0 (UNRELEASED) (Fixes: :issue:`565`) - Fix scanner so that mtime is respected when deciding which files can be skipped. +**MPD frontend** + +- Add support for ``composer``, ``comment``, ``genre``, and ``performer``. + These tags can be used with ``list ...``, ``search ...``, and + ``find ...`` and their variants, and are supported in the ``any`` tag also v0.16.1 (2013-11-02) ==================== diff --git a/docs/ext/mpd.rst b/docs/ext/mpd.rst index 9dbcbe11..6c619e92 100644 --- a/docs/ext/mpd.rst +++ b/docs/ext/mpd.rst @@ -34,7 +34,6 @@ Items on this list will probably not be supported in the near future. - Crossfade is not supported - Replay gain is not supported - ``stats`` does not provide any statistics -- ``list`` does not support listing tracks by genre - ``decoders`` does not provide information about available decoders The following items are currently not supported, but should be added in the From 484efab28e5e8cc1cbe8335ab989bb14e833e33b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 10 Nov 2013 21:37:48 +0100 Subject: [PATCH 25/43] utils: Remove Python 2.6 workaround --- mopidy/utils/log.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index 715aca1a..dee0fb96 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -12,9 +12,8 @@ def setup_logging(config, verbosity_level, save_debug_log): setup_console_logging(config, verbosity_level) if save_debug_log: setup_debug_logging_to_file(config) - if hasattr(logging, 'captureWarnings'): - # New in Python 2.7 - logging.captureWarnings(True) + + logging.captureWarnings(True) if config['logging']['config_file']: logging.config.fileConfig(config['logging']['config_file']) From cf5589b4eba1315ce1931cd3b4bcb7930f25dfd5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Nov 2013 13:56:05 +0100 Subject: [PATCH 26/43] avahi: Style and code cleanups in zeroconf module. - Prepare for handling missing dbus directly in module. - Constants should always be all caps. - Extracted helpers from class to convey intent via their naming. - Moved imports out of class, imports should always be on the top. - Made sure calling publish mutiple times does not re-convert text. - Made sure calling unpublish without publish does not break. --- mopidy/utils/zeroconf.py | 69 +++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/mopidy/utils/zeroconf.py b/mopidy/utils/zeroconf.py index 4e802f7c..c69fb950 100644 --- a/mopidy/utils/zeroconf.py +++ b/mopidy/utils/zeroconf.py @@ -1,17 +1,30 @@ -import dbus +from __future__ import unicode_literals -__all__ = ["Zeroconf"] +try: + import dbus +except ImportError: + dbus = None -avahi_IF_UNSPEC = -1 -avahi_PROTO_UNSPEC = -1 -avahi_PublishFlags_None = 0 +import re + +_AVAHI_IF_UNSPEC = -1 +_AVAHI_PROTO_UNSPEC = -1 +_AVAHI_PUBLISHFLAGS_NONE = 0 + + +def _filter_loopback_and_meta_addresses(host): + # TODO: see if we can find a cleaner way of handling this. + if re.search(r'(? Date: Sat, 9 Nov 2013 14:04:38 +0100 Subject: [PATCH 27/43] avahi: Improve error handling for dbus errors. - Handle dbus not being installed. - Handle not finding the system bus. - Handle not finding the avahi service. - Return if publish succeeded. --- mopidy/utils/zeroconf.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/mopidy/utils/zeroconf.py b/mopidy/utils/zeroconf.py index c69fb950..62545e27 100644 --- a/mopidy/utils/zeroconf.py +++ b/mopidy/utils/zeroconf.py @@ -1,17 +1,21 @@ from __future__ import unicode_literals +import logging +import re + +logger = logging.getLogger('mopidy.utils.zerconf') + try: import dbus except ImportError: dbus = None -import re - _AVAHI_IF_UNSPEC = -1 _AVAHI_PROTO_UNSPEC = -1 _AVAHI_PUBLISHFLAGS_NONE = 0 + def _filter_loopback_and_meta_addresses(host): # TODO: see if we can find a cleaner way of handling this. if re.search(r'(? Date: Sat, 9 Nov 2013 14:21:41 +0100 Subject: [PATCH 28/43] avahi: Convert MPD and HTTP to use cleaned up versions - Move imports to top as they should be. - Report state based on publish return value. - Remove overly broad except clauses. - Unpublish before stopping servers. --- mopidy/frontends/http/actor.py | 31 ++++++++++++++----------------- mopidy/frontends/mpd/actor.py | 32 ++++++++++++++------------------ 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index 47602b33..d5c37f09 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -11,6 +11,7 @@ from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from mopidy import models from mopidy.core import CoreListener +from mopidy.utils import zeroconf from . import ws @@ -22,6 +23,7 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): super(HttpFrontend, self).__init__() self.config = config self.core = core + self.zeroconf_service = None self._setup_server() self._setup_websocket_plugin() app = self._create_app() @@ -90,30 +92,25 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): logger.debug('Starting HTTP server') cherrypy.engine.start() logger.info('HTTP server running at %s', cherrypy.server.base()) - try: - if self.config_section['zeroconf_enabled']: - name = self.config_section['zeroconf_name'] - from mopidy.utils.zeroconf import Zeroconf - self.service = Zeroconf( - stype="_http._tcp", - name=name, port=self.port, host=self.hostname, - text=["path=/"]) - self.service.publish() + if self.config_section['zeroconf_enabled']: + name = self.config_section['zeroconf_name'] + self.zeroconf_service = zeroconf.Zeroconf( + stype="_http._tcp", name=name, port=self.port, + host=self.hostname) - logger.info('Registered with Avahi as %s', name) - except Exception as e: - logger.warning('Avahi registration failed (%s)', e) + if self.zeroconf_service.publish(): + logger.info('Registered HTTP with zeroconf as %s', name) + else: + logger.warning('Registering HTTP with zeroconf failed.') def on_stop(self): + if self.zeroconf_service: + self.zeroconf_service.unpublish() + logger.debug('Stopping HTTP server') cherrypy.engine.exit() logger.info('Stopped HTTP server') - try: - if self.service: - self.service.unpublish() - except Exception as e: - logger.warning('Avahi unregistration failed (%s)', e) def on_event(self, name, **data): event = data diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index b252ee2d..51735837 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -7,7 +7,7 @@ import pykka from mopidy.core import CoreListener from mopidy.frontends.mpd import session -from mopidy.utils import encoding, network, process +from mopidy.utils import encoding, network, process, zeroconf logger = logging.getLogger('mopidy.frontends.mpd') @@ -20,6 +20,7 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): self.config_section = config['mpd'] self.hostname = hostname self.port = port + self.zeroconf_service = None try: network.Server( @@ -40,27 +41,22 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): logger.info('MPD server running at [%s]:%s', hostname, port) def on_start(self): - try: - if self.config_section['zeroconf_enabled']: - name = self.config_section['zeroconf_name'] + if self.config_section['zeroconf_enabled']: + name = self.config_section['zeroconf_name'] + self.zeroconf_service = zeroconf.Zeroconf( + stype="_mpd._tcp", name=name, port=self.port, + host=self.hostname) - from mopidy.utils.zeroconf import Zeroconf - self.service = Zeroconf( - stype="_mpd._tcp", - name=name, port=self.port, host=self.hostname) - self.service.publish() - - logger.info('Registered with Avahi as %s', name) - except Exception as e: - logger.warning('Avahi registration failed (%s)', e) + if self.zeroconf_service.publish(): + logger.info('Registered MPD with zeroconf as %s', name) + else: + logger.warning('Registering MPD with zeroconf failed.') def on_stop(self): + if self.zeroconf_service: + self.zeroconf_service.unpublish() + process.stop_actors_by_class(session.MpdSession) - try: - if self.service: - self.service.unpublish() - except Exception as e: - logger.warning('Avahi unregistration failed (%s)', e) def send_idle(self, subsystem): listeners = pykka.ActorRegistry.get_by_class(session.MpdSession) From 6c6932c374d5be7de6b69d50ae40a09f423218b0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Nov 2013 17:22:37 +0100 Subject: [PATCH 29/43] avahi: Only convert single string at a time in helper --- mopidy/utils/zeroconf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/zeroconf.py b/mopidy/utils/zeroconf.py index 62545e27..8af63777 100644 --- a/mopidy/utils/zeroconf.py +++ b/mopidy/utils/zeroconf.py @@ -24,7 +24,7 @@ def _filter_loopback_and_meta_addresses(host): def _convert_text_to_dbus_bytes(text): - return [[dbus.Byte(ord(c)) for c in s] for s in text] + return [dbus.Byte(ord(c)) for c in text] class Zeroconf: @@ -62,7 +62,7 @@ class Zeroconf: bus.get_object("org.freedesktop.Avahi", server.EntryGroupNew()), "org.freedesktop.Avahi.EntryGroup") - text = _convert_text_to_dbus_bytes(self.text) + text = [_convert_text_to_dbus_bytes(t) for t in self.text] self.group.AddService(_AVAHI_IF_UNSPEC, _AVAHI_PROTO_UNSPEC, dbus.UInt32(_AVAHI_PUBLISHFLAGS_NONE), self.name, self.stype, self.domain, self.host, From fb31193ed057ea87d5477ab302e0cd61c9b79dcf Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 11 Nov 2013 21:26:11 +0100 Subject: [PATCH 30/43] avahi: Fixes from review comments --- mopidy/frontends/http/actor.py | 8 ++++---- mopidy/frontends/mpd/actor.py | 8 ++++---- mopidy/utils/zeroconf.py | 5 ++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index d5c37f09..ad5441c3 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -96,13 +96,13 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): if self.config_section['zeroconf_enabled']: name = self.config_section['zeroconf_name'] self.zeroconf_service = zeroconf.Zeroconf( - stype="_http._tcp", name=name, port=self.port, - host=self.hostname) + stype='_http._tcp', name=name, + host=self.hostname, port=self.port) if self.zeroconf_service.publish(): - logger.info('Registered HTTP with zeroconf as %s', name) + logger.info('Registered HTTP with Zeroconf as %s', name) else: - logger.warning('Registering HTTP with zeroconf failed.') + logger.warning('Registering HTTP with Zeroconf failed.') def on_stop(self): if self.zeroconf_service: diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index 51735837..575f4159 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -44,13 +44,13 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): if self.config_section['zeroconf_enabled']: name = self.config_section['zeroconf_name'] self.zeroconf_service = zeroconf.Zeroconf( - stype="_mpd._tcp", name=name, port=self.port, - host=self.hostname) + stype="_mpd._tcp", name=name, + port=self.port, host=self.hostname) if self.zeroconf_service.publish(): - logger.info('Registered MPD with zeroconf as %s', name) + logger.info('Registered MPD with Zeroconf as %s', name) else: - logger.warning('Registering MPD with zeroconf failed.') + logger.warning('Registering MPD with Zeroconf failed.') def on_stop(self): if self.zeroconf_service: diff --git a/mopidy/utils/zeroconf.py b/mopidy/utils/zeroconf.py index 8af63777..34e9a182 100644 --- a/mopidy/utils/zeroconf.py +++ b/mopidy/utils/zeroconf.py @@ -15,7 +15,6 @@ _AVAHI_PROTO_UNSPEC = -1 _AVAHI_PUBLISHFLAGS_NONE = 0 - def _filter_loopback_and_meta_addresses(host): # TODO: see if we can find a cleaner way of handling this. if re.search(r'(? Date: Mon, 11 Nov 2013 21:39:14 +0100 Subject: [PATCH 31/43] avahi: Add hostname and port template values to names --- mopidy/frontends/http/actor.py | 3 ++- mopidy/frontends/http/ext.conf | 2 +- mopidy/frontends/mpd/actor.py | 3 ++- mopidy/frontends/mpd/ext.conf | 2 +- mopidy/utils/zeroconf.py | 9 +++++++-- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index ad5441c3..1cd0c5f4 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -100,7 +100,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): host=self.hostname, port=self.port) if self.zeroconf_service.publish(): - logger.info('Registered HTTP with Zeroconf as %s', name) + logger.info('Registered HTTP with Zeroconf as "%s"', + self.zeroconf_service.name) else: logger.warning('Registering HTTP with Zeroconf failed.') diff --git a/mopidy/frontends/http/ext.conf b/mopidy/frontends/http/ext.conf index f3df5f1a..891aeb5b 100644 --- a/mopidy/frontends/http/ext.conf +++ b/mopidy/frontends/http/ext.conf @@ -4,7 +4,7 @@ hostname = 127.0.0.1 port = 6680 static_dir = zeroconf_enabled = true -zeroconf_name = Mopidy +zeroconf_name = Mopidy HTTP server on $hostname [loglevels] cherrypy = warning diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index 575f4159..a40834b2 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -48,7 +48,8 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): port=self.port, host=self.hostname) if self.zeroconf_service.publish(): - logger.info('Registered MPD with Zeroconf as %s', name) + logger.info('Registered MPD with Zeroconf as "%s"', + self.zeroconf_service.name) else: logger.warning('Registering MPD with Zeroconf failed.') diff --git a/mopidy/frontends/mpd/ext.conf b/mopidy/frontends/mpd/ext.conf index d51c04f6..ad3b333a 100644 --- a/mopidy/frontends/mpd/ext.conf +++ b/mopidy/frontends/mpd/ext.conf @@ -6,4 +6,4 @@ password = max_connections = 20 connection_timeout = 60 zeroconf_enabled = true -zeroconf_name = Mopidy (MPD) +zeroconf_name = Mopidy MPD server on $hostname diff --git a/mopidy/utils/zeroconf.py b/mopidy/utils/zeroconf.py index 34e9a182..ba4e327d 100644 --- a/mopidy/utils/zeroconf.py +++ b/mopidy/utils/zeroconf.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals import logging import re +import socket +import string logger = logging.getLogger('mopidy.utils.zerconf') @@ -31,13 +33,16 @@ class Zeroconf: def __init__(self, name, port, stype="_http._tcp", domain="", host="", text=[]): - self.name = name + self.group = None self.stype = stype self.domain = domain self.port = port self.text = text self.host = _filter_loopback_and_meta_addresses(host) - self.group = None + + template = string.Template(name) + self.name = template.safe_substitute( + hostname=self.host or socket.getfqdn(), port=self.port) def publish(self): if not dbus: From c964d15ac46adc1994aeab93629d07bf39d245fb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 11 Nov 2013 21:42:23 +0100 Subject: [PATCH 32/43] avahi: Simplify config to single value --- mopidy/frontends/http/__init__.py | 3 +-- mopidy/frontends/http/actor.py | 5 ++--- mopidy/frontends/http/ext.conf | 3 +-- mopidy/frontends/mpd/__init__.py | 3 +-- mopidy/frontends/mpd/actor.py | 5 ++--- mopidy/frontends/mpd/ext.conf | 3 +-- 6 files changed, 8 insertions(+), 14 deletions(-) diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index d5f8f1bc..64cb88f9 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -21,8 +21,7 @@ class Extension(ext.Extension): schema['hostname'] = config.Hostname() schema['port'] = config.Port() schema['static_dir'] = config.Path(optional=True) - schema['zeroconf_enabled'] = config.Boolean() - schema['zeroconf_name'] = config.String() + schema['zeroconf'] = config.String(optional=True) return schema def validate_environment(self): diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index 1cd0c5f4..3e46dc63 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -93,10 +93,9 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): cherrypy.engine.start() logger.info('HTTP server running at %s', cherrypy.server.base()) - if self.config_section['zeroconf_enabled']: - name = self.config_section['zeroconf_name'] + if self.config_section['zeroconf']: self.zeroconf_service = zeroconf.Zeroconf( - stype='_http._tcp', name=name, + stype='_http._tcp', name=self.config_section['zeroconf'], host=self.hostname, port=self.port) if self.zeroconf_service.publish(): diff --git a/mopidy/frontends/http/ext.conf b/mopidy/frontends/http/ext.conf index 891aeb5b..fc239230 100644 --- a/mopidy/frontends/http/ext.conf +++ b/mopidy/frontends/http/ext.conf @@ -3,8 +3,7 @@ enabled = true hostname = 127.0.0.1 port = 6680 static_dir = -zeroconf_enabled = true -zeroconf_name = Mopidy HTTP server on $hostname +zeroconf = Mopidy HTTP server on $hostname [loglevels] cherrypy = warning diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 28f9c951..571d6455 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -23,8 +23,7 @@ class Extension(ext.Extension): schema['password'] = config.Secret(optional=True) schema['max_connections'] = config.Integer(minimum=1) schema['connection_timeout'] = config.Integer(minimum=1) - schema['zeroconf_enabled'] = config.Boolean() - schema['zeroconf_name'] = config.String() + schema['zeroconf'] = config.String(optional=True) return schema def validate_environment(self): diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index a40834b2..6e277078 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -41,10 +41,9 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): logger.info('MPD server running at [%s]:%s', hostname, port) def on_start(self): - if self.config_section['zeroconf_enabled']: - name = self.config_section['zeroconf_name'] + if self.config_section['zeroconf']: self.zeroconf_service = zeroconf.Zeroconf( - stype="_mpd._tcp", name=name, + stype="_mpd._tcp", name=self.config_section['zeroconf'], port=self.port, host=self.hostname) if self.zeroconf_service.publish(): diff --git a/mopidy/frontends/mpd/ext.conf b/mopidy/frontends/mpd/ext.conf index ad3b333a..c62c37ef 100644 --- a/mopidy/frontends/mpd/ext.conf +++ b/mopidy/frontends/mpd/ext.conf @@ -5,5 +5,4 @@ port = 6600 password = max_connections = 20 connection_timeout = 60 -zeroconf_enabled = true -zeroconf_name = Mopidy MPD server on $hostname +zeroconf = Mopidy MPD server on $hostname From f03c049485a22d0df63c88a455e3ff78c3f05618 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 11 Nov 2013 22:04:13 +0100 Subject: [PATCH 33/43] avahi: More review comments and some other fixes. - Switched to newstyle class - Switched to safe values in kwargs --- mopidy/frontends/mpd/actor.py | 4 ++-- mopidy/utils/zeroconf.py | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index 6e277078..87c1a571 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -43,8 +43,8 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): def on_start(self): if self.config_section['zeroconf']: self.zeroconf_service = zeroconf.Zeroconf( - stype="_mpd._tcp", name=self.config_section['zeroconf'], - port=self.port, host=self.hostname) + stype='_mpd._tcp', name=self.config_section['zeroconf'], + host=self.hostname, port=self.port) if self.zeroconf_service.publish(): logger.info('Registered MPD with Zeroconf as "%s"', diff --git a/mopidy/utils/zeroconf.py b/mopidy/utils/zeroconf.py index ba4e327d..c1781867 100644 --- a/mopidy/utils/zeroconf.py +++ b/mopidy/utils/zeroconf.py @@ -28,17 +28,17 @@ def _convert_text_to_dbus_bytes(text): return [dbus.Byte(ord(c)) for c in text] -class Zeroconf: - """Publish a network service with zeroconf using Avahi.""" +class Zeroconf(object): + """Publish a network service with Zeroconf using Avahi.""" - def __init__(self, name, port, stype="_http._tcp", - domain="", host="", text=[]): + def __init__(self, name, port, stype=None, domain=None, + host=None, text=None): self.group = None - self.stype = stype - self.domain = domain + self.stype = stype or '_http._tcp' + self.domain = domain or '' self.port = port - self.text = text - self.host = _filter_loopback_and_meta_addresses(host) + self.text = text or [] + self.host = _filter_loopback_and_meta_addresses(host or '') template = string.Template(name) self.name = template.safe_substitute( @@ -59,12 +59,12 @@ class Zeroconf: logger.debug('Zeroconf publish failed: Avahi service not running.') return False - server = dbus.Interface(bus.get_object("org.freedesktop.Avahi", "/"), - "org.freedesktop.Avahi.Server") + server = dbus.Interface(bus.get_object('org.freedesktop.Avahi', '/'), + 'org.freedesktop.Avahi.Server') self.group = dbus.Interface( - bus.get_object("org.freedesktop.Avahi", server.EntryGroupNew()), - "org.freedesktop.Avahi.EntryGroup") + bus.get_object('org.freedesktop.Avahi', server.EntryGroupNew()), + 'org.freedesktop.Avahi.EntryGroup') text = [_convert_text_to_dbus_bytes(t) for t in self.text] self.group.AddService(_AVAHI_IF_UNSPEC, _AVAHI_PROTO_UNSPEC, From 660a1b738290afa45d9c2f0fbcabf0b171e34b9d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 11 Nov 2013 22:32:46 +0100 Subject: [PATCH 34/43] avahi: Remove use of config_section --- mopidy/frontends/http/actor.py | 12 +++++++----- mopidy/frontends/mpd/actor.py | 14 +++++++------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index 3e46dc63..4e3493d4 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -23,16 +23,18 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): super(HttpFrontend, self).__init__() self.config = config self.core = core + + self.hostname = config['http']['hostname'] + self.port = config['http']['port'] + self.zeroconf_name = config['http']['zeroconf'] self.zeroconf_service = None + self._setup_server() self._setup_websocket_plugin() app = self._create_app() self._setup_logging(app) def _setup_server(self): - self.config_section = self.config['http'] - self.hostname = self.config_section['hostname'] - self.port = self.config_section['port'] cherrypy.config.update({ 'engine.autoreload_on': False, 'server.socket_host': self.hostname, @@ -93,9 +95,9 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): cherrypy.engine.start() logger.info('HTTP server running at %s', cherrypy.server.base()) - if self.config_section['zeroconf']: + if self.zeroconf_name: self.zeroconf_service = zeroconf.Zeroconf( - stype='_http._tcp', name=self.config_section['zeroconf'], + stype='_http._tcp', name=self.zeroconf_name, host=self.hostname, port=self.port) if self.zeroconf_service.publish(): diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index 87c1a571..9df7ba07 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -15,16 +15,16 @@ logger = logging.getLogger('mopidy.frontends.mpd') class MpdFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(MpdFrontend, self).__init__() + hostname = network.format_hostname(config['mpd']['hostname']) - port = config['mpd']['port'] - self.config_section = config['mpd'] self.hostname = hostname - self.port = port + self.port = config['mpd']['port'] + self.zeroconf_name = config['mpd']['zeroconf'] self.zeroconf_service = None try: network.Server( - hostname, port, + self.hostname, self.port, protocol=session.MpdSession, protocol_kwargs={ 'config': config, @@ -38,12 +38,12 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): encoding.locale_decode(error)) sys.exit(1) - logger.info('MPD server running at [%s]:%s', hostname, port) + logger.info('MPD server running at [%s]:%s', self.hostname, self.port) def on_start(self): - if self.config_section['zeroconf']: + if self.zeroconf_name: self.zeroconf_service = zeroconf.Zeroconf( - stype='_mpd._tcp', name=self.config_section['zeroconf'], + stype='_mpd._tcp', name=self.zeroconf_name, host=self.hostname, port=self.port) if self.zeroconf_service.publish(): From 924553b62efdc979e797e5ce107698045145a673 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 Nov 2013 22:40:45 +0100 Subject: [PATCH 35/43] http: Fix tests --- tests/frontends/http/events_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/frontends/http/events_test.py b/tests/frontends/http/events_test.py index 2c6b241e..5150db9b 100644 --- a/tests/frontends/http/events_test.py +++ b/tests/frontends/http/events_test.py @@ -28,6 +28,7 @@ class HttpEventsTest(unittest.TestCase): 'hostname': '127.0.0.1', 'port': 6680, 'static_dir': None, + 'zeroconf': '', } } self.http = actor.HttpFrontend(config=config, core=mock.Mock()) From df4f99cf7b59ba19d8d981ecb799ca6649af85a1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 Nov 2013 22:46:21 +0100 Subject: [PATCH 36/43] docs: Update changelog --- docs/changelog.rst | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f9ccbb3e..c20715bc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,10 +18,25 @@ v0.17.0 (UNRELEASED) - 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. + - Library scanning has been switched back to custom code due to various issues - with GStreamer's built in scanner in 0.10. This also fixes the scanner slowdown. - (Fixes: :issue:`565`) -- Fix scanner so that mtime is respected when deciding which files can be skipped. + with GStreamer's built in scanner in 0.10. This also fixes the scanner + slowdown. (Fixes: :issue:`565`) + +- Fix scanner so that mtime is respected when deciding which files can be + skipped. + +**MPD frontend** + +- The MPD service is now published as a Zeroconf service if avahi-deamon is + running on the system. Some MPD clients will use this to present Mopidy as an + available server on the local network without needing any configuration. + +**HTTP frontend** + +- The HTTP service is now published as a Zeroconf service if avahi-deamon is + running on the system. Some browsers will present HTTP Zeroconf services on + the local network as "local sites" bookmarks. v0.16.1 (2013-11-02) From f7c3272045899eb409539c6f694c15be82e3b77f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 Nov 2013 22:46:32 +0100 Subject: [PATCH 37/43] docs: Update authors --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 3794a267..8269452d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,3 +28,4 @@ - Pavol Babincak - Javier Domingo - Lasse Bigum +- David Eisner From d2280d9a86e59be861008fac290a05596b749ef2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 Nov 2013 22:52:36 +0100 Subject: [PATCH 38/43] docs: Add {mpd,http}/zeroconf descriptions --- docs/changelog.rst | 8 ++++++-- docs/ext/http.rst | 7 +++++++ docs/ext/mpd.rst | 7 +++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c20715bc..5e01bd47 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -30,13 +30,17 @@ v0.17.0 (UNRELEASED) - The MPD service is now published as a Zeroconf service if avahi-deamon is running on the system. Some MPD clients will use this to present Mopidy as an - available server on the local network without needing any configuration. + available server on the local network without needing any configuration. See + the :confval:`mpd/zeroconf` config value to change the service name or + disable the service. (Fixes: :issue:`39`) **HTTP frontend** - The HTTP service is now published as a Zeroconf service if avahi-deamon is running on the system. Some browsers will present HTTP Zeroconf services on - the local network as "local sites" bookmarks. + the local network as "local sites" bookmarks. See the + :confval:`http/zeroconf` config value to change the service name or disable + the service. (Fixes: :issue:`39`) v0.16.1 (2013-11-02) diff --git a/docs/ext/http.rst b/docs/ext/http.rst index 65bddb73..ce79588e 100644 --- a/docs/ext/http.rst +++ b/docs/ext/http.rst @@ -59,6 +59,13 @@ Configuration values Change this to have Mopidy serve e.g. files for your JavaScript client. "/mopidy" will continue to work as usual even if you change this setting. +.. confval:: http/zeroconf + + Name of the HTTP service when published through Zeroconf. The variables + ``$hostname`` and ``$port`` can be used in the name. + + Set to an empty string to disable Zeroconf for HTTP. + Usage ===== diff --git a/docs/ext/mpd.rst b/docs/ext/mpd.rst index 9dbcbe11..52cb8ef2 100644 --- a/docs/ext/mpd.rst +++ b/docs/ext/mpd.rst @@ -97,6 +97,13 @@ Configuration values Number of seconds an MPD client can stay inactive before the connection is closed by the server. +.. confval:: mpd/zeroconf + + Name of the MPD service when published through Zeroconf. The variables + ``$hostname`` and ``$port`` can be used in the name. + + Set to an empty string to disable Zeroconf for MPD. + Usage ===== From aa7b360d0d73d5ed80795ee171f766c7520a2d7f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 Nov 2013 22:59:45 +0100 Subject: [PATCH 39/43] docs: Fix typos --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5e01bd47..de9bccc7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,7 +28,7 @@ v0.17.0 (UNRELEASED) **MPD frontend** -- The MPD service is now published as a Zeroconf service if avahi-deamon is +- The MPD service is now published as a Zeroconf service if avahi-daemon is running on the system. Some MPD clients will use this to present Mopidy as an available server on the local network without needing any configuration. See the :confval:`mpd/zeroconf` config value to change the service name or @@ -36,7 +36,7 @@ v0.17.0 (UNRELEASED) **HTTP frontend** -- The HTTP service is now published as a Zeroconf service if avahi-deamon is +- The HTTP service is now published as a Zeroconf service if avahi-daemon is running on the system. Some browsers will present HTTP Zeroconf services on the local network as "local sites" bookmarks. See the :confval:`http/zeroconf` config value to change the service name or disable From 50be4a531664db3df06c8a9484d11c44455103e6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 Nov 2013 23:13:25 +0100 Subject: [PATCH 40/43] docs: Update changelog --- docs/changelog.rst | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index de9bccc7..727c7885 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,16 +15,19 @@ v0.17.0 (UNRELEASED) **Local backend** -- 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. - - Library scanning has been switched back to custom code due to various issues with GStreamer's built in scanner in 0.10. This also fixes the scanner slowdown. (Fixes: :issue:`565`) -- Fix scanner so that mtime is respected when deciding which files can be - skipped. +- 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. + +- The scanner will now extract multiple artists from files with multiple artist + tags. + +- Fix scanner so that time of last modification is respected when deciding + which files can be skipped. **MPD frontend** From 04788abaac1e9165e73a5c84817505d54a08978f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 12 Nov 2013 00:00:22 +0100 Subject: [PATCH 41/43] core: Change tracklist.{filter,remove} usage The criterias are now a mapping between field names and one or more values. This aligns tracklist.{filter,remove} with the API of library.{find_exact,search}, and allows for e.g. batch removals. An exception is raised immediately if the API is used in the old way to ease migration and debugging. --- docs/changelog.rst | 11 ++++ mopidy/core/tracklist.py | 53 +++++++++---------- .../mpd/protocol/current_playlist.py | 16 +++--- mopidy/frontends/mpd/protocol/playback.py | 2 +- tests/backends/local/tracklist_test.py | 28 +++++----- tests/core/events_test.py | 2 +- tests/core/tracklist_test.py | 14 +++-- 7 files changed, 70 insertions(+), 56 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 727c7885..a157e417 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,17 @@ v0.17.0 (UNRELEASED) - The search field ``track`` has been renamed to ``track_name`` to avoid confusion with ``track_no``. (Fixes: :issue:`535`) +- The signature of the tracklist's + :meth:`~mopidy.core.TracklistController.filter` and + :meth:`~mopidy.core.TracklistController.remove` methods have changed. + Previously, they expected e.g. ``tracklist.filter(tlid=17)``. Now, the value + must always be a list, e.g. ``tracklist.filter(tlid=[17])``. This change + allows you to get or remove multiple tracks with a single call, e.g. + ``tracklist.remove(tlid=[1, 2, 7])``. This is especially useful for web + clients, as requests can be batched. This also brings the interface closer to + the library's :meth:`~mopidy.core.LibraryController.find_exact` and + :meth:`~mopidy.core.LibraryController.search` methods. + **Local backend** - Library scanning has been switched back to custom code due to various issues diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index 012dd796..d3cc0d75 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import collections import logging import random @@ -290,57 +291,53 @@ class TracklistController(object): def filter(self, criteria=None, **kwargs): """ - Filter the tracklist by the given criterias. The value of the field to - check can be a list, a tuple or a set. + Filter the tracklist by the given criterias. + + A criteria consists of a model field to check and a list of values to + compare it against. If the model field matches one of the values, it + may be returned. + + Only tracks that matches all the given criterias are returned. Examples:: - # Returns track with TLID 7 (tracklist ID) - filter({'tlid': 7}) - filter(tlid=7) - - # Returns tracks with TLIDs 1, 2, 3 and 4 + # Returns tracks with TLIDs 1, 2, 3, or 4 (tracklist ID) filter({'tlid': [1, 2, 3, 4]}) filter(tlid=[1, 2, 3, 4]) - # Returns track with ID 1 - filter({'id': 1}) - filter(id=1) - - # Returns track with IDs 1 5 and 7 + # Returns track with IDs 1, 5, or 7 filter({'id': [1, 5, 7]}) filter(id=[1, 5, 7]) - # Returns track with URI 'xyz' - filter({'uri': 'xyz'}) - filter(uri='xyz') - - # Returns track with URIs 'xyz' and 'abc' + # Returns track with URIs 'xyz' or 'abc' filter({'uri': ['xyz', 'abc']}) filter(uri=['xyz', 'abc']) - # Returns track with ID 1 and URI 'xyz' - filter({'id': 1, 'uri': 'xyz'}) - filter(id=1, uri='xyz') + # Returns tracks with ID 1 and URI 'xyz' + filter({'id': [1], 'uri': ['xyz']}) + filter(id=[1], uri=['xyz']) - # Returns track with IDs (1, 3 or 6) and URIs ('xyz' or 'abc') + # Returns track with a matching ID (1, 3 or 6) and a matching URI + # ('xyz' or 'abc') filter({'id': [1, 3, 6], 'uri': ['xyz', 'abc']}) filter(id=[1, 3, 6], uri=['xyz', 'abc']) :param criteria: on or more criteria to match by - :type criteria: dict + :type criteria: dict, of (string, list) pairs :rtype: list of :class:`mopidy.models.TlTrack` """ criteria = criteria or kwargs matches = self._tl_tracks - for (key, value) in criteria.iteritems(): - if not type(value) in [list, tuple, set]: - value = [value] + for (key, values) in criteria.iteritems(): + if (not isinstance(values, collections.Iterable) + or isinstance(values, basestring)): + # Fail hard if anyone is using the <0.17 calling style + raise ValueError('Filter values must be iterable: %r' % values) if key == 'tlid': - matches = filter(lambda ct: ct.tlid in value, matches) + matches = filter(lambda ct: ct.tlid in values, matches) else: matches = filter( - lambda ct: getattr(ct.track, key) in value, matches) + lambda ct: getattr(ct.track, key) in values, matches) return matches def move(self, start, end, to_position): @@ -454,7 +451,7 @@ class TracklistController(object): """Private method used by :class:`mopidy.core.PlaybackController`.""" if not self.consume: return False - self.remove(tlid=tl_track.tlid) + self.remove(tlid=[tl_track.tlid]) return True def _trigger_tracklist_changed(self): diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 20452203..bc040067 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -76,7 +76,7 @@ def delete_range(context, start, end=None): if not tl_tracks: raise MpdArgError('Bad song index', command='delete') for (tlid, _) in tl_tracks: - context.core.tracklist.remove(tlid=tlid) + context.core.tracklist.remove(tlid=[tlid]) @handle_request(r'^delete "(?P\d+)"$') @@ -86,7 +86,7 @@ def delete_songpos(context, songpos): songpos = int(songpos) (tlid, _) = context.core.tracklist.slice( songpos, songpos + 1).get()[0] - context.core.tracklist.remove(tlid=tlid) + context.core.tracklist.remove(tlid=[tlid]) except IndexError: raise MpdArgError('Bad song index', command='delete') @@ -101,7 +101,7 @@ def deleteid(context, tlid): Deletes the song ``SONGID`` from the playlist """ tlid = int(tlid) - tl_tracks = context.core.tracklist.remove(tlid=tlid).get() + tl_tracks = context.core.tracklist.remove(tlid=[tlid]).get() if not tl_tracks: raise MpdNoExistError('No such song', command='deleteid') @@ -157,7 +157,7 @@ def moveid(context, tlid, to): """ tlid = int(tlid) to = int(to) - tl_tracks = context.core.tracklist.filter(tlid=tlid).get() + tl_tracks = context.core.tracklist.filter(tlid=[tlid]).get() if not tl_tracks: raise MpdNoExistError('No such song', command='moveid') position = context.core.tracklist.index(tl_tracks[0]).get() @@ -195,7 +195,7 @@ def playlistfind(context, tag, needle): - does not add quotes around the tag. """ if tag == 'filename': - tl_tracks = context.core.tracklist.filter(uri=needle).get() + tl_tracks = context.core.tracklist.filter(uri=[needle]).get() if not tl_tracks: return None position = context.core.tracklist.index(tl_tracks[0]).get() @@ -215,7 +215,7 @@ def playlistid(context, tlid=None): """ if tlid is not None: tlid = int(tlid) - tl_tracks = context.core.tracklist.filter(tlid=tlid).get() + tl_tracks = context.core.tracklist.filter(tlid=[tlid]).get() if not tl_tracks: raise MpdNoExistError('No such song', command='playlistid') position = context.core.tracklist.index(tl_tracks[0]).get() @@ -380,8 +380,8 @@ def swapid(context, tlid1, tlid2): """ tlid1 = int(tlid1) tlid2 = int(tlid2) - tl_tracks1 = context.core.tracklist.filter(tlid=tlid1).get() - tl_tracks2 = context.core.tracklist.filter(tlid=tlid2).get() + tl_tracks1 = context.core.tracklist.filter(tlid=[tlid1]).get() + tl_tracks2 = context.core.tracklist.filter(tlid=[tlid2]).get() if not tl_tracks1 or not tl_tracks2: raise MpdNoExistError('No such song', command='swapid') position1 = context.core.tracklist.index(tl_tracks1[0]).get() diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index b9289d8a..0d6bfe75 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -151,7 +151,7 @@ def playid(context, tlid): tlid = int(tlid) if tlid == -1: return _play_minus_one(context) - tl_tracks = context.core.tracklist.filter(tlid=tlid).get() + tl_tracks = context.core.tracklist.filter(tlid=[tlid]).get() if not tl_tracks: raise MpdNoExistError('No such song', command='playid') return context.core.playback.play(tl_tracks[0]).get() diff --git a/tests/backends/local/tracklist_test.py b/tests/backends/local/tracklist_test.py index 90eadeb9..ac135a25 100644 --- a/tests/backends/local/tracklist_test.py +++ b/tests/backends/local/tracklist_test.py @@ -71,34 +71,34 @@ class LocalTracklistProviderTest(unittest.TestCase): def test_filter_by_tlid(self): tl_track = self.controller.tl_tracks[1] self.assertEqual( - [tl_track], self.controller.filter(tlid=tl_track.tlid)) + [tl_track], self.controller.filter(tlid=[tl_track.tlid])) @populate_tracklist def test_filter_by_uri(self): tl_track = self.controller.tl_tracks[1] self.assertEqual( - [tl_track], self.controller.filter(uri=tl_track.track.uri)) + [tl_track], self.controller.filter(uri=[tl_track.track.uri])) @populate_tracklist def test_filter_by_uri_returns_nothing_for_invalid_uri(self): - self.assertEqual([], self.controller.filter(uri='foobar')) + self.assertEqual([], self.controller.filter(uri=['foobar'])) def test_filter_by_uri_returns_single_match(self): track = Track(uri='a') self.controller.add([Track(uri='z'), track, Track(uri='y')]) - self.assertEqual(track, self.controller.filter(uri='a')[0].track) + self.assertEqual(track, self.controller.filter(uri=['a'])[0].track) def test_filter_by_uri_returns_multiple_matches(self): track = Track(uri='a') self.controller.add([Track(uri='z'), track, track]) - tl_tracks = self.controller.filter(uri='a') + tl_tracks = self.controller.filter(uri=['a']) self.assertEqual(track, tl_tracks[0].track) self.assertEqual(track, tl_tracks[1].track) def test_filter_by_uri_returns_nothing_if_no_match(self): self.controller.playlist = Playlist( - tracks=[Track(uri='z'), Track(uri='y')]) - self.assertEqual([], self.controller.filter(uri='a')) + tracks=[Track(uri=['z']), Track(uri=['y'])]) + self.assertEqual([], self.controller.filter(uri=['a'])) def test_filter_by_multiple_criteria_returns_elements_matching_all(self): track1 = Track(uri='a', name='x') @@ -106,18 +106,18 @@ class LocalTracklistProviderTest(unittest.TestCase): track3 = Track(uri='b', name='y') self.controller.add([track1, track2, track3]) self.assertEqual( - track1, self.controller.filter(uri='a', name='x')[0].track) + track1, self.controller.filter(uri=['a'], name=['x'])[0].track) self.assertEqual( - track2, self.controller.filter(uri='b', name='x')[0].track) + track2, self.controller.filter(uri=['b'], name=['x'])[0].track) self.assertEqual( - track3, self.controller.filter(uri='b', name='y')[0].track) + track3, self.controller.filter(uri=['b'], name=['y'])[0].track) def test_filter_by_criteria_that_is_not_present_in_all_elements(self): track1 = Track() track2 = Track(uri='b') track3 = Track() self.controller.add([track1, track2, track3]) - self.assertEqual(track2, self.controller.filter(uri='b')[0].track) + self.assertEqual(track2, self.controller.filter(uri=['b'])[0].track) @populate_tracklist def test_clear(self): @@ -227,17 +227,17 @@ class LocalTracklistProviderTest(unittest.TestCase): track1 = self.controller.tracks[1] track2 = self.controller.tracks[2] version = self.controller.version - self.controller.remove(uri=track1.uri) + self.controller.remove(uri=[track1.uri]) self.assertLess(version, self.controller.version) self.assertNotIn(track1, self.controller.tracks) self.assertEqual(track2, self.controller.tracks[1]) @populate_tracklist def test_removing_track_that_does_not_exist_does_nothing(self): - self.controller.remove(uri='/nonexistant') + self.controller.remove(uri=['/nonexistant']) def test_removing_from_empty_playlist_does_nothing(self): - self.controller.remove(uri='/nonexistant') + self.controller.remove(uri=['/nonexistant']) @populate_tracklist def test_remove_lists(self): diff --git a/tests/core/events_test.py b/tests/core/events_test.py index 6d192b87..5d646840 100644 --- a/tests/core/events_test.py +++ b/tests/core/events_test.py @@ -105,7 +105,7 @@ class BackendEventsTest(unittest.TestCase): self.core.tracklist.add([Track(uri='dummy:a')]).get() send.reset_mock() - self.core.tracklist.remove(uri='dummy:a').get() + self.core.tracklist.remove(uri=['dummy:a']).get() self.assertEqual(send.call_args[0][0], 'tracklist_changed') diff --git a/tests/core/tracklist_test.py b/tests/core/tracklist_test.py index 9f17f6de..596a20a6 100644 --- a/tests/core/tracklist_test.py +++ b/tests/core/tracklist_test.py @@ -37,7 +37,7 @@ class TracklistTest(unittest.TestCase): self.assertEqual(tl_tracks, self.core.tracklist.tl_tracks[-1:]) def test_remove_removes_tl_tracks_matching_query(self): - tl_tracks = self.core.tracklist.remove(name='foo') + tl_tracks = self.core.tracklist.remove(name=['foo']) self.assertEqual(2, len(tl_tracks)) self.assertListEqual(self.tl_tracks[:2], tl_tracks) @@ -46,7 +46,7 @@ class TracklistTest(unittest.TestCase): self.assertListEqual(self.tl_tracks[2:], self.core.tracklist.tl_tracks) def test_remove_works_with_dict_instead_of_kwargs(self): - tl_tracks = self.core.tracklist.remove({'name': 'foo'}) + tl_tracks = self.core.tracklist.remove({'name': ['foo']}) self.assertEqual(2, len(tl_tracks)) self.assertListEqual(self.tl_tracks[:2], tl_tracks) @@ -55,15 +55,21 @@ class TracklistTest(unittest.TestCase): self.assertListEqual(self.tl_tracks[2:], self.core.tracklist.tl_tracks) def test_filter_returns_tl_tracks_matching_query(self): - tl_tracks = self.core.tracklist.filter(name='foo') + tl_tracks = self.core.tracklist.filter(name=['foo']) self.assertEqual(2, len(tl_tracks)) self.assertListEqual(self.tl_tracks[:2], tl_tracks) def test_filter_works_with_dict_instead_of_kwargs(self): - tl_tracks = self.core.tracklist.filter({'name': 'foo'}) + tl_tracks = self.core.tracklist.filter({'name': ['foo']}) self.assertEqual(2, len(tl_tracks)) self.assertListEqual(self.tl_tracks[:2], tl_tracks) + def test_filter_fails_if_values_isnt_iterable(self): + self.assertRaises(ValueError, self.core.tracklist.filter, tlid=3) + + def test_filter_fails_if_values_is_a_string(self): + self.assertRaises(ValueError, self.core.tracklist.filter, uri='a') + # TODO Extract tracklist tests from the base backend tests From f5e94d8f7cb38267379881014343453670b0d23e Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Wed, 13 Nov 2013 00:40:47 +0100 Subject: [PATCH 42/43] Address review comments --- mopidy/frontends/mpd/protocol/music_db.py | 2 +- mopidy/models.py | 8 ++++---- tests/backends/local/library_test.py | 19 ++++++++----------- tests/data/library_tag_cache | 2 +- tests/frontends/mpd/protocol/status_test.py | 1 + tests/frontends/mpd/translator_test.py | 1 + 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 0d84171c..c2035b15 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -106,7 +106,7 @@ def find(context, mpd_query): 'composer' not in query and 'performer' not in query): result_tracks += [_artist_as_track(a) for a in _get_artists(results)] - if 'album' not in query and 'genre' not in query: + if 'album' not in query: result_tracks += [_album_as_track(a) for a in _get_albums(results)] result_tracks += _get_tracks(results) return translator.tracks_to_mpd_format(result_tracks) diff --git a/mopidy/models.py b/mopidy/models.py index 5ab2ed92..04d71591 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -219,10 +219,10 @@ class Track(ImmutableObject): :type artists: list of :class:`Artist` :param album: track album :type album: :class:`Album` - :param composer: track composer - :type composer: string - :param performer: track performer - :type performer: string + :param composers: track composers + :type composers: string + :param performers: track performers + :type performers: string :param genre: track genre :type genre: string :param track_no: track number in album diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index 39980fcc..c38fd74f 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -48,8 +48,7 @@ class LocalLibraryProviderTest(unittest.TestCase): uri='local:track:path4', name='track4', artists=[artists[2]], album=albums[3], date='2004', length=60000, track_no=4, - comment='Music server with support for MPD/HTTP clients ' - 'and Spotify streaming http://www.mopidy.com'), + comment='This is a fantastic track'), Track( uri='local:track:path5', name='track5', genre='genre1', album=albums[3], length=4000, composers=[artists[4]]), @@ -242,12 +241,11 @@ class LocalLibraryProviderTest(unittest.TestCase): def test_find_exact_comment(self): result = self.library.find_exact( - comment=['Music server with support for MPD/HTTP clients ' - 'and Spotify streaming http://www.mopidy.com']) + comment=['This is a fantastic track']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) result = self.library.find_exact( - comment=['Music server with support for MPD/HTTP clients']) + comment=['This is a fantastic']) self.assertEqual(list(result[0].tracks), []) def test_find_exact_any(self): @@ -295,8 +293,7 @@ class LocalLibraryProviderTest(unittest.TestCase): # Matches on track comment result = self.library.find_exact( - any=['Music server with support for MPD/HTTP clients ' - 'and Spotify streaming http://www.mopidy.com']) + any=['This is a fantastic track']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) # Matches on URI @@ -458,10 +455,10 @@ class LocalLibraryProviderTest(unittest.TestCase): self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_search_comment(self): - result = self.library.search(comment=['mopidy']) + result = self.library.search(comment=['fantastic']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) - result = self.library.search(comment=['Potify']) + result = self.library.search(comment=['antasti']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) def test_search_any(self): @@ -501,10 +498,10 @@ class LocalLibraryProviderTest(unittest.TestCase): self.assertEqual(list(result[0].tracks), self.tracks[5:6]) # Matches on track comment - result = self.library.search(any=['http']) + result = self.library.search(any=['fanta']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) - result = self.library.search(any=['streaming']) + result = self.library.search(any=['is a fan']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) # Matches on URI diff --git a/tests/data/library_tag_cache b/tests/data/library_tag_cache index fb89a26d..6d00cf97 100644 --- a/tests/data/library_tag_cache +++ b/tests/data/library_tag_cache @@ -37,7 +37,7 @@ Title: track4 Album: album4 Date: 2004 Track: 4 -Comment: Music server with support for MPD/HTTP clients and Spotify streaming http://www.mopidy.com +Comment: This is a fantastic track Time: 60 key: key5 file: /path5 diff --git a/tests/frontends/mpd/protocol/status_test.py b/tests/frontends/mpd/protocol/status_test.py index bd75efb5..1cf5f253 100644 --- a/tests/frontends/mpd/protocol/status_test.py +++ b/tests/frontends/mpd/protocol/status_test.py @@ -21,6 +21,7 @@ class StatusHandlerTest(protocol.BaseTestCase): self.assertInResponse('Title: ') self.assertInResponse('Album: ') self.assertInResponse('Track: 0') + self.assertNotInResponse('Date: ') self.assertInResponse('Pos: 0') self.assertInResponse('Id: 0') self.assertInResponse('OK') diff --git a/tests/frontends/mpd/translator_test.py b/tests/frontends/mpd/translator_test.py index 1b89c283..a1578791 100644 --- a/tests/frontends/mpd/translator_test.py +++ b/tests/frontends/mpd/translator_test.py @@ -41,6 +41,7 @@ class TrackMpdFormatTest(unittest.TestCase): self.assertIn(('Title', ''), result) self.assertIn(('Album', ''), result) self.assertIn(('Track', 0), result) + self.assertNotIn(('Date', ''), result) self.assertEqual(len(result), 6) def test_track_to_mpd_format_with_position(self): From d529202b88b7bf598aa0b4d615adc50617a3f908 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 14 Nov 2013 21:28:27 +0100 Subject: [PATCH 43/43] docs: Update changelog --- docs/changelog.rst | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index dec41bc0..f425b94f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,9 @@ v0.17.0 (UNRELEASED) **Core** +- The :class:`~mopidy.models.Track` model has grown fields for ``composers``, + ``performers``, ``genre``, and ``comment``. + - The search field ``track`` has been renamed to ``track_name`` to avoid confusion with ``track_no``. (Fixes: :issue:`535`) @@ -37,6 +40,9 @@ v0.17.0 (UNRELEASED) - The scanner will now extract multiple artists from files with multiple artist tags. +- The scanner will now extract composers and performers, as well as genre, + bitrate, and comments. (Fixes: :issue:`577`) + - Fix scanner so that time of last modification is respected when deciding which files can be skipped. @@ -48,6 +54,10 @@ v0.17.0 (UNRELEASED) the :confval:`mpd/zeroconf` config value to change the service name or disable the service. (Fixes: :issue:`39`) +- Add support for ``composer``, ``performer``, ``comment``, ``genre``, and + ``performer``. These tags can be used with ``list ...``, ``search ...``, and + ``find ...`` and their variants, and are supported in the ``any`` tag also + **HTTP frontend** - The HTTP service is now published as a Zeroconf service if avahi-daemon is @@ -56,11 +66,6 @@ v0.17.0 (UNRELEASED) :confval:`http/zeroconf` config value to change the service name or disable the service. (Fixes: :issue:`39`) -**MPD frontend** - -- Add support for ``composer``, ``comment``, ``genre``, and ``performer``. - These tags can be used with ``list ...``, ``search ...``, and - ``find ...`` and their variants, and are supported in the ``any`` tag also v0.16.1 (2013-11-02) ====================