From acad477c8a2debe60f6bddd40cd37ac2277ea3cc Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 2 Jun 2011 18:41:43 +0200 Subject: [PATCH 01/43] Make it possible to stop scanner more cleanly --- bin/mopidy-scan | 5 ++++- mopidy/scanner.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bin/mopidy-scan b/bin/mopidy-scan index 84cfee57..718deb73 100755 --- a/bin/mopidy-scan +++ b/bin/mopidy-scan @@ -20,7 +20,10 @@ if __name__ == '__main__': print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_PATH scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug) - scanner.start() + try: + scanner.start() + except KeyboardInterrupt: + scanner.stop() print >> sys.stderr, 'Done' diff --git a/mopidy/scanner.py b/mopidy/scanner.py index c603c578..b9c770de 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -82,8 +82,11 @@ class Scanner(object): data = dict([(k, data[k]) for k in data.keys()]) data['uri'] = unicode(self.uribin.get_property('uri')) data['duration'] = self.get_duration() - self.data_callback(data) - self.next_uri() + try: + self.data_callback(data) + self.next_uri() + except KeyboardInterrupt: + self.stop() def process_error(self, bus, message): if self.error_callback: From b6c196b8abf7d25ed3bd1a0b55afbff715ca48a0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 4 Jun 2011 00:13:40 +0200 Subject: [PATCH 02/43] Ensure that date is not none before using it --- mopidy/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index b9c770de..d3c61fc7 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -24,7 +24,7 @@ def translator(data): _retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs) _retrieve(gst.TAG_ARTIST, 'name', artist_kwargs) - if gst.TAG_DATE in data: + if gst.TAG_DATE in data and data[gst.TAG_DATE]: date = data[gst.TAG_DATE] date = datetime.date(date.year, date.month, date.day) track_kwargs['date'] = date From 48d7cd986577081a1b951f4ead695f65fc08b650 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 4 Jun 2011 00:14:04 +0200 Subject: [PATCH 03/43] Use TAG_DURATION constant instead of 'duration' --- mopidy/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index d3c61fc7..695cda27 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -81,7 +81,7 @@ class Scanner(object): data = message.parse_tag() data = dict([(k, data[k]) for k in data.keys()]) data['uri'] = unicode(self.uribin.get_property('uri')) - data['duration'] = self.get_duration() + data[gst.TAG_DURATION] = self.get_duration() try: self.data_callback(data) self.next_uri() From e25fbb35dc47bdfbbf8d610c61e2a0cc61fdd3c1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 4 Jun 2011 00:14:17 +0200 Subject: [PATCH 04/43] Note why get_state is needed --- mopidy/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 695cda27..c3eda9ae 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -96,7 +96,7 @@ class Scanner(object): self.next_uri() def get_duration(self): - self.pipe.get_state() + self.pipe.get_state() # Block untill state change is done. try: return self.pipe.query_duration( gst.FORMAT_TIME, None)[0] // gst.MSECOND From ea8b4fc2b0fa0f8044bca28925adc8145ef1e5f9 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 4 Jun 2011 00:17:27 +0200 Subject: [PATCH 05/43] Remove if __name__ = '__main__' as it was redundant in mopidy-scanner --- bin/mopidy-scan | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/bin/mopidy-scan b/bin/mopidy-scan index 718deb73..962c402a 100755 --- a/bin/mopidy-scan +++ b/bin/mopidy-scan @@ -1,34 +1,33 @@ #!/usr/bin/env python -if __name__ == '__main__': - import sys +import sys - from mopidy import settings - from mopidy.scanner import Scanner, translator - from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format +from mopidy import settings +from mopidy.scanner import Scanner, translator +from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format - tracks = [] +tracks = [] - def store(data): - track = translator(data) - tracks.append(track) - print >> sys.stderr, 'Added %s' % track.uri +def store(data): + track = translator(data) + tracks.append(track) + print >> sys.stderr, 'Added %s' % track.uri - def debug(uri, error): - print >> sys.stderr, 'Failed %s: %s' % (uri, error) +def debug(uri, error): + print >> sys.stderr, 'Failed %s: %s' % (uri, error) - print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_PATH +print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_PATH - scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug) - try: - scanner.start() - except KeyboardInterrupt: - scanner.stop() +scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug) +try: + scanner.start() +except KeyboardInterrupt: + scanner.stop() - print >> sys.stderr, 'Done' +print >> sys.stderr, 'Done' - for a in tracks_to_tag_cache_format(tracks): - if len(a) == 1: - print (u'%s' % a).encode('utf-8') - else: - print (u'%s: %s' % a).encode('utf-8') +for a in tracks_to_tag_cache_format(tracks): + if len(a) == 1: + print (u'%s' % a).encode('utf-8') + else: + print (u'%s: %s' % a).encode('utf-8') From 710f434455ffab01d665d97d6fa0a0e7e5592c16 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 4 Jun 2011 00:32:55 +0200 Subject: [PATCH 06/43] Switch to proper logging for mopidy scanner --- bin/mopidy-scan | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bin/mopidy-scan b/bin/mopidy-scan index 962c402a..b87e8eb9 100755 --- a/bin/mopidy-scan +++ b/bin/mopidy-scan @@ -1,22 +1,26 @@ #!/usr/bin/env python import sys +import logging from mopidy import settings +from mopidy.utils.log import setup_console_logging from mopidy.scanner import Scanner, translator from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format +setup_console_logging(2) + tracks = [] def store(data): track = translator(data) tracks.append(track) - print >> sys.stderr, 'Added %s' % track.uri + logging.debug('Added %s', track.uri) def debug(uri, error): - print >> sys.stderr, 'Failed %s: %s' % (uri, error) + logging.error('Failed %s: %s', uri, error) -print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_PATH +logging.info('Scanning %s', settings.LOCAL_MUSIC_PATH) scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug) try: @@ -24,7 +28,7 @@ try: except KeyboardInterrupt: scanner.stop() -print >> sys.stderr, 'Done' +logging.info('Done') for a in tracks_to_tag_cache_format(tracks): if len(a) == 1: From 503c98b98ee99196088d7e0f88dab7e7d5d98165 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 4 Jun 2011 01:29:28 +0200 Subject: [PATCH 07/43] Cleanup of gst code for scanner --- mopidy/scanner.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index c3eda9ae..eade1ce5 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -57,17 +57,16 @@ class Scanner(object): self.error_callback = error_callback self.loop = gobject.MainLoop() - caps = gst.Caps('audio/x-raw-int') fakesink = gst.element_factory_make('fakesink') - pad = fakesink.get_pad('sink') self.uribin = gst.element_factory_make('uridecodebin') - self.uribin.connect('pad-added', self.process_new_pad, pad) - self.uribin.set_property('caps', caps) + self.uribin.set_property('caps', gst.Caps('audio/x-raw-int')) + self.uribin.connect('pad-added', self.process_new_pad, + fakesink.get_pad('sink')) self.pipe = gst.element_factory_make('pipeline') - self.pipe.add(fakesink) self.pipe.add(self.uribin) + self.pipe.add(fakesink) bus = self.pipe.get_bus() bus.add_signal_watch() From f091433a53c8b5ac3c27b4d35affb26daa673ee4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 4 Jun 2011 01:47:48 +0200 Subject: [PATCH 08/43] Cleanup error feedback --- bin/mopidy-scan | 10 +++++----- mopidy/scanner.py | 4 ++-- tests/scanner_test.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/mopidy-scan b/bin/mopidy-scan index b87e8eb9..b8b4fd3e 100755 --- a/bin/mopidy-scan +++ b/bin/mopidy-scan @@ -15,12 +15,12 @@ tracks = [] def store(data): track = translator(data) tracks.append(track) - logging.debug('Added %s', track.uri) + logging.debug(u'Added %s', track.uri) -def debug(uri, error): - logging.error('Failed %s: %s', uri, error) +def debug(uri, error, debug): + logging.error(u'Failed %s: %s - %s', uri, error, debug) -logging.info('Scanning %s', settings.LOCAL_MUSIC_PATH) +logging.info(u'Scanning %s', settings.LOCAL_MUSIC_PATH) scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug) try: @@ -28,7 +28,7 @@ try: except KeyboardInterrupt: scanner.stop() -logging.info('Done') +logging.info(u'Done') for a in tracks_to_tag_cache_format(tracks): if len(a) == 1: diff --git a/mopidy/scanner.py b/mopidy/scanner.py index eade1ce5..17e8127d 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -90,8 +90,8 @@ class Scanner(object): def process_error(self, bus, message): if self.error_callback: uri = self.uribin.get_property('uri') - errors = message.parse_error() - self.error_callback(uri, errors) + error, debug = message.parse_error() + self.error_callback(uri, error, debug) self.next_uri() def get_duration(self): diff --git a/tests/scanner_test.py b/tests/scanner_test.py index b98c5aa9..b2f2f2fd 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -144,9 +144,9 @@ class ScannerTest(unittest.TestCase): uri = data['uri'][len('file://'):] self.data[uri] = data - def error_callback(self, uri, errors): + def error_callback(self, uri, error, debug): uri = uri[len('file://'):] - self.errors[uri] = errors + self.errors[uri] = (error, debug) def test_data_is_set(self): self.scan('scanner/simple') From 89104bd326587b84b8af5dd6151e836baa20ae22 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 4 Jun 2011 01:52:11 +0200 Subject: [PATCH 09/43] Add skipped test case for date missing in scanner tests --- tests/scanner_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index b2f2f2fd..f403a221 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -4,7 +4,7 @@ from datetime import date from mopidy.scanner import Scanner, translator from mopidy.models import Track, Artist, Album -from tests import path_to_data_dir +from tests import path_to_data_dir, SkipTest class FakeGstDate(object): def __init__(self, year, month, day): @@ -184,3 +184,7 @@ class ScannerTest(unittest.TestCase): def test_other_media_is_ignored(self): self.scan('scanner/image') self.assert_(self.errors) + + @SkipTest + def test_song_without_time_is_handeled(self): + pass From eba5cff9d10170e0dca63368e5b3ae2236fb6360 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 4 Jun 2011 02:21:42 +0200 Subject: [PATCH 10/43] Work around strange wma issue in scanner --- bin/mopidy-scan | 3 ++- mopidy/scanner.py | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/bin/mopidy-scan b/bin/mopidy-scan index b8b4fd3e..869aa662 100755 --- a/bin/mopidy-scan +++ b/bin/mopidy-scan @@ -4,10 +4,11 @@ import sys import logging from mopidy import settings -from mopidy.utils.log import setup_console_logging +from mopidy.utils.log import setup_console_logging, setup_root_logger from mopidy.scanner import Scanner, translator from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format +setup_root_logger() setup_console_logging(2) tracks = [] diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 17e8127d..b2e254da 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -77,10 +77,21 @@ class Scanner(object): pad.link(target_pad) def process_tags(self, bus, message): - data = message.parse_tag() - data = dict([(k, data[k]) for k in data.keys()]) - data['uri'] = unicode(self.uribin.get_property('uri')) - data[gst.TAG_DURATION] = self.get_duration() + taglist = message.parse_tag() + data = { + 'uri': unicode(self.uribin.get_property('uri')), + gst.TAG_DURATION: self.get_duration(), + } + + 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] + try: self.data_callback(data) self.next_uri() From 474805c9bee6178875e0936dd9ce899f222d5857 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Jun 2011 23:43:45 +0200 Subject: [PATCH 11/43] UFixMPD server by correctly giving the socket to asyncore.dispatcher --- mopidy/frontends/mpd/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 927e2a00..aa1f98ac 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -20,7 +20,7 @@ class MpdServer(asyncore.dispatcher): def start(self): """Start MPD server.""" try: - self.socket = network.create_socket() + self.set_socket(network.create_socket()) self.set_reuse_addr() hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) port = settings.MPD_SERVER_PORT From e09729fe7742dd62e6e94b00db4e2ad40951d516 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Jun 2011 23:48:00 +0200 Subject: [PATCH 12/43] No need to close the socket when you're told that the socket is closed --- mopidy/frontends/mpd/server.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index aa1f98ac..980e497d 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -39,7 +39,3 @@ class MpdServer(asyncore.dispatcher): logger.info(u'MPD client connection from [%s]:%s', client_socket_address[0], client_socket_address[1]) MpdSession(self, client_socket, client_socket_address) - - def handle_close(self): - """Called by asyncore when the socket is closed.""" - self.close() From a1932b3e9889f1f4650ad32f88cad05a6e80e5b0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Jun 2011 23:50:32 +0200 Subject: [PATCH 13/43] We deal with tracks, not songs --- mopidy/backends/local/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index cc039ce0..5da80a18 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -174,7 +174,7 @@ class LocalLibraryProvider(BaseLibraryProvider): tracks = parse_mpd_tag_cache(tag_cache, music_folder) - logger.info('Loading songs in %s from %s', music_folder, tag_cache) + logger.info('Loading tracks in %s from %s', music_folder, tag_cache) for track in tracks: self._uri_mapping[track.uri] = track From 3b21b7bf7d26978041ed49d7d3c7d4ce95a381fa Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 8 Jun 2011 02:28:41 +0200 Subject: [PATCH 14/43] This is kind of redundant --- mopidy/frontends/mpd/server.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 980e497d..62e443fb 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -14,9 +14,6 @@ class MpdServer(asyncore.dispatcher): for each client connection. """ - def __init__(self): - asyncore.dispatcher.__init__(self) - def start(self): """Start MPD server.""" try: From b6d1ff2b7463c9fae38ca545b36b1c608074e4f3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 9 Jun 2011 13:54:12 +0200 Subject: [PATCH 15/43] Add scanner improvements to changelog --- docs/changes.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index b4d56711..c1061a37 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -44,6 +44,17 @@ No description yet. authentication is turned on, but the connected user has not been authenticated yet. +- Tag cache generator: + + - Made it possible to CTRL^c mopidy-scan. + + - Fixed bug with bad dates. + + - Use logging not print statements. + + - Found and worked around strange WMA metadata behaviour, should be fixed + properly. + v0.4.1 (2011-05-06) =================== From 06bee1cd214fd959021ff40c0e0e30cf1bfc47e4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 9 Jun 2011 13:55:15 +0200 Subject: [PATCH 16/43] Typo fix --- mopidy/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index b2e254da..3bcf03d9 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -106,7 +106,7 @@ class Scanner(object): self.next_uri() def get_duration(self): - self.pipe.get_state() # Block untill state change is done. + self.pipe.get_state() # Block until state change is done. try: return self.pipe.query_duration( gst.FORMAT_TIME, None)[0] // gst.MSECOND From a9bcfaa805948f6a94c3466c010722ebc6457af1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 15:17:07 +0200 Subject: [PATCH 17/43] Rename SHOUTCAST_OUTPUT_SERVER to SHOUTCAST_OUTPUT_HOSTNAME to be consistent with MPD_SERVER_HOSTNAME --- mopidy/outputs/shoutcast.py | 4 ++-- mopidy/settings.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/outputs/shoutcast.py b/mopidy/outputs/shoutcast.py index 4298bba5..d2605514 100644 --- a/mopidy/outputs/shoutcast.py +++ b/mopidy/outputs/shoutcast.py @@ -21,9 +21,9 @@ class ShoutcastOutput(BaseOutput): def modify_bin(self): self.set_properties(self.bin.get_by_name('shoutcast'), { - u'ip': settings.SHOUTCAST_OUTPUT_SERVER, - u'mount': settings.SHOUTCAST_OUTPUT_MOUNT, + u'ip': settings.SHOUTCAST_OUTPUT_HOSTNAME, u'port': settings.SHOUTCAST_OUTPUT_PORT, + u'mount': settings.SHOUTCAST_OUTPUT_MOUNT, u'username': settings.SHOUTCAST_OUTPUT_USERNAME, u'password': settings.SHOUTCAST_OUTPUT_PASSWORD, }) diff --git a/mopidy/settings.py b/mopidy/settings.py index 414c79ee..f92f51ca 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -183,8 +183,8 @@ OUTPUTS = ( #: #: Default:: #: -#: SHOUTCAST_OUTPUT_SERVER = u'127.0.0.1' -SHOUTCAST_OUTPUT_SERVER = u'127.0.0.1' +#: SHOUTCAST_OUTPUT_HOSTNAME = u'127.0.0.1' +SHOUTCAST_OUTPUT_HOSTNAME = u'127.0.0.1' #: User to authenticate as against Shoutcast server. #: From a7a686f4ad662cf50ede65cee8712721c324cf9d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 15:18:29 +0200 Subject: [PATCH 18/43] docs: How to use the SHOUTcast output --- docs/settings.rst | 26 +++++++++++++++++++++ mopidy/settings.py | 56 ++++++++++++++++++++++++++++------------------ 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 1d4a4972..917a71ba 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -92,6 +92,7 @@ To make a ``tag_cache`` of your local music available for Mopidy: .. _use_mpd_on_a_network: + Connecting from other machines on the network ============================================= @@ -119,6 +120,31 @@ file:: LASTFM_PASSWORD = u'mysecret' +Streaming audio through a SHOUTcast/Icecast server +================================================== + +If you want to play the audio on another computer than the one running Mopidy, +you can stream the audio from Mopidy through an SHOUTcast or Icecast audio +streaming server. Multiple media players can then be connected to the streaming +server simultaneously. To use the SHOUTcast output, do the following: + +#. Install, configure and start the Icecast server. It can be found in the + ``icecast2`` package in Debian/Ubuntu. + +#. Add ``mopidy.outputs.shoutcast.ShoutcastOutput`` output to the + :attr:`mopidy.settings.OUTPUTS` setting. + +#. Check the default values for the following settings, and alter them to match + your Icecast setup if needed: + + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_HOSTNAME` + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PORT` + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_USERNAME` + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PASSWORD` + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_MOUNT` + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_ENCODER` + + Available settings ================== diff --git a/mopidy/settings.py b/mopidy/settings.py index f92f51ca..6721e0a6 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -157,16 +157,16 @@ MIXER_MAX_VOLUME = 100 #: Listens on all interfaces, both IPv4 and IPv6. MPD_SERVER_HOSTNAME = u'127.0.0.1' -#: The password required for connecting to the MPD server. -#: -#: Default: :class:`None`, which means no password required. -MPD_SERVER_PASSWORD = None - #: Which TCP port Mopidy's MPD server should listen to. #: #: Default: 6600 MPD_SERVER_PORT = 6600 +#: The password required for connecting to the MPD server. +#: +#: Default: :class:`None`, which means no password required. +MPD_SERVER_PASSWORD = None + #: List of outputs to use. See :mod:`mopidy.outputs` for all available #: backends #: @@ -179,42 +179,54 @@ OUTPUTS = ( u'mopidy.outputs.local.LocalOutput', ) -#: Servar that runs Shoutcast server to send stream to. +#: Hostname of the SHOUTcast server which Mopidy should stream audio to. +#: +#: Used by :mod:`mopidy.outputs.shoutcast`. #: #: Default:: #: #: SHOUTCAST_OUTPUT_HOSTNAME = u'127.0.0.1' SHOUTCAST_OUTPUT_HOSTNAME = u'127.0.0.1' -#: User to authenticate as against Shoutcast server. +#: Port of the SHOUTcast server. #: -#: Default:: -#: -#: SHOUTCAST_OUTPUT_USERNAME = u'source' -SHOUTCAST_OUTPUT_USERNAME = u'source' - -#: Password to authenticate with against Shoutcast server. -#: -#: Default:: -#: -#: SHOUTCAST_OUTPUT_PASSWORD = u'hackme' -SHOUTCAST_OUTPUT_PASSWORD = u'hackme' - -#: Port to use for streaming to Shoutcast server. +#: Used by :mod:`mopidy.outputs.shoutcast`. #: #: Default:: #: #: SHOUTCAST_OUTPUT_PORT = 8000 SHOUTCAST_OUTPUT_PORT = 8000 -#: Mountpoint to use for the stream on the Shoutcast server. +#: User to authenticate as against SHOUTcast server. +#: +#: Used by :mod:`mopidy.outputs.shoutcast`. +#: +#: Default:: +#: +#: SHOUTCAST_OUTPUT_USERNAME = u'source' +SHOUTCAST_OUTPUT_USERNAME = u'source' + +#: Password to authenticate with against SHOUTcast server. +#: +#: Used by :mod:`mopidy.outputs.shoutcast`. +#: +#: Default:: +#: +#: SHOUTCAST_OUTPUT_PASSWORD = u'hackme' +SHOUTCAST_OUTPUT_PASSWORD = u'hackme' + +#: Mountpoint to use for the stream on the SHOUTcast server. +#: +#: Used by :mod:`mopidy.outputs.shoutcast`. #: #: Default:: #: #: SHOUTCAST_OUTPUT_MOUNT = u'/stream' SHOUTCAST_OUTPUT_MOUNT = u'/stream' -#: Encoder to use to process audio data before streaming. +#: Encoder to use to process audio data before streaming to SHOUTcast server. +#: +#: Used by :mod:`mopidy.outputs.shoutcast`. #: #: Default:: #: From ed59030a50a4a8450d1e708624a5f469f5a3424f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 16:10:28 +0200 Subject: [PATCH 19/43] Update changelog for 0.5.0 release --- docs/changes.rst | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index a01ff931..65add741 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -8,7 +8,9 @@ This change log is used to track all major changes to Mopidy. v0.5.0 (in development) ======================= -No description yet. +Since last time we've added support for audio streaming to SHOUTcast servers +and fixed the longstanding playlist loading issue in the Spotify backend. As +always the release has a bunch of bug fixes. Please note that 0.5.0 requires some updated dependencies, as listed under *Important changes* below. @@ -20,22 +22,16 @@ Please note that 0.5.0 requires some updated dependencies, as listed under automatically be upgraded. If you are not installing from APT, follow the instructions at :doc:`/installation/libspotify/`. -- Mopidy now supports running with 1-n outputs at the same time. This feature - was mainly added to facilitate Shoutcast support, which Mopidy has also +- Mopidy now supports running with 1 to N outputs at the same time. This feature + was mainly added to facilitate SHOUTcast support, which Mopidy has also gained. In its current state outputs can not be toggled during runtime. **Changes** -- Fix local backend time query errors that where coming from stopped pipeline. - (Fixes: :issue:`87`) +- Local backend: -- Support passing options to GStreamer. See :option:`--help-gst` for a list of - available options. (Fixes: :issue:`95`) - -- Improve :option:`--list-settings` output. (Fixes: :issue:`91`) - -- Replace not decodable characters returned from Spotify instead of throwing an - exception, as we won't try to figure out the encoding of non-UTF-8-data. + - Fix local backend time query errors that where coming from stopped + pipeline. (Fixes: :issue:`87`) - Spotify backend: @@ -44,6 +40,9 @@ Please note that 0.5.0 requires some updated dependencies, as listed under workaround of searching and reconnecting to make the playlists appear are no longer necessary. (Fixes: :issue:`59`) + - Replace not decodable characters returned from Spotify instead of throwing + an exception, as we won't try to figure out the encoding of non-UTF-8-data. + - MPD frontend: - Refactoring and cleanup. Most notably, all request handlers now get an @@ -59,16 +58,22 @@ Please note that 0.5.0 requires some updated dependencies, as listed under authentication is turned on, but the connected user has not been authenticated yet. +- Command line usage: + + - Support passing options to GStreamer. See :option:`--help-gst` for a list + of available options. (Fixes: :issue:`95`) + + - Improve :option:`--list-settings` output. (Fixes: :issue:`91`) + - Tag cache generator: - - Made it possible to CTRL^c mopidy-scan. + - Made it possible to abort :command:`mopidy-scan` with CTRL+C. - - Fixed bug with bad dates. + - Fixed bug regarding handling of bad dates. - - Use logging not print statements. + - Use :mod:`logging` instead of ``print`` statements. - - Found and worked around strange WMA metadata behaviour, should be fixed - properly. + - Found and worked around strange WMA metadata behaviour. v0.4.1 (2011-05-06) From 35cc1dcb34a8c96d1fcf50ee6467907e8d56addc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 17:00:48 +0200 Subject: [PATCH 20/43] Do not print stack trace on settings validation error --- mopidy/core.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index e510b698..ca5b92a1 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -16,7 +16,8 @@ sys.argv[1:] = gstreamer_args from pykka.registry import ActorRegistry -from mopidy import get_version, settings, OptionalDependencyError +from mopidy import (get_version, settings, OptionalDependencyError, + SettingsError) from mopidy.gstreamer import GStreamer from mopidy.utils import get_class from mopidy.utils.log import setup_logging @@ -65,7 +66,11 @@ def parse_options(): def setup_settings(): get_or_create_folder('~/.mopidy/') get_or_create_file('~/.mopidy/settings.py') - settings.validate() + try: + settings.validate() + except SettingsError, e: + logger.error(e.message) + sys.exit(1) def setup_gobject_loop(): GObjectEventThread().start() From e97b32d041496d42b55e9d56c5439919bae5fc05 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Thu, 9 Jun 2011 17:18:35 +0200 Subject: [PATCH 21/43] rename SPOTIFY_HIGH_BITRATE setting to SPOTIFY_BITRATE, and use actual bitrate value to define preferred bitrate --- mopidy/backends/spotify/__init__.py | 1 + mopidy/backends/spotify/session_manager.py | 11 +++++------ mopidy/backends/spotify/translator.py | 4 ++-- mopidy/settings.py | 8 +++++--- mopidy/utils/settings.py | 7 +++++++ tests/utils/settings_test.py | 8 ++++++++ 6 files changed, 28 insertions(+), 11 deletions(-) diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 9dababc0..da839b26 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -11,6 +11,7 @@ from mopidy.gstreamer import GStreamer logger = logging.getLogger('mopidy.backends.spotify') ENCODING = 'utf-8' +BITRATES = {96: 2, 160: 0, 320: 1} class SpotifyBackend(ThreadingActor, Backend): """ diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 388b29c3..4b6abe85 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -8,6 +8,7 @@ from pykka.registry import ActorRegistry from mopidy import get_version, settings from mopidy.backends.base import Backend +from mopidy.backends.spotify import BITRATES from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.models import Playlist from mopidy.gstreamer import GStreamer @@ -58,12 +59,10 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): return logger.info(u'Connected to Spotify') self.session = session - if settings.SPOTIFY_HIGH_BITRATE: - logger.debug(u'Preferring high bitrate from Spotify') - self.session.set_preferred_bitrate(1) - else: - logger.debug(u'Preferring normal bitrate from Spotify') - self.session.set_preferred_bitrate(0) + + logger.debug(u'Preferred Spotify bitrate is %s kbps.', settings.SPOTIFY_BITRATE) + self.session.set_preferred_bitrate(BITRATES[settings.SPOTIFY_BITRATE]) + self.container_manager = SpotifyContainerManager(self) self.container_manager.watch(self.session.playlist_container()) self.connected.set() diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index 21abdf78..91a2a9ae 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -4,7 +4,7 @@ import logging from spotify import Link, SpotifyError from mopidy import settings -from mopidy.backends.spotify import ENCODING +from mopidy.backends.spotify import ENCODING, BITRATES from mopidy.models import Artist, Album, Track, Playlist logger = logging.getLogger('mopidy.backends.spotify.translator') @@ -44,7 +44,7 @@ class SpotifyTranslator(object): track_no=spotify_track.index(), date=date, length=spotify_track.duration(), - bitrate=(settings.SPOTIFY_HIGH_BITRATE and 320 or 160), + bitrate=BITRATES[settings.SPOTIFY_BITRATE], ) @classmethod diff --git a/mopidy/settings.py b/mopidy/settings.py index 6721e0a6..9ac63719 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -248,11 +248,13 @@ SPOTIFY_USERNAME = u'' #: Used by :mod:`mopidy.backends.spotify`. SPOTIFY_PASSWORD = u'' -#: Do you prefer high bitrate (320k)? +#: Spotify preferred bitrate. +#: +#: Available values are 96, 160, and 320. #: #: Used by :mod:`mopidy.backends.spotify`. # #: Default:: #: -#: SPOTIFY_HIGH_BITRATE = False # 160k -SPOTIFY_HIGH_BITRATE = False +#: SPOTIFY_BITRATE = 160 +SPOTIFY_BITRATE = 160 diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 2bd6e6f3..05d40a4a 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -8,6 +8,7 @@ import sys from mopidy import SettingsError from mopidy.utils.log import indent +from mopidy.backends.spotify import BITRATES as SPOTIFY_BITRATES logger = logging.getLogger('mopidy.utils.settings') @@ -107,6 +108,7 @@ def validate_settings(defaults, settings): 'SERVER': None, 'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME', 'SERVER_PORT': 'MPD_SERVER_PORT', + 'SPOTIFY_HIGH_BITRATE': 'SPOTIFY_BITRATE', 'SPOTIFY_LIB_APPKEY': None, 'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH', } @@ -127,6 +129,11 @@ def validate_settings(defaults, settings): 'longer available.') continue + if setting == 'SPOTIFY_BITRATE': + if value not in SPOTIFY_BITRATES.keys(): + errors[setting] = (u'Unavailable Spotify bitrate. ' + + u'Available bitrates are 96, 160, and 320.') + if setting not in defaults: errors[setting] = u'Unknown setting. Is it misspelled?' continue diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index 1ffff9a6..748eae85 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -10,6 +10,7 @@ class ValidateSettingsTest(unittest.TestCase): self.defaults = { 'MPD_SERVER_HOSTNAME': '::', 'MPD_SERVER_PORT': 6600, + 'SPOTIFY_BITRATE': 160, } def test_no_errors_yields_empty_dict(self): @@ -42,6 +43,13 @@ class ValidateSettingsTest(unittest.TestCase): '"mopidy.backends.despotify.DespotifyBackend" is no longer ' + 'available.') + def test_unavailable_bitrate_setting_returns_error(self): + result = validate_settings(self.defaults, + {'SPOTIFY_BITRATE': 50}) + self.assertEqual(result['SPOTIFY_BITRATE'], + u'Unavailable Spotify bitrate. ' + + u'Available bitrates are 96, 160, and 320.') + def test_two_errors_are_both_reported(self): result = validate_settings(self.defaults, {'FOO': '', 'BAR': ''}) From 232155007999dd7b2a8388da5f64ec1073c1ce76 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 17:35:22 +0200 Subject: [PATCH 22/43] Fix import cycle --- mopidy/utils/settings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 05d40a4a..01fee23d 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -8,7 +8,6 @@ import sys from mopidy import SettingsError from mopidy.utils.log import indent -from mopidy.backends.spotify import BITRATES as SPOTIFY_BITRATES logger = logging.getLogger('mopidy.utils.settings') @@ -130,7 +129,7 @@ def validate_settings(defaults, settings): continue if setting == 'SPOTIFY_BITRATE': - if value not in SPOTIFY_BITRATES.keys(): + if value not in (96, 160, 320): errors[setting] = (u'Unavailable Spotify bitrate. ' + u'Available bitrates are 96, 160, and 320.') From 3432c84e6869abbf11c5b0d387a1fe2d807c90af Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 17:38:33 +0200 Subject: [PATCH 23/43] Add new SPOTIFY_BITRATE setting to changelog --- docs/changes.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 65add741..4b6f74ca 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -22,9 +22,14 @@ Please note that 0.5.0 requires some updated dependencies, as listed under automatically be upgraded. If you are not installing from APT, follow the instructions at :doc:`/installation/libspotify/`. -- Mopidy now supports running with 1 to N outputs at the same time. This feature - was mainly added to facilitate SHOUTcast support, which Mopidy has also - gained. In its current state outputs can not be toggled during runtime. +- If you have explicitly set the :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` + setting, you must update your settings file. The new setting is named + :attr:`mopidy.settings.SPOTIFY_BITRATE` and accepts the integer values 96, + 160, and 320. + +- Mopidy now supports running with 1 to N outputs at the same time. This + feature was mainly added to facilitate SHOUTcast support, which Mopidy has + also gained. In its current state outputs can not be toggled during runtime. **Changes** From a58257a653700b98eb2496c7a47d310b78b94a0b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 17:40:54 +0200 Subject: [PATCH 24/43] Do not install mopidy.desktop as it is really hacky and not that useful --- setup.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/setup.py b/setup.py index 3d9d4fdf..a8cf8ed1 100644 --- a/setup.py +++ b/setup.py @@ -69,11 +69,6 @@ for dirpath, dirnames, filenames in os.walk(project_dir): data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) -if os.geteuid() == 0: - # Only try to install this file if we are root - data_files.append( - ('/usr/local/share/applications', ['data/mopidy.desktop'])) - setup( name='Mopidy', version=get_version(), From 74dbc0ba8830e6a0246ba67633a9ca26d8554510 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 17:49:31 +0200 Subject: [PATCH 25/43] docs: v0.3 suddenly became v0.6+ --- docs/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/settings.rst b/docs/settings.rst index 917a71ba..f0888670 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -56,7 +56,7 @@ You may also want to change some of the ``LOCAL_*`` settings. See Currently, Mopidy supports using Spotify *or* local storage as a music source. We're working on using both sources simultaneously, and will - hopefully have support for this in the 0.3 release. + hopefully have support for this in the 0.6 release. .. _generating_a_tag_cache: From 9f552465e782db9212ee07b1c086e10d081b4edf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 17:49:44 +0200 Subject: [PATCH 26/43] docs: Update link to wishlist label --- docs/development/roadmap.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index cec8e9c7..6280762c 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -26,7 +26,7 @@ Feature wishlist We maintain our collection of sane or less sane ideas for future Mopidy features as `issues `_ at GitHub labeled with `the "wishlist" label -`_. Feel free to vote +`_. Feel free to vote up any feature you would love to see in Mopidy, but please refrain from adding a comment just to say "I want this too!". You are of course free to add comments if you have suggestions for how the feature should work or be From 11e85dd479dbc529fa5a0a7f808e78e942e18d6a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 17:56:10 +0200 Subject: [PATCH 27/43] docs: Remove outdated internals docs --- docs/_static/thread_communication.png | Bin 46887 -> 0 bytes docs/development/index.rst | 1 - docs/development/internals.rst | 113 -------------------------- 3 files changed, 114 deletions(-) delete mode 100644 docs/_static/thread_communication.png delete mode 100644 docs/development/internals.rst diff --git a/docs/_static/thread_communication.png b/docs/_static/thread_communication.png deleted file mode 100644 index 95bf1892dff82ebfc806f4595ded3abfec6061fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46887 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 z z3kqyS>b4#D5e0y`c-5b&bjM8FPG3KTf4jwJMqqzMz^y%RP@Pp_lr6&4olXTSMTQkJ z{nHAoHC^>|buV>(hf~!Cu7`=iy2_Y^(_*&`4b(O0idf&!pD=z@c%H&|o~MH}L)m92?)w7ms8R5o*|8gTd8 zSb)U62lgP`=9rDwChEc`AwE7RFA3pBpD+-B;$TJg1p9F!fZp5C!(m=O+&9(H)dghb z39~3UkifxMcvEk%io#i`EkQySL4!!X z_i8`tFlZl;PDvTS(g{`b9&1uW8b(xA@YJ<|{LQXjd3y3n5_IekGjF1!hcXs4Dk)X? zmbVG?cUsB11~i$~7RkzM5c7zC9lo6K<+BgL>#xII3ctBvH0dZV?hULRItfaPVxCb4 zq&TGcV!v^tTr7E{gD?fOVB9b^ZEtT1fpk2FZ%1xAY<>U?kEJml7#KhrGO*OY1#%I1 zW$xWM;Ao)@Y#$h4EFFrLHtWgT9X9C>9V`@Zob(D-sw8m(QF*rRiJ^on`eKh~w>^&O z_a7<{^=06mn}^;n@crLZAUR|@>Mt5^M8>4i9N0-8gx#nK<%fJf`AG0CxP3EayD5hmf!klvt9CqUQ5XTPok)_0c4w7;;J4hgAn6aZW( ze0cI4nM=q~5*HVjCODT7WdlCQq(z5Go?WA{>G{8QT_sRqfF6I~Cn5y*T|~$6qqK~S z9spe+l6%3Y4c&Z1vzd{XOSt19)a8^=9ItzMBWZE(-c>+dM*KrS6OrAtxH4h(x0+{O zix?w7T}P&7z^(X7!3vrW7<(X0rc>#2{#UWr471*??$e_gtgHoDBMMXAzaJye>PIc(T@0qRSy?s3@)wx?C?sYI zq>z>Vu-f4V%i{GMK|S-k^`DNp?n!fA4*0Fvxt!*!bmmbm-@adT02pyR{tvt%eC5BH znf0vzcpZ+&zr2;&>rz&emq0L zKlcXI*Pq&se>`+KUfX783cs`BS(;ID%OR|quaNkLQh6x&#LL^n z0!~F}T_BG@8h$ghC~Iz>1%{^f?(S~m0u!!OkUF#-9lIiR_)KuH`rEyw&ZUq=A1f-R zfYy9#lI{RgAmWLGJo4$s5A>50jHCaKX8Iu#f8$^8S`8BW6>|Vr#g*8N=*er<2(ao*>zm^CJ(?!Pkr<@@ZioByl%PyEM)9`q{t zta!`SDsvg3G=MRGWNk#J^t~NH<<@1hUOf)iu(~~)frz6hG(%{dJo(Oc<24n2v+16( zR(Jnuo#XICtN(|n#iVRKjK+cedQ9x=VZfkF9`G3Zeu$c^qzyUPQk3G2XBcFU@L8ksSJ_ZE^ z{r1x5vtS6)0Z0WXO#zy5@o4^eB#H2?wM!9m8bug=nx+Np#jW{QjW{;Q9>x%n!DNJZ z0m+1=i^7=9CCBiG-`JF#t$ft=8!d8+U7!4&TnMD#G7(G=k8GXK8%l?neC{@izu8dY zg7kHC9zw`i&G)_o(y-F-PO9bU&Rj5Yka3xOwsp^wrI_$)|3+lzd)8^0dgW}^+}*hW zLgR>k0xnZA09-r>>2AP^!K|wbRoHGa(i;6ws_s4Q0vMpbg205eD#fZNhNlh^)y~^l zk$BQOqvNJDAlLog*=&@P&;i7Oq1*bOd558EU?7b}y>bdHx`@jWXy+`YCWqge|8LvJ zM43j@s8#w1l!d9rFA_8R&?7N(@z-B}J&X*1`p1KOk^J`U<>aD&X~VhO{?J^xJ;ooF zE5OQ*+#^8F)18NZBVVPr{@f_1uoeH+u;SM5DmQyE)v$ORdT-3NZ^xXINgyz2x2v3Y zgb_Ik;c(yra=;jq_#L!{AJOa%Va(yhdz3C|2oYLQ#yuU#**USAj!n2O;MC z8MhqcA%1wih)wq_h$??}B)sVL2bvE&*iSPkPF(-Z8>89Zc8aA=rJO&DhPV_sGQ?c@ z>C-1aV`1KSRU(vTZs0PqcSudgWnYq zL1tP!yR~!6)Y!D@FRD%At9Vf(jC}QF0kA8hG0<`$t}oWbaZOUVHCzNg3v{TLK2w%U z@Ww4~h&bQpKa~A>C6^QNrw+F9p{*_V3EI|!9Chi{Q9{J=g?e`S-me}O2gTWyx=iTi zu#qYHMWF;jP$9d6OXmbL9$g4+j2I@UJ9zWU>#&z=Acq7${<%<&kzleS(mMzF>a))B zmP3-!?b`&p+Sjn)gR5F$$0r0z?ChR@<3SV z5C&d)d!H$QC%&8lG3J9D!xaVwA#3Z6L@XHF&rVNIUk?>Cxfw}`wZ0h%fAvM50B=RU z&2nrk|Ca$~rJic_<>iTVi!(xJiQ`*zA6%u;ueJ1Hd>!h-BBxKE zb_X8x75HboJ{jBE7EI00LkDv{7Upc2?6<~)?}8I^3A;N21l$PBTVJv)1>^UF)zwvZ zP&FwrlGS@O7y@_$)!F06-M&)7%wlY!kIBXD)Ie>P$x0sg#aaz+D_dfnNFy`m2jx z_iT8{VN(4x;G^%mkc(Z)%garxK4>)%!)ry`GbuJ8&|C-5QNW-?F}y&OZZIbIr!|N% zVet3V-~GM>)6DgTPboV_i0oRZEFkDi9+7ts2j zb^WjECm`M9A3yr=@#B{;8zPptKR0I}Ei3y~=Y|?m!&IaR(+_Ua`gLfwo>N3l85oav$w>$Bp?uI$&$A z!h}l*8Qm&=1*^~8_7 zwZ*k^@R1&TPQ2dk?ye1tu$Ta#Pa;409Jy5dq<$ECEgc25szVyjWe@M*t6yDlDSHMM z$jg+Jhf-wHZo}h*n(cy_RV%-b*FEJ3_X`Sg1&X3Z6XssapfkcnZGt%I8yI*#vlRRS z$fI7|r$}{14)#m;Mc@2Apgg7VfRlj9>$u`#GI&S;FXE%iZUhbegL>5&n_`u@!D13{ z`vlZMH?~wIu!oOYnofw0g_EtJ3XGK>8807!%h}svYnQ1So?I7nX$zCQ-}z>iT=62? zuG(COVeGPb;lPK!c@r+u4?PBtNQXC1|2@oWG|MKyZ~l}ubS!gde{W-RD$ZxAM>{$u zKK>|tIanq4@^_QR7Zo0;JIehjj%PUhHY+66PB?Iul$G5ouc&ycqjUU%=PwXgW<5*Y zeB^k*zCDtK;b+;j&&J1yYbKyAjyiWfIb?zfQHmLKE7@rl2ueBfyT=PV9)W_*K&2XSgIqD!+YMeZ~%2S%R@7Mbz>tgHuf;M zj>2bRn(vG>(p>!#d3Nm}{AKf&)d-1*s=9Sw$c+AcBRueh7xj7E?AGxz=0GRiOV*H0 z7hWXdslQi!UHnORI82l{I*7uhABjOpO1H0v3I~`}T`=e94^CT>Z-bWluNW4O9~;C{ z7B5$hgtkaU$P>xbgMf`U6T5yjXsKf){A?mN%g+1iETh1& zP`K=!s{21L+iSTEhq21HYUJN1g)mSRV_VakChyqU*-7`(QngYcJ5LFFF$yDOUqRSO zu)888lw1aDyYd3D&sAl>tW6U6F?_#dB)xQ8zj&IRh90C7n&4l; z;pr!CH5&ePPW#xQQRra5Kx63&bFc6yCR~8CBxwcL1MUym-;K&UJER*#`P!55U|?1!li+8uk0uEesZW!YJu! zK;G50wO?Rj=}17$4Wm*$MY^;K6^V-ij?HGLZ5hNLm6iL)R{XaSCnSXDRzq`_lu-25 zd$4xImX=b0o24Ag$-j2$8W_A7Fsg#?%k z5G(*fW@fC=YPi8p#e>D|I1t89S zfX#;-kMBooI_ZDzlJSGlk&*lfAt9e?y_}Jmj+Fj__Ct(S%}BvAa{Fqr36Jb=^Yhw4F0$Y*OR=yasBH67(`Uz zpb7ODaL|T8blj{Q^QZDYK@0iKiKQSu@yV}HC*vi=n6B!DJb@BAji<^e{))FYg=oNt zGuLR~)%*AFb#Q%t{YEdo_Q!Y72FD+do@;kwX=AQb8TkGF#XmAGTD~VVa~2<;@ALu^ zq0u+|?3R|3jegsVzZK^zt`|?cy5~(>+=Rrx1Tz$3`MbHfxx&|?6LnH?px_c^6rk_g z;5LuBR}zPb(MIq)62qg!e9^kM4ddhEKaVbpV}4j3QwXtgPckfWxZ!OgF%4fg#rH% z5KO_M@v0QG!CP2cm{(ZNir!_=xrC2jaR!o()VGx;0bzT49>k6V)4S9u z6?YfgbHXSFZ7>*0zQK?tHR>D8YTU(OFwGpU`?91n&Q%^B9-BKWO@~l0>;(_Yz-tEZ zrCewg1IYamNldM9E}0P%+>m2?qXK((m`E0^<3T-ro0f)aT)J!3%rf;rM&^QmWT3m+ z1)mBGK6#svci9s`QPEQXO@`O(p5`c+&UP{uFmw}x=`%!(wqOpPF2u*bJw@%X-lKix z>eVt>pD*|`GfRiDUb_W_g(SeF9YXQDWL>E>K!>BE}wY8%^s0Pw$d;&urq=H6+z z>yO^t-SeFXU%)Wr3@-b12H1?Rsi`F|M6YkzJpLJCKaM|D)sySUZYCuqP0h@FK_-(& zU~CW|2QiXQtKfEd(M3BDfC=ZGmDPIUnsR{|4f2E{-2KJ^AP+djk*t+TPwa z87V)pvok3`VJ*xqM^R?v7G4M1QFCwa$*dmI?1AUc4@KOxJP9_+3V2%89q(H{w`oHD zIPnl8VTb=yumLJBHS7C7^#l2i*j%Lx6ciNeX|9#@c$~q|dE_fG9X))gy}kXjUXJ&* zVDt%aI3I?|iX~)X_}8p9+VF;B2PM7{(##dB=A-%uZmCq& zQWcxx!AF{3L5ZmVY4)y}S*8u>x8!m|pk1Cq#lCx|*}qqbl|N4T_nv_Xy1&Tl;SH47 zi;DcYaD1lc=FUMgeA^%G7a7U8_dU4usNp17qGN%+ol{z?9XInB8`BszD|m4hHizs- zI{10G#p64Z;BM)6rrrVy>gc_@dYs?g9YC^&-@F)Yyaq>Y>?4=o$+dq4MGs$Jv=tH; zx)GRfj(PPu?>&_9e7m(7w70wam*K`>JJU%?BJWtEXqW?3OgDhds&?7c}8iDYFoY?2Y-dps}IUETNlGro`C_woDht~*?= z>-9XJ=Xo5*c^off#=5w?F6QdfW-33-dA#eofo*0VeX^BKuwxzt`av%gKr%jHGRGjF z4U2&=A^K5z+Q@QaF(g46MoD+>Oanx*wG{GtFSJ=u0W)70Ik^-B`~?WLt|*V29zOS4s)PhjA( zK?e|_uYOsdA9TfI>{zP)#^npy+3{tsMn?L*!s%qMs(f#!PjU9IP~%%kuUP!zrWdzY(M2fa zTX1-RF#( zRvR5B?XLv{JTt^~M_NqXIO&^OR}JH|VzDNlJ~7%^TvC#Q1wsl1cVn`3FB>KM;{eCn zwe5F`meI}JC@ega5_{k=)Mdq;gSjUXWX=SKv(g?*{~PZ4izx9=p1{*;L0E+=+?}qT ztx)|FJ1I8AZ;7`stiu7~|IdQ~l%2bGJ40QY(>olNm}s?o|C+rImtY*kJCW=-DFed& zDt@nN6rdvJkUoNZ{4zLrCpDGj;l7xw`9eF_Y`Z8d@p$b$1J#tCxJy$l)V0DFKWo=r z*+{{EsfC@NJ}pE~PHLZ@A6G|b=lgh2d2^oB@-=0ew_c{K!gSQu1Zv?o#72>SUV<#rh*`oF)_-!w8+dkE6e}|9uqP ze6L?wP3_i~mCEj{n7z=cqi5taNm@ae>^ry_w9fCP4h)y0O@<1)m%q69vv-s z^KeB>O4V$9+mzF!72QVfGQ%qEQ06SZHG64bjwVNNS>a4Sy+uj|q~m`60TP%+cN7AL4F}mXB6xaad9XjW2)FW--SBhxtNiCP%#dfBfVh7X;em z{{}I>Cn)qf*$_7Y+Z1hyIhNVljQ_rkj*Sd;RlL^&p@uvUxWzx;{=_SloAwO`Ui@R?> zdNiNiu5TK8me`Xu{HS-OhNhWcc^d794*?X;Hc8S)S);6C-^Ji4z|UU@E!#8-{${(m zxgg~*N+WQfPwnkn!BwDY%JkTvr6A1@xoFTn7NUcyc=c)q1w|hB!=|=W)O_ondbjL# z3mKzUU}9UBRJQ=N7A&di;7+B#j73*~2#I>>2ppC2PTg^o39HTGO1{oGP0est%u8-HI(|GG5{B8ReCR-q zK*aba-f-UeUfXtjWB+ay7m;Cd}frM!blN7cW{gjEvZzwW5T5tU$-^={X;&fK^;vGq5avUGx-- zrvev*?Aq1_=XTWG!7z)m26+7XW>Yqk#`HNJ1)PD{IirIXpYf2N+fMzi_Ry&T4CjXo z&^(;~0FhJpreNmJ>l0m=O5^yJ&VvEgWuSvALE(`f35sUzoZ+5^85C42?$xWMzl=71`OSF$j~g4y zty;Kc&z>cau5ttx6cy#Q_Io}UYj$COR&#LrmS3^!w;G0bv>xw;?j6*@yNU5JF^6wr zAY^7zieJ1ay0hH{h&dT)u>Ao0Ac;k#Tjat`e%8)ubb+O-F@s+!eeXVTdkx<`L66nC zcKw$CFf1}RH%FO-As|;EJRw~nVc?+p;+kMYA+)H4+SIw$IKZ?i*?c=?A%u9Jdts|~ zsq84z(I@*CFI_sHU+6pplC4A3vuH+QGQ6HGz3=jwXqs$^hz zvCT;q0Bs_?oM+{5lkAJpd0&2y9@s%)6dT1T56-4>FkweK-lJVe$MzUNd5lS3sHlAS zUI13|rt?8zcdnh?+K{ejGgv2$Y;&{zIw4C0uwv+~@NIE@l$iw5bAf zCEv zyO(1il+%9ZnPpmPD%ydW9t9V$!hXiu+NDdNYTriq>Tw{vpeCAwi)(vk$c-EB0AX)i z;uF_fiT8gFKyxx%Qr^K4a}7WLEQ)bc=1QxymFN($i&6wpIB%81_U$uF>XV(Yq?8Ud z(+G6xm+>8&p(utYnO|oHEaz>o)=H`+>LDJ(S*J|9`iI_x^&B3@dg63Ex;fCu#@P-W zMPa5;u#f|p%>0z-0=*pk>)dkQ_3J&@qj2d8x`n>n?k+dR#Dz|O6IekywFo9z2-;)u zwdf31iHIzziP2RAEompijz;=rMMeJ>NBuFjgVngpYzIDYETr4w%Uz#wZW*>v&ff01 z@Yg&tyMkRq4TT}torDC-ZKqk*qBJ3$g5Ck9<9WTqdCg|z9S}2bnaNGP(9|2)IhW&b zoYu>xCiZw~TA8QEgc)46^eMgU`|W?vHUtuo8!L(aFS^cCXy|fsa%PXN5J+KVVe$2P zl=pgNtqcW)n`b+%wnN01Qyz<$m;^U&T*YoaTba+vMRqCs`h|3dK~7K( zCmc}B81$~`y!86S%A^H9M`T1?W+^DvAg)4xL&JB@Wv@a3L^ z4=5ue;|*?1t}A!q;uxK_P_WRY6{lC=T*p#!BH=Cu2>jh}Z&MKfgE+|dDO(&IWaw8) z%nW_ul9?$3GEg7Gulrj=a_j{ak9#Xbh}^sOs68xt4DJ3*N<~!__hdBeMsfT=EXl)T z0A29bv4>GnjK17%`T24h`}d1KFz!muZRq*4H)S_c%OfqSEv|LNYySRj0$JZQ={WLX zezqQS$ZEc&9?+9t#stHN%iT6|=n#LPlA795{=oI0(ziA1%Lo5G$fBCk#Y}Uz*N@3D zAjISkpc9mx(M+#mH`g^w&8C3VC-L!S_?@V%jF&dC3iLR9VzSPx`pykV1} zx!=&On6yaI#?LE6ag7)exZvvg#l2*;kHQ*TeM_*1$1&-KPa_qY)`Mz_V;+}Vtc-dT zwVVkQin|Ed!`<#~7*`4id$2A)A3TQMk|xsEjzt#)KsOHrM8H_Dgz%a%-{DU}TFKN# zeWyzV9Y$}zU;bI^=HKMze;n73S3XThkiW-IgAL{6gpKg*-PH`#o89E@y14nE{!a=< z+JRw{mB@?JQU`9WuqJfzn!A5AiFEJMoKLh_Qsgb(CTy#Y*DLVv(}bYm5~~^H z+W4E^hB($JlnXMEzi^y5U7u<>FZ<#E2_o`}2z6jVtN}N~`<_6>BYjeL%TODdXT3an(eAvF+_#h)m8^b4oA$QuUbB};nKIIC>ga)ux_MxX&|ipQTU$HPs{4Q)Y`*K2PY3uhJj+1qP=iipYlE31BD>~(Iq*i zi1`?k^bf$feoW|d#aZd!bX$F8QMs!{STa|oOX$LxRNSdkzo7dZ9@?>xlUqjyw*^y! z8>qiH=a{9u@*^F)eo%~xZg?yT;ZTTTv-okdhBF~4+i-|v_x||FQ_KM%dvr4Yr1gKL zwX`LhMZd-9{K#q7hc?yJHxWH!(Y@VQs8L4t4NBZKlH%;w?8$s~Or zlpImY)2^xWksdJZK{ zM1^myufuR!Q_jbMX4rUhV4QM6CO|Jo=UFMXZvag5F_Zw>=e?)S$vH}ogW}^d9xBXK z8Wr)n!3+1oO>_>jaL*vvKZT*Z70yaBx8VFC1;0-TRe#T#HQQe=+PFe zceB4Pf0K%=0)_qinX$>{iY0+{J$|mP7h2=Q*53Vcokv_e$j#96UB!)G+VK$O|1a8c zeOVEArBmdL_1#b&JaXX<;lsZd_!}S@e^TLK;6?%WaXvfxM36M+m4awS%~c3b_r(K) z_FqgYq+{2!plamJ#A{b_964dG*`}eB@#!}#W2Ub8gTH?U4b?D+_|BLA95nEq{%fJi zA;7(UZ*>(FXOmC(df8F(At$4q3m;O)Ewcv~o_r1_5}+uX~j%_+0C-uShcLBVH@^JDr_U`4WA%R5w z@L<0i5iuK&FyJz~+ZzH)FA;PUKn@d&@*401-;`AGL#e1(@_OypKVZW(kIb@XB>Is( zMhCAT1t5&D_AwtHwdCgJCSWCI|CK9OGI*Zq5-SP-f~Su~PN0c^TdjK=3xNHwMW$hI z;b~%G?s4JV*8aoT3_E`6lz-hgEbBdyLc)f}z7W(gSL!UFa(Hb3(L{qc91qcQXIAC^ zjVFlvVgN7P|Aab7(SPro3q^G-#D#^q+5KG}O2*rNm%w?OPGa3-9@$(3B&%S!vQT-W z{@SC$DSv;!abi(dF<`&}q!0TnT)e!qQT%3504g-Y^oBw~_X*zCybkhUyLZ+*niALi zErAnU7q@uZ?aWLzJO}P?Z_xdtEvEWo;_mwoU;-D_cV@&Zx5h>tOg$fc3SYi?hf$*8qp#VIAt!on*o8?&L)*AvP1$Re$5T z-oG+>`{tegUP{z#<`W*b9~OFxaTOOKz2!U%puE$2pISAn#(ONH%m_*yQ5zJRr5Q$*aNyAkma=VH(&Oh zJdvr*zYH*kIX0TsPIy4VmVx3B&bXsdD+Jx$TO3g%3auwyLVD7xw4QVk=}8M|Jt?_- z{ZUHs@_?H9%oU6yucBa>;Gmf&<5mj@FaVRA7px^Tg~k5Lp#QJ0V9gB|b2h%-z7wLU z{|JzRZwhWAig2@$arXHQk$Et&%R#m0v2{j$^+8Lbx9$H?jp^|ozcf1ceq0=AY1{6%BEWEq zlXmUe1$Ei7r~D8gKEmjr0o<98N{MQ~JfX^;MWXJDh>^g#iMvC`j^w5CVzeV&^N*_y>6j+ww9p;%>)Qg?Xf z^2>FM-Y=-wFQgL03;<^G8nBj!H~Dr-3Nz}+#fsz3fC=hEX>wh=+gbgH_qxOJwOB&C z!KZu17W5eKfqI#8`EHwO%zriAwtPdZipjc5_|QU6A_)qM>begEs)^_kH+Hh+=L zh<<DPSQtJwvb-J*=Q}5Fxcj*K`gAWVV=P$6{!U0AefpU*V=M!n}6& z+_^YV{d2rB@Tp*~z+Dje;6W}t@_6d2mHroOlTsEAgrC}v*?uhr)Hsw*@#ymz8jAj_ zF~4Xx&{Mn`T&H*A<1KdAzKL^5Yxq;^EBi(20>-MqkA(gpcVy z@P{f)4=6sAqH@4_KKhL9X4zE9_VYfnEnAJojO8}mNL(7@!au#UJor1@!dY*!nX(Zj zi?dRk=!se%8)<8^)<|6jZmgYg)<2D1q98=zJL4^^Hb_xgLC9rP*1Y!?<{yO=*4ST? z!l~q@F-{_Z9eE85L_zf(6R1I@{W;5^S*l!y&qzZw_zDday-K=JOf4X#bb-jMo5D|- zfZwr9;iq}~(7bzn?vPKay8{2_yHQbV59Ecd8gbwC%g=yjE|*vG2az@<(fXf=jpelu z9z3|aV-cf97vbF*X>3v|F0QeWy>j7nO)T5(?+YDec*Q?y!s+!>EJG+%Ro7ST&LsxW zr+*hFO|u(z7XK_s(ayAEhUmhj)>z@d0TyTsF{~Gd7B$kpKDH4sC)g=PD=SgT?LRgS zlI~hTl@Gt?3(n3nA00@Ti(bWI|FL7dAlVO#25U_1E_kIjf%Nx*aNn;4*Kky1mRT#L z41e9($`Vq2O*8(!_VAlU-PftU+>pS5BUni9vYl^#Jhwp_dZrer?3MznS_TY@Q&3RQ zfHwb&FQu46iB|#VQxKNOY+VW&KyL4_2ku=DVo}$xjVdZ{YKkDn$c4~FSkx*DWN-uH z;+{2YRz5by3ZP+DXy}EN1QVcDB12d54JWT`x4T!(8^eSHU=QXVbeWo6Xw$S&74KU? z>~bvttw9!2rLdX_9arjIt8oWe6&^ornM9N1y5`aOqDh3wza_nb68kT$*&g9Q6t3`y zh&&qU+=3C8a{iy|4x)HtzppX4+)zw5Usq6cvdiL^S#WFTMjA}?qr;h1S?9Ra!n6W5A-&&jsAR6zxfR0_iJouY+ zJz-CtoX)ks>VN&ZE2K(v9N;_sW3SXi7qw2KKtTRHR91Jw{&$>IQ#inE{L%_uql;xn zFVgr5m%8p-9nA~n2|a(q`!|77n9lP>?Cb)(1{8F4zeuRV-ZCHD=gW<8;LFgE9{g?H z$5_?>WboIRppDM>)Z1HxM-mI^~ztvq0czGV@YaX4qS*?7-rQ6aJ6Fsn^4-%HqVrmF%s8bZo8$jScmvjIiF3YCR(`<0XVZyuJuL9WaXDJ{mg zOEz+92lD>xH|6BsZu+KO8lv3&%3?m@ais2RObLeunQetL!88@B&xWLlR-;roR+~Oj z3+JKXI``?Al&8efa=XsEtu|UhaGkqU^qEgR2_av>jXv`LfnHpX`=V$|zdOk2!)bSt zX{z{n9fE)Dn0Pwq4XDLd zQ2pe?bce5}DN6uOUUGVRdc~>DCN7u5 zv*woapX*$(Rm;aPnxCD0Ud;A$_#4P(BFD(yA4Xb5Nd4%jzKhAL+eQb7h}1?&)5lnb zSevUa`UA34Dl+{icI*DOe{38}Xck}J{CH`EZG!HJlKn4K(4GqIe=d3bITCMgtQB59 zKl$`Wdax+$C)k<`=Xg$vfBuJv<6wB}$9|hwtkpstF$e7__P~M2&3k*bsC6rv>MW@LeqPC~$#4PL0nv{WLZhJKZxEs*F^~1CP9L!#Gc7VxD zJ*OXmFM(op3*9t~I?kukt$S32}7>*?K%eDsKQ&t~n=*0Osz zH*Ft3&ZGLu!M7+9E~!=glsL#khy;-c-k?W*Plk{qPin#4FNFvYFo<)PZo5?Ol}*d1 zlmPFSeI@YmNg|5R`@Z4_)?L+;rP`oPBnE^w~NHhB<#HjhNL+Yn>N$Kzk*<>V^zXm8<+#mr2Hxco|-3Gwjs>0)2*E9;c0 zRHvd=x2lDPevB`C?43u&S?rBgvd8=oHxTIF?~sAYnJZX9J@Zb0?&dz`XUTHbL$lU>@-Wey}rq4#tLB%4lDUw zI1LoDf3DvoL5_~!aXwOMui|!YiE$i1`7$m8zF{M^FC3OybTz-7Q{yOx$RSy8kUAiN z&Q_Io89c`u%I5TP)zmWNtDTm(cC`|X_lLU(N9j0xHN>FL3w4ed+_G%o&fg|PY5A?Y zTvSL$QaaA%yIRFbilaqM-!VGJV9P z5j^Zx97?-$HY?$@PReBX{rfElvB-gmbB1^4b*LE7sBmHL5GqcHFFt+xWOKVLt}pu)5@LddmTkE8;EQOoPtiPooR={;mjau?V-bxSnR_UQuleP>V zb1xekWsC+iB-tK*-{QDq{$TbNIFSGekYalgxvUaZ&TAdJ8sEhR0jLNd^*td5{;d$` zD20MYG|;PnSI!`&HxFkq5qgd@^UgIskt2 zPXX1PPYy)1hD0~4^7fTW_Bua4_^bAGDRd<6qu5Sn2u>FLz52)hHwu0(1NNx{K;#%? zcNpL}c88D9-UzS5nlXJhxxWu?6z9_Si?EKE``$v{ai?G8pS3!$v@p7?xDK$>-N;BK z;OkD|MI^xXRE{iqJI@to?xNpVhR^=){r|}(A zW%Jl2hf9=lm>{9yr-^W?@Zc+~&P9uB%>PBmj}B5k|98dJ#0=1(4h-s??)E?RRw-4# zAV>fI>8*Sei1!C$6rSBL8)@v5W2Ib2PSL~f+!Kx3{joVcbs5}UgMzzD+L^k|34=LW<+R#71i1+b`fm^&}3HIx6$Hp$iQ)98;+IsUX+Yijx6k3TzfC8Ak?xS%z zzX`Gw%X&ewD56D3eXBMx z@=G|+`aaNg*fvaaMUliJ?|K47AO|Zl{>VQ;2n~J`Ie=E`{K9RnBxgVm8<-qpe|G3@ z+C!g91w63s2xhhbN3~#mOuEp6=gnqh5{9&V0}A(?l&*V$I*Z_Bxd?To1@_~5Kr_QW z^dYI)4O;_5s>n_CErn){j|gcinwkWNFX2${VPobWAc#oMsPl?3v$U+QWv2PW?$Uew zB;-R&pqVic|9J;TmIN7zAKrCI-Ku%oLM&WvqvJp;P6#G9=&R6qh7 zRirYgV(-r2Z8ip{vAn+WC4gB_Qwk%zc*7Pk<7{Yavf`wmrL9t`a(PF=z z9w)G@Rhg1O-b|&<@e>mhw@>l>#-(w~52&fC`Zwtvw^_imuAcUGv;KYauo=xifv))- zZ6g%hE)i$N_RLEQ zsD7`Q#9u(+ziCo^;PJ^t`!`&8yg|_?;6!&t06X=((sU|M-^7GZuV;|KQ6azj=g&iI zWy*uX>R*Js7`Kz@9SgGkIB}!*BrMZ3l1StVEIqNtabl#enPYPkpH8|K!iD!JD!PzR zRPZ1(O92S(*|jSS&MAb_#g@hb?DF|X!HmlxizMG=ByzkXuIK>dURPAy0R*maXtSiG zJJx<)leO>bZ+oneQKqT6O+A-a>4w$%C_Uy^Ds0x&Aw11* zQINXv-57MzmamxyF`k!dkWj(gdGorXB5rjE9)&LcXz^gMPdchJNp38C_6*6a#|YsT z05{#YBtu;$^aWZ}6I{b;v5)TD$pK^pyZZgGm?TD_T2_d~0$~sbZ$=Uui40?x?(M^l zp_10+_?O1H&T|sce?*SuKohiK^XAQZ85M=R2kO$Ihbxr@_5-WS7Yz^VsW~j9+i+#X z@ba0f95vdUJlZoBhwQSK1q48%D6oeq_HvzpZ*-65RY)h+oJh#fGureUx$h);^RoP0J<(%Yt1T!(uG8I0Ba$p%p|o(Px68e zekF=G511aj_(uktxgJ5=OU4B(&Ap91auL?EYpFT2-K_0zkHBeS4+19Kv@mt$|b3Q@w(@=%e_}L$;_O*CF3o*VBw$53ynyX@epTQ4_%fsKk(#zQ*|#w(ik> z|MBDO($wK&RgoGbQtmSS$_K!vRd0$3+BjFX_J4xP6xo(OSRWEb>Q;!DIcM{Dd(0W^ zk_EcX(cRm#{khPxIP0X$e1X@n=|ZS>4Xj2iVWkh}LY%0-yyC5Ty=prBn|c}ehtFoD zA_>K;0gubt_3M?PpMvZAE$Hk0DkCP8K8hEUa18C|3*7E;Us*1=4S+s0)g-P1fp~1@ z4e;bcKAnzl6;h92(C7UbNGX-%ASrmUNt<0!2qmB6J53WR-|DF3WG%RV`rsFZHPLpH z%98SOdLB<-IP%^H=MO0%Y=BAS;INdQ7-_}3{tAuwQ;cNJgVW;rdD7rax5~j}2lCW# zv8N)xh`h0e$BvzZ(j|vk-QhDOeXr^|hwO4wX5reez9staDXe#gon9aV5 zqsThP{Ktp4G#Ci8ULa4U$W>xln=MZW2&yLX%b$A&D(kj;a^V9}+(yn7dA_wz2QgRL z(p$8_4r>_+)j~IOq2}=Ps0*~eK@L4fj@%C7m6?4{X`_!jd5Fyf7pa7^5Zf*7t23FR zoYM*znR;X_f4l--2LP)FK->Yx&Tu-x#_C&s+8al`x%8R6>;4a%Lp=)Lgtki*c~3cR z{d$flzZ|j3Y5?ti$WAO4!`ZE@hGhNkKkbEEL_EBAnO)izoqtFINugT&{MGITIBO^; zS49mlTO68PZ^;vk3lPy@dk|~KvSP(WLpCo(7_hy>AWhP%i5q#`%4z3?w_3POgVo7H zC^uH8F)Qstxqq8(`vg5m0-%0!dVDW1@b-*)w}4fDfUFOUr3kLR2j}1wnl)%vqj5_R z%fJIkCWl%ircsQXbw6!Y86EW?9xo3fBVR(df{$J3{02GepwH{s|Mhg4{%=l~`L_G{ zGKUzmNJT_^lgjAlBZ_)K+CiWH+Jn3xSP*pLV!>O2o#}aOYkM1IV`|oCHL+{=44$=Y zue|pz74}n_d-n#wumHAoQcw&*&NuGAHcoxtR3Q}KaCO>NJvTXKK|N=5Oh9(Sq127S zzimlHQ!U{*kaVm`3NlQY{u+00l)+>_Jwa5+io%t$`3)NKrhQEKo z^%+}aRYN59z*?cT*l?E8$K@7ft6BTujMRZ0-@TBQDe~`{8Bt9ubkSMs{z_(4#2pr$ za)y18CppVM@6KEetl94YebmRI0>|9iRFB&|4KViA=uD<%^2b8SV%XJC)UO>M8(1-#jVUj7MQ)7z- zRHm1UU>eGiGP80wA45ELm|C+T*7w#RrYWX&7*aNG7K-;geNzFH4 zBZbsO7jlO{CfJUH?bc04$zFmKRbk{OL_tYHK2eyXzl49n>a`<%jnNGKz{DS;I90)W z671v&w89*g8Q9#Fup}Mjw}s3XfE0EtYz8Rg-i~Dx%TMNB24W&4-%ht zn2lkTCk!%zb+WRu$!f#buds;hAkpNg2(NKpz;AYz*G|Y>?eR9@$B}E}zFNow#LaN0 z>wfx1eT}Sy?uN9@T>~mXF+VC9SKyW#V-UfF<4$a_syoFZZV~R#@1ZNFofH?RvHkLV zA8uTrE;pA3hU0Nn_PsoxxiW0N8=&;NFq=U=xxj_?kRBI6lC02PT38e6w+RkDS{E;ryLD#BLTk`==Ck>+v<`D*Bc5{|uK!=~K=b{VNQ zSAA$~Hy*hb4(&ZA+!Dk(!!i_xg>$1iA$CQ@3nosz*sWRph=I;)CexO6DD{=~@Z|Uc z=d5*`H}4~rFyowctT7y7b9Ob*j@c(HiNEsZ*!~3?`klyt*sXL3WHBvUc<4!Jdt4El zvKIS7<~cm)cjqmTcAc~AdYU={nj#i9RaFo2gaQr>`r4sq*Lak)k}$1Ywcw029{G}d zALab;B*75TQ#T+i2*_SK&Gj;uZcl+v>5n2e5_=lgjP3LYxRtWFW_So%Z~A_dc+X1J zIgRf@Ixzm4I)Nd|7EaRrd64c;gmizJr2AteP2xh*BqFww!cRg`{1J+h@2sew%pj<& zi_N1-2vnINar=2sk-ICe{C{(IQC9J=dGU5lMqGp|B2U^Jql@G=&EL?_bLJLO>*9k+ zL^Yf&3t^Y6WW`iDi}aFo5AWRBiaDjnsKY1FGs4yR!PBR^$=cYU?D&^g zXTk=j%<#IX2?LY?$|_Zqyc0MYS6>g4sIEtkR7mgy>gE#jjDIwk%zWo?ItFVT$7@LA zMM7jW$=(5cPmyi1AF}K!6!GmHLM%>;|2Kr&}3&(GDxHk4k9zLn1L-v2% zz~d$ZCx1SlE^EG8lIfY(PGKER-(^HW7w$EQy(p~n|FR(|vP zTi=k7l_E|Gtb7aZ8729y3lKD{-v!%T?fk|Dy(_wCJkh`&P*p8EpYR?y5u#R(GV!Ry zqMToP`nYp4>tDuN1dM3y-yejBp2Yg%1N^HLW|f9OwgZBLPeMlWlq`-9T2ER(jmKwN z2VAP`<3KGgl5Qvl-J&*Na7cj0Gr2HAmNd6zxDZ9Oe>_)Ot^2gJ#16w6i3c!g=Y2H7a_YvAT)CdKd5GOwaA(g31;t5)6I5Ds}3ZyHsJ z^S5`S*-$TZ_6{8#otPReq3Viz0~5s!EttUgG5l(jrks{~GUXh8mDDuKdHrAdnY&8E zowHwMI4VhzPeMp5wK2}K?U#SX+>3`Zwe_kba<ejxA?LXM;!{^en%Qw=PwDQ zQ7B7qQ{}eq{i)5@3aJ@>3XXDq;y5&vd51)jrxy|qxCsE+8#l&Jn*HM zTe6al$ls3_zIMN1#{;dxYv$@3^7&3Pj`iIg{EG1@0o}oY?+LWgSrenNy<>G*;|I@u z2s-vCBclO#w;|%ytz!Z9)S_jEopBzCzytPlNNF4%z=By#rZ68!0Fx-7cX0H8`qN?g z^&Sb1zFlysd5R(oDRC$QRb1_h^T*fgv`Z;`+s2@mE8u{;5C>Q${kVN53~>i1#(LL7 zA=WXbaE0X_(v?qsdU*%UNCo0rEg>Pr?iLvtDkX|5Wpu=TUu(R!5sU$I!Wbb8s{Z!laJ6%T4{W z=1M?NvVJ5p`>ZdS4hUmc*E#jh<0o0N%lej1P(KQ`AUJIca5Q7Bm@NMd)IW-7PCO%* zLL>w**zqJ`cK95(cbJ-*1_qTp=i~_(kK%nhcGth)t?a73XeiEmBI)UmqWNamNKoo+*!#|`6#7+h=5>r@1B8@+uT?rx~UJY2PnU?&U$2SU;FpeR4L zlL^ICRR>%sH?x;S;%RE?!4ru@+aVj4A%O5X{{y5jKk-Y#t>e8OdzYS^O!d-z+W-9f zGw>S%kyn~8!#hvEA?*ws7fBnTWd95P_3PY@sEmId=^G6;<4DT&+el$(@E%g)XWS%n zZK@nK`5kmPAyEA$<(_P@6aXV?4MW_S3F)dc_|5kXYcV-mAL+^MM%p^Yl7Bhcv{B`s z|I2Hzb=m=`@FY{cl@^6by_{DB^=|~-EfXJd`;uF)zMLA49nxx!)JeRI6<|#pqL)QWXkv7VSxXW_W)1Le|meS{+G&*)q$|I zB7td7O&TOf0?CmYTkH{$KVeE%BB{3ni9iM+>57xm4wac0 z(QmN*7{VfId>viJmxLJm4H{)C$t2nkg`z5)>Ib~?45SJtU;uRXONF8(ng@f;3^P{U z>V08gXQgoC;hbaRFJC0NJQnC9Il^x&;lZ&uUdJ)X|9HRgtM(zl3w426-`;!ocaDxe zKPICOPr=J!bt;#Sw|;25KC6(#{$ewW$#P|c<>SqoQd}9MPad01s##_vh-lcG*Cksb zxa3RhDTbN|upJ(-L4}MoH1a5qaZhlNJ=TgvPgF&^4m?8eHu-H7liEc~>YH{ok8NTO z4e|MK!m;4su^l{tr1OBn7^&Xplx$I07qAEyBP_P_EklO^Pav#}gs6LW?mUcLkT?=B z+SHpwNIMez9n8H#$|iQDq}B6^Zk_{o{YsDP-gZ;spM5rsJFq+y>KiTJR( zGr?ze@Q-~S5Aqb3^==Y3e;itTr9d;3d+gc1Og-N&2t?+sMGEL9Y9bKE8i$>{>r?E| z7nim6Ye5s5F;Fgfq$Z^^I!(pbzFk?@!Z@wO`%{|*GGzinLQavbBd+LHVc*g>KH5XN zQl}Umor6?yn#rD#Q^WTj3Cv<(GxqI*am{wGctjZ{@syxN{_sh(f#;BKX{LkYQ45P8 z%^K}lb{J}}ac6vfww|MO#^kNdraJk<@SGi>1tBA{cP;F@foqJ8)?`H^O~HGR^U%Eh z;HGa1xD{E1u|jZ_HOhtUtP+f8)GRj`5pT*(uShy0$g;8`FhDEdUCIYC&RVK;7NvUaATxbW)}+G~R&mXJ+UA!?NPKMPpuAM~r!wGXmS|Fr zgB&7ByBA9Juh^Q7T@B7>dSde-AKsPrWDY~MWf3xPHA!xs1z2i>K}fb3awRly7blTd z1Y)!S*rx<=b1w<~pt_1^=6TlLrd!ban$;qLFrvISVkba{z@pX)5<{(&7Zg9;zU;+| zUJX&BO75T_auVXxWe1KtySDbTjnoY(TZ=bCDmR3wfG+_*bq*)iOAOE2;g~6#HrKk$ zFx}ieAS}Epq2KMffqs;_FUFpF?pm0}ZF^>lm*{5zm|#J0i0PYHj+ViNx?i8|%Qbm8 zmR01*{Hiqr9bRmhJ*%tbxzjKj4(5ia!lTn3ACPqPow?~GF$tz&bVjC!Q@GK+B*LtC z)21}LuklFI5Hps>yqbh=TfDl4c95+aTujosMg0sgI55@-dYH1GI(hO5P}M=L7zBtl zDe{M0Uq21S$wsspS)Os|<*)Cz6ZA^#{j$$g(Apx@mG@}B=L;mIKX&au3xm4tqnss~ zqpxFzw0cj6j%gRK?a=yYmHfG<&g1E=@OPrv+`YusngE9pMOl3&k>N5voAE=ydWl3U zBf2-=ZUp;yr~M4zejc-}L*~pUZ)&`NUGu!Fr%LCjdD+G%Hq`XKXq1s0TNH6ze87gJ(}+9+R$KKzH*#ykI;IC7b1d?c(1GC zOEMT4lU@%tISw6jJW3LUtk=g@`9+3vhZHL!0rIU)kF{y=TMAJp|j*E zfwOLhlI)D`Ps;ms6PY4=t+U2L00O)PoQJd;XvU8AcZaFVj-Tnv3y+R22bkT>EBnqcrs-sKN+J1_-@?n5U5H`&&Fc(gL->TRG0rfD zpMn}0F6N29`3_qGY$vXZ;89v-z%!3bTivfV`tnYK4B zQ-2=TEjjLAjDK*wV3UlK?5tb%L$#50492ZZFnQe;FbNOF`4iI?z6?xRX>_h40H6N~ z#A@0~-STdGj}apP8-l*9!}~b>ls7#AUwsayx~DLCVNPDrHMVEpAw%+Ru*F9+4cy7h zY((d%dP_#rKL~c{vwgMfj3;)qL!Qml>7XsB-Tr9Lfh<}B(jDxdV{zR zW}Hn33X<;2p21TBbxFN`Lo`?eV{(}RRCqgsLFzD6AcFRkSraS8G*482&1pV!+t*(k z54S&02`pS%&o%*_K;k#}7K5)?g;wx9Lh%?1(#PN$`54K3UB#BnsjQHVM;7~?3N z+~qj&(NV?s@u9Rt#SDA@Pi^rJ0qKsFs5|K4*y;9pIRSl}RT^@G0Yy)QnzMp##^zK! z-4dCPCw2t3#m6*-W~>ghciUwu?*2+#x#n!^0a>3F9``c0ggAAslf@ai5mZZZa{Od6 zs=4ba6ZlQ$p3B%JrF*^NZQE}4*?J9)-wJfUU7}XRFdLyR;K#k~b(|b~7mrmIzr_}( z?$t&}S_0KoI(M(n+HF>c+n0_5>W-^B=JC{L_Sc_&fln*-jL{p*mRiLbjvEwN#u`VN zAJ@C35J!J-^uko!FyU-o^dA2>>M3T;7o=eUo%8Cn)4n4|jubx`oL;IQ6k{J$*H93j zb={KfNdMUkD|CSe5N;F{)PRqMr-YFGeHUwIyWJSe_==@xSw9JNk=bG*>~J!kDCMq2 zAMw2Fm10}^NQ=i+(}!7$#6QeP=|)R26maazYPE)f8>L_NrCZ)NwlF#>s+UjaCo#E= ze2ba`?JVjyqTQ1jM)wyn$-v{xSC5CvyzKJKNvc=-uKk zF_aSQ?7fMF)N9muT>1DYL{nuGh-o9wrN-T@`Sn+MQyxV{2}TW!)@O@+VUYf^(=Mb( z{_Y2dcl0CK6?r|xhvx?O3P~$tvmn0#4zs61-e1mS)irK7ZMy1S?(i|w(2(!%Kx#E} z@+N+>PsG;DD&Ml~Ie3*t-(u5|Uqtp%_U_)jSUaiHQYk1v`-c6fca27vLM@1eM{#i# z|9`>}e;=ytKCCMI%7*L2$2-Krb zFdEF1WhKI#qS2r(e8gRuV?FZ%tPkfNOOSEn%#C6y3 zdoncL+hf5jwvx?;sEArp5i(~jdr!SrieY#{MT!4Ks4Tl({?_Cg^5RK{X{(O0s_*OF z23_1KiI{)9aqylr^?Hq{WI$#)_FllTT}yTsM~bnr@%$7m$yK-PpCW*3q$?#lM$6&O zgCvWHkI&$dXsr41kcX4~np@X-2j-QRbQhxcF8l}&8Hao=%Z#9Pv3z}oHG>HTCBELJ zD_er5vtWYO_09k?F4a=dmun4uS z%Q%;Gsy?|QygZGMD?_$ATfCykNo(_7^vpwO32HDS>jSh-Kni*;z|K=mU(2m?WrE8d z4M@b0LM3`6cjmy^3_P9&P-c7CM4aHpH5H3+mpA0CgqRCgAtWY-Rye+jW7ko#8b|9y zIOtrDv5R!FKEVK03?R-BQ^?Rg0s9geD42AT8V-Kl^4uR|M@{(4T`_TmQipt%pN!}& zeQ^^mC23_h#b05xzc640o>qF4)3Wh#CbC6JOG@I9NdE>b2+PpsO#P5LI7VGSk5HSg zW18h#N}lvmIJM?{nt>9p!e91W=WP3GjK@JJOVu#w>2_Zq9gF_H7Vbhzd~ePc6K5xC zO22KPiV{2PIv!%CEo+&a@O=N|NtSs>rc|YNcZ^NZtAWX;DqajT<2L6M+>bq9-jwMk zYT{}1p?$-L_NVGI9eOtG8QX<0Uc^qpr%*X-q(Fekr4&1l^+W_1Tov!P+MC|mzA`(J zLb(mwEjg59w7p^6?XdN*l8T$@Eg@zRbeJ`T)rK`=%Rbf=mCP+^-0hekZi1wVym|BH zv|9SpvX*9TLiYKRxAxC?vy?&+MniPSykH6Ni#X6Ov$_BLHbH0FgkA{_@0zE`|7teL z+M{?2|F?+FzhYtk_Ek1x2eB4oes`to`F=|_efOR>yh1a~%lYypZKaMLEinP0O{ttg mh@xK^`hUV+Q{?@J$!SkL&zq)JxDhU&LX}sOi{5tl!v6u7X5iHT diff --git a/docs/development/index.rst b/docs/development/index.rst index 14c49dbd..321b3242 100644 --- a/docs/development/index.rst +++ b/docs/development/index.rst @@ -7,4 +7,3 @@ Development roadmap contributing - internals diff --git a/docs/development/internals.rst b/docs/development/internals.rst deleted file mode 100644 index 4b4d3b14..00000000 --- a/docs/development/internals.rst +++ /dev/null @@ -1,113 +0,0 @@ -********* -Internals -********* - -Some of the following notes and details will hopefully be useful when you start -developing on Mopidy, while some may only be useful when you get deeper into -specific parts of Mopidy. - -In addition to what you'll find here, don't forget the :doc:`/api/index`. - - -Class instantiation and usage -============================= - -The following diagram shows how Mopidy is wired together with the MPD client, -the Spotify service, and the speakers. - -**Legend** - -- Filled red boxes are the key external systems. -- Gray boxes are external dependencies. -- Blue circles lives in the ``main`` process, also known as ``CoreProcess``. - It is processing messages put on the core queue. -- Purple circles lives in a process named ``MpdProcess``, running an - :mod:`asyncore` loop. -- Green circles lives in a process named ``GStreamerProcess``. -- Brown circle is a thread living in the ``CoreProcess``. - -.. digraph:: class_instantiation_and_usage - - "main" [ color="blue" ] - "CoreProcess" [ color="blue" ] - - # Frontend - "MPD client" [ color="red", style="filled", shape="box" ] - "MpdFrontend" [ color="blue" ] - "MpdProcess" [ color="purple" ] - "MpdServer" [ color="purple" ] - "MpdSession" [ color="purple" ] - "MpdDispatcher" [ color="blue" ] - - # Backend - "Libspotify\nBackend" [ color="blue" ] - "Libspotify\nSessionManager" [ color="brown" ] - "pyspotify" [ color="gray", shape="box" ] - "libspotify" [ color="gray", shape="box" ] - "Spotify" [ color="red", style="filled", shape="box" ] - - # Output/mixer - "GStreamer\nOutput" [ color="blue" ] - "GStreamer\nSoftwareMixer" [ color="blue" ] - "GStreamer\nProcess" [ color="green" ] - "GStreamer" [ color="gray", shape="box" ] - "Speakers" [ color="red", style="filled", shape="box" ] - - "main" -> "CoreProcess" [ label="create" ] - - # Frontend - "CoreProcess" -> "MpdFrontend" [ label="create" ] - "MpdFrontend" -> "MpdProcess" [ label="create" ] - "MpdFrontend" -> "MpdDispatcher" [ label="create" ] - "MpdProcess" -> "MpdServer" [ label="create" ] - "MpdServer" -> "MpdSession" [ label="create one\nper client" ] - "MpdSession" -> "MpdDispatcher" [ - label="pass requests\nvia core_queue" ] - "MpdDispatcher" -> "MpdSession" [ - label="pass response\nvia reply_to pipe" ] - "MpdDispatcher" -> "Libspotify\nBackend" [ label="use backend API" ] - "MPD client" -> "MpdServer" [ label="connect" ] - "MPD client" -> "MpdSession" [ label="request" ] - "MpdSession" -> "MPD client" [ label="response" ] - - # Backend - "CoreProcess" -> "Libspotify\nBackend" [ label="create" ] - "Libspotify\nBackend" -> "Libspotify\nSessionManager" [ - label="creates and uses" ] - "Libspotify\nSessionManager" -> "Libspotify\nBackend" [ - label="pass commands\nvia core_queue" ] - "Libspotify\nSessionManager" -> "pyspotify" [ label="use Python\nwrapper" ] - "pyspotify" -> "Libspotify\nSessionManager" [ label="use callbacks" ] - "pyspotify" -> "libspotify" [ label="use C library" ] - "libspotify" -> "Spotify" [ label="use service" ] - "Libspotify\nSessionManager" -> "GStreamer\nProcess" [ - label="pass commands\nand audio data\nvia output_queue" ] - - # Output/mixer - "Libspotify\nBackend" -> "GStreamer\nSoftwareMixer" [ - label="create and\nuse mixer API" ] - "GStreamer\nSoftwareMixer" -> "GStreamer\nProcess" [ - label="pass commands\nvia output_queue" ] - "CoreProcess" -> "GStreamer\nOutput" [ label="create" ] - "GStreamer\nOutput" -> "GStreamer\nProcess" [ label="create" ] - "GStreamer\nProcess" -> "GStreamer" [ label="use library" ] - "GStreamer" -> "Speakers" [ label="play audio" ] - - -Thread/process communication -============================ - -.. warning:: - This section is currently outdated. - -- Everything starts with ``Main``. -- ``Main`` creates a ``Core`` process which runs the frontend, backend, and - mixer. -- Mixers *may* create an additional process for communication with external - devices, like ``NadTalker`` in this example. -- Backend libraries *may* have threads of their own, like ``despotify`` here - which has additional threads in the ``Core`` process. -- ``Server`` part currently runs in the same process and thread as ``Main``. -- ``Client`` is some external client talking to ``Server`` over a socket. - -.. image:: /_static/thread_communication.png From 23121d4a1587b0f05fbb5f8e829d1c5c5dea5a9a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 18:08:19 +0200 Subject: [PATCH 28/43] docs: Update how to use a custom audio sink --- docs/installation/gstreamer.rst | 11 +++++++++-- docs/installation/index.rst | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 72d55908..08e16378 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -73,5 +73,12 @@ Using a custom audio sink ========================= If you for some reason want to use some other GStreamer audio sink than -``autoaudiosink``, you can change :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK` -in your ``settings.py`` file. +``autoaudiosink``, you can add ``mopidy.outputs.custom.CustomOutput`` to the +:attr:`mopidy.settings.OUTPUTS` setting, and set the +:attr:`mopidy.settings.CUSTOM_OUTPUT` setting to a partial GStreamer pipeline +description describing the GStreamer sink you want to use. + +Example of ``settings.py`` for OSS4:: + + OUTPUTS = (u'mopidy.outputs.custom.CustomOutput',) + CUSTOM_OUTPUT = u'oss4sink' diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 1f497e3a..5101cc84 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -9,8 +9,8 @@ setup and whether you want to use stable releases or less stable development versions. -Install dependencies -==================== +Requirements +============ .. toctree:: :hidden: From 48943321608beb65a9fe6deb57afbb7b16a6fdca Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 18:12:51 +0200 Subject: [PATCH 29/43] docs: New email address for knutz3n --- docs/authors.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authors.rst b/docs/authors.rst index 01e810e4..af84f842 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -5,7 +5,7 @@ Authors Contributors to Mopidy in the order of appearance: - Stein Magnus Jodal -- Johannes Knutsen +- Johannes Knutsen - Thomas Adamcik - Kristian Klette From 7bd281942ad8b38858c0a2ea45fdf0ea9d2948e7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 18:16:50 +0200 Subject: [PATCH 30/43] docs: Add new state transition from STOPPED to PAUSED in graph --- mopidy/backends/base/playback.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 4ea7a13f..530c4840 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -263,6 +263,7 @@ class PlaybackController(object): .. digraph:: state_transitions "STOPPED" -> "PLAYING" [ label="play" ] + "STOPPED" -> "PAUSED" [ label="pause" ] "PLAYING" -> "STOPPED" [ label="stop" ] "PLAYING" -> "PAUSED" [ label="pause" ] "PLAYING" -> "PLAYING" [ label="play" ] From d6b3d51d3e88ace641f1d9935713918c42c11a07 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 18:55:10 +0200 Subject: [PATCH 31/43] docs: Add dependencies and settings to all backends, frontends and outputs --- mopidy/backends/local/__init__.py | 6 +++++- mopidy/backends/spotify/__init__.py | 7 ++++++- mopidy/frontends/mpd/__init__.py | 6 +++++- mopidy/outputs/custom.py | 8 ++++++++ mopidy/outputs/local.py | 8 ++++++++ mopidy/outputs/shoutcast.py | 13 +++++++++++++ 6 files changed, 45 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 5da80a18..93cf3534 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -22,7 +22,11 @@ class LocalBackend(ThreadingActor, Backend): """ A backend for playing music from a local music archive. - **Issues:** http://github.com/mopidy/mopidy/issues/labels/backend-local + **Issues:** https://github.com/mopidy/mopidy/issues?labels=backend-local + + **Dependencies:** + + - None **Settings:** diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index da839b26..87997059 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -28,7 +28,12 @@ class SpotifyBackend(ThreadingActor, Backend): trade mark of the Spotify Group. **Issues:** - http://github.com/mopidy/mopidy/issues/labels/backend-spotify + https://github.com/mopidy/mopidy/issues?labels=backend-spotify + + **Dependencies:** + + - libspotify == 0.0.8 (libspotify8 package from apt.mopidy.com) + - pyspotify == 1.2 (python-spotify package from apt.mopidy.com) **Settings:** diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 24c21c38..175aa0ee 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -13,11 +13,15 @@ class MpdFrontend(ThreadingActor, BaseFrontend): """ The MPD frontend. + **Dependencies:** + + - None + **Settings:** - :attr:`mopidy.settings.MPD_SERVER_HOSTNAME` - - :attr:`mopidy.settings.MPD_SERVER_PASSWORD` - :attr:`mopidy.settings.MPD_SERVER_PORT` + - :attr:`mopidy.settings.MPD_SERVER_PASSWORD` """ def __init__(self): diff --git a/mopidy/outputs/custom.py b/mopidy/outputs/custom.py index c5ca30bb..09239a44 100644 --- a/mopidy/outputs/custom.py +++ b/mopidy/outputs/custom.py @@ -21,6 +21,14 @@ class CustomOutput(BaseOutput): these cases setup :attr:`mopidy.settings.CUSTOM_OUTPUT` with a :command:`gst-launch` compatible string describing the target setup. + **Dependencies:** + + - None + + **Settings:** + + - :attr:`mopidy.settings.CUSTOM_OUTPUT` """ + def describe_bin(self): return settings.CUSTOM_OUTPUT diff --git a/mopidy/outputs/local.py b/mopidy/outputs/local.py index e004a076..8101e026 100644 --- a/mopidy/outputs/local.py +++ b/mopidy/outputs/local.py @@ -6,6 +6,14 @@ class LocalOutput(BaseOutput): This output will normally tell GStreamer to choose whatever it thinks is best for your system. In other words this is usually a sane choice. + + **Dependencies:** + + - None + + **Settings:** + + - None """ def describe_bin(self): diff --git a/mopidy/outputs/shoutcast.py b/mopidy/outputs/shoutcast.py index d2605514..ffe09aae 100644 --- a/mopidy/outputs/shoutcast.py +++ b/mopidy/outputs/shoutcast.py @@ -13,6 +13,19 @@ class ShoutcastOutput(BaseOutput): supports Shoutcast. The output supports setting for: server address, port, mount point, user, password and encoder to use. Please see :class:`mopidy.settings` for details about settings. + + **Dependencies:** + + - A SHOUTcast/Icecast server + + **Settings:** + + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_HOSTNAME` + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PORT` + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_USERNAME` + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PASSWORD` + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_MOUNT` + - :attr:`mopidy.settings.SHOUTCAST_OUTPUT_ENCODER` """ def describe_bin(self): From 9c7bf9fc73da3b44aabd92b133d8753a43302bbc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jun 2011 18:55:50 +0200 Subject: [PATCH 32/43] docs: Add inheritance diagrams for outputs --- docs/modules/outputs.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/modules/outputs.rst b/docs/modules/outputs.rst index 37ff0390..87d23dab 100644 --- a/docs/modules/outputs.rst +++ b/docs/modules/outputs.rst @@ -4,10 +4,11 @@ The following GStreamer audio outputs implements the :ref:`output-api`. -.. inheritance-diagram:: mopidy.outputs - +.. inheritance-diagram:: mopidy.outputs.custom .. autoclass:: mopidy.outputs.custom.CustomOutput +.. inheritance-diagram:: mopidy.outputs.local .. autoclass:: mopidy.outputs.local.LocalOutput +.. inheritance-diagram:: mopidy.outputs.shoutcast .. autoclass:: mopidy.outputs.shoutcast.ShoutcastOutput From 8d240ea686901e78fc58de10c0634b28ce079c07 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Thu, 9 Jun 2011 19:47:41 +0200 Subject: [PATCH 33/43] Read missing local settings from stdin by default --- mopidy/utils/settings.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 01fee23d..9516b334 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -1,6 +1,7 @@ # Absolute import needed to import ~/.mopidy/settings.py and not ourselves from __future__ import absolute_import from copy import copy +import getpass import logging import os from pprint import pformat @@ -16,8 +17,23 @@ class SettingsProxy(object): self.default = self._get_settings_dict_from_module( default_settings_module) self.local = self._get_local_settings() + self._read_missing_settings_from_stdin(self.default, self.local) self.runtime = {} + def _read_missing_settings_from_stdin(self, default, local): + for setting, value in default.iteritems(): + if isinstance(value, basestring) and len(value) == 0: + local[setting] = self._read_from_stdin(setting + u': ') + + def _read_from_stdin(self, prompt): + if u'_PASSWORD' in prompt: + return (getpass.getpass(prompt) + .decode(getpass.sys.stdin.encoding, 'ignore')) + else: + sys.stdout.write(prompt) + return (sys.stdin.readline().strip() + .decode(sys.stdin.encoding, 'ignore')) + def _get_local_settings(self): dotdir = os.path.expanduser(u'~/.mopidy/') settings_file = os.path.join(dotdir, u'settings.py') From efa38d2449c6e390890ba8850fea21f147af3c89 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Thu, 9 Jun 2011 20:03:02 +0200 Subject: [PATCH 34/43] Read interactive settings optional by adding --interactive option --- mopidy/core.py | 9 ++++++--- mopidy/utils/settings.py | 5 +++-- tests/help_test.py | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index ca5b92a1..e8857b94 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -30,7 +30,7 @@ logger = logging.getLogger('mopidy.core') def main(): options = parse_options() setup_logging(options.verbosity_level, options.save_debug_log) - setup_settings() + setup_settings(options.interactive) setup_gobject_loop() setup_gstreamer() setup_mixer() @@ -49,6 +49,9 @@ def parse_options(): parser.add_option('--help-gst', action='store_true', dest='help_gst', help='show GStreamer help options') + parser.add_option('-i', '--interactive', + action='store_true', dest='interactive', + help='ask interactively for required settings which is missing') parser.add_option('-q', '--quiet', action='store_const', const=0, dest='verbosity_level', help='less output (warning level)') @@ -63,11 +66,11 @@ def parse_options(): help='list current settings') return parser.parse_args(args=mopidy_args)[0] -def setup_settings(): +def setup_settings(interactive): get_or_create_folder('~/.mopidy/') get_or_create_file('~/.mopidy/settings.py') try: - settings.validate() + settings.validate(interactive) except SettingsError, e: logger.error(e.message) sys.exit(1) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 9516b334..a5b3bed2 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -17,7 +17,6 @@ class SettingsProxy(object): self.default = self._get_settings_dict_from_module( default_settings_module) self.local = self._get_local_settings() - self._read_missing_settings_from_stdin(self.default, self.local) self.runtime = {} def _read_missing_settings_from_stdin(self, default, local): @@ -79,7 +78,9 @@ class SettingsProxy(object): else: super(SettingsProxy, self).__setattr__(attr, value) - def validate(self): + def validate(self, interactive): + if interactive: + self._read_missing_settings_from_stdin(self.default, self.local) if self.get_errors(): logger.error(u'Settings validation errors: %s', indent(self.get_errors_as_string())) diff --git a/tests/help_test.py b/tests/help_test.py index dccccc9c..25f534c2 100644 --- a/tests/help_test.py +++ b/tests/help_test.py @@ -14,6 +14,7 @@ class HelpTest(unittest.TestCase): self.assert_('--version' in output) self.assert_('--help' in output) self.assert_('--help-gst' in output) + self.assert_('--interactive' in output) self.assert_('--quiet' in output) self.assert_('--verbose' in output) self.assert_('--save-debug-log' in output) From ded513faed372b9df6fad80b1482408b61b10132 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Thu, 9 Jun 2011 20:04:05 +0200 Subject: [PATCH 35/43] Catch keyboard interrupts in all core setup methods --- mopidy/core.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index e8857b94..98575478 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -28,15 +28,15 @@ from mopidy.utils.settings import list_settings_optparse_callback logger = logging.getLogger('mopidy.core') def main(): - options = parse_options() - setup_logging(options.verbosity_level, options.save_debug_log) - setup_settings(options.interactive) - setup_gobject_loop() - setup_gstreamer() - setup_mixer() - setup_backend() - setup_frontends() try: + options = parse_options() + setup_logging(options.verbosity_level, options.save_debug_log) + setup_settings(options.interactive) + setup_gobject_loop() + setup_gstreamer() + setup_mixer() + setup_backend() + setup_frontends() while ActorRegistry.get_all(): time.sleep(1) logger.info(u'No actors left. Exiting...') From c31db049791bf4fbffc4882d190ea4f9d613f1c4 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Thu, 9 Jun 2011 20:08:50 +0200 Subject: [PATCH 36/43] Move private methods to be closer it's caller --- mopidy/utils/settings.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index a5b3bed2..73268345 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -19,20 +19,6 @@ class SettingsProxy(object): self.local = self._get_local_settings() self.runtime = {} - def _read_missing_settings_from_stdin(self, default, local): - for setting, value in default.iteritems(): - if isinstance(value, basestring) and len(value) == 0: - local[setting] = self._read_from_stdin(setting + u': ') - - def _read_from_stdin(self, prompt): - if u'_PASSWORD' in prompt: - return (getpass.getpass(prompt) - .decode(getpass.sys.stdin.encoding, 'ignore')) - else: - sys.stdout.write(prompt) - return (sys.stdin.readline().strip() - .decode(sys.stdin.encoding, 'ignore')) - def _get_local_settings(self): dotdir = os.path.expanduser(u'~/.mopidy/') settings_file = os.path.join(dotdir, u'settings.py') @@ -86,6 +72,20 @@ class SettingsProxy(object): indent(self.get_errors_as_string())) raise SettingsError(u'Settings validation failed.') + def _read_missing_settings_from_stdin(self, default, local): + for setting, value in default.iteritems(): + if isinstance(value, basestring) and len(value) == 0: + local[setting] = self._read_from_stdin(setting + u': ') + + def _read_from_stdin(self, prompt): + if u'_PASSWORD' in prompt: + return (getpass.getpass(prompt) + .decode(getpass.sys.stdin.encoding, 'ignore')) + else: + sys.stdout.write(prompt) + return (sys.stdin.readline().strip() + .decode(sys.stdin.encoding, 'ignore')) + def get_errors(self): return validate_settings(self.default, self.local) From 33e70de66147d83702b7382a4a617a0e2e4a2eac Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Thu, 9 Jun 2011 20:28:38 +0200 Subject: [PATCH 37/43] Test interactive input --- tests/utils/settings_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index 748eae85..d1481ce5 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -149,6 +149,12 @@ class SettingsProxyTest(unittest.TestCase): actual = self.settings.TEST self.assertEqual(actual, './test') + def test_interactive_input_of_missing_defaults(self): + self.settings.default['TEST'] = '' + interactive_input = 'input' + self.settings._read_from_stdin = lambda _: interactive_input + self.settings.validate(interactive=True) + self.assertEqual(interactive_input, self.settings.TEST) class FormatSettingListTest(unittest.TestCase): def setUp(self): From 5e0a85c1b6195949c1cd9953db689d0a81a2326d Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Thu, 9 Jun 2011 20:38:52 +0200 Subject: [PATCH 38/43] Get encoding from sys module directly instead of getpass.sys --- mopidy/utils/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 73268345..a10b3a78 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -80,7 +80,7 @@ class SettingsProxy(object): def _read_from_stdin(self, prompt): if u'_PASSWORD' in prompt: return (getpass.getpass(prompt) - .decode(getpass.sys.stdin.encoding, 'ignore')) + .decode(sys.stdin.encoding, 'ignore')) else: sys.stdout.write(prompt) return (sys.stdin.readline().strip() From 32915b6832f2eed418d370944ae99e041f75099b Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Thu, 9 Jun 2011 20:47:20 +0200 Subject: [PATCH 39/43] Add --interactive feature to changelog --- docs/changes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 4b6f74ca..2c240bfa 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -70,6 +70,9 @@ Please note that 0.5.0 requires some updated dependencies, as listed under - Improve :option:`--list-settings` output. (Fixes: :issue:`91`) + - Added :option:`--interactive` for reading missing local settings from + ``stdin``. (Fixes: :issue:`96`) + - Tag cache generator: - Made it possible to abort :command:`mopidy-scan` with CTRL+C. From 3cdf1aa35deec2623d90929789efbc72b583ae97 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 00:33:14 +0200 Subject: [PATCH 40/43] Add util function for stopping all actors which also tries to stop actors that was started after the function was called --- mopidy/core.py | 4 ++-- mopidy/utils/process.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index 98575478..6ed9a601 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -22,7 +22,7 @@ from mopidy.gstreamer import GStreamer from mopidy.utils import get_class from mopidy.utils.log import setup_logging from mopidy.utils.path import get_or_create_folder, get_or_create_file -from mopidy.utils.process import GObjectEventThread +from mopidy.utils.process import GObjectEventThread, stop_all_actors from mopidy.utils.settings import list_settings_optparse_callback logger = logging.getLogger('mopidy.core') @@ -42,7 +42,7 @@ def main(): logger.info(u'No actors left. Exiting...') except KeyboardInterrupt: logger.info(u'User interrupt. Exiting...') - ActorRegistry.stop_all() + stop_all_actors() def parse_options(): parser = optparse.OptionParser(version=u'Mopidy %s' % get_version()) diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index 7f6cf664..47ae6856 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -5,11 +5,18 @@ import gobject gobject.threads_init() from pykka import ActorDeadError +from pykka.registry import ActorRegistry from mopidy import SettingsError logger = logging.getLogger('mopidy.utils.process') +def stop_all_actors(): + num_actors = len(ActorRegistry.get_all()) + while num_actors: + logger.debug(u'Stopping %d actor(s)...', num_actors) + ActorRegistry.stop_all() + num_actors = len(ActorRegistry.get_all()) class BaseThread(threading.Thread): def __init__(self): From f8965e053b72c7817b46a10986d733cf015a44b8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 00:34:06 +0200 Subject: [PATCH 41/43] Register SIGTERM handler --- mopidy/core.py | 5 ++++- mopidy/utils/process.py | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/mopidy/core.py b/mopidy/core.py index 6ed9a601..b89a5456 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -1,5 +1,6 @@ import logging import optparse +import signal import sys import time @@ -22,12 +23,14 @@ from mopidy.gstreamer import GStreamer from mopidy.utils import get_class from mopidy.utils.log import setup_logging from mopidy.utils.path import get_or_create_folder, get_or_create_file -from mopidy.utils.process import GObjectEventThread, stop_all_actors +from mopidy.utils.process import (GObjectEventThread, exit_handler, + stop_all_actors) from mopidy.utils.settings import list_settings_optparse_callback logger = logging.getLogger('mopidy.core') def main(): + signal.signal(signal.SIGTERM, exit_handler) try: options = parse_options() setup_logging(options.verbosity_level, options.save_debug_log) diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index 47ae6856..5b09148d 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -1,4 +1,5 @@ import logging +import signal import threading import gobject @@ -11,6 +12,13 @@ from mopidy import SettingsError logger = logging.getLogger('mopidy.utils.process') +def exit_handler(signum, frame): + """A :mod:`signal` handler which will exit the program on signal.""" + signals = dict((k, v) for v, k in signal.__dict__.iteritems() + if v.startswith('SIG') and not v.startswith('SIG_')) + logger.info(u'Got %s. Exiting...', signals[signum]) + stop_all_actors() + def stop_all_actors(): num_actors = len(ActorRegistry.get_all()) while num_actors: From 05c533014e17cf73e2a1121e769aca83edd04cb0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 09:54:04 +0200 Subject: [PATCH 42/43] Ensure tests are not affected by local settings --- tests/utils/settings_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index d1481ce5..dd0fe89b 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -71,6 +71,7 @@ class ValidateSettingsTest(unittest.TestCase): class SettingsProxyTest(unittest.TestCase): def setUp(self): self.settings = SettingsProxy(default_settings_module) + self.settings.local.clear() def test_set_and_get_attr(self): self.settings.TEST = 'test' @@ -156,6 +157,7 @@ class SettingsProxyTest(unittest.TestCase): self.settings.validate(interactive=True) self.assertEqual(interactive_input, self.settings.TEST) + class FormatSettingListTest(unittest.TestCase): def setUp(self): self.settings = SettingsProxy(default_settings_module) From c0a39afa31d5e060d9fb2862f0d6a3a23317cd53 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 14:30:32 +0200 Subject: [PATCH 43/43] Do not interactively ask for settings when they are already set locally --- mopidy/utils/settings.py | 8 ++++---- tests/utils/settings_test.py | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index a10b3a78..500477e2 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -66,16 +66,16 @@ class SettingsProxy(object): def validate(self, interactive): if interactive: - self._read_missing_settings_from_stdin(self.default, self.local) + self._read_missing_settings_from_stdin(self.current, self.runtime) if self.get_errors(): logger.error(u'Settings validation errors: %s', indent(self.get_errors_as_string())) raise SettingsError(u'Settings validation failed.') - def _read_missing_settings_from_stdin(self, default, local): - for setting, value in default.iteritems(): + def _read_missing_settings_from_stdin(self, current, runtime): + for setting, value in current.iteritems(): if isinstance(value, basestring) and len(value) == 0: - local[setting] = self._read_from_stdin(setting + u': ') + runtime[setting] = self._read_from_stdin(setting + u': ') def _read_from_stdin(self, prompt): if u'_PASSWORD' in prompt: diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index dd0fe89b..973c2280 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -157,6 +157,13 @@ class SettingsProxyTest(unittest.TestCase): self.settings.validate(interactive=True) self.assertEqual(interactive_input, self.settings.TEST) + def test_interactive_input_not_needed_when_setting_is_set_locally(self): + self.settings.default['TEST'] = '' + self.settings.local['TEST'] = 'test' + self.settings._read_from_stdin = lambda _: self.fail( + 'Should not read from stdin') + self.settings.validate(interactive=True) + class FormatSettingListTest(unittest.TestCase): def setUp(self):