diff --git a/.mailmap b/.mailmap index b38c3f66..7098b6d4 100644 --- a/.mailmap +++ b/.mailmap @@ -5,3 +5,5 @@ Kristian Klette Johannes Knutsen Johannes Knutsen John Bäckstrand +Alexandre Petitjean +Alexandre Petitjean diff --git a/AUTHORS b/AUTHORS index 1e3faf0e..ddb1602e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -22,3 +22,4 @@ - Janez Troha - Tobias Sauerwein - alzeih +- Alexandre Petitjean diff --git a/docs/changelog.rst b/docs/changelog.rst index a8c9dee6..c3a80955 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,11 @@ v0.15.0 (UNRELEASED) - :option:`mopidy --show-config` will now take into consideration any :option:`mopidy --option` arguments appearing later on the command line. +**Audio** + +- Added support for viusalization. :confval:`audio/visualizer` can now be set + to GStreamer visualizers. + **Local backend** - An album's number of discs and a track's disc number are now extracted when @@ -33,6 +38,9 @@ v0.15.0 (UNRELEASED) hierarchy from your Spotify account is available in Mopidy. (Fixes: :issue:`62`) +- Fix proxy config values that was broken with the config system change in + 0.14. (Fixes: :issue:`472`) + v0.14.2 (2013-07-01) ==================== diff --git a/docs/conf.py b/docs/conf.py index c0dba916..f3e4166c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -101,7 +101,7 @@ master_doc = 'index' # General information about the project. project = 'Mopidy' -copyright = '2010-2013, Stein Magnus Jodal and contributors' +copyright = '2009-2013, Stein Magnus Jodal and contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/config.rst b/docs/config.rst index 9f997b23..6fd7579d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -90,6 +90,16 @@ Core configuration values ``gst-inspect-0.10`` to see what output properties can be set on the sink. For example: ``gst-inspect-0.10 shout2send`` +.. confval:: audio/visualizer + + Visualizer to use. + + Can be left blank if no visualizer is desired. Otherwise this expects a + GStreamer visualizer. Typical values are ``monoscope``, ``goom``, + ``goom2k1`` or one of the `libvisual`_ visualizers. + +.. _libvisual: http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-base-plugins/html/gst-plugins-base-plugins-plugin-libvisual.html + .. confval:: logging/console_format The log format used for informational logging. diff --git a/docs/index.rst b/docs/index.rst index fb91244d..ca40c96c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,7 +48,7 @@ About :maxdepth: 1 authors - licenses + license changelog versioning diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 00000000..98928f63 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,10 @@ +******* +License +******* + +Mopidy is copyright 2009-2013 Stein Magnus Jodal and contributors. For a list +of contributors, see :doc:`authors`. For details on who have contributed what, +please refer to our git repository. + +Mopidy is licensed under the `Apache License, Version 2.0 +`_. diff --git a/docs/licenses.rst b/docs/licenses.rst deleted file mode 100644 index fc2530e5..00000000 --- a/docs/licenses.rst +++ /dev/null @@ -1,34 +0,0 @@ -******** -Licenses -******** - -For a list of contributors, see :doc:`authors`. For details on who have -contributed what, please refer to our git repository. - -Source code license -=================== - -Copyright 2009-2013 Stein Magnus Jodal and contributors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - -Documentation license -===================== - -Copyright 2010-2013 Stein Magnus Jodal and contributors - -This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 -Unported License. To view a copy of this license, visit -http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative -Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. diff --git a/js/README.md b/js/README.md index 0e5e17c9..eddfa99f 100644 --- a/js/README.md +++ b/js/README.md @@ -51,15 +51,15 @@ Building from source 1. Install [Node.js](http://nodejs.org/) and npm. There is a PPA if you're running Ubuntu: - sudo apt-get install python-software-properties - sudo add-apt-repository ppa:chris-lea/node.js - sudo apt-get update - sudo apt-get install nodejs npm + sudo apt-get install python-software-properties + sudo add-apt-repository ppa:chris-lea/node.js + sudo apt-get update + sudo apt-get install nodejs 2. Enter the `js/` in Mopidy's Git repo dir and install all dependencies: - cd js/ - npm install + cd js/ + npm install That's it. diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index af0a0c68..d78df9b7 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -22,6 +22,22 @@ mixers.register_mixers() MB = 1 << 20 +# GST_PLAY_FLAG_VIDEO (1<<0) +# GST_PLAY_FLAG_AUDIO (1<<1) +# GST_PLAY_FLAG_TEXT (1<<2) +# GST_PLAY_FLAG_VIS (1<<3) +# GST_PLAY_FLAG_SOFT_VOLUME (1<<4) +# GST_PLAY_FLAG_NATIVE_AUDIO (1<<5) +# GST_PLAY_FLAG_NATIVE_VIDEO (1<<6) +# GST_PLAY_FLAG_DOWNLOAD (1<<7) +# GST_PLAY_FLAG_BUFFERING (1<<8) +# GST_PLAY_FLAG_DEINTERLACE (1<<9) +# GST_PLAY_FLAG_SOFT_COLORBALANCE (1<<10) + +# Default flags to use for playbin: AUDIO, SOFT_VOLUME, DOWNLOAD +PLAYBIN_FLAGS = (1<<1) | (1<<4) | (1<<7) +PLAYBIN_VIS_FLAGS = PLAYBIN_FLAGS | (1<<3) + class Audio(pykka.ThreadingActor): """ @@ -55,6 +71,7 @@ class Audio(pykka.ThreadingActor): try: self._setup_playbin() self._setup_output() + self._setup_visualizer() self._setup_mixer() self._setup_message_processor() except gobject.GError as ex: @@ -78,9 +95,7 @@ class Audio(pykka.ThreadingActor): def _setup_playbin(self): playbin = gst.element_factory_make('playbin2') - - fakesink = gst.element_factory_make('fakesink') - playbin.set_property('video-sink', fakesink) + playbin.set_property('flags', PLAYBIN_FLAGS) self._connect(playbin, 'about-to-finish', self._on_about_to_finish) self._connect(playbin, 'notify::source', self._on_new_source) @@ -149,6 +164,19 @@ class Audio(pykka.ThreadingActor): 'Failed to create audio output "%s": %s', output_desc, ex) process.exit_process() + def _setup_visualizer(self): + visualizer_element = self._config['audio']['visualizer'] + if not visualizer_element: + return + try: + visualizer = gst.element_factory_make(visualizer_element) + self._playbin.set_property('vis-plugin', visualizer) + self._playbin.set_property('flags', PLAYBIN_VIS_FLAGS) + logger.info('Audio visualizer set to "%s"', visualizer_element) + except gobject.GError as ex: + logger.error( + 'Failed to create audio visualizer "%s": %s', visualizer_element, ex) + def _setup_mixer(self): mixer_desc = self._config['audio']['mixer'] track_desc = self._config['audio']['mixer_track'] diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 8f520896..125b6ada 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -33,9 +33,17 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): self.cache_location = config['spotify']['cache_dir'] self.settings_location = config['spotify']['cache_dir'] + full_proxy = '' + if config['proxy']['hostname']: + full_proxy = config['proxy']['hostname'] + if config['proxy']['port']: + full_proxy += ':' + str(config['proxy']['port']) + if config['proxy']['scheme']: + full_proxy = config['proxy']['scheme'] + "://" + full_proxy + PyspotifySessionManager.__init__( self, config['spotify']['username'], config['spotify']['password'], - proxy=config['proxy']['hostname'], + proxy=full_proxy, proxy_username=config['proxy']['username'], proxy_password=config['proxy']['password']) diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index e9ae7d86..0767b50c 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -24,9 +24,13 @@ _audio_schema = ConfigSchema('audio') _audio_schema['mixer'] = String() _audio_schema['mixer_track'] = String(optional=True) _audio_schema['output'] = String() +_audio_schema['visualizer'] = String(optional=True) _proxy_schema = ConfigSchema('proxy') +_proxy_schema['scheme'] = String(optional=True, + choices=['http', 'https', 'socks4', 'socks5']) _proxy_schema['hostname'] = Hostname(optional=True) +_proxy_schema['port'] = Port(optional=True) _proxy_schema['username'] = String(optional=True) _proxy_schema['password'] = Secret(optional=True) diff --git a/mopidy/config/convert.py b/mopidy/config/convert.py index 6cb20fcd..3c3edb85 100644 --- a/mopidy/config/convert.py +++ b/mopidy/config/convert.py @@ -39,6 +39,7 @@ def convert(settings): helper('audio/output', 'OUTPUT') helper('proxy/hostname', 'SPOTIFY_PROXY_HOST') + helper('proxy/port', 'SPOTIFY_PROXY_PORT') helper('proxy/username', 'SPOTIFY_PROXY_USERNAME') helper('proxy/password', 'SPOTIFY_PROXY_PASSWORD') diff --git a/mopidy/config/default.conf b/mopidy/config/default.conf index b525ef47..06749ee1 100644 --- a/mopidy/config/default.conf +++ b/mopidy/config/default.conf @@ -11,8 +11,11 @@ pykka = info mixer = autoaudiomixer mixer_track = output = autoaudiosink +visualizer = [proxy] +scheme = hostname = +port = username = password = diff --git a/mopidy/config/types.py b/mopidy/config/types.py index ec0be1de..29651940 100644 --- a/mopidy/config/types.py +++ b/mopidy/config/types.py @@ -71,9 +71,9 @@ class String(ConfigValue): def deserialize(self, value): value = decode(value).strip() validators.validate_required(value, self._required) - validators.validate_choice(value, self._choices) if not value: return None + validators.validate_choice(value, self._choices) return value def serialize(self, value, display=False): @@ -111,12 +111,16 @@ class Secret(ConfigValue): class Integer(ConfigValue): """Integer value.""" - def __init__(self, minimum=None, maximum=None, choices=None): + def __init__(self, minimum=None, maximum=None, choices=None, optional=False): + self._required = not optional self._minimum = minimum self._maximum = maximum self._choices = choices def deserialize(self, value): + validators.validate_required(value, self._required) + if not value: + return None value = int(value) validators.validate_choice(value, self._choices) validators.validate_minimum(value, self._minimum) @@ -222,9 +226,9 @@ class Port(Integer): allocate a port for us. """ # TODO: consider probing if port is free or not? - def __init__(self, choices=None): + def __init__(self, choices=None, optional=False): super(Port, self).__init__( - minimum=0, maximum=2 ** 16 - 1, choices=choices) + minimum=0, maximum=2 ** 16 - 1, choices=choices, optional=optional) class Path(ConfigValue): diff --git a/tests/audio/actor_test.py b/tests/audio/actor_test.py index c311bdc3..617131cc 100644 --- a/tests/audio/actor_test.py +++ b/tests/audio/actor_test.py @@ -21,6 +21,7 @@ class AudioTest(unittest.TestCase): 'mixer': 'fakemixer track_max_volume=65536', 'mixer_track': None, 'output': 'fakesink', + 'visualizer': None, } } self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) @@ -70,6 +71,7 @@ class AudioTest(unittest.TestCase): 'mixer': 'fakemixer track_max_volume=40', 'mixer_track': None, 'output': 'fakesink', + 'visualizer': None, } } self.audio = audio.Audio.start(config=config).proxy() diff --git a/tests/config/types_test.py b/tests/config/types_test.py index 88c8f067..74e9672d 100644 --- a/tests/config/types_test.py +++ b/tests/config/types_test.py @@ -98,6 +98,11 @@ class StringTest(unittest.TestCase): self.assertIsInstance(result, bytes) self.assertEqual(b'', result) + def test_deserialize_enforces_choices_optional(self): + value = types.String(optional=True, choices=['foo', 'bar', 'baz']) + self.assertEqual(None, value.deserialize(b'')) + self.assertRaises(ValueError, value.deserialize, b'foobar') + class SecretTest(unittest.TestCase): def test_deserialize_passes_through(self): @@ -163,6 +168,10 @@ class IntegerTest(unittest.TestCase): self.assertEqual(5, value.deserialize('5')) self.assertRaises(ValueError, value.deserialize, '15') + def test_deserialize_respects_optional(self): + value = types.Integer(optional=True) + self.assertEqual(None, value.deserialize('')) + class BooleanTest(unittest.TestCase): def test_deserialize_conversion_success(self):