From 79cbdb4fbbd99536501051a3abf4fd3d0121d241 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Dec 2012 23:43:48 +0100 Subject: [PATCH 1/5] mpd: Add MPD_SERVER_CONNECTION_TIMEOUT setting --- docs/changes.rst | 4 ++++ mopidy/frontends/mpd/actor.py | 3 ++- mopidy/settings.py | 10 ++++++++++ mopidy/utils/network.py | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index b5217200..e72abc02 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -15,6 +15,10 @@ v0.11.0 (in development) **MPD frontend** +- Add :attr:`mopidy.settings.MPD_SERVER_CONNECTION_TIMEOUT` setting which + controls how long an MPD client can stay inactive before the connection is + closed by the server. + - Add support for the ``findadd`` command. - Updated to match the MPD 0.17 protocol (Fixes: :issue:`228`): diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index d3c718c4..11e07aa7 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -23,7 +23,8 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener): network.Server( hostname, port, protocol=session.MpdSession, protocol_kwargs={'core': core}, - max_connections=settings.MPD_SERVER_MAX_CONNECTIONS) + max_connections=settings.MPD_SERVER_MAX_CONNECTIONS, + timeout=settings.MPD_SERVER_CONNECTION_TIMEOUT) except IOError as error: logger.error( 'MPD server startup failed: %s', diff --git a/mopidy/settings.py b/mopidy/settings.py index 0a272035..c2081e27 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -174,6 +174,16 @@ MIXER = 'autoaudiomixer' #: MIXER_TRACK = None MIXER_TRACK = None +#: Number of seconds an MPD client can stay inactive before the connection is +#: closed by the server. +#: +#: Used by :mod:`mopidy.frontends.mpd`. +#: +#: Default:: +#: +#: MPD_SERVER_CONNECTION_TIMEOUT = 60 +MPD_SERVER_CONNECTION_TIMEOUT = 60 + #: Which address Mopidy's MPD server should bind to. #: #: Used by :mod:`mopidy.frontends.mpd`. diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 604350d1..1ffb12d6 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -291,7 +291,7 @@ class Connection(object): return True def timeout_callback(self): - self.stop('Client timeout out after %s seconds' % self.timeout) + self.stop('Client inactive for %ds; closing connection' % self.timeout) return False From 30edba0a3e5bc9cab85f7f8313f9e2a336bb6d76 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Dec 2012 00:25:47 +0100 Subject: [PATCH 2/5] spotify: Unbreak search by URI --- mopidy/backends/spotify/library.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index ca6ee92a..bfdcb4f5 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -70,6 +70,13 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): if not query: return self._get_all_tracks() + if 'uri' in query.keys(): + result = [] + for uri in query['uri']: + tracks = self.lookup(uri) + result += tracks + return result + spotify_query = self._translate_search_query(query) logger.debug('Spotify search query: %s' % spotify_query) @@ -110,14 +117,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): def _translate_search_query(self, mopidy_query): spotify_query = [] for (field, values) in mopidy_query.iteritems(): - if field == 'uri': - tracks = [] - for value in values: - track = self.lookup(value) - if track: - tracks.append(track) - return tracks - elif field == 'track': + if field == 'track': field = 'title' elif field == 'date': field = 'year' From cb78dc634180d72a6b0ce96b974bb614b5ad62ce Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Dec 2012 00:26:29 +0100 Subject: [PATCH 3/5] spotify: Spotify wants 'track', not 'title' (#272) --- mopidy/backends/spotify/library.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index bfdcb4f5..cd6db63d 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -117,9 +117,7 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): def _translate_search_query(self, mopidy_query): spotify_query = [] for (field, values) in mopidy_query.iteritems(): - if field == 'track': - field = 'title' - elif field == 'date': + if field == 'date': field = 'year' if not hasattr(values, '__iter__'): values = [values] From 08f017842560907b15b4e580f70bd25825db3b4c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Dec 2012 00:46:35 +0100 Subject: [PATCH 4/5] mpd: Extract query translators for direct testing --- mopidy/frontends/mpd/protocol/music_db.py | 93 +++-------------------- mopidy/frontends/mpd/translator.py | 78 +++++++++++++++++++ 2 files changed, 88 insertions(+), 83 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 7cdfc5e0..393561de 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -1,11 +1,8 @@ from __future__ import unicode_literals -import re -import shlex - -from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented +from mopidy.frontends.mpd import translator +from mopidy.frontends.mpd.exceptions import MpdNotImplemented from mopidy.frontends.mpd.protocol import handle_request, stored_playlists -from mopidy.frontends.mpd.translator import tracks_to_mpd_format QUERY_RE = ( @@ -13,35 +10,6 @@ QUERY_RE = ( r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$') -def _build_query(mpd_query): - """ - Parses a MPD query string and converts it to the Mopidy query format. - """ - query_pattern = ( - r'"?(?:[Aa]lbum|[Aa]rtist|[Ff]ile[name]*|[Tt]itle|[Aa]ny)"? "[^"]+"') - query_parts = re.findall(query_pattern, mpd_query) - query_part_pattern = ( - r'"?(?P([Aa]lbum|[Aa]rtist|[Ff]ile[name]*|[Tt]itle|[Aa]ny))"? ' - r'"(?P[^"]+)"') - query = {} - for query_part in query_parts: - m = re.match(query_part_pattern, query_part) - field = m.groupdict()['field'].lower() - if field == 'title': - field = 'track' - elif field in ('file', 'filename'): - field = 'uri' - field = str(field) # Needed for kwargs keys on OS X and Windows - what = m.groupdict()['what'] - if not what: - raise ValueError - if field in query: - query[field].append(what) - else: - query[field] = [what] - return query - - @handle_request(r'^count "(?P[^"]+)" "(?P[^"]*)"$') def count(context, tag, needle): """ @@ -84,11 +52,11 @@ def find(context, mpd_query): - uses "file" instead of "filename". """ try: - query = _build_query(mpd_query) + query = translator.query_from_mpd_search_format(mpd_query) except ValueError: return result = context.core.library.find_exact(**query).get() - return tracks_to_mpd_format(result) + return translator.tracks_to_mpd_format(result) @handle_request(r'^findadd ' + QUERY_RE) @@ -102,7 +70,7 @@ def findadd(context, mpd_query): current playlist. Parameters have the same meaning as for ``find``. """ try: - query = _build_query(mpd_query) + query = translator.query_from_mpd_search_format(mpd_query) except ValueError: return result = context.core.library.find_exact(**query).get() @@ -196,7 +164,7 @@ def list_(context, field, mpd_query=None): """ field = field.lower() try: - query = _list_build_query(field, mpd_query) + query = translator.query_from_mpd_list_format(field, mpd_query) except ValueError: return if field == 'artist': @@ -209,47 +177,6 @@ def list_(context, field, mpd_query=None): pass # TODO We don't have genre in our internal data structures yet -def _list_build_query(field, mpd_query): - """Converts a ``list`` query to a Mopidy query.""" - if mpd_query is None: - return {} - try: - # shlex does not seem to be friends with unicode objects - tokens = shlex.split(mpd_query.encode('utf-8')) - except ValueError as error: - if str(error) == 'No closing quotation': - raise MpdArgError('Invalid unquoted character', command='list') - else: - raise - tokens = [t.decode('utf-8') for t in tokens] - if len(tokens) == 1: - if field == 'album': - if not tokens[0]: - raise ValueError - return {'artist': [tokens[0]]} - else: - raise MpdArgError( - 'should be "Album" for 3 arguments', command='list') - elif len(tokens) % 2 == 0: - query = {} - while tokens: - key = tokens[0].lower() - key = str(key) # Needed for kwargs keys on OS X and Windows - value = tokens[1] - tokens = tokens[2:] - if key not in ('artist', 'album', 'date', 'genre'): - raise MpdArgError('not able to parse args', command='list') - if not value: - raise ValueError - if key in query: - query[key].append(value) - else: - query[key] = [value] - return query - else: - raise MpdArgError('not able to parse args', command='list') - - def _list_artist(context, query): artists = set() tracks = context.core.library.find_exact(**query).get() @@ -367,11 +294,11 @@ def search(context, mpd_query): - uses "file" instead of "filename". """ try: - query = _build_query(mpd_query) + query = translator.query_from_mpd_search_format(mpd_query) except ValueError: return result = context.core.library.search(**query).get() - return tracks_to_mpd_format(result) + return translator.tracks_to_mpd_format(result) @handle_request(r'^searchadd ' + QUERY_RE) @@ -388,7 +315,7 @@ def searchadd(context, mpd_query): not case sensitive. """ try: - query = _build_query(mpd_query) + query = translator.query_from_mpd_search_format(mpd_query) except ValueError: return result = context.core.library.search(**query).get() @@ -411,7 +338,7 @@ def searchaddpl(context, playlist_name, mpd_query): not case sensitive. """ try: - query = _build_query(mpd_query) + query = translator.query_from_mpd_search_format(mpd_query) except ValueError: return result = context.core.library.search(**query).get() diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 0c95f044..ef7c8a1c 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -2,10 +2,12 @@ from __future__ import unicode_literals import os import re +import shlex import urllib from mopidy import settings from mopidy.frontends.mpd import protocol +from mopidy.frontends.mpd.exceptions import MpdArgError from mopidy.models import TlTrack from mopidy.utils.path import mtime as get_mtime, uri_to_path, split_path @@ -134,6 +136,82 @@ def playlist_to_mpd_format(playlist, *args, **kwargs): return tracks_to_mpd_format(playlist.tracks, *args, **kwargs) +def query_from_mpd_list_format(field, mpd_query): + """ + Converts an MPD ``list`` query to a Mopidy query. + """ + if mpd_query is None: + return {} + try: + # shlex does not seem to be friends with unicode objects + tokens = shlex.split(mpd_query.encode('utf-8')) + except ValueError as error: + if str(error) == 'No closing quotation': + raise MpdArgError('Invalid unquoted character', command='list') + else: + raise + tokens = [t.decode('utf-8') for t in tokens] + if len(tokens) == 1: + if field == 'album': + if not tokens[0]: + raise ValueError + return {'artist': [tokens[0]]} + else: + raise MpdArgError( + 'should be "Album" for 3 arguments', command='list') + elif len(tokens) % 2 == 0: + query = {} + while tokens: + key = tokens[0].lower() + key = str(key) # Needed for kwargs keys on OS X and Windows + value = tokens[1] + tokens = tokens[2:] + if key not in ('artist', 'album', 'date', 'genre'): + raise MpdArgError('not able to parse args', command='list') + if not value: + raise ValueError + if key in query: + query[key].append(value) + else: + query[key] = [value] + return query + else: + raise MpdArgError('not able to parse args', command='list') + + +def query_from_mpd_search_format(mpd_query): + """ + Parses an MPD ``search`` or ``find`` query and converts it to the Mopidy + query format. + + :param mpd_query: the MPD search query + :type mpd_query: string + """ + query_pattern = ( + r'"?(?:[Aa]lbum|[Aa]rtist|[Ff]ile[name]*|[Tt]itle|[Aa]ny)"? "[^"]+"') + query_parts = re.findall(query_pattern, mpd_query) + query_part_pattern = ( + r'"?(?P([Aa]lbum|[Aa]rtist|[Ff]ile[name]*|[Tt]itle|[Aa]ny))"? ' + r'"(?P[^"]+)"') + query = {} + for query_part in query_parts: + m = re.match(query_part_pattern, query_part) + field = m.groupdict()['field'].lower() + if field == 'title': + field = 'track' + elif field in ('file', 'filename'): + field = 'uri' + field = str(field) # Needed for kwargs keys on OS X and Windows + what = m.groupdict()['what'] + if not what: + raise ValueError + if field in query: + query[field].append(what) + else: + query[field] = [what] + return query + + def tracks_to_tag_cache_format(tracks): """ Format list of tracks for output to MPD tag cache From f9dc3e3d81bb6b578fe055f22cf6210f96b18097 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 20 Dec 2012 00:48:04 +0100 Subject: [PATCH 5/5] mpd: Rename test file to match src file --- tests/frontends/mpd/{serializer_test.py => translator_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/frontends/mpd/{serializer_test.py => translator_test.py} (100%) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/translator_test.py similarity index 100% rename from tests/frontends/mpd/serializer_test.py rename to tests/frontends/mpd/translator_test.py