From ac2e413ec03c68b39819292a3ecf28dadd61d470 Mon Sep 17 00:00:00 2001 From: David Eisner Date: Fri, 1 Nov 2013 09:27:31 +0000 Subject: [PATCH 001/156] 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 002/156] 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 003/156] 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 24944bd8e3ebf91a7adc7d582f1f44dc65f9c358 Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Thu, 31 Oct 2013 22:33:48 +0100 Subject: [PATCH 004/156] Split artist and albumartist dependency, update tests based on this --- mopidy/backends/local/translator.py | 1 - mopidy/frontends/mpd/translator.py | 5 ++--- tests/backends/local/library_test.py | 15 ++++++++++++-- tests/backends/local/translator_test.py | 26 +++++++++++++------------ tests/data/advanced_tag_cache | 5 +++++ tests/data/library_tag_cache | 10 ++++++++++ tests/data/simple_tag_cache | 1 + tests/data/utf8_tag_cache | 1 + 8 files changed, 46 insertions(+), 18 deletions(-) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 7cd46fbb..3a02a8af 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -120,7 +120,6 @@ def _convert_mpd_data(data, tracks): if 'artist' in data: artist_kwargs['name'] = data['artist'] - albumartist_kwargs['name'] = data['artist'] if 'albumartist' in data: albumartist_kwargs['name'] = data['albumartist'] diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 9b331395..d25cad44 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -44,9 +44,6 @@ def track_to_mpd_format(track, position=None): track.track_no, track.album.num_tracks))) else: result.append(('Track', track.track_no)) - if track.album is not None and track.album.artists: - artists = artists_to_mpd_format(track.album.artists) - result.append(('AlbumArtist', artists)) if position is not None and tlid is not None: result.append(('Pos', position)) result.append(('Id', tlid)) @@ -55,6 +52,8 @@ def track_to_mpd_format(track, position=None): # FIXME don't use first and best artist? # FIXME don't duplicate following code? if track.album is not None and track.album.artists: + artists = artists_to_mpd_format(track.album.artists) + result.append(('AlbumArtist', artists)) artists = filter( lambda a: a.musicbrainz_id is not None, track.album.artists) if artists: diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index 0e33b412..af09b4bb 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -26,6 +26,7 @@ class LocalLibraryProviderTest(unittest.TestCase): Album(name='album1', artists=[artists[0]]), Album(name='album2', artists=[artists[1]]), Album(name='album3', artists=[artists[2]]), + Album(name='album4'), ] tracks = [ @@ -41,6 +42,10 @@ 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', + artists=[artists[2]], album=albums[3], + date='2004', length=60000, track_no=4), ] config = { @@ -152,6 +157,12 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(artist=['artist2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) + import logging + logger = logging.getLogger('mopidy.backends.local') + logger.debug("==TEST= tracks: {}".format(self.tracks[2:3])) + result = self.library.find_exact(artist=['artist3']) + self.assertEqual(list(result[0].tracks), self.tracks[3:4]) + def test_find_exact_album(self): result = self.library.find_exact(album=['album1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) @@ -210,7 +221,7 @@ class LocalLibraryProviderTest(unittest.TestCase): # Matches on track album artists result = self.library.find_exact(any=['artist3']) - self.assertEqual(list(result[0].tracks), self.tracks[2:3]) + self.assertEqual(list(result[0].tracks), [self.tracks[3], self.tracks[2]]) # Matches on track year result = self.library.find_exact(any=['2002']) @@ -353,7 +364,7 @@ class LocalLibraryProviderTest(unittest.TestCase): # Matches on track album artists result = self.library.search(any=['Tist3']) - self.assertEqual(list(result[0].tracks), self.tracks[2:3]) + self.assertEqual(list(result[0].tracks), [self.tracks[3], self.tracks[2]]) # Matches on URI result = self.library.search(any=['TH1']) diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index 5ed07fca..1719500c 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -93,28 +93,30 @@ class URItoM3UTest(unittest.TestCase): expected_artists = [Artist(name='name')] expected_albums = [ - Album(name='albumname', artists=expected_artists, num_tracks=2)] + Album(name='albumname', artists=expected_artists, num_tracks=2), + Album(name='albumname', num_tracks=2) + ] expected_tracks = [] -def generate_track(path, ident): +def generate_track(path, ident, album_id): uri = 'local:track:%s' % path track = Track( uri=uri, name='trackname', artists=expected_artists, - album=expected_albums[0], track_no=1, date='2006', length=4000, + album=expected_albums[album_id], track_no=1, date='2006', length=4000, last_modified=1272319626) expected_tracks.append(track) -generate_track('song1.mp3', 6) -generate_track('song2.mp3', 7) -generate_track('song3.mp3', 8) -generate_track('subdir1/song4.mp3', 2) -generate_track('subdir1/song5.mp3', 3) -generate_track('subdir2/song6.mp3', 4) -generate_track('subdir2/song7.mp3', 5) -generate_track('subdir1/subsubdir/song8.mp3', 0) -generate_track('subdir1/subsubdir/song9.mp3', 1) +generate_track('song1.mp3', 6, 0) +generate_track('song2.mp3', 7, 0) +generate_track('song3.mp3', 8, 1) +generate_track('subdir1/song4.mp3', 2, 0) +generate_track('subdir1/song5.mp3', 3, 0) +generate_track('subdir2/song6.mp3', 4, 1) +generate_track('subdir2/song7.mp3', 5, 1) +generate_track('subdir1/subsubdir/song8.mp3', 0, 0) +generate_track('subdir1/subsubdir/song9.mp3', 1, 1) class MPDTagCacheToTracksTest(unittest.TestCase): diff --git a/tests/data/advanced_tag_cache b/tests/data/advanced_tag_cache index 3288275f..be299fb6 100644 --- a/tests/data/advanced_tag_cache +++ b/tests/data/advanced_tag_cache @@ -11,6 +11,7 @@ key: song8.mp3 file: subdir1/subsubdir/song8.mp3 Time: 4 Artist: name +AlbumArtist: name Title: trackname Album: albumname Track: 1/2 @@ -32,6 +33,7 @@ key: song4.mp3 file: subdir1/song4.mp3 Time: 4 Artist: name +AlbumArtist: name Title: trackname Album: albumname Track: 1/2 @@ -41,6 +43,7 @@ key: song5.mp3 file: subdir1/song5.mp3 Time: 4 Artist: name +AlbumArtist: name Title: trackname Album: albumname Track: 1/2 @@ -76,6 +79,7 @@ key: song1.mp3 file: /song1.mp3 Time: 4 Artist: name +AlbumArtist: name Title: trackname Album: albumname Track: 1/2 @@ -85,6 +89,7 @@ key: song2.mp3 file: /song2.mp3 Time: 4 Artist: name +AlbumArtist: name Title: trackname Album: albumname Track: 1/2 diff --git a/tests/data/library_tag_cache b/tests/data/library_tag_cache index e9e87c1b..904c5e57 100644 --- a/tests/data/library_tag_cache +++ b/tests/data/library_tag_cache @@ -6,6 +6,7 @@ songList begin key: key1 file: /path1 Artist: artist1 +AlbumArtist: artist1 Title: track1 Album: album1 Date: 2001-02-03 @@ -14,6 +15,7 @@ Time: 4 key: key2 file: /path2 Artist: artist2 +AlbumArtist: artist2 Title: track2 Album: album2 Date: 2002 @@ -28,4 +30,12 @@ Album: album3 Date: 2003 Track: 3 Time: 4 +key: key4 +file: /path4 +Artist: artist3 +Title: track4 +Album: album4 +Date: 2004 +Track: 4 +Time: 60 songList end diff --git a/tests/data/simple_tag_cache b/tests/data/simple_tag_cache index cc71ac6d..07a474b3 100644 --- a/tests/data/simple_tag_cache +++ b/tests/data/simple_tag_cache @@ -7,6 +7,7 @@ key: song1.mp3 file: /song1.mp3 Time: 4 Artist: name +AlbumArtist: name Title: trackname Album: albumname Track: 1/2 diff --git a/tests/data/utf8_tag_cache b/tests/data/utf8_tag_cache index 6642ec77..6f6abe60 100644 --- a/tests/data/utf8_tag_cache +++ b/tests/data/utf8_tag_cache @@ -7,6 +7,7 @@ key: song1.mp3 file: /song1.mp3 Time: 4 Artist: æøå +AlbumArtist: æøå Title: æøå Album: æøå mtime: 1272319626 From f295cbd3cbd68cdcf1e494407549297d8d08cf2f Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sat, 2 Nov 2013 02:51:06 +0100 Subject: [PATCH 005/156] Fix flake8 issues --- tests/backends/local/library_test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index af09b4bb..90002c69 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -221,7 +221,8 @@ class LocalLibraryProviderTest(unittest.TestCase): # Matches on track album artists result = self.library.find_exact(any=['artist3']) - self.assertEqual(list(result[0].tracks), [self.tracks[3], self.tracks[2]]) + self.assertEqual(list(result[0].tracks), [self.tracks[3], + self.tracks[2]]) # Matches on track year result = self.library.find_exact(any=['2002']) @@ -364,7 +365,8 @@ class LocalLibraryProviderTest(unittest.TestCase): # Matches on track album artists result = self.library.search(any=['Tist3']) - self.assertEqual(list(result[0].tracks), [self.tracks[3], self.tracks[2]]) + self.assertEqual(list(result[0].tracks), [self.tracks[3], + self.tracks[2]]) # Matches on URI result = self.library.search(any=['TH1']) From b0d43444c254acd75a5ca65add7e2871b318c5c0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 2 Nov 2013 22:18:26 +0100 Subject: [PATCH 006/156] local: Remove debug logging in tests --- tests/backends/local/library_test.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index 90002c69..9206b3b4 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -157,9 +157,6 @@ class LocalLibraryProviderTest(unittest.TestCase): result = self.library.find_exact(artist=['artist2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) - import logging - logger = logging.getLogger('mopidy.backends.local') - logger.debug("==TEST= tracks: {}".format(self.tracks[2:3])) result = self.library.find_exact(artist=['artist3']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) From 838f584e2b7b2c35f7efe8d48f31b5c1d9770687 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 2 Nov 2013 22:18:37 +0100 Subject: [PATCH 007/156] local: Formatting --- tests/backends/local/library_test.py | 8 ++++---- tests/backends/local/translator_test.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index 9206b3b4..ab95d4de 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -218,8 +218,8 @@ class LocalLibraryProviderTest(unittest.TestCase): # Matches on track album artists result = self.library.find_exact(any=['artist3']) - self.assertEqual(list(result[0].tracks), [self.tracks[3], - self.tracks[2]]) + self.assertEqual( + list(result[0].tracks), [self.tracks[3], self.tracks[2]]) # Matches on track year result = self.library.find_exact(any=['2002']) @@ -362,8 +362,8 @@ class LocalLibraryProviderTest(unittest.TestCase): # Matches on track album artists result = self.library.search(any=['Tist3']) - self.assertEqual(list(result[0].tracks), [self.tracks[3], - self.tracks[2]]) + self.assertEqual( + list(result[0].tracks), [self.tracks[3], self.tracks[2]]) # Matches on URI result = self.library.search(any=['TH1']) diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index 1719500c..07990e47 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -94,8 +94,8 @@ class URItoM3UTest(unittest.TestCase): expected_artists = [Artist(name='name')] expected_albums = [ Album(name='albumname', artists=expected_artists, num_tracks=2), - Album(name='albumname', num_tracks=2) - ] + Album(name='albumname', num_tracks=2), +] expected_tracks = [] From a44b7c06a59aaf26eefd78d320887832ab714ad1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 2 Nov 2013 22:20:02 +0100 Subject: [PATCH 008/156] docs: Update changelog --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0e560f3d..d98e7867 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,10 @@ v0.17.0 (UNRELEASED) - Fix search filtering by track number. +- When scanning, we no longer default the album artist to be the same as the + track artist. Album artist is now only populated if the scanned file got an + explicit album artist set. + **MPD frontend** - Add support for ``list "albumartist" ...``. From 9593da08b6d9b5be71b9bcdfc2325b4e25e0d877 Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Thu, 31 Oct 2013 22:47:33 +0100 Subject: [PATCH 009/156] Rename track in MPD to track_name to avoid confusion --- mopidy/backends/local/library.py | 4 ++-- mopidy/frontends/mpd/translator.py | 2 +- tests/backends/local/library_test.py | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index d2af41fc..3afbd184 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -91,7 +91,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider): if field == 'uri': result_tracks = filter(uri_filter, result_tracks) - elif field == 'track': + elif field == 'track_name': result_tracks = filter(track_filter, result_tracks) elif field == 'album': result_tracks = filter(album_filter, result_tracks) @@ -149,7 +149,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider): if field == 'uri': result_tracks = filter(uri_filter, result_tracks) - elif field == 'track': + elif field == 'track_name': result_tracks = filter(track_filter, result_tracks) elif field == 'album': result_tracks = filter(album_filter, result_tracks) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index d25cad44..880d1411 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -234,7 +234,7 @@ def query_from_mpd_search_format(mpd_query): m = MPD_SEARCH_QUERY_PART_RE.match(query_part) field = m.groupdict()['field'].lower() if field == 'title': - field = 'track' + field = 'track_name' elif field == 'track': field = 'track_no' elif field in ('file', 'filename'): diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index ab95d4de..56afc4ea 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -107,7 +107,7 @@ class LocalLibraryProviderTest(unittest.TestCase): self.assertEqual(tracks, []) def test_find_exact_no_hits(self): - result = self.library.find_exact(track=['unknown track']) + result = self.library.find_exact(track_name=['unknown track']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(artist=['unknown artist']) @@ -144,10 +144,10 @@ class LocalLibraryProviderTest(unittest.TestCase): self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_find_exact_track(self): - result = self.library.find_exact(track=['track1']) + result = self.library.find_exact(track_name=['track1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) - result = self.library.find_exact(track=['track2']) + result = self.library.find_exact(track_name=['track2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_find_exact_artist(self): @@ -240,7 +240,7 @@ class LocalLibraryProviderTest(unittest.TestCase): test = lambda: self.library.find_exact(albumartist=['']) self.assertRaises(LookupError, test) - test = lambda: self.library.find_exact(track=['']) + test = lambda: self.library.find_exact(track_name=['']) self.assertRaises(LookupError, test) test = lambda: self.library.find_exact(album=['']) @@ -256,7 +256,7 @@ class LocalLibraryProviderTest(unittest.TestCase): self.assertRaises(LookupError, test) def test_search_no_hits(self): - result = self.library.search(track=['unknown track']) + result = self.library.search(track_name=['unknown track']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(artist=['unknown artist']) @@ -291,10 +291,10 @@ class LocalLibraryProviderTest(unittest.TestCase): self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_search_track(self): - result = self.library.search(track=['Rack1']) + result = self.library.search(track_name=['Rack1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) - result = self.library.search(track=['Rack2']) + result = self.library.search(track_name=['Rack2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_search_artist(self): @@ -380,7 +380,7 @@ class LocalLibraryProviderTest(unittest.TestCase): test = lambda: self.library.search(albumartist=['']) self.assertRaises(LookupError, test) - test = lambda: self.library.search(track=['']) + test = lambda: self.library.search(track_name=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(album=['']) From 7339d4839c83c0157d6c8cd4304cb7b72491539e Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sat, 2 Nov 2013 02:56:33 +0100 Subject: [PATCH 010/156] Update filter name to match track_name change --- mopidy/backends/local/library.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index 3afbd184..86d960c1 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -72,7 +72,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider): q = value.strip() uri_filter = lambda t: q == t.uri - track_filter = lambda t: q == t.name + track_name_filter = lambda t: q == t.name album_filter = lambda t: q == getattr(t, 'album', Album()).name artist_filter = lambda t: filter( lambda a: q == a.name, t.artists) @@ -83,7 +83,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider): date_filter = lambda t: q == t.date any_filter = lambda t: ( uri_filter(t) or - track_filter(t) or + track_name_filter(t) or album_filter(t) or artist_filter(t) or albumartist_filter(t) or @@ -92,7 +92,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider): if field == 'uri': result_tracks = filter(uri_filter, result_tracks) elif field == 'track_name': - result_tracks = filter(track_filter, result_tracks) + result_tracks = filter(track_name_filter, result_tracks) elif field == 'album': result_tracks = filter(album_filter, result_tracks) elif field == 'artist': @@ -129,7 +129,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider): q = value.strip().lower() uri_filter = lambda t: q in t.uri.lower() - track_filter = lambda t: q in t.name.lower() + track_name_filter = lambda t: q in t.name.lower() album_filter = lambda t: q in getattr( t, 'album', Album()).name.lower() artist_filter = lambda t: filter( @@ -141,7 +141,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider): date_filter = lambda t: t.date and t.date.startswith(q) any_filter = lambda t: ( uri_filter(t) or - track_filter(t) or + track_name_filter(t) or album_filter(t) or artist_filter(t) or albumartist_filter(t) or @@ -150,7 +150,7 @@ class LocalLibraryProvider(base.BaseLibraryProvider): if field == 'uri': result_tracks = filter(uri_filter, result_tracks) elif field == 'track_name': - result_tracks = filter(track_filter, result_tracks) + result_tracks = filter(track_name_filter, result_tracks) elif field == 'album': result_tracks = filter(album_filter, result_tracks) elif field == 'artist': From 640337bc680cf3cce498ac7f9314c0e2faf75c25 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 2 Nov 2013 22:38:20 +0100 Subject: [PATCH 011/156] docs: Update changelog --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index d98e7867..12a19057 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,11 @@ This changelog is used to track all major changes to Mopidy. v0.17.0 (UNRELEASED) ==================== +**Core** + +- The search field ``track`` has been renamed to ``track_name`` to avoid + confusion with ``track_no``. + **Local backend** - Fix search filtering by track number. From f90f5f608e74395d07c1039153cdf05809799c4e Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sat, 2 Nov 2013 23:44:33 +0100 Subject: [PATCH 012/156] 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 013/156] 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 014/156] 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 015/156] 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 4b920e66d40f001797f4fa720a74e853b4ae728b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 3 Nov 2013 12:24:56 +0100 Subject: [PATCH 016/156] docs: Add refs to fixed issues --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index bcb8e32a..44ca89f7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,7 +11,7 @@ v0.17.0 (UNRELEASED) **Core** - The search field ``track`` has been renamed to ``track_name`` to avoid - confusion with ``track_no``. + confusion with ``track_no``. (Fixes: :issue:`535`) **Local backend** @@ -40,7 +40,7 @@ in Debian. **MPD frontend** - Add support for ``list "albumartist" ...`` which was missed when ``find`` and - ``search`` learned to handle ``albumartist`` in 0.16.0. + ``search`` learned to handle ``albumartist`` in 0.16.0. (Fixes: :issue:`553`) v0.16.0 (2013-10-27) From 0ab1aacbc5b57d84cda296f5ff66e9e33a67eb1d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 4 Nov 2013 23:36:02 +0100 Subject: [PATCH 017/156] docs: Remove redundant Read The Docs config --- docs/conf.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 81b0d41b..77ee897e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,11 +8,6 @@ import os import sys -# -- Read The Docs configuration ---------------------------------------------- - -RTD_NEW_THEME = True - - # -- Workarounds to have autodoc generate API docs ---------------------------- sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) From 483ced3c16fce1372301685f3326d53be9bcaaae Mon Sep 17 00:00:00 2001 From: David Eisner Date: Tue, 5 Nov 2013 10:33:04 +0000 Subject: [PATCH 018/156] 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 019/156] 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 020/156] 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 021/156] 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 022/156] 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 023/156] 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 024/156] 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 025/156] 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 d6ab78a86cfaf311369c769618c371fe8e4b1e0d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Nov 2013 13:25:26 +0100 Subject: [PATCH 026/156] audio: Re-add improved version of python based scanner. - Unlike the old python version we do not wait for the first audio handoff, we only progress until the PAUSED state. This ensure we don't block on empty files. - Instead of using the signal watch and running the main loop we simply poll the messages from the bus directly allowing for a synchronous code flow. - Between each file the pipeline is always returned to NULL, this is done as we found that gst 0.10 will slow down as the uribin does not cleanup the children it creates for handling each file. This issue is not present in 1.0. - This also works around a segfault that was likely caused by a race condition that seems to trigger in the 0.10 version of the pbutils discoverer. - This version of the scanner also fixes the per track slow down, and works out to be considerably faster than even the built in discoverer from 1.0. - Finally this removes the WMA hack as I kan no longer find any evidence of it being needed. --- mopidy/audio/scan.py | 88 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 mopidy/audio/scan.py diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py new file mode 100644 index 00000000..a1c15e2a --- /dev/null +++ b/mopidy/audio/scan.py @@ -0,0 +1,88 @@ +from __future__ import unicode_literals + +import pygst +pygst.require('0.10') +import gst +import gobject + +import time + +from mopidy import exceptions + + +class Scanner(object): + def __init__(self, timeout=1000): + self.timeout_ms = timeout + + sink = gst.element_factory_make('fakesink') + + audio_caps = gst.Caps(b'audio/x-raw-int; audio/x-raw-float') + pad_added = lambda src, pad: pad.link(sink.get_pad('sink')) + + self.uribin = gst.element_factory_make('uridecodebin') + self.uribin.set_property('caps', audio_caps) + self.uribin.connect('pad-added', pad_added) + + self.pipe = gst.element_factory_make('pipeline') + self.pipe.add(self.uribin) + self.pipe.add(sink) + + self.bus = self.pipe.get_bus() + self.bus.set_flushing(True) + + def scan(self, uri): + try: + self._setup(uri) + data = self._collect() + # Make sure uri and duration does not come from tags. + data[b'uri'] = uri + data[gst.TAG_DURATION] = self._query_duration() + finally: + self._reset() + + return data + + def _setup(self, uri): + """Primes the pipeline for collection.""" + self.pipe.set_state(gst.STATE_READY) + self.uribin.set_property(b'uri', uri) + self.bus.set_flushing(False) + self.pipe.set_state(gst.STATE_PAUSED) + + def _collect(self): + """Polls for messages to collect data.""" + start = time.time() + timeout_s = self.timeout_ms / float(1000) + poll_timeout_ns = 1000 + data = {} + + while time.time() - start < timeout_s: + message = self.bus.poll(gst.MESSAGE_ANY, poll_timeout_ns) + + if message is None: + pass # polling the bus timed out. + elif message.type == gst.MESSAGE_ERROR: + raise exceptions.ScannerError(message.parse_error()[0]) + elif message.type == gst.MESSAGE_EOS: + return data + elif message.type == gst.MESSAGE_ASYNC_DONE: + if message.src == self.pipe: + return data + elif message.type == gst.MESSAGE_TAG: + taglist = message.parse_tag() + for key in taglist.keys(): + data[key] = taglist[key] + + raise exceptions.ScannerError('Timeout after %dms' % self.timeout_ms) + + def _reset(self): + """Ensures we cleanup child elements and flush the bus.""" + self.bus.set_flushing(True) + self.pipe.set_state(gst.STATE_NULL) + + def _query_duration(self): + try: + duration = self.pipe.query_duration(gst.FORMAT_TIME, None)[0] + return duration // gst.MSECOND + except gst.QueryError: + return None From 0a2d74eff173de1d01e8a471324eb1628628c19c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Nov 2013 14:47:02 +0100 Subject: [PATCH 027/156] scanner: Update to use new mopidy.audio.scan Also adds the check less than 100ms check back to the scanner. --- mopidy/audio/scan.py | 4 ++++ mopidy/scanner.py | 46 +++---------------------------------------- tests/scanner_test.py | 3 ++- 3 files changed, 9 insertions(+), 44 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index a1c15e2a..4540bc05 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -40,6 +40,10 @@ class Scanner(object): finally: self._reset() + # TODO: this should be an option or just moved out. + if data[gst.TAG_DURATION] < 100: + raise exceptions.ScannerError( + 'Rejecting file with less than 100ms audio data.') return data def _setup(self, uri): diff --git a/mopidy/scanner.py b/mopidy/scanner.py index dd21fdb4..56639bc9 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -19,9 +19,9 @@ sys.argv[1:] = [] import pygst pygst.require('0.10') import gst -import gst.pbutils from mopidy import config as config_lib, exceptions, ext +from mopidy.audio import scan from mopidy.models import Track, Artist, Album from mopidy.utils import log, path, versioning @@ -103,11 +103,12 @@ def main(): logging.info('Found %d new or modified tracks.', len(uris_update)) logging.info('Scanning new and modified tracks.') - scanner = Scanner(config['local']['scan_timeout']) + scanner = scan.Scanner(config['local']['scan_timeout']) for uri in uris_update: try: data = scanner.scan(uri) data[b'mtime'] = os.path.getmtime(path.uri_to_path(uri)) + # TODO: check minumum time track = translator(data) local_updater.add(track) logging.debug('Added %s', track.uri) @@ -183,46 +184,5 @@ def translator(data): return Track(**track_kwargs) -class Scanner(object): - def __init__(self, timeout=1000): - self.discoverer = gst.pbutils.Discoverer(timeout * 1000000) - - def scan(self, uri): - try: - info = self.discoverer.discover_uri(uri) - except gobject.GError as e: - # Loosing traceback is non-issue since this is from C code. - raise exceptions.ScannerError(e) - - data = {} - audio_streams = info.get_audio_streams() - - if not audio_streams: - raise exceptions.ScannerError('Did not find any audio streams.') - - for stream in audio_streams: - taglist = stream.get_tags() - if not taglist: - continue - for key in taglist.keys(): - # XXX: For some crazy reason some wma files spit out lists - # here, not sure if this is due to better data in headers or - # wma being stupid. So ugly hack for now :/ - if type(taglist[key]) is list: - data[key] = taglist[key][0] - else: - data[key] = taglist[key] - - # Never trust metadata for these fields: - data[b'uri'] = uri - data[b'duration'] = info.get_duration() // gst.MSECOND - - if data[b'duration'] < 100: - raise exceptions.ScannerError( - 'Rejecting file with less than 100ms audio data.') - - return data - - if __name__ == '__main__': main() diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 1102c525..47cc8116 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -3,8 +3,9 @@ from __future__ import unicode_literals import unittest from mopidy import exceptions +from mopidy.audio.scan import Scanner from mopidy.models import Track, Artist, Album -from mopidy.scanner import Scanner, translator +from mopidy.scanner import translator from mopidy.utils import path as path_lib from tests import path_to_data_dir From 4cadba0ac70a9121ed6320e4eb027671d7148fd1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Nov 2013 15:12:15 +0100 Subject: [PATCH 028/156] scanner: Add progress tracking and process sorted uris --- mopidy/scanner.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 56639bc9..4430ea31 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -5,6 +5,7 @@ import datetime import logging import os import sys +import time import gobject gobject.threads_init() @@ -104,7 +105,9 @@ def main(): logging.info('Scanning new and modified tracks.') scanner = scan.Scanner(config['local']['scan_timeout']) - for uri in uris_update: + progress = Progress(len(uris_update)) + + for uri in sorted(uris_update): try: data = scanner.scan(uri) data[b'mtime'] = os.path.getmtime(path.uri_to_path(uri)) @@ -115,10 +118,27 @@ def main(): except exceptions.ScannerError as error: logging.warning('Failed %s: %s', uri, error) - logging.info('Done scanning; commiting changes.') + progress.increment() + + logging.info('Commiting changes.') local_updater.commit() +class Progress(object): + def __init__(self, total): + self.count = 0 + self.total = total + self.start = time.time() + + def increment(self, force=False): + self.count += 1 + if self.count % 1000 == 0 or self.count == self.total: + duration = time.time() - self.start + remainder = duration / self.count * (self.total - self.count) + logging.info('Scanned %d of %d files in %ds, ~%ds left.', + self.count, self.total, duration, remainder) + + def parse_args(): parser = argparse.ArgumentParser() parser.add_argument( From 20b060284210de0d9ad13b3e2b1b87e12d3a8814 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Nov 2013 16:17:47 +0100 Subject: [PATCH 029/156] audio/scanner: Move translator into audio. Tries to move more of the gst bits and pieces we are leaking into audio. --- mopidy/audio/scan.py | 51 +++++++++++++++ mopidy/scanner.py | 63 ++----------------- tests/{scanner_test.py => audio/scan_test.py} | 7 +-- 3 files changed, 58 insertions(+), 63 deletions(-) rename tests/{scanner_test.py => audio/scan_test.py} (97%) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 4540bc05..f12b8ff6 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -5,9 +5,11 @@ pygst.require('0.10') import gst import gobject +import datetime import time from mopidy import exceptions +from mopidy.models import Track, Artist, Album class Scanner(object): @@ -90,3 +92,52 @@ class Scanner(object): return duration // gst.MSECOND except gst.QueryError: return None + + +def audio_data_to_track(data): + """Convert taglist data + our extras to a track.""" + albumartist_kwargs = {} + album_kwargs = {} + artist_kwargs = {} + track_kwargs = {} + + def _retrieve(source_key, target_key, target): + if source_key in data: + target[target_key] = data[source_key] + + _retrieve(gst.TAG_ALBUM, 'name', album_kwargs) + _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) + + if gst.TAG_DATE in data and data[gst.TAG_DATE]: + date = data[gst.TAG_DATE] + try: + date = datetime.date(date.year, date.month, date.day) + except ValueError: + pass # Ignore invalid dates + 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)] + + track_kwargs['uri'] = data['uri'] + track_kwargs['last_modified'] = int(data['mtime']) + track_kwargs['length'] = data[gst.TAG_DURATION] + track_kwargs['album'] = Album(**album_kwargs) + track_kwargs['artists'] = [Artist(**artist_kwargs)] + + return Track(**track_kwargs) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 4430ea31..02b04cb8 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import argparse -import datetime import logging import os import sys @@ -10,20 +9,13 @@ import time import gobject gobject.threads_init() - # Extract any command line arguments. This needs to be done before GStreamer is # imported, so that GStreamer doesn't hijack e.g. ``--help``. mopidy_args = sys.argv[1:] sys.argv[1:] = [] - -import pygst -pygst.require('0.10') -import gst - from mopidy import config as config_lib, exceptions, ext from mopidy.audio import scan -from mopidy.models import Track, Artist, Album from mopidy.utils import log, path, versioning @@ -81,11 +73,13 @@ def main(): logging.info('Checking tracks from library.') for track in local_updater.load(): try: + # TODO: convert local to file uri / path stat = os.stat(path.uri_to_path(track.uri)) if int(stat.st_mtime) > track.last_modified: uris_update.add(track.uri) uris_library.add(track.uri) except OSError: + logging.debug('Missing file %s', track.uri) uris_remove.add(track.uri) logging.info('Removing %d moved or deleted tracks.', len(uris_remove)) @@ -111,8 +105,8 @@ def main(): try: data = scanner.scan(uri) data[b'mtime'] = os.path.getmtime(path.uri_to_path(uri)) - # TODO: check minumum time - track = translator(data) + # TODO: check minumum time here instead of in scanner. + track = scan.audio_data_to_track(data) local_updater.add(track) logging.debug('Added %s', track.uri) except exceptions.ScannerError as error: @@ -155,54 +149,5 @@ def parse_args(): return parser.parse_args(args=mopidy_args) -# TODO: move into scanner. -def translator(data): - albumartist_kwargs = {} - album_kwargs = {} - artist_kwargs = {} - track_kwargs = {} - - def _retrieve(source_key, target_key, target): - if source_key in data: - target[target_key] = data[source_key] - - _retrieve(gst.TAG_ALBUM, 'name', album_kwargs) - _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) - - if gst.TAG_DATE in data and data[gst.TAG_DATE]: - date = data[gst.TAG_DATE] - try: - date = datetime.date(date.year, date.month, date.day) - except ValueError: - pass # Ignore invalid dates - 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)] - - track_kwargs['uri'] = data['uri'] - track_kwargs['last_modified'] = int(data['mtime']) - track_kwargs['length'] = data[gst.TAG_DURATION] - track_kwargs['album'] = Album(**album_kwargs) - track_kwargs['artists'] = [Artist(**artist_kwargs)] - - return Track(**track_kwargs) - - if __name__ == '__main__': main() diff --git a/tests/scanner_test.py b/tests/audio/scan_test.py similarity index 97% rename from tests/scanner_test.py rename to tests/audio/scan_test.py index 47cc8116..eb92635f 100644 --- a/tests/scanner_test.py +++ b/tests/audio/scan_test.py @@ -3,9 +3,8 @@ from __future__ import unicode_literals import unittest from mopidy import exceptions -from mopidy.audio.scan import Scanner +from mopidy.audio import scan from mopidy.models import Track, Artist, Album -from mopidy.scanner import translator from mopidy.utils import path as path_lib from tests import path_to_data_dir @@ -77,7 +76,7 @@ class TranslatorTest(unittest.TestCase): def check(self): expected = self.build_track() - actual = translator(self.data) + actual = scan.audio_data_to_track(self.data) self.assertEqual(expected, actual) def test_basic_data(self): @@ -152,7 +151,7 @@ class ScannerTest(unittest.TestCase): def scan(self, path): paths = path_lib.find_files(path_to_data_dir(path)) uris = (path_lib.path_to_uri(p) for p in paths) - scanner = Scanner() + scanner = scan.Scanner() for uri in uris: key = uri[len('file://'):] try: From 16ac5277f68a53b9aa9826698ec06f8b120a7a76 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Nov 2013 16:25:24 +0100 Subject: [PATCH 030/156] audio: Make min duration in scanner configurable. --- mopidy/audio/scan.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index f12b8ff6..f04fa2fd 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -13,8 +13,9 @@ from mopidy.models import Track, Artist, Album class Scanner(object): - def __init__(self, timeout=1000): + def __init__(self, timeout=1000, min_duration=100): self.timeout_ms = timeout + self.min_duration_ms = min_duration sink = gst.element_factory_make('fakesink') @@ -42,10 +43,9 @@ class Scanner(object): finally: self._reset() - # TODO: this should be an option or just moved out. - if data[gst.TAG_DURATION] < 100: - raise exceptions.ScannerError( - 'Rejecting file with less than 100ms audio data.') + if data[gst.TAG_DURATION] < self.min_duration_ms: + raise exceptions.ScannerError('Rejecting file with less than %dms ' + 'audio data.' % self.min_duration_ms) return data def _setup(self, uri): From 469f414c4cc9b8cdd1f4b062121c9c95f7dbdf13 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Nov 2013 16:28:53 +0100 Subject: [PATCH 031/156] audio: Don't do duration conversion to ms in scanner --- mopidy/audio/scan.py | 7 +++---- tests/audio/scan_test.py | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index f04fa2fd..b1565ee3 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -43,7 +43,7 @@ class Scanner(object): finally: self._reset() - if data[gst.TAG_DURATION] < self.min_duration_ms: + if data[gst.TAG_DURATION] < self.min_duration_ms * gst.MSECOND: raise exceptions.ScannerError('Rejecting file with less than %dms ' 'audio data.' % self.min_duration_ms) return data @@ -88,8 +88,7 @@ class Scanner(object): def _query_duration(self): try: - duration = self.pipe.query_duration(gst.FORMAT_TIME, None)[0] - return duration // gst.MSECOND + return self.pipe.query_duration(gst.FORMAT_TIME, None)[0] except gst.QueryError: return None @@ -136,7 +135,7 @@ def audio_data_to_track(data): track_kwargs['uri'] = data['uri'] track_kwargs['last_modified'] = int(data['mtime']) - track_kwargs['length'] = data[gst.TAG_DURATION] + track_kwargs['length'] = data[gst.TAG_DURATION] // gst.MSECOND track_kwargs['album'] = Album(**album_kwargs) track_kwargs['artists'] = [Artist(**artist_kwargs)] diff --git a/tests/audio/scan_test.py b/tests/audio/scan_test.py index eb92635f..b53b0b57 100644 --- a/tests/audio/scan_test.py +++ b/tests/audio/scan_test.py @@ -31,7 +31,7 @@ class TranslatorTest(unittest.TestCase): 'album-disc-count': 3, 'date': FakeGstDate(2006, 1, 1,), 'container-format': 'ID3 tag', - 'duration': 4531, + 'duration': 4531000000, 'musicbrainz-trackid': 'mbtrackid', 'musicbrainz-albumid': 'mbalbumid', 'musicbrainz-artistid': 'mbartistid', @@ -182,8 +182,8 @@ class ScannerTest(unittest.TestCase): def test_duration_is_set(self): self.scan('scanner/simple') - self.check('scanner/simple/song1.mp3', 'duration', 4680) - self.check('scanner/simple/song1.ogg', 'duration', 4680) + self.check('scanner/simple/song1.mp3', 'duration', 4680000000) + self.check('scanner/simple/song1.ogg', 'duration', 4680000000) def test_artist_is_set(self): self.scan('scanner/simple') From e6381a1a0a57eae80295f3ae22a6cb8b589741f8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Nov 2013 17:50:38 +0100 Subject: [PATCH 032/156] audio: Add mtime to scanner results for file: uris --- mopidy/audio/scan.py | 8 ++++++++ mopidy/scanner.py | 2 -- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index b1565ee3..4906b46c 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -6,10 +6,12 @@ import gst import gobject import datetime +import os import time from mopidy import exceptions from mopidy.models import Track, Artist, Album +from mopidy.utils import path class Scanner(object): @@ -39,6 +41,7 @@ class Scanner(object): data = self._collect() # Make sure uri and duration does not come from tags. data[b'uri'] = uri + data[b'mtime'] = self._query_mtime(uri) data[gst.TAG_DURATION] = self._query_duration() finally: self._reset() @@ -92,6 +95,11 @@ class Scanner(object): except gst.QueryError: return None + def _query_mtime(self, uri): + if not uri.startswith('file:'): + return None + return os.path.getmtime(path.uri_to_path(uri)) + def audio_data_to_track(data): """Convert taglist data + our extras to a track.""" diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 02b04cb8..ee54c04d 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -104,8 +104,6 @@ def main(): for uri in sorted(uris_update): try: data = scanner.scan(uri) - data[b'mtime'] = os.path.getmtime(path.uri_to_path(uri)) - # TODO: check minumum time here instead of in scanner. track = scan.audio_data_to_track(data) local_updater.add(track) logging.debug('Added %s', track.uri) From dc3cf427b60c9cafb821304bd3b42c13e0e8cd03 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Nov 2013 18:40:12 +0100 Subject: [PATCH 033/156] scanner/mpd: Add TODOs for later and fix flake8 warning --- mopidy/audio/scan.py | 1 - mopidy/frontends/mpd/translator.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 4906b46c..82803379 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import pygst pygst.require('0.10') import gst -import gobject import datetime import os diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 880d1411..236b814f 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -301,6 +301,7 @@ def _add_to_tag_cache(result, dirs, files, media_dir): relative_path = os.path.relpath(path, base_path) relative_uri = urllib.quote(relative_path) + # TODO: use track.last_modified track_result['file'] = relative_uri track_result['mtime'] = get_mtime(path) track_result['key'] = os.path.basename(text_path) From 86926e8011b1802ed4a51efbef2cce9e07b8fb24 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Nov 2013 21:14:11 +0100 Subject: [PATCH 034/156] scanner: Remove unused argument for progress helper. --- mopidy/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index ee54c04d..30fb553b 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -122,7 +122,7 @@ class Progress(object): self.total = total self.start = time.time() - def increment(self, force=False): + def increment(self): self.count += 1 if self.count % 1000 == 0 or self.count == self.total: duration = time.time() - self.start From f8dedc0b848a7fdce6caf360279b5a450729dcc1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Nov 2013 21:46:41 +0100 Subject: [PATCH 035/156] local: Add local_to_file_uri translator --- mopidy/backends/local/playback.py | 8 ++++---- mopidy/backends/local/translator.py | 9 ++++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/local/playback.py b/mopidy/backends/local/playback.py index 98c92a85..9fbb736d 100644 --- a/mopidy/backends/local/playback.py +++ b/mopidy/backends/local/playback.py @@ -6,14 +6,14 @@ import os from mopidy.backends import base from mopidy.utils import path +from . import translator + logger = logging.getLogger('mopidy.backends.local') class LocalPlaybackProvider(base.BasePlaybackProvider): def change_track(self, track): media_dir = self.backend.config['local']['media_dir'] - # TODO: check that type is correct. - file_path = path.uri_to_path(track.uri).split(b':', 1)[1] - file_path = os.path.join(media_dir, file_path) - track = track.copy(uri=path.path_to_uri(file_path)) + uri = translator.local_to_file_uri(track.uri, media_dir) + track = track.copy(uri=uri) return super(LocalPlaybackProvider, self).change_track(track) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 3a02a8af..3c6b8151 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -6,11 +6,18 @@ import urlparse from mopidy.models import Track, Artist, Album from mopidy.utils.encoding import locale_decode -from mopidy.utils.path import path_to_uri +from mopidy.utils.path import path_to_uri, uri_to_path logger = logging.getLogger('mopidy.backends.local') +def local_to_file_uri(uri, media_dir): + # TODO: check that type is correct. + file_path = uri_to_path(uri).split(b':', 1)[1] + file_path = os.path.join(media_dir, file_path) + return path_to_uri(file_path) + + def parse_m3u(file_path, media_dir): r""" Convert M3U file list of uris From e7dd7e26773f493a5bf1dd6355fb05078dbb3c37 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 7 Nov 2013 00:03:00 +0100 Subject: [PATCH 036/156] local/scanner: Make checking mtime and skipping known files work again This change just patches over the worst of the inconsistencies in how the scanner mixes local and file uris to get us to a working state again. Ideally, this still needs a real cleanup when we finish the plugable library providers and/or json library work. --- mopidy/backends/local/library.py | 7 +++++-- mopidy/scanner.py | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index 86d960c1..9791c518 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -8,7 +8,7 @@ from mopidy.backends import base from mopidy.frontends.mpd import translator as mpd_translator from mopidy.models import Album, SearchResult -from .translator import parse_mpd_tag_cache +from .translator import local_to_file_uri, parse_mpd_tag_cache logger = logging.getLogger('mopidy.backends.local') @@ -189,7 +189,10 @@ class LocalLibraryUpdateProvider(base.BaseLibraryProvider): def load(self): tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir) for track in tracks: - self._tracks[track.uri] = track + # TODO: this should use uris as is, i.e. hack that should go away + # with tag caches. + uri = local_to_file_uri(track.uri, self._media_dir) + self._tracks[uri] = track.copy(uri=uri) return tracks def add(self, track): diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 30fb553b..b61517ef 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -16,6 +16,7 @@ sys.argv[1:] = [] from mopidy import config as config_lib, exceptions, ext from mopidy.audio import scan +from mopidy.backends.local import translator from mopidy.utils import log, path, versioning @@ -66,6 +67,8 @@ def main(): media_dir = config['local']['media_dir'] excluded_extensions = config['local']['excluded_file_extensions'] + # TODO: cleanup to consistently use local urls, not a random mix of local + # and file uris depending on how the data was loaded. uris_library = set() uris_update = set() uris_remove = set() @@ -73,20 +76,20 @@ def main(): logging.info('Checking tracks from library.') for track in local_updater.load(): try: - # TODO: convert local to file uri / path - stat = os.stat(path.uri_to_path(track.uri)) + uri = translator.local_to_file_uri(track.uri, media_dir) + stat = os.stat(path.uri_to_path(uri)) if int(stat.st_mtime) > track.last_modified: - uris_update.add(track.uri) - uris_library.add(track.uri) + uris_update.add(uri) + uris_library.add(uri) except OSError: logging.debug('Missing file %s', track.uri) uris_remove.add(track.uri) - logging.info('Removing %d moved or deleted tracks.', len(uris_remove)) + logging.info('Removing %d missing tracks.', len(uris_remove)) for uri in uris_remove: local_updater.remove(uri) - logging.info('Checking %s for new or modified tracks.', media_dir) + logging.info('Checking %s for unknown tracks.', media_dir) for uri in path.find_uris(config['local']['media_dir']): if os.path.splitext(path.uri_to_path(uri))[1] in excluded_extensions: logging.debug('Skipped %s: File extension excluded.', uri) @@ -95,8 +98,8 @@ def main(): if uri not in uris_library: uris_update.add(uri) - logging.info('Found %d new or modified tracks.', len(uris_update)) - logging.info('Scanning new and modified tracks.') + logging.info('Found %d unknown tracks.', len(uris_update)) + logging.info('Scanning...') scanner = scan.Scanner(config['local']['scan_timeout']) progress = Progress(len(uris_update)) From 8b5f30e5ffe736ef95e64a8a0ec5bd91883ed28c Mon Sep 17 00:00:00 2001 From: Javier Domingo Cansino Date: Thu, 7 Nov 2013 09:34:40 +0100 Subject: [PATCH 037/156] 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 038/156] 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 039/156] 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 040/156] 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 5b78ff94ff74318493f05dd97b247e7bd4585273 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 7 Nov 2013 19:33:46 +0100 Subject: [PATCH 041/156] docs: Remove `count` from MPD limitations list --- docs/ext/mpd.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/ext/mpd.rst b/docs/ext/mpd.rst index b4d0e1c8..9dbcbe11 100644 --- a/docs/ext/mpd.rst +++ b/docs/ext/mpd.rst @@ -33,7 +33,6 @@ Items on this list will probably not be supported in the near future. - Stickers are not supported - Crossfade is not supported - Replay gain is not supported -- ``count`` does not provide any statistics - ``stats`` does not provide any statistics - ``list`` does not support listing tracks by genre - ``decoders`` does not provide information about available decoders From 93918cb1e059b7ce73e7be54414fcb53b9f3ced3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 7 Nov 2013 23:25:55 +0100 Subject: [PATCH 042/156] local: flake8 fixes --- mopidy/backends/local/playback.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mopidy/backends/local/playback.py b/mopidy/backends/local/playback.py index 9fbb736d..b264dac7 100644 --- a/mopidy/backends/local/playback.py +++ b/mopidy/backends/local/playback.py @@ -1,10 +1,8 @@ from __future__ import unicode_literals import logging -import os from mopidy.backends import base -from mopidy.utils import path from . import translator From bdba83b1be3371a42ec8cdcefae47d976e72b3a4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 00:09:17 +0100 Subject: [PATCH 043/156] main: Move argparse building to a function. --- mopidy/__main__.py | 4 ++- mopidy/commands.py | 69 ++++++++++++++++++++++++---------------------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index aa0c751e..c8af0600 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -30,7 +30,9 @@ def main(): signal.signal(signal.SIGTERM, process.exit_handler) signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) - args = commands.parser.parse_args(args=mopidy_args) + parser = commands.build_parser() + args = parser.parse_args(args=mopidy_args) + if args.show_config: commands.show_config(args) if args.show_deps: diff --git a/mopidy/commands.py b/mopidy/commands.py index 598be043..0f7e6d9b 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -21,39 +21,42 @@ def config_override_type(value): '%s must have the format section/key=value' % value) -parser = argparse.ArgumentParser() -parser.add_argument( - '--version', action='version', - version='Mopidy %s' % versioning.get_version()) -parser.add_argument( - '-q', '--quiet', - action='store_const', const=-1, dest='verbosity_level', - help='less output (warning level)') -parser.add_argument( - '-v', '--verbose', - action='count', dest='verbosity_level', - help='more output (debug level)') -parser.add_argument( - '--save-debug-log', - action='store_true', dest='save_debug_log', - help='save debug log to "./mopidy.log"') -parser.add_argument( - '--show-config', - action='store_true', dest='show_config', - help='show current config') -parser.add_argument( - '--show-deps', - action='store_true', dest='show_deps', - help='show dependencies and their versions') -parser.add_argument( - '--config', - action='store', dest='config_files', type=config_files_type, - default=b'$XDG_CONFIG_DIR/mopidy/mopidy.conf', - help='config files to use, colon seperated, later files override') -parser.add_argument( - '-o', '--option', - action='append', dest='config_overrides', type=config_override_type, - help='`section/key=value` values to override config options') +def build_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + '--version', action='version', + version='Mopidy %s' % versioning.get_version()) + parser.add_argument( + '-q', '--quiet', + action='store_const', const=-1, dest='verbosity_level', + help='less output (warning level)') + parser.add_argument( + '-v', '--verbose', + action='count', dest='verbosity_level', + help='more output (debug level)') + parser.add_argument( + '--save-debug-log', + action='store_true', dest='save_debug_log', + help='save debug log to "./mopidy.log"') + parser.add_argument( + '--show-config', + action='store_true', dest='show_config', + help='show current config') + parser.add_argument( + '--show-deps', + action='store_true', dest='show_deps', + help='show dependencies and their versions') + parser.add_argument( + '--config', + action='store', dest='config_files', type=config_files_type, + default=b'$XDG_CONFIG_DIR/mopidy/mopidy.conf', + help='config files to use, colon seperated, later files override') + parser.add_argument( + '-o', '--option', + action='append', dest='config_overrides', type=config_override_type, + help='`section/key=value` values to override config options') + + return parser def show_config(args): From 51b778fcd655a9cd6b2f53ec228e7dd6a352ca50 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 00:59:05 +0100 Subject: [PATCH 044/156] main: Create helper for logging boostraping --- mopidy/__main__.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index c8af0600..539d6576 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -38,20 +38,9 @@ def main(): if args.show_deps: commands.show_deps() - # TODO: figure out a way to make the boilerplate in this file reusable in - # scanner and other places we need it. + bootstrap_logging(args) try: - # Initial config without extensions to bootstrap logging. - logging_initialized = False - logging_config, _ = config_lib.load( - args.config_files, [], args.config_overrides) - - # TODO: setup_logging needs defaults in-case config values are None - log.setup_logging( - logging_config, args.verbosity_level, args.save_debug_log) - logging_initialized = True - create_file_structures() check_old_locations() @@ -84,11 +73,20 @@ def main(): except KeyboardInterrupt: pass except Exception as ex: - if logging_initialized: - logger.exception(ex) + logger.exception(ex) raise +def bootstrap_logging(args): + # Initial config without extensions to bootstrap logging. + logging_config, _ = config_lib.load( + args.config_files, [], args.config_overrides) + + # TODO: setup_logging needs defaults in-case config values are None + log.setup_logging( + logging_config, args.verbosity_level, args.save_debug_log) + + def create_file_structures(): path.get_or_create_dir(b'$XDG_DATA_DIR/mopidy') path.get_or_create_file(b'$XDG_CONFIG_DIR/mopidy/mopidy.conf') From f49973304cd8fe2ac43e6906da6b291d4328e41a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 01:12:45 +0100 Subject: [PATCH 045/156] main: Start unifying command handling - Removes show_deps and show_config from commands module. These are now handled directly in the main() method pending subcommands. - Unifies show_config with general main() config handling. - Sets default verbosity level to zero. - Reduce verbosity when --show-config or --show-deps is called. - Update console logging to consider verbosity < 0 quiet/ --- mopidy/__main__.py | 40 +++++++++++++++++++++++++++++----------- mopidy/commands.py | 32 ++------------------------------ mopidy/utils/log.py | 2 +- 3 files changed, 32 insertions(+), 42 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 539d6576..af83dcd8 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -21,7 +21,7 @@ from mopidy import commands, ext from mopidy.audio import Audio from mopidy import config as config_lib from mopidy.core import Core -from mopidy.utils import log, path, process +from mopidy.utils import deps, log, path, process logger = logging.getLogger('mopidy.main') @@ -33,10 +33,8 @@ def main(): parser = commands.build_parser() args = parser.parse_args(args=mopidy_args) - if args.show_config: - commands.show_config(args) - if args.show_deps: - commands.show_deps() + if args.show_deps or args.show_config: + args.verbosity_level -= 1 bootstrap_logging(args) @@ -45,20 +43,40 @@ def main(): check_old_locations() installed_extensions = ext.load_extensions() - config, config_errors = config_lib.load( args.config_files, installed_extensions, args.config_overrides) - # Filter out disabled extensions and remove any config errors for them. enabled_extensions = [] for extension in installed_extensions: - enabled = config[extension.ext_name]['enabled'] - if ext.validate_extension(extension) and enabled: + if not ext.validate_extension(extension): + config[extension.ext_name] = {b'enabled': False} + config_errors[extension.ext_name] = { + b'enabled': b'extension disabled by self check.'} + elif not config[extension.ext_name]['enabled']: + config[extension.ext_name] = {b'enabled': False} + config_errors[extension.ext_name] = { + b'enabled': b'extension disabled by user config.'} + else: enabled_extensions.append(extension) - elif extension.ext_name in config_errors: - del config_errors[extension.ext_name] log_extension_info(installed_extensions, enabled_extensions) + + # TODO: move to 'mopidy config' and 'mopidy deps' + if args.show_config: + logger.info('Dumping sanitized user config and exiting.') + print config_lib.format( + config, installed_extensions, config_errors) + sys.exit(0) + if args.show_deps: + logger.info('Dumping debug info about dependencies and exiting.') + print deps.format_dependency_list() + sys.exit(0) + + # Remove errors for extensions that are not enabled: + for extension in installed_extensions: + if extension not in enabled_extensions: + config_errors.pop(extension.ext_name, None) + check_config_errors(config_errors) # Read-only config from here on, please. diff --git a/mopidy/commands.py b/mopidy/commands.py index 0f7e6d9b..36269dca 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -1,10 +1,8 @@ from __future__ import unicode_literals import argparse -import sys -from mopidy import config as config_lib, ext -from mopidy.utils import deps, versioning +from mopidy.utils import versioning def config_files_type(value): @@ -23,6 +21,7 @@ def config_override_type(value): def build_parser(): parser = argparse.ArgumentParser() + parser.set_defaults(verbosity_level=0) parser.add_argument( '--version', action='version', version='Mopidy %s' % versioning.get_version()) @@ -57,30 +56,3 @@ def build_parser(): help='`section/key=value` values to override config options') return parser - - -def show_config(args): - """Prints the effective config and exits.""" - extensions = ext.load_extensions() - config, errors = config_lib.load( - args.config_files, extensions, args.config_overrides) - - # Clear out any config for disabled extensions. - for extension in extensions: - if not ext.validate_extension(extension): - config[extension.ext_name] = {b'enabled': False} - errors[extension.ext_name] = { - b'enabled': b'extension disabled itself.'} - elif not config[extension.ext_name]['enabled']: - config[extension.ext_name] = {b'enabled': False} - errors[extension.ext_name] = { - b'enabled': b'extension disabled by config.'} - - print config_lib.format(config, extensions, errors) - sys.exit(0) - - -def show_deps(): - """Prints a list of all dependencies and exits.""" - print deps.format_dependency_list() - sys.exit(0) diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index 715aca1a..9f3c973d 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -34,7 +34,7 @@ def setup_root_logger(): def setup_console_logging(config, verbosity_level): - if verbosity_level == -1: + if verbosity_level < 0: log_level = logging.WARNING log_format = config['logging']['console_format'] elif verbosity_level >= 1: From 9539c2ac35635bb59511ce8f73c376cfd7702836 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 01:27:28 +0100 Subject: [PATCH 046/156] main: Switch to subcommands - show-deps replaced with 'mopidy deps' - show-config replaced with 'mopidy config' - Just running mopidy now displays help, run 'mopidy run' to start server. --- mopidy/__main__.py | 16 ++++++++++++---- mopidy/commands.py | 8 -------- tests/help_test.py | 1 - 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index af83dcd8..bc5ca86c 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -31,9 +31,18 @@ def main(): signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) parser = commands.build_parser() + subparser = parser.add_subparsers(title='commands', metavar='COMMAND') + + run_parser = subparser.add_parser('run', help='start mopidy server') + run_parser.set_defaults(command='run') + config_parser = subparser.add_parser('config', help='show current config') + config_parser.set_defaults(command='config') + deps_parser = subparser.add_parser('deps', help='show dependencies') + deps_parser.set_defaults(command='deps') + args = parser.parse_args(args=mopidy_args) - if args.show_deps or args.show_config: + if args.command in ('config', 'deps'): args.verbosity_level -= 1 bootstrap_logging(args) @@ -61,13 +70,12 @@ def main(): log_extension_info(installed_extensions, enabled_extensions) - # TODO: move to 'mopidy config' and 'mopidy deps' - if args.show_config: + if args.command == 'config': logger.info('Dumping sanitized user config and exiting.') print config_lib.format( config, installed_extensions, config_errors) sys.exit(0) - if args.show_deps: + elif args.command == 'deps': logger.info('Dumping debug info about dependencies and exiting.') print deps.format_dependency_list() sys.exit(0) diff --git a/mopidy/commands.py b/mopidy/commands.py index 36269dca..480e0f04 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -37,14 +37,6 @@ def build_parser(): '--save-debug-log', action='store_true', dest='save_debug_log', help='save debug log to "./mopidy.log"') - parser.add_argument( - '--show-config', - action='store_true', dest='show_config', - help='show current config') - parser.add_argument( - '--show-deps', - action='store_true', dest='show_deps', - help='show dependencies and their versions') parser.add_argument( '--config', action='store', dest='config_files', type=config_files_type, diff --git a/tests/help_test.py b/tests/help_test.py index 574e4fd7..75d545bc 100644 --- a/tests/help_test.py +++ b/tests/help_test.py @@ -22,6 +22,5 @@ class HelpTest(unittest.TestCase): self.assertIn('--quiet', output) self.assertIn('--verbose', output) self.assertIn('--save-debug-log', output) - self.assertIn('--show-config', output) self.assertIn('--config', output) self.assertIn('--option', output) From 4f036776757ad7dd8522fae066964624bf80a04a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 01:48:14 +0100 Subject: [PATCH 047/156] main: Improve main bootstrapping sequence - Parses args in two pases to allow for setup of logging well before doing extension sub-commands. --- mopidy/__main__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index bc5ca86c..32102b29 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -40,18 +40,20 @@ def main(): deps_parser = subparser.add_parser('deps', help='show dependencies') deps_parser.set_defaults(command='deps') - args = parser.parse_args(args=mopidy_args) + bootstrap_args = parser.parse_known_args(args=mopidy_args)[0] - if args.command in ('config', 'deps'): - args.verbosity_level -= 1 - - bootstrap_logging(args) + if bootstrap_args.command in ('config', 'deps'): + bootstrap_args.verbosity_level -= 1 + bootstrap_logging(bootstrap_args) try: create_file_structures() check_old_locations() installed_extensions = ext.load_extensions() + # TODO: install extension subcommands. + + args = parser.parse_args(args=mopidy_args) config, config_errors = config_lib.load( args.config_files, installed_extensions, args.config_overrides) From 25fedc77006fd60b28e8da5e6943fa1c4aa82515 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 18:47:55 +0100 Subject: [PATCH 048/156] loggin: Add DelayedHandler to root logger. The delayed handler: - Accepts and buffers logs until they are released. - Upon release the logs are re-posted to the root logger. - After release log records are ignored. This allows us to avoid the silly tricks we've been doing with parsing args and config early for the sake of bootstraping logging. Now we can just start logging and once the logging has been setup the messages are released and handled according to the correct settings. --- mopidy/__main__.py | 11 +++++++++-- mopidy/utils/log.py | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 32102b29..201c8826 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -21,12 +21,15 @@ from mopidy import commands, ext from mopidy.audio import Audio from mopidy import config as config_lib from mopidy.core import Core -from mopidy.utils import deps, log, path, process +from mopidy.utils import deps, log, path, process, versioning logger = logging.getLogger('mopidy.main') def main(): + log.bootstrap_delayed_logging() + logger.info('Starting Mopidy %s', versioning.get_version()) + signal.signal(signal.SIGTERM, process.exit_handler) signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) @@ -54,9 +57,14 @@ def main(): # TODO: install extension subcommands. args = parser.parse_args(args=mopidy_args) + if args.command in ('deps', 'config'): + args.verbosity_level -= 1 + config, config_errors = config_lib.load( args.config_files, installed_extensions, args.config_overrides) + log.setup_logging(config, args.verbosity_level, args.save_debug_log) + enabled_extensions = [] for extension in installed_extensions: if not ext.validate_extension(extension): @@ -92,7 +100,6 @@ def main(): # Read-only config from here on, please. proxied_config = config_lib.Proxy(config) - log.setup_log_levels(proxied_config) ext.register_gstreamer_elements(enabled_extensions) # Anything that wants to exit after this point must use diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index 9f3c973d..f7877614 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -4,14 +4,40 @@ import logging import logging.config import logging.handlers -from . import versioning + +class DelayedHandler(logging.Handler): + def __init__(self): + logging.Handler.__init__(self) + self._released = False + self._buffer = [] + + def handle(self, record): + if not self._released: + self._buffer.append(record) + + def release(self): + self._released = True + root = logging.getLogger('') + while self._buffer: + root.handle(self._buffer.pop(0)) + + +_delayed_handler = DelayedHandler() + + +def bootstrap_delayed_logging(): + root = logging.getLogger('') + root.setLevel(logging.DEBUG) + root.addHandler(_delayed_handler) def setup_logging(config, verbosity_level, save_debug_log): - setup_root_logger() setup_console_logging(config, verbosity_level) + setup_log_levels(config) + if save_debug_log: setup_debug_logging_to_file(config) + if hasattr(logging, 'captureWarnings'): # New in Python 2.7 logging.captureWarnings(True) @@ -19,8 +45,7 @@ def setup_logging(config, verbosity_level, save_debug_log): if config['logging']['config_file']: logging.config.fileConfig(config['logging']['config_file']) - logger = logging.getLogger('mopidy.utils.log') - logger.info('Starting Mopidy %s', versioning.get_version()) + _delayed_handler.release() def setup_log_levels(config): @@ -28,11 +53,6 @@ def setup_log_levels(config): logging.getLogger(name).setLevel(level) -def setup_root_logger(): - root = logging.getLogger('') - root.setLevel(logging.DEBUG) - - def setup_console_logging(config, verbosity_level): if verbosity_level < 0: log_level = logging.WARNING From 03750a8bf22251ec96060b070e7bcf113b9f13f8 Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Fri, 8 Nov 2013 21:25:21 +0100 Subject: [PATCH 049/156] 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 050/156] 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 7144876dc5dbc69e8f167f89c3e4870d4def6410 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 18:53:52 +0100 Subject: [PATCH 051/156] main: Move default subparsers into commands module Also switches to using dest for storing the chosen sub-parser. --- mopidy/__main__.py | 18 ++---------------- mopidy/commands.py | 9 ++++++++- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 201c8826..a765548d 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -33,26 +33,12 @@ def main(): signal.signal(signal.SIGTERM, process.exit_handler) signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) - parser = commands.build_parser() - subparser = parser.add_subparsers(title='commands', metavar='COMMAND') - - run_parser = subparser.add_parser('run', help='start mopidy server') - run_parser.set_defaults(command='run') - config_parser = subparser.add_parser('config', help='show current config') - config_parser.set_defaults(command='config') - deps_parser = subparser.add_parser('deps', help='show dependencies') - deps_parser.set_defaults(command='deps') - - bootstrap_args = parser.parse_known_args(args=mopidy_args)[0] - - if bootstrap_args.command in ('config', 'deps'): - bootstrap_args.verbosity_level -= 1 - bootstrap_logging(bootstrap_args) - try: create_file_structures() check_old_locations() + parser, subparser = commands.build_parser() + installed_extensions = ext.load_extensions() # TODO: install extension subcommands. diff --git a/mopidy/commands.py b/mopidy/commands.py index 480e0f04..9d65549e 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -47,4 +47,11 @@ def build_parser(): action='append', dest='config_overrides', type=config_override_type, help='`section/key=value` values to override config options') - return parser + subparser = parser.add_subparsers( + title='commands', metavar='COMMAND', dest='command') + + subparser.add_parser('run', help='start mopidy server') + subparser.add_parser('config', help='show current config') + subparser.add_parser('deps', help='show dependencies') + + return parser, subparser From 518cac5eab773afd56aa486bee1ec6e4bad56d08 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 21:59:43 +0100 Subject: [PATCH 052/156] ext/backends: Add get_sub_commands and BaseSubCommandProvider --- mopidy/backends/base.py | 31 +++++++++++++++++++++++++++++++ mopidy/ext.py | 8 ++++++++ 2 files changed, 39 insertions(+) diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 6b980f06..9dc9befa 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -279,3 +279,34 @@ class BasePlaylistsProvider(object): *MUST be implemented by subclass.* """ raise NotImplementedError + + +class BaseSubCommandProvider(object): + """Sub-classes may optionally add arguments to the passed in parser. + + :param parser: parser you may add arguments to + :type parser: :class:`argparse.ArgumentParser` + """ + + name = None + """What the sub-command should be called. Will be run as ``mopidy NAME`` + + Example: ``scan`` + """ + + help = None + """Optional help text for the sub-command, will be displayed in help.""" + + def __init__(self, parser): + pass + + def run(self, args, config): + """Run the sub-command implemented by this provider. + + *MUST be implemented by subclass.* + + :param args: the argments object from argpase. + :param config: read only version of the mopidy config. + :returns: integer exit value for the process. + """ + raise NotImplementedError diff --git a/mopidy/ext.py b/mopidy/ext.py index c239c374..aa6b4cd0 100644 --- a/mopidy/ext.py +++ b/mopidy/ext.py @@ -87,6 +87,14 @@ class Extension(object): """ return [] + def get_sub_commands(self): + """List of sub-command classes + + :returns: list of + :class:`~mopidy.backends.base.BaseSubCommandProvider` subclasses + """ + return [] + def register_gstreamer_elements(self): """Hook for registering custom GStreamer elements From ef10c2e178e723d46457ef2dddef7cb7a491eebd Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 22:04:26 +0100 Subject: [PATCH 053/156] main: Wire in actual execution of sub-commands. --- mopidy/__main__.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index a765548d..bce3804c 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -38,9 +38,13 @@ def main(): check_old_locations() parser, subparser = commands.build_parser() - installed_extensions = ext.load_extensions() - # TODO: install extension subcommands. + extension_sub_commands = {} + + for extension in installed_extensions: + for cls in extension.get_sub_commands(): + cmd_parser = subparser.add_parser(cls.name, help=cls.help) + extension_sub_commands[cls.name] = (extension, cls(cmd_parser)) args = parser.parse_args(args=mopidy_args) if args.command in ('deps', 'config'): @@ -86,11 +90,25 @@ def main(): # Read-only config from here on, please. proxied_config = config_lib.Proxy(config) - ext.register_gstreamer_elements(enabled_extensions) + if args.command in extension_sub_commands: + extension, cmd = extension_sub_commands[args.command] - # Anything that wants to exit after this point must use - # mopidy.utils.process.exit_process as actors have been started. - start(proxied_config, enabled_extensions) + if extension not in enabled_extensions: + parser.error('Can not run sub-command %s from the disabled ' + 'extension %s.' % (cmd.name, extension.ext_name)) + + sys.exit(cmd.run(args, proxied_config)) + + if args.command == 'run': + ext.register_gstreamer_elements(enabled_extensions) + + # Anything that wants to exit after this point must use + # mopidy.utils.process.exit_process as actors have been started. + start(proxied_config, enabled_extensions) + sys.exit(0) + + parser.error( + 'Unknown command %s, this should never happen.' % args.command) except KeyboardInterrupt: pass except Exception as ex: From 2581062ae033fbdf46f90c299f1a850c16994179 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 23:18:26 +0100 Subject: [PATCH 054/156] ext/backends: Add extensions to sub commands run --- mopidy/__main__.py | 4 +++- mopidy/backends/base.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index bce3804c..035e0ae9 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -97,7 +97,9 @@ def main(): parser.error('Can not run sub-command %s from the disabled ' 'extension %s.' % (cmd.name, extension.ext_name)) - sys.exit(cmd.run(args, proxied_config)) + logging.info('Running %s command provided by %s.', cmd.name, + extension.ext_name) + sys.exit(cmd.run(args, proxied_config, enabled_extensions)) if args.command == 'run': ext.register_gstreamer_elements(enabled_extensions) diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 9dc9befa..2fb20d03 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -300,13 +300,14 @@ class BaseSubCommandProvider(object): def __init__(self, parser): pass - def run(self, args, config): + def run(self, args, config, extensions): """Run the sub-command implemented by this provider. *MUST be implemented by subclass.* :param args: the argments object from argpase. :param config: read only version of the mopidy config. + :param extensions: list of enabled extensions. :returns: integer exit value for the process. """ raise NotImplementedError From b5f8480eea08eac2206d22c62dabfbf4e9c84cfc Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 23:20:19 +0100 Subject: [PATCH 055/156] local: Add 'mopidy scan' command via extension sub-commands. --- mopidy/backends/local/__init__.py | 4 ++ mopidy/backends/local/scan.py | 105 ++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 mopidy/backends/local/scan.py diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 6c66c70d..ad70279b 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -36,3 +36,7 @@ class Extension(ext.Extension): def get_library_updaters(self): from .library import LocalLibraryUpdateProvider return [LocalLibraryUpdateProvider] + + def get_sub_commands(self): + from .scan import ScanSubCommand + return [ScanSubCommand] diff --git a/mopidy/backends/local/scan.py b/mopidy/backends/local/scan.py new file mode 100644 index 00000000..ab62f81b --- /dev/null +++ b/mopidy/backends/local/scan.py @@ -0,0 +1,105 @@ +from __future__ import unicode_literals + +import logging +import os +import time + +from mopidy import exceptions +from mopidy.audio import scan +from mopidy.backends import base +from mopidy.utils import path + +logger = logging.getLogger('mopidy.backends.local.scan') + + +class ScanSubCommand(base.BaseSubCommandProvider): + name = b'scan' + help = b'scan local media files' + + def run(self, args, config, extensions): + media_dir = config['local']['media_dir'] + scan_timeout = config['local']['scan_timeout'] + excluded_file_extensions = config['local']['excluded_file_extensions'] + + updaters = {} + for e in extensions: + for updater_class in e.get_library_updaters(): + if updater_class and 'local' in updater_class.uri_schemes: + updaters[e.ext_name] = updater_class + + if not updaters: + logging.error('No usable library updaters found.') + return 1 + elif len(updaters) > 1: + logging.error('More than one library updater found. ' + 'Provided by: %s', ', '.join(updaters.keys())) + return 1 + + local_updater = updaters.values()[0](config) + + uris_library = set() + uris_update = set() + uris_remove = set() + + logging.info('Checking tracks from library.') + for track in local_updater.load(): + try: + # TODO: convert local to file uri / path + stat = os.stat(path.uri_to_path(track.uri)) + if int(stat.st_mtime) > track.last_modified: + uris_update.add(track.uri) + uris_library.add(track.uri) + except OSError: + logging.debug('Missing file %s', track.uri) + uris_remove.add(track.uri) + + logging.info('Removing %d moved or deleted tracks.', len(uris_remove)) + for uri in uris_remove: + local_updater.remove(uri) + + logging.info('Checking %s for new or modified tracks.', media_dir) + for uri in path.find_uris(config['local']['media_dir']): + file_extension = os.path.splitext(path.uri_to_path(uri))[1] + if file_extension in excluded_file_extensions: + logging.debug('Skipped %s: File extension excluded.', uri) + continue + + if uri not in uris_library: + uris_update.add(uri) + + logging.info('Found %d new or modified tracks.', len(uris_update)) + logging.info('Scanning new and modified tracks.') + + scanner = scan.Scanner(scan_timeout) + progress = Progress(len(uris_update)) + + for uri in sorted(uris_update): + try: + data = scanner.scan(uri) + track = scan.audio_data_to_track(data) + local_updater.add(track) + logging.debug('Added %s', track.uri) + except exceptions.ScannerError as error: + logging.warning('Failed %s: %s', uri, error) + + progress.increment() + + logging.info('Commiting changes.') + local_updater.commit() + return 0 + + +# TODO: move to utils? +class Progress(object): + def __init__(self, total): + self.count = 0 + self.total = total + self.start = time.time() + + def increment(self): + self.count += 1 + if self.count % 1000 == 0 or self.count == self.total: + duration = time.time() - self.start + remainder = duration / self.count * (self.total - self.count) + logging.info('Scanned %d of %d files in %ds, ~%ds left.', + self.count, self.total, duration, remainder) From 1a3ff456f94ce9c257054e3f73f9596d0e414ac7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 23:21:26 +0100 Subject: [PATCH 056/156] scanner: Remove old scanner in favour of sub-commands --- mopidy/scanner.py | 151 ---------------------------------------------- setup.py | 1 - 2 files changed, 152 deletions(-) delete mode 100644 mopidy/scanner.py diff --git a/mopidy/scanner.py b/mopidy/scanner.py deleted file mode 100644 index 30fb553b..00000000 --- a/mopidy/scanner.py +++ /dev/null @@ -1,151 +0,0 @@ -from __future__ import unicode_literals - -import argparse -import logging -import os -import sys -import time - -import gobject -gobject.threads_init() - -# Extract any command line arguments. This needs to be done before GStreamer is -# imported, so that GStreamer doesn't hijack e.g. ``--help``. -mopidy_args = sys.argv[1:] -sys.argv[1:] = [] - -from mopidy import config as config_lib, exceptions, ext -from mopidy.audio import scan -from mopidy.utils import log, path, versioning - - -def main(): - args = parse_args() - # TODO: support config files and overrides (shared from main?) - config_files = [b'/etc/mopidy/mopidy.conf', - b'$XDG_CONFIG_DIR/mopidy/mopidy.conf'] - config_overrides = [] - - # TODO: decide if we want to avoid this boilerplate some how. - # Initial config without extensions to bootstrap logging. - logging_config, _ = config_lib.load(config_files, [], config_overrides) - log.setup_root_logger() - log.setup_console_logging(logging_config, args.verbosity_level) - - extensions = ext.load_extensions() - config, errors = config_lib.load( - config_files, extensions, config_overrides) - log.setup_log_levels(config) - - if not config['local']['media_dir']: - logging.warning('Config value local/media_dir is not set.') - return - - if not config['local']['scan_timeout']: - logging.warning('Config value local/scan_timeout is not set.') - return - - # TODO: missing config error checking and other default setup code. - - updaters = {} - for e in extensions: - for updater_class in e.get_library_updaters(): - if updater_class and 'local' in updater_class.uri_schemes: - updaters[e.ext_name] = updater_class - - if not updaters: - logging.error('No usable library updaters found.') - return - elif len(updaters) > 1: - logging.error('More than one library updater found. ' - 'Provided by: %s', ', '.join(updaters.keys())) - return - - local_updater = updaters.values()[0](config) # TODO: switch to actor? - - media_dir = config['local']['media_dir'] - excluded_extensions = config['local']['excluded_file_extensions'] - - uris_library = set() - uris_update = set() - uris_remove = set() - - logging.info('Checking tracks from library.') - for track in local_updater.load(): - try: - # TODO: convert local to file uri / path - stat = os.stat(path.uri_to_path(track.uri)) - if int(stat.st_mtime) > track.last_modified: - uris_update.add(track.uri) - uris_library.add(track.uri) - except OSError: - logging.debug('Missing file %s', track.uri) - uris_remove.add(track.uri) - - logging.info('Removing %d moved or deleted tracks.', len(uris_remove)) - for uri in uris_remove: - local_updater.remove(uri) - - logging.info('Checking %s for new or modified tracks.', media_dir) - for uri in path.find_uris(config['local']['media_dir']): - if os.path.splitext(path.uri_to_path(uri))[1] in excluded_extensions: - logging.debug('Skipped %s: File extension excluded.', uri) - continue - - if uri not in uris_library: - uris_update.add(uri) - - logging.info('Found %d new or modified tracks.', len(uris_update)) - logging.info('Scanning new and modified tracks.') - - scanner = scan.Scanner(config['local']['scan_timeout']) - progress = Progress(len(uris_update)) - - for uri in sorted(uris_update): - try: - data = scanner.scan(uri) - track = scan.audio_data_to_track(data) - local_updater.add(track) - logging.debug('Added %s', track.uri) - except exceptions.ScannerError as error: - logging.warning('Failed %s: %s', uri, error) - - progress.increment() - - logging.info('Commiting changes.') - local_updater.commit() - - -class Progress(object): - def __init__(self, total): - self.count = 0 - self.total = total - self.start = time.time() - - def increment(self): - self.count += 1 - if self.count % 1000 == 0 or self.count == self.total: - duration = time.time() - self.start - remainder = duration / self.count * (self.total - self.count) - logging.info('Scanned %d of %d files in %ds, ~%ds left.', - self.count, self.total, duration, remainder) - - -def parse_args(): - parser = argparse.ArgumentParser() - parser.add_argument( - '--version', action='version', - version='Mopidy %s' % versioning.get_version()) - parser.add_argument( - '-q', '--quiet', - action='store_const', const=0, dest='verbosity_level', - help='less output (warning level)') - parser.add_argument( - '-v', '--verbose', - action='count', default=1, dest='verbosity_level', - help='more output (debug level)') - return parser.parse_args(args=mopidy_args) - - -if __name__ == '__main__': - main() diff --git a/setup.py b/setup.py index a448a029..511a16e8 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,6 @@ setup( entry_points={ 'console_scripts': [ 'mopidy = mopidy.__main__:main', - 'mopidy-scan = mopidy.scanner:main', 'mopidy-convert-config = mopidy.config.convert:main', ], 'mopidy.ext': [ From 51d1e2265515b5ef0a0d35c22c15417c04197ee9 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Nov 2013 23:51:45 +0100 Subject: [PATCH 057/156] docs: Update changelog for PR #566 and #567 --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 44ca89f7..f9ccbb3e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,10 @@ 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. v0.16.1 (2013-11-02) From c776565f656dd1a3a3812e9f8ddb388a1835e1c3 Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sat, 9 Nov 2013 01:22:27 +0100 Subject: [PATCH 058/156] 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 059/156] 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 060/156] 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 67c028c31e51a181451d9636432629b89dabe130 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Nov 2013 13:23:52 +0100 Subject: [PATCH 061/156] docs: Update docs with respect to sub-commands --- docs/api/backends.rst | 5 +++ docs/commands/mopidy-scan.rst | 59 -------------------------------- docs/commands/mopidy.rst | 63 +++++++++++++++++++++++------------ docs/conf.py | 7 ---- docs/contributing.rst | 4 +-- docs/ext/local.rst | 15 +++------ docs/running.rst | 4 +-- docs/troubleshooting.rst | 4 +-- 8 files changed, 57 insertions(+), 104 deletions(-) delete mode 100644 docs/commands/mopidy-scan.rst diff --git a/docs/api/backends.rst b/docs/api/backends.rst index ec78f250..a85feb03 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -39,6 +39,11 @@ Library provider .. autoclass:: mopidy.backends.base.BaseLibraryProvider :members: +Sub-command provider +==================== + +.. autoclass:: mopidy.backends.base.BaseSubCommandProvider + :members: Backend listener ================ diff --git a/docs/commands/mopidy-scan.rst b/docs/commands/mopidy-scan.rst deleted file mode 100644 index e8c25f77..00000000 --- a/docs/commands/mopidy-scan.rst +++ /dev/null @@ -1,59 +0,0 @@ -.. _mopidy-scan-cmd: - -******************* -mopidy-scan command -******************* - -Synopsis -======== - -mopidy-scan - [-h] [--version] [-q] [-v] - - -Description -=========== - -Mopidy is a music server which can play music both from multiple sources, like -your local hard drive, radio streams, and from Spotify and SoundCloud. Searches -combines results from all music sources, and you can mix tracks from all -sources in your play queue. Your playlists from Spotify or SoundCloud are also -available for use. - -The ``mopidy-scan`` command is used to index a music library to make it -available for playback with ``mopidy``. - - -Options -======= - -.. program:: mopidy-scan - -.. cmdoption:: --version - - Show Mopidy's version number and exit. - -.. cmdoption:: -h, --help - - Show help message and exit. - -.. cmdoption:: -q, --quiet - - Show less output: warning level and higher. - -.. cmdoption:: -v, --verbose - - Show more output: debug level and higher. - - -See also -======== - -:ref:`mopidy(1) ` - - -Reporting bugs -============== - -Report bugs to Mopidy's issue tracker at - diff --git a/docs/commands/mopidy.rst b/docs/commands/mopidy.rst index df4766c3..c4a35973 100644 --- a/docs/commands/mopidy.rst +++ b/docs/commands/mopidy.rst @@ -8,8 +8,8 @@ Synopsis ======== mopidy - [-h] [--version] [-q] [-v] [--save-debug-log] [--show-config] - [--show-deps] [--config CONFIG_FILES] [-o CONFIG_OVERRIDES] + [-h] [--version] [-q] [-v] [--save-debug-log] [--config CONFIG_FILES] + [-o CONFIG_OVERRIDES] COMMAND ... Description @@ -21,7 +21,7 @@ combines results from all music sources, and you can mix tracks from all sources in your play queue. Your playlists from Spotify or SoundCloud are also available for use. -The ``mopidy`` command is used to start the server. +The ``mopidy run`` command is used to start the server. Options @@ -50,16 +50,6 @@ Options Save debug log to the file specified in the :confval:`logging/debug_file` config value, typically ``./mopidy.log``. -.. cmdoption:: --show-config - - Show the current effective config. All configuration sources are merged - together to show the effective document. Secret values like passwords are - masked out. Config for disabled extensions are not included. - -.. cmdoption:: --show-deps - - Show dependencies, their versions and installation location. - .. cmdoption:: --config Specify config file to use. To use multiple config files, separate them @@ -72,6 +62,37 @@ Options be provided multiple times. +Built in sub-commands +===================== + +.. cmdoption:: run + + Run the mopidy server. + +.. cmdoption:: config + + Show the current effective config. All configuration sources are merged + together to show the effective document. Secret values like passwords are + masked out. Config for disabled extensions are not included. + +.. cmdoption:: deps + + Show dependencies, their versions and installation location. + + +Extension sub-commands +====================== + +Additionally, extensions can provide extra sub-commands. See ``mopidy --help`` +for a list of what is availbale on your system and ``mopidy COMMAND --help`` +for command specific help. Sub-commands for disabled extensions will be listed, +but can not be run. + +.. cmdoption:: local + + Scan local media files present in your library. + + Files ===== @@ -88,34 +109,32 @@ Examples To start the music server, run:: - mopidy + mopidy run To start the server with an additional config file than can override configs set in the default config files, run:: - mopidy --config ./my-config.conf + mopidy --config ./my-config.conf run To start the server and change a config value directly on the command line, run:: - mopidy --option mpd/enabled=false + mopidy --option mpd/enabled=false run The :option:`--option` flag may be repeated multiple times to change multiple configs:: - mopidy -o mpd/enabled=false -o spotify/bitrate=320 + mopidy -o mpd/enabled=false -o spotify/bitrate=320 run -The :option:`--show-config` output shows the effect of the :option:`--option` -flags:: +``mopidy config`` output shows the effect of the :option:`--option` flags:: - mopidy -o mpd/enabled=false -o spotify/bitrate=320 --show-config + mopidy -o mpd/enabled=false -o spotify/bitrate=320 config See also ======== -:ref:`mopidy-scan(1) `, :ref:`mopidy-convert-config(1) -` +:ref:`mopidy-convert-config(1) ` Reporting bugs ============== diff --git a/docs/conf.py b/docs/conf.py index 77ee897e..eb23daf5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -141,13 +141,6 @@ man_pages = [ '', '1' ), - ( - 'commands/mopidy-scan', - 'mopidy-scan', - 'index music for playback with mopidy', - '', - '1' - ), ( 'commands/mopidy-convert-config', 'mopidy-convert-config', diff --git a/docs/contributing.rst b/docs/contributing.rst index 22df8ced..b25c5f6d 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -70,9 +70,9 @@ repo. #. Now you can run the Mopidy command, and it will run using the code in the Git repo:: - mopidy + mopidy run - If you do any changes to the code, you'll just need to restart ``mopidy`` + If you do any changes to the code, you'll just need to restart ``mopidy run`` to see the changes take effect. diff --git a/docs/ext/local.rst b/docs/ext/local.rst index f6b281bd..79307ea4 100644 --- a/docs/ext/local.rst +++ b/docs/ext/local.rst @@ -71,7 +71,7 @@ music... Generating a tag cache ---------------------- -The program :command:`mopidy-scan` will scan the path set in the +The program :command:`mopidy scan` will scan the path set in the :confval:`local/media_dir` config value for any media files and build a MPD compatible ``tag_cache``. @@ -80,16 +80,11 @@ To make a ``tag_cache`` of your local music available for Mopidy: #. Ensure that the :confval:`local/media_dir` config value points to where your music is located. Check the current setting by running:: - mopidy --show-config + mopidy config -#. Scan your media library. The command outputs the ``tag_cache`` to - standard output, which means that you will need to redirect the output to a - file yourself:: +#. Scan your media library. The command writes the ``tag_cache`` to + the :confval:`local/tag_cache_file`:: - mopidy-scan > tag_cache - -#. Move the ``tag_cache`` file to the location - set in the :confval:`local/tag_cache_file` config value, or change the - config value to point to where your ``tag_cache`` file is. + mopidy scan #. Start Mopidy, find the music library in a client, and play some local music! diff --git a/docs/running.rst b/docs/running.rst index c96805e4..389f6917 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -4,10 +4,10 @@ Running Mopidy To start Mopidy, simply open a terminal and run:: - mopidy + mopidy run For a complete reference to the Mopidy commands and their command line options, -see :ref:`mopidy-cmd` and :ref:`mopidy-scan-cmd`. +see :ref:`mopidy-cmd`. When Mopidy says ``MPD server running at [127.0.0.1]:6600`` it's ready to accept connections by any MPD client. Check out our non-exhaustive diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 7ee4d417..f344b1cf 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -28,7 +28,7 @@ accepted, but large logs should still be shared through a pastebin. Effective configuration ======================= -The command :option:`mopidy --show-config` will print your full effective +The command ``mopidy config`` will print your full effective configuration the way Mopidy sees it after all defaults and all config files have been merged into a single config document. Any secret values like passwords are masked out, so the output of the command should be safe to share @@ -38,7 +38,7 @@ with others for debugging. Installed dependencies ====================== -The command :option:`mopidy --show-deps` will list the paths to and versions of +The command ``mopidy deps`` will list the paths to and versions of any dependency Mopidy or the extensions might need to work. This is very useful data for checking that you're using the right versions, and that you're using the right installation if you have multiple installations of a dependency on From 3945e437de2be2ce1a0d88af0490385eeb2e1318 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Nov 2013 13:24:19 +0100 Subject: [PATCH 062/156] docs: Fix minor doc issues found during review - Adds autodoc for mopidy.audio.scan - Updates ``file://`` to ``local:`` - Minor language fix. --- docs/api/audio.rst | 7 +++++++ docs/ext/local.rst | 2 +- docs/extensiondev.rst | 2 +- mopidy/audio/scan.py | 16 ++++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/api/audio.rst b/docs/api/audio.rst index 2b9f6cc5..550ca890 100644 --- a/docs/api/audio.rst +++ b/docs/api/audio.rst @@ -28,3 +28,10 @@ Audio listener .. autoclass:: mopidy.audio.AudioListener :members: + + +Audio scanner +============= + +.. autoclass:: mopidy.audio.scan.Scanner + :members: diff --git a/docs/ext/local.rst b/docs/ext/local.rst index 79307ea4..2615856b 100644 --- a/docs/ext/local.rst +++ b/docs/ext/local.rst @@ -6,7 +6,7 @@ Mopidy-Local Extension for playing music from a local music archive. -This backend handles URIs starting with ``file:``. +This backend handles URIs starting with ``local:``. Known issues diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 428751de..9c8464f6 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -353,7 +353,7 @@ Example backend If you want to extend Mopidy to support new music and playlist sources, you want to implement a backend. A backend does not have access to Mopidy's core -API at all and got a bunch of interfaces to implement. +API at all and have a bunch of interfaces to implement. The skeleton of a backend would look like this. See :ref:`backend-api` for more details. diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 82803379..435cac87 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -14,6 +14,15 @@ from mopidy.utils import path class Scanner(object): + """ + Helper to get tags and other relevant info from URIs. + + :param timeout: timeout for scanning a URI in ms + :type event: int + :param min_duration: minimum duration of scanned URI in ms, -1 for all. + :type event: int + """ + def __init__(self, timeout=1000, min_duration=100): self.timeout_ms = timeout self.min_duration_ms = min_duration @@ -35,6 +44,13 @@ class Scanner(object): self.bus.set_flushing(True) def scan(self, uri): + """ + Scan the given uri collecting relevant metadata. + + :param uri: URI of the resource to scan. + :type event: string + :return: Dictionary of tags, duration, mtime and uri information. + """ try: self._setup(uri) data = self._collect() From 5cd0938e0de85df6356196ab02198f214f8b411c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Nov 2013 13:38:09 +0100 Subject: [PATCH 063/156] docs: Update changelog with sub-commands work --- docs/changelog.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 44ca89f7..5dd3aa67 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,6 +19,22 @@ v0.17.0 (UNRELEASED) track artist. Album artist is now only populated if the scanned file got an explicit album artist set. +**Sub-commands** + +- Swtiched to sub-commands for mopidy this implies the following changes + (fixes :issue:`437`): + + ===================== ============= + Old command New command + ===================== ============= + mopidy mopidy run + mopidy --show-deps mopidy deps + mopidy --show-config mopidy config + mopidy-scan mopidy scan + +- Added hooks for extensions to create their own custom sub-commands and + converted ``mopidy-scan`` as first user of new API (Fixes :issue:`436`). + v0.16.1 (2013-11-02) ==================== From 4e6ebbe9556e3b2d82d5445e77e56891ab637824 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Nov 2013 18:20:22 +0100 Subject: [PATCH 064/156] local/docs: Update based on review comments - Bunch of typos and wording improvements from review. - Fixed mopidy.backends.local.scan botched merge. - Document and enforce that sub-command name needs to be bytes. --- docs/changelog.rst | 4 +-- docs/commands/mopidy.rst | 2 +- mopidy/__main__.py | 11 ++++---- mopidy/backends/base.py | 7 +++--- mopidy/backends/local/scan.py | 47 +++++++++++++++++++---------------- 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 115d4d26..c611c8b6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,8 +25,8 @@ v0.17.0 (UNRELEASED) **Sub-commands** -- Swtiched to sub-commands for mopidy this implies the following changes - (fixes :issue:`437`): +- Switched to sub-commands for the ``mopidy`` command , this implies the + following changes (fixes :issue:`437`): ===================== ============= Old command New command diff --git a/docs/commands/mopidy.rst b/docs/commands/mopidy.rst index c4a35973..c03a40cf 100644 --- a/docs/commands/mopidy.rst +++ b/docs/commands/mopidy.rst @@ -53,7 +53,7 @@ Options .. cmdoption:: --config Specify config file to use. To use multiple config files, separate them - with colon. The later files override the earlier ones if there's a + with a colon. The later files override the earlier ones if there's a conflict. .. cmdoption:: -o