diff --git a/docs/conf.py b/docs/conf.py
index 55f415b4..1e9f9cb2 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -32,7 +32,8 @@ class Mock(object):
def __getattr__(self, name):
if name in ('__file__', '__path__'):
return '/dev/null'
- elif name[0] == name[0].upper() and not name.startswith('MIXER_TRACK'):
+ elif (name[0] == name[0].upper()
+ and not name.startswith('MIXER_TRACK_')):
return type(name, (), {})
else:
return Mock()
@@ -266,3 +267,12 @@ latex_documents = [
needs_sphinx = '1.0'
extlinks = {'issue': ('https://github.com/mopidy/mopidy/issues/%s', '#')}
+
+
+def setup(app):
+ from sphinx.ext.autodoc import cut_lines
+ app.connect(b'autodoc-process-docstring', cut_lines(4, what=['module']))
+ app.add_object_type(
+ b'confval', 'confval',
+ objname='configuration value',
+ indextemplate='pair: %s; configuration value')
diff --git a/docs/settings.rst b/docs/config.rst
similarity index 57%
rename from docs/settings.rst
rename to docs/config.rst
index 4c9acd96..cd75e6dc 100644
--- a/docs/settings.rst
+++ b/docs/config.rst
@@ -1,32 +1,38 @@
-********
-Settings
-********
+*************
+Configuration
+*************
-Mopidy has lots of settings. Luckily, you only need to change a few, and stay
-ignorant of the rest. Below you can find guides for typical configuration
-changes you may want to do, and a complete listing of available settings.
+Mopidy has quite a few config values to tweak. Luckily, you only need to change
+a few, and stay ignorant of the rest. Below you can find guides for typical
+configuration changes you may want to do, and a listing of the available config
+values.
-Changing settings
-=================
+Changing configuration
+======================
-Mopidy reads settings from the file ``~/.config/mopidy/settings.py``, where
-``~`` means your *home directory*. If your username is ``alice`` and you are
-running Linux, the settings file should probably be at
-``/home/alice/.config/mopidy/settings.py``.
+Mopidy primarily reads config from the file ``~/.config/mopidy/mopidy.conf``,
+where ``~`` means your *home directory*. If your username is ``alice`` and you
+are running Linux, the settings file should probably be at
+``/home/alice/.config/mopidy/mopidy.conf``.
-You can either create the settings file yourself, or run the ``mopidy``
+You can either create the configuration file yourself, or run the ``mopidy``
command, and it will create an empty settings file for you.
-When you have created the settings file, open it in a text editor, and add
+When you have created the configuration file, open it in a text editor, and add
settings you want to change. If you want to keep the default value for a
setting, you should *not* redefine it in your own settings file.
-A complete ``~/.config/mopidy/settings.py`` may look as simple as this::
+A complete ``~/.config/mopidy/mopidy.conf`` may look as simple as this:
- MPD_SERVER_HOSTNAME = u'::'
- SPOTIFY_USERNAME = u'alice'
- SPOTIFY_PASSWORD = u'mysecret'
+.. code-block:: ini
+
+ [mpd]
+ hostname = ::
+
+ [spotify]
+ username = alice
+ password = mysecret
.. _music-from-spotify:
@@ -35,10 +41,16 @@ Music from Spotify
==================
If you are using the Spotify backend, which is the default, enter your Spotify
-Premium account's username and password into the file, like this::
+Premium account's username and password into the file, like this:
- SPOTIFY_USERNAME = u'myusername'
- SPOTIFY_PASSWORD = u'mysecret'
+.. code-block:: ini
+
+ [spotify]
+ username = myusername
+ password = mysecret
+
+This will only work if you have the Spotify Premium subscription. Spotify
+Unlimited will not work.
.. _music-from-local-storage:
@@ -48,9 +60,8 @@ Music from local storage
If you want use Mopidy to play music you have locally at your machine instead
of or in addition to using Spotify, you need to review and maybe change some of
-the ``LOCAL_*`` settings. See :mod:`mopidy.settings`, for a full list of
-available settings. Then you need to generate a tag cache for your local
-music...
+the local backend config values. See :ref:`local-backend`, for a complete list.
+Then you need to generate a tag cache for your local music...
.. _generating-a-tag-cache:
@@ -58,28 +69,26 @@ music...
Generating a tag cache
----------------------
-Before Mopidy 0.3 the local storage backend relied purely on ``tag_cache``
-files generated by the original MPD server. To remedy this the command
-:command:`mopidy-scan` was created. The program will scan your current
-:attr:`mopidy.settings.LOCAL_MUSIC_PATH` and build a MPD compatible
-``tag_cache``.
+The program :command:`mopidy-scan` will scan the path set in the
+:confval:`local/media_dir` config value for any media files and build a MPD
+compatible ``tag_cache``.
To make a ``tag_cache`` of your local music available for Mopidy:
-#. Ensure that :attr:`mopidy.settings.LOCAL_MUSIC_PATH` points to where your
+#. Ensure that the :confval:`local/media_dir` config value points to where your
music is located. Check the current setting by running::
- mopidy --list-settings
+ mopidy --show-config
-#. Scan your music library. The command outputs the ``tag_cache`` to
- ``stdout``, which means that you will need to redirect the output to a file
- yourself::
+#. Scan your media library. The command outputs the ``tag_cache`` to
+ standard output, which means that you will need to redirect the output to a
+ file yourself::
mopidy-scan > tag_cache
#. Move the ``tag_cache`` file to the location
- :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` is set to, or change the
- setting to point to where your ``tag_cache`` file is.
+ set in the :confval:`local/tag_cache_file` config value, or change the
+ config value to point to where your ``tag_cache`` file is.
#. Start Mopidy, find the music library in a client, and play some local music!
@@ -91,14 +100,14 @@ Connecting from other machines on the network
As a secure default, Mopidy only accepts connections from ``localhost``. If you
want to open it for connections from other machines on your network, see
-the documentation for :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`.
+the documentation for the :confval:`mpd/hostname` config value.
If you open up Mopidy for your local network, you should consider turning on
-MPD password authentication by setting
-:attr:`mopidy.settings.MPD_SERVER_PASSWORD` to the password you want to use.
-If the password is set, Mopidy will require MPD clients to provide the password
-before they can do anything else. Mopidy only supports a single password, and
-do not support different permission schemes like the original MPD server.
+MPD password authentication by setting the :confval:`mpd/password` config value
+to the password you want to use. If the password is set, Mopidy will require
+MPD clients to provide the password before they can do anything else. Mopidy
+only supports a single password, and do not support different permission
+schemes like the original MPD server.
Scrobbling tracks to Last.fm
@@ -107,10 +116,13 @@ Scrobbling tracks to Last.fm
If you want to submit the tracks you are playing to your `Last.fm
`_ profile, make sure you've installed the dependencies
found at :mod:`mopidy.frontends.scrobbler` and add the following to your
-settings file::
+settings file:
- LASTFM_USERNAME = u'myusername'
- LASTFM_PASSWORD = u'mysecret'
+.. code-block:: ini
+
+ [scrobbler]
+ username = myusername
+ password = mysecret
.. _install-desktop-file:
@@ -137,7 +149,7 @@ in the Ubuntu Sound Menu, and may be restarted by selecting it there.
The Ubuntu Sound Menu interacts with Mopidy's MPRIS frontend,
:mod:`mopidy.frontends.mpris`. The MPRIS frontend supports the minimum
requirements of the `MPRIS specification `_. The
-``TrackList`` and the ``Playlists`` interfaces of the spec are not supported.
+``TrackList`` interface of the spec is not supported.
Using a custom audio sink
@@ -161,13 +173,16 @@ sound from Mopidy either, as Mopidy by default uses GStreamer's
against Mopidy.
If you for some reason want to use some other GStreamer audio sink than
-``autoaudiosink``, you can set the setting :attr:`mopidy.settings.OUTPUT` to a
+``autoaudiosink``, you can set the :confval:`audio/output` config value to a
partial GStreamer pipeline description describing the GStreamer sink you want
to use.
-Example of ``settings.py`` for using OSS4::
+Example ``mopidy.conf`` for using OSS4:
- OUTPUT = u'oss4sink'
+.. code-block:: ini
+
+ [audio]
+ output = oss4sink
Again, this is the equivalent of the following ``gst-inspect`` command, so make
this work first::
@@ -186,38 +201,40 @@ 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.
-#. Set :attr:`mopidy.settings.OUTPUT` to ``lame ! shout2send``. An Ogg Vorbis
- encoder could be used instead of the lame MP3 encoder.
+#. Set the :confval:`audio/output` config value to ``lame ! shout2send``. An
+ Ogg Vorbis encoder could be used instead of the lame MP3 encoder.
#. You might also need to change the ``shout2send`` default settings, run
``gst-inspect-0.10 shout2send`` to see the available settings. Most likely
you want to change ``ip``, ``username``, ``password``, and ``mount``. For
example, to set the username and password, use:
- ``lame ! shout2send username="foobar" password="s3cret"``.
+
+ .. code-block:: ini
+
+ [audio]
+ output = lame ! shout2send username="alice" password="secret"
Other advanced setups are also possible for outputs. Basically, anything you
can use with the ``gst-launch-0.10`` command can be plugged into
-:attr:`mopidy.settings.OUTPUT`.
+:confval:`audio/output`.
-Custom settings
-===============
+Custom configuration values
+===========================
-Mopidy's settings validator will stop you from defining any settings in your
-settings file that Mopidy doesn't know about. This may sound obnoxious, but it
-helps you detect typos in your settings, and deprecated settings that should be
-removed or updated.
+Mopidy's settings validator will stop you from defining any config values in
+your settings file that Mopidy doesn't know about. This may sound obnoxious,
+but it helps us detect typos in your settings, and deprecated settings that
+should be removed or updated.
-If you're extending Mopidy in some way, and want to use Mopidy's settings
-system, you can prefix your settings with ``CUSTOM_`` to get around the
-settings validator. We recommend that you choose names like
-``CUSTOM_MYAPP_MYSETTING`` so that multiple custom extensions to Mopidy can be
-used at the same time without any danger of naming collisions.
+If you're extending Mopidy, and want to use Mopidy's configuration
+system, you can add new sections to the config without triggering the config
+validator. We recommend that you choose a good and unique name for the config
+section so that multiple extensions to Mopidy can be used at the same time
+without any danger of naming collisions.
Available settings
==================
-.. automodule:: mopidy.settings
- :synopsis: Available settings and their default values
- :members:
+.. note:: TODO: Document config values of the new config system
diff --git a/docs/index.rst b/docs/index.rst
index 54745298..aae7e675 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -42,7 +42,7 @@ User documentation
installation/index
installation/raspberrypi
- settings
+ config
running
clients/index
authors
diff --git a/docs/installation/index.rst b/docs/installation/index.rst
index ab81b753..2eade257 100644
--- a/docs/installation/index.rst
+++ b/docs/installation/index.rst
@@ -42,7 +42,7 @@ in the same way as you get updates to the rest of your distribution.
sudo apt-get update
sudo apt-get install mopidy
-#. Finally, you need to set a couple of :doc:`settings `, and then
+#. Finally, you need to set a couple of :doc:`config values `, and then
you're ready to :doc:`run Mopidy `.
When a new release of Mopidy is out, and you can't wait for you system to
@@ -89,8 +89,8 @@ package found in AUR.
install `python2-pylast
`_ from AUR.
-#. Finally, you need to set a couple of :doc:`settings `, and then
- you're ready to :doc:`run Mopidy `.
+#. Finally, you need to set a couple of :doc:`config values `, and
+ then you're ready to :doc:`run Mopidy `.
OS X: Install from Homebrew and Pip
@@ -140,8 +140,8 @@ Pip.
sudo pip install -U pyspotify pylast mopidy
-#. Finally, you need to set a couple of :doc:`settings `, and then
- you're ready to :doc:`run Mopidy `.
+#. Finally, you need to set a couple of :doc:`config values `, and
+ then you're ready to :doc:`run Mopidy `.
Otherwise: Install from source using Pip
@@ -264,5 +264,5 @@ can install Mopidy from PyPI using Pip.
sudo pip install mopidy==dev
-#. Finally, you need to set a couple of :doc:`settings `, and then
- you're ready to :doc:`run Mopidy `.
+#. Finally, you need to set a couple of :doc:`config values `, and
+ then you're ready to :doc:`run Mopidy `.
diff --git a/docs/modules/frontends/scrobbler.rst b/docs/modules/frontends/scrobbler.rst
index eee65724..2af9fcff 100644
--- a/docs/modules/frontends/scrobbler.rst
+++ b/docs/modules/frontends/scrobbler.rst
@@ -1,6 +1,6 @@
-**********************************************
-:mod:`mopidy.frontends.scrobble` -- Scrobbler
-**********************************************
+***********************************************
+:mod:`mopidy.frontends.scrobbler` -- Scrobbler
+***********************************************
.. automodule:: mopidy.frontends.scrobbler
:synopsis: Music scrobbler frontend
diff --git a/mopidy/__init__.py b/mopidy/__init__.py
index 416f4fbf..fc2b611b 100644
--- a/mopidy/__init__.py
+++ b/mopidy/__init__.py
@@ -24,8 +24,3 @@ warnings.filterwarnings('ignore', 'could not open display')
__version__ = '0.13.0'
-
-
-from mopidy import settings as default_settings_module
-from mopidy.utils.settings import SettingsProxy
-settings = SettingsProxy(default_settings_module)
diff --git a/mopidy/__main__.py b/mopidy/__main__.py
index 3783d150..7e1677ef 100644
--- a/mopidy/__main__.py
+++ b/mopidy/__main__.py
@@ -28,7 +28,7 @@ sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
-from mopidy import exceptions, settings
+from mopidy import exceptions
from mopidy.audio import Audio
from mopidy.config import default_config, config_schemas
from mopidy.core import Core
@@ -48,8 +48,10 @@ def main():
config_files = options.config.split(':')
config_overrides = options.overrides
+ extensions = [] # Make sure it is defined before the finally block
+
try:
- extensions = [] # Make sure it is defined before the finally block
+ create_file_structures()
logging_config = load_config(config_files, config_overrides)
log.setup_logging(
logging_config, options.verbosity_level, options.save_debug_log)
@@ -58,8 +60,7 @@ def main():
extensions = filter_enabled_extensions(raw_config, extensions)
config = validate_config(raw_config, config_schemas, extensions)
log.setup_log_levels(config)
- check_old_folders()
- setup_settings()
+ check_old_locations()
# Anything that wants to exit after this point must use
# mopidy.utils.process.exit_process as actors have been started.
@@ -68,8 +69,6 @@ def main():
core = setup_core(audio, backends)
setup_frontends(config, extensions, core)
loop.run()
- except exceptions.SettingsError as ex:
- logger.error(ex.message)
except KeyboardInterrupt:
logger.info('Interrupted. Exiting...')
except Exception as ex:
@@ -167,17 +166,20 @@ def show_config_callback(option, opt, value, parser):
sys.exit(0)
-def check_old_folders():
- # TODO: add old settings and pre extension storage locations?
- old_settings_folder = os.path.expanduser('~/.mopidy')
+def check_old_locations():
+ dot_mopidy_dir = path.expand_path('~/.mopidy')
+ if os.path.isdir(dot_mopidy_dir):
+ logger.warning(
+ 'Old Mopidy dot dir found at %s. Please migrate your config to '
+ 'the ini-file based config format. See release notes for further '
+ 'instructions.', dot_mopidy_dir)
- if not os.path.isdir(old_settings_folder):
- return
-
- logger.warning(
- 'Old settings folder found at %s, settings.py should be moved '
- 'to %s, any cache data should be deleted. See release notes for '
- 'further instructions.', old_settings_folder, path.SETTINGS_PATH)
+ old_settings_file = path.expand_path('$XDG_CONFIG_DIR/mopidy/settings.py')
+ if os.path.isfile(old_settings_file):
+ logger.warning(
+ 'Old Mopidy settings file found at %s. Please migrate your '
+ 'config to the ini-file based config format. See release notes '
+ 'for further instructions.', old_settings_file)
def load_extensions():
@@ -234,8 +236,10 @@ def filter_enabled_extensions(raw_config, extensions):
else:
disabled_names.append(extension.ext_name)
- logging.info('Enabled extensions: %s', ', '.join(enabled_names))
- logging.info('Disabled extensions: %s', ', '.join(disabled_names))
+ logging.info(
+ 'Enabled extensions: %s', ', '.join(enabled_names) or 'none')
+ logging.info(
+ 'Disabled extensions: %s', ', '.join(disabled_names) or 'none')
return enabled_extensions
@@ -306,15 +310,9 @@ def validate_config(raw_config, schemas, extensions=None):
return config
-def setup_settings():
- path.get_or_create_folder(path.SETTINGS_PATH)
- path.get_or_create_folder(path.DATA_PATH)
- path.get_or_create_file(path.SETTINGS_FILE)
- try:
- settings.validate()
- except exceptions.SettingsError as ex:
- logger.error(ex.message)
- sys.exit(1)
+def create_file_structures():
+ path.get_or_create_dir('$XDG_DATA_DIR/mopidy')
+ path.get_or_create_file('$XDG_CONFIG_DIR/mopidy/mopidy.conf')
def setup_audio(config):
@@ -328,12 +326,19 @@ def stop_audio():
def setup_backends(config, extensions, audio):
- logger.info('Starting Mopidy backends')
- backends = []
+ backend_classes = []
for extension in extensions:
- for backend_class in extension.get_backend_classes():
- backend = backend_class.start(config=config, audio=audio).proxy()
- backends.append(backend)
+ backend_classes.extend(extension.get_backend_classes())
+
+ logger.info(
+ 'Starting Mopidy backends: %s',
+ ', '.join(b.__name__ for b in backend_classes) or 'none')
+
+ backends = []
+ for backend_class in backend_classes:
+ backend = backend_class.start(config=config, audio=audio).proxy()
+ backends.append(backend)
+
return backends
@@ -355,10 +360,16 @@ def stop_core():
def setup_frontends(config, extensions, core):
- logger.info('Starting Mopidy frontends')
+ frontend_classes = []
for extension in extensions:
- for frontend_class in extension.get_frontend_classes():
- frontend_class.start(config=config, core=core)
+ frontend_classes.extend(extension.get_frontend_classes())
+
+ logger.info(
+ 'Starting Mopidy frontends: %s',
+ ', '.join(f.__name__ for f in frontend_classes) or 'none')
+
+ for frontend_class in frontend_classes:
+ frontend_class.start(config=config, core=core)
def stop_frontends(extensions):
diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py
index 42dee084..5d92f3c4 100644
--- a/mopidy/audio/actor.py
+++ b/mopidy/audio/actor.py
@@ -9,7 +9,6 @@ import logging
import pykka
-from mopidy import settings
from mopidy.utils import process
from . import mixers, utils
@@ -28,11 +27,14 @@ class Audio(pykka.ThreadingActor):
"""
Audio output through `GStreamer `_.
- **Settings:**
+ **Default config:**
- - :attr:`mopidy.settings.OUTPUT`
- - :attr:`mopidy.settings.MIXER`
- - :attr:`mopidy.settings.MIXER_TRACK`
+ .. code-block:: ini
+
+ [audio]
+ mixer = autoaudiomixer
+ mixer_track =
+ output = autoaudiosink
"""
#: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState`
@@ -41,6 +43,8 @@ class Audio(pykka.ThreadingActor):
def __init__(self, config):
super(Audio, self).__init__()
+ self._config = config
+
self._playbin = None
self._signal_ids = {} # {(element, event): signal_id}
@@ -143,47 +147,51 @@ class Audio(pykka.ThreadingActor):
self._playbin.set_state(gst.STATE_NULL)
def _setup_output(self):
+ output_desc = self._config['audio']['output']
try:
output = gst.parse_bin_from_description(
- settings.OUTPUT, ghost_unconnected_pads=True)
+ output_desc, ghost_unconnected_pads=True)
self._playbin.set_property('audio-sink', output)
- logger.info('Audio output set to "%s"', settings.OUTPUT)
+ logger.info('Audio output set to "%s"', output_desc)
except gobject.GError as ex:
logger.error(
- 'Failed to create audio output "%s": %s', settings.OUTPUT, ex)
+ 'Failed to create audio output "%s": %s', output_desc, ex)
process.exit_process()
def _setup_mixer(self):
- if not settings.MIXER:
+ mixer_desc = self._config['audio']['mixer']
+ track_desc = self._config['audio']['mixer_track']
+
+ if mixer_desc is None:
logger.info('Not setting up audio mixer')
return
- if settings.MIXER == 'software':
+ if mixer_desc == 'software':
self._software_mixing = True
logger.info('Audio mixer is using software mixing')
return
try:
mixerbin = gst.parse_bin_from_description(
- settings.MIXER, ghost_unconnected_pads=False)
+ mixer_desc, ghost_unconnected_pads=False)
except gobject.GError as ex:
logger.warning(
- 'Failed to create audio mixer "%s": %s', settings.MIXER, ex)
+ 'Failed to create audio mixer "%s": %s', mixer_desc, ex)
return
# We assume that the bin will contain a single mixer.
mixer = mixerbin.get_by_interface(b'GstMixer')
if not mixer:
logger.warning(
- 'Did not find any audio mixers in "%s"', settings.MIXER)
+ 'Did not find any audio mixers in "%s"', mixer_desc)
return
if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS:
logger.warning(
- 'Setting audio mixer "%s" to READY failed', settings.MIXER)
+ 'Setting audio mixer "%s" to READY failed', mixer_desc)
return
- track = self._select_mixer_track(mixer, settings.MIXER_TRACK)
+ track = self._select_mixer_track(mixer, track_desc)
if not track:
logger.warning('Could not find usable audio mixer track')
return
@@ -198,8 +206,9 @@ class Audio(pykka.ThreadingActor):
def _select_mixer_track(self, mixer, track_label):
# Ignore tracks without volumes, then look for track with
- # label == settings.MIXER_TRACK, otherwise fallback to first usable
- # track hoping the mixer gave them to us in a sensible order.
+ # label equal to the audio/mixer_track config value, otherwise fallback
+ # to first usable track hoping the mixer gave them to us in a sensible
+ # order.
usable_tracks = []
for track in mixer.list_tracks():
diff --git a/mopidy/audio/mixers/auto.py b/mopidy/audio/mixers/auto.py
index 96359da1..f1dde3f9 100644
--- a/mopidy/audio/mixers/auto.py
+++ b/mopidy/audio/mixers/auto.py
@@ -6,10 +6,10 @@ This is Mopidy's default mixer.
None
-**Settings**
+**Configuration**
-If this wasn't the default, you would set :attr:`mopidy.settings.MIXER` to
-``autoaudiomixer`` to use this mixer.
+If this wasn't the default, you would set the :confval:`audio/mixer` config
+value to ``autoaudiomixer`` to use this mixer.
"""
from __future__ import unicode_literals
diff --git a/mopidy/audio/mixers/fake.py b/mopidy/audio/mixers/fake.py
index 738491b5..98afca2a 100644
--- a/mopidy/audio/mixers/fake.py
+++ b/mopidy/audio/mixers/fake.py
@@ -4,9 +4,10 @@
None
-**Settings**
+**Configuration**
-Set :attr:`mopidy.settings.MIXER` to ``fakemixer`` to use this mixer.
+Set the :confval:`audio/mixer:` config value to ``fakemixer`` to use this
+mixer.
"""
from __future__ import unicode_literals
diff --git a/mopidy/audio/mixers/nad.py b/mopidy/audio/mixers/nad.py
index 8481de55..9259d291 100644
--- a/mopidy/audio/mixers/nad.py
+++ b/mopidy/audio/mixers/nad.py
@@ -7,10 +7,11 @@ serial cable.
.. literalinclude:: ../../../../requirements/external_mixers.txt
-**Settings**
+**Configuration**
-Set :attr:`mopidy.settings.MIXER` to ``nadmixer`` to use it. You probably also
-needs to add some properties to the ``MIXER`` setting.
+Set the :confval:`audio/mixer` config value to ``nadmixer`` to use it. You
+probably also needs to add some properties to the :confval:`audio/mixer` config
+value.
Supported properties includes:
@@ -34,15 +35,13 @@ Supported properties includes:
Configuration examples::
# Minimum configuration, if the amplifier is available at /dev/ttyUSB0
- MIXER = u'nadmixer'
+ mixer = nadmixer
# Minimum configuration, if the amplifier is available elsewhere
- MIXER = u'nadmixer port=/dev/ttyUSB3'
+ mixer = nadmixer port=/dev/ttyUSB3
# Full configuration
- MIXER = (
- u'nadmixer port=/dev/ttyUSB0 '
- u'source=aux speakers-a=on speakers-b=off')
+ mixer = nadmixer port=/dev/ttyUSB0 source=aux speakers-a=on speakers-b=off
"""
from __future__ import unicode_literals
@@ -132,7 +131,7 @@ class NadTalker(pykka.ThreadingActor):
calibrating the NAD amplifier's volume.
"""
- # Serial link settings
+ # Serial link config
BAUDRATE = 115200
BYTESIZE = 8
PARITY = 'N'
diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py
index 17fd659e..0367cf15 100644
--- a/mopidy/backends/local/__init__.py
+++ b/mopidy/backends/local/__init__.py
@@ -7,18 +7,10 @@ from mopidy.utils import config, formatting
default_config = """
[local]
-
-# If the local extension should be enabled or not
enabled = true
-
-# Path to folder with local music
-music_path = $XDG_MUSIC_DIR
-
-# Path to playlist folder with m3u files for local music
-playlist_path = $XDG_DATA_DIR/mopidy/playlists
-
-# Path to tag cache for local music
-tag_cache_file = $XDG_DATA_DIR/mopidy/tag_cache
+media_dir = $XDG_MUSIC_DIR
+playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists
+tag_cache_file = $XDG_DATA_DIR/mopidy/local/tag_cache
"""
__doc__ = """A backend for playing music from a local music archive.
@@ -36,6 +28,24 @@ https://github.com/mopidy/mopidy/issues?labels=Local+backend
None
+**Configuration**
+
+.. confval:: local/enabled
+
+ If the local extension should be enabled or not.
+
+.. confval:: local/media_dir
+
+ Path to directory with local media files.
+
+.. confval:: local/playlists_dir
+
+ Path to playlists directory with m3u files for local media.
+
+.. confval:: local/tag_cache_file
+
+ Path to tag cache for local media.
+
**Default config**
.. code-block:: ini
@@ -55,9 +65,9 @@ class Extension(ext.Extension):
def get_config_schema(self):
schema = config.ExtensionConfigSchema()
- schema['music_path'] = config.String()
- schema['playlist_path'] = config.String()
- schema['tag_cache_file'] = config.String()
+ schema['media_dir'] = config.Path()
+ schema['playlists_dir'] = config.Path()
+ schema['tag_cache_file'] = config.Path()
return schema
def validate_environment(self):
diff --git a/mopidy/backends/local/actor.py b/mopidy/backends/local/actor.py
index abad75ca..1817e133 100644
--- a/mopidy/backends/local/actor.py
+++ b/mopidy/backends/local/actor.py
@@ -5,6 +5,7 @@ import logging
import pykka
from mopidy.backends import base
+from mopidy.utils import encoding, path
from .library import LocalLibraryProvider
from .playlists import LocalPlaylistsProvider
@@ -16,8 +17,34 @@ class LocalBackend(pykka.ThreadingActor, base.Backend):
def __init__(self, config, audio):
super(LocalBackend, self).__init__()
+ self.config = config
+
+ self.create_dirs_and_files()
+
self.library = LocalLibraryProvider(backend=self)
self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
self.playlists = LocalPlaylistsProvider(backend=self)
self.uri_schemes = ['file']
+
+ def create_dirs_and_files(self):
+ try:
+ path.get_or_create_dir(self.config['local']['media_dir'])
+ except EnvironmentError as error:
+ logger.warning(
+ 'Could not create local media dir: %s',
+ encoding.locale_decode(error))
+
+ try:
+ path.get_or_create_dir(self.config['local']['playlists_dir'])
+ except EnvironmentError as error:
+ logger.warning(
+ 'Could not create local playlists dir: %s',
+ encoding.locale_decode(error))
+
+ try:
+ path.get_or_create_file(self.config['local']['tag_cache_file'])
+ except EnvironmentError as error:
+ logger.warning(
+ 'Could not create empty tag cache file: %s',
+ encoding.locale_decode(error))
diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py
index f2a1a520..669e72d7 100644
--- a/mopidy/backends/local/library.py
+++ b/mopidy/backends/local/library.py
@@ -1,8 +1,6 @@
from __future__ import unicode_literals
import logging
-
-from mopidy import settings
from mopidy.backends import base
from mopidy.models import Album, SearchResult
@@ -15,19 +13,24 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
def __init__(self, *args, **kwargs):
super(LocalLibraryProvider, self).__init__(*args, **kwargs)
self._uri_mapping = {}
+ self._media_dir = self.backend.config['local']['media_dir']
+ self._tag_cache_file = self.backend.config['local']['tag_cache_file']
self.refresh()
def refresh(self, uri=None):
- tracks = parse_mpd_tag_cache(
- settings.LOCAL_TAG_CACHE_FILE, settings.LOCAL_MUSIC_PATH)
+ logger.debug(
+ 'Loading local tracks from %s using %s',
+ self._media_dir, self._tag_cache_file)
- logger.info(
- 'Loading tracks from %s using %s',
- settings.LOCAL_MUSIC_PATH, settings.LOCAL_TAG_CACHE_FILE)
+ tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir)
for track in tracks:
self._uri_mapping[track.uri] = track
+ logger.info(
+ 'Loaded %d local tracks from %s using %s',
+ len(tracks), self._media_dir, self._tag_cache_file)
+
def lookup(self, uri):
try:
return [self._uri_mapping[uri]]
diff --git a/mopidy/backends/local/playlists.py b/mopidy/backends/local/playlists.py
index 53f7aaae..cd370eaa 100644
--- a/mopidy/backends/local/playlists.py
+++ b/mopidy/backends/local/playlists.py
@@ -5,7 +5,6 @@ import logging
import os
import shutil
-from mopidy import settings
from mopidy.backends import base, listener
from mopidy.models import Playlist
from mopidy.utils import formatting, path
@@ -19,7 +18,8 @@ logger = logging.getLogger('mopidy.backends.local')
class LocalPlaylistsProvider(base.BasePlaylistsProvider):
def __init__(self, *args, **kwargs):
super(LocalPlaylistsProvider, self).__init__(*args, **kwargs)
- self._path = settings.LOCAL_PLAYLIST_PATH
+ self._media_dir = self.backend.config['local']['media_dir']
+ self._playlists_dir = self.backend.config['local']['playlists_dir']
self.refresh()
def create(self, name):
@@ -42,16 +42,14 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
return playlist
def refresh(self):
- logger.info('Loading playlists from %s', self._path)
-
playlists = []
- for m3u in glob.glob(os.path.join(self._path, '*.m3u')):
+ for m3u in glob.glob(os.path.join(self._playlists_dir, '*.m3u')):
uri = path.path_to_uri(m3u)
name = os.path.splitext(os.path.basename(m3u))[0]
tracks = []
- for track_uri in parse_m3u(m3u, settings.LOCAL_MUSIC_PATH):
+ for track_uri in parse_m3u(m3u, self._media_dir):
try:
# TODO We must use core.library.lookup() to support tracks
# from other backends
@@ -65,6 +63,10 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
self.playlists = playlists
listener.BackendListener.send('playlists_loaded')
+ logger.info(
+ 'Loaded %d local playlists from %s',
+ len(playlists), self._playlists_dir)
+
def save(self, playlist):
assert playlist.uri, 'Cannot save playlist without URI'
@@ -86,13 +88,13 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
def _get_m3u_path(self, name):
name = formatting.slugify(name)
- file_path = os.path.join(self._path, name + '.m3u')
- path.check_file_path_is_inside_base_dir(file_path, self._path)
+ file_path = os.path.join(self._playlists_dir, name + '.m3u')
+ path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
return file_path
def _save_m3u(self, playlist):
file_path = path.uri_to_path(playlist.uri)
- path.check_file_path_is_inside_base_dir(file_path, self._path)
+ path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
with open(file_path, 'w') as file_handle:
for track in playlist.tracks:
if track.uri.startswith('file://'):
@@ -103,16 +105,18 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
def _delete_m3u(self, uri):
file_path = path.uri_to_path(uri)
- path.check_file_path_is_inside_base_dir(file_path, self._path)
+ path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir)
if os.path.exists(file_path):
os.remove(file_path)
def _rename_m3u(self, playlist):
src_file_path = path.uri_to_path(playlist.uri)
- path.check_file_path_is_inside_base_dir(src_file_path, self._path)
+ path.check_file_path_is_inside_base_dir(
+ src_file_path, self._playlists_dir)
dst_file_path = self._get_m3u_path(playlist.name)
- path.check_file_path_is_inside_base_dir(dst_file_path, self._path)
+ path.check_file_path_is_inside_base_dir(
+ dst_file_path, self._playlists_dir)
shutil.move(src_file_path, dst_file_path)
diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py
index a36be927..683ad6b4 100644
--- a/mopidy/backends/local/translator.py
+++ b/mopidy/backends/local/translator.py
@@ -10,7 +10,7 @@ from mopidy.utils.path import path_to_uri
logger = logging.getLogger('mopidy.backends.local')
-def parse_m3u(file_path, music_folder):
+def parse_m3u(file_path, media_dir):
r"""
Convert M3U file list of uris
@@ -49,7 +49,7 @@ def parse_m3u(file_path, music_folder):
if line.startswith('file://'):
uris.append(line)
else:
- path = path_to_uri(music_folder, line)
+ path = path_to_uri(media_dir, line)
uris.append(path)
return uris
diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py
index 8ea0ca11..c26a42e7 100644
--- a/mopidy/backends/spotify/__init__.py
+++ b/mopidy/backends/spotify/__init__.py
@@ -8,24 +8,12 @@ from mopidy.utils import config, formatting
default_config = """
[spotify]
-
-# If the Spotify extension should be enabled or not
enabled = true
-
-# Your Spotify Premium username
username =
-
-# Your Spotify Premium password
password =
-
-# The preferred audio bitrate. Valid values are 96, 160, 320
bitrate = 160
-
-# Max number of seconds to wait for Spotify operations to complete
timeout = 10
-
-# Path to the Spotify data cache. Cannot be shared with other Spotify apps
-cache_path = $XDG_CACHE_DIR/mopidy/spotify
+cache_dir = $XDG_CACHE_DIR/mopidy/spotify
"""
__doc__ = """A backend for playing music from Spotify
@@ -52,6 +40,32 @@ https://github.com/mopidy/mopidy/issues?labels=Spotify+backend
.. literalinclude:: ../../../requirements/spotify.txt
+**Configuration**
+
+.. confval:: spotify/enabled
+
+ If the Spotify extension should be enabled or not.
+
+.. confval:: spotify/username
+
+ Your Spotify Premium username.
+
+.. confval:: spotify/password
+
+ Your Spotify Premium password.
+
+.. confval:: spotify/bitrate
+
+ The preferred audio bitrate. Valid values are 96, 160, 320.
+
+.. confval:: spotify/timeout
+
+ Max number of seconds to wait for Spotify operations to complete.
+
+.. confval:: spotify/cache_dir
+
+ Path to the Spotify data cache. Cannot be shared with other Spotify apps.
+
**Default config**
.. code-block:: ini
@@ -75,7 +89,7 @@ class Extension(ext.Extension):
schema['password'] = config.String(secret=True)
schema['bitrate'] = config.Integer(choices=(96, 160, 320))
schema['timeout'] = config.Integer(minimum=0)
- schema['cache_path'] = config.String()
+ schema['cache_dir'] = config.Path()
return schema
def validate_environment(self):
diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py
index 22ad4632..c0592ea7 100644
--- a/mopidy/backends/spotify/session_manager.py
+++ b/mopidy/backends/spotify/session_manager.py
@@ -30,8 +30,8 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
def __init__(self, config, audio, backend_ref):
- self.cache_location = config['spotify']['cache_path']
- self.settings_location = config['spotify']['cache_path']
+ self.cache_location = config['spotify']['cache_dir']
+ self.settings_location = config['spotify']['cache_dir']
PyspotifySessionManager.__init__(
self, config['spotify']['username'], config['spotify']['password'],
@@ -182,7 +182,7 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
bitrate=self.bitrate, username=self.username))
playlists = filter(None, playlists)
self.backend.playlists.playlists = playlists
- logger.info('Loaded %d Spotify playlist(s)', len(playlists))
+ logger.info('Loaded %d Spotify playlists', len(playlists))
BackendListener.send('playlists_loaded')
def logout(self):
diff --git a/mopidy/backends/stream/__init__.py b/mopidy/backends/stream/__init__.py
index 9a393bed..11918500 100644
--- a/mopidy/backends/stream/__init__.py
+++ b/mopidy/backends/stream/__init__.py
@@ -7,11 +7,7 @@ from mopidy.utils import config, formatting
default_config = """
[stream]
-
-# If the stream extension should be enabled or not
enabled = true
-
-# Whitelist of URI schemas to support streaming from
protocols =
http
https
@@ -21,11 +17,12 @@ protocols =
rtsp
"""
-__doc__ = """A backend for playing music for streaming music.
+__doc__ = """
+A backend for playing music for streaming music.
-This backend will handle streaming of URIs in
-:attr:`mopidy.settings.STREAM_PROTOCOLS` assuming the right plugins are
-installed.
+This backend will handle streaming of URIs matching the
+:confval:`stream/protocols` config value, assuming the needed GStreamer plugins
+are installed.
**Issues**
@@ -35,6 +32,16 @@ https://github.com/mopidy/mopidy/issues?labels=Stream+backend
None
+**Configuration**
+
+.. confval:: stream/enabled
+
+ If the stream extension should be enabled or not.
+
+.. confval:: stream/protocols
+
+ Whitelist of URI schemas to allow streaming from.
+
**Default config**
.. code-block:: ini
diff --git a/mopidy/backends/stream/actor.py b/mopidy/backends/stream/actor.py
index d6eb31d3..86df447d 100644
--- a/mopidy/backends/stream/actor.py
+++ b/mopidy/backends/stream/actor.py
@@ -5,7 +5,7 @@ import urlparse
import pykka
-from mopidy import audio as audio_lib, settings
+from mopidy import audio as audio_lib
from mopidy.backends import base
from mopidy.models import Track
@@ -21,7 +21,7 @@ class StreamBackend(pykka.ThreadingActor, base.Backend):
self.playlists = None
self.uri_schemes = audio_lib.supported_uri_schemes(
- settings.STREAM_PROTOCOLS)
+ config['stream']['protocols'])
# TODO: Should we consider letting lookup know how to expand common playlist
diff --git a/mopidy/config.py b/mopidy/config.py
index 3d242a95..88fc3419 100644
--- a/mopidy/config.py
+++ b/mopidy/config.py
@@ -27,7 +27,7 @@ config_schemas = {} # TODO: use ordered dict?
config_schemas['logging'] = config.ConfigSchema()
config_schemas['logging']['console_format'] = config.String()
config_schemas['logging']['debug_format'] = config.String()
-config_schemas['logging']['debug_file'] = config.String()
+config_schemas['logging']['debug_file'] = config.Path()
config_schemas['logging.levels'] = config.LogLevelConfigSchema()
diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py
index d588a376..32d55f23 100644
--- a/mopidy/frontends/http/__init__.py
+++ b/mopidy/frontends/http/__init__.py
@@ -7,30 +7,9 @@ from mopidy.utils import config, formatting
default_config = """
[http]
-
-# If the HTTP extension should be enabled or not
enabled = true
-
-# Which address the HTTP server should bind to
-#
-# 127.0.0.1
-# Listens only on the IPv4 loopback interface
-# ::1
-# Listens only on the IPv6 loopback interface
-# 0.0.0.0
-# Listens on all IPv4 interfaces
-# ::
-# Listens on all interfaces, both IPv4 and IPv6
hostname = 127.0.0.1
-
-# Which TCP port the HTTP server should listen to
port = 6680
-
-# Which directory the HTTP server should serve at "/"
-#
-# Change this to have Mopidy serve e.g. files for your JavaScript client.
-# "/mopidy" will continue to work as usual even if you change this setting.
-#
static_dir =
[logging.levels]
@@ -49,6 +28,36 @@ https://github.com/mopidy/mopidy/issues?labels=HTTP+frontend
.. literalinclude:: ../../../requirements/http.txt
+**Configuration**
+
+.. confval:: http/enabled
+
+ If the HTTP extension should be enabled or not.
+
+.. confval:: http/hostname
+
+ Which address the HTTP server should bind to.
+
+ ``127.0.0.1``
+ Listens only on the IPv4 loopback interface
+ ``::1``
+ Listens only on the IPv6 loopback interface
+ ``0.0.0.0``
+ Listens on all IPv4 interfaces
+ ``::``
+ Listens on all interfaces, both IPv4 and IPv6
+
+.. confval:: http/port
+
+ Which TCP port the HTTP server should listen to.
+
+.. confval:: http/static_dir
+
+ Which directory the HTTP server should serve at "/"
+
+ Change this to have Mopidy serve e.g. files for your JavaScript client.
+ "/mopidy" will continue to work as usual even if you change this setting.
+
**Default config**
.. code-block:: ini
@@ -61,19 +70,19 @@ Setup
The frontend is enabled by default if all dependencies are available.
-When it is enabled it starts a web server at the port specified by
-:attr:`mopidy.settings.HTTP_SERVER_PORT`.
+When it is enabled it starts a web server at the port specified by the
+:confval:`http/port` config value.
.. warning:: Security
As a simple security measure, the web server is by default only available
- from localhost. To make it available from other computers, change
- :attr:`mopidy.settings.HTTP_SERVER_HOSTNAME`. Before you do so, note that
- the HTTP frontend does not feature any form of user authentication or
- authorization. Anyone able to access the web server can use the full core
- API of Mopidy. Thus, you probably only want to make the web server
- available from your local network or place it behind a web proxy which
- takes care or user authentication. You have been warned.
+ from localhost. To make it available from other computers, change the
+ :confval:`http/hostname` config value. Before you do so, note that the HTTP
+ frontend does not feature any form of user authentication or authorization.
+ Anyone able to access the web server can use the full core API of Mopidy.
+ Thus, you probably only want to make the web server available from your
+ local network or place it behind a web proxy which takes care or user
+ authentication. You have been warned.
Using a web based Mopidy client
@@ -81,10 +90,11 @@ Using a web based Mopidy client
The web server can also host any static files, for example the HTML, CSS,
JavaScript, and images needed for a web based Mopidy client. To host static
-files, change :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point to the
-root directory of your web client, e.g.::
+files, change the ``http/static_dir`` to point to the root directory of your
+web client, e.g.::
- HTTP_SERVER_STATIC_DIR = u'/home/alice/dev/the-client'
+ [http]
+ static_dir = /home/alice/dev/the-client
If the directory includes a file named ``index.html``, it will be served on the
root of Mopidy's web server.
@@ -405,8 +415,8 @@ Example to get started with
2. Create an empty directory for your web client.
-3. Change the setting :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point
- to your new directory.
+3. Change the :confval:`http/static_dir` config value to point to your new
+ directory.
4. Start/restart Mopidy.
@@ -533,7 +543,7 @@ class Extension(ext.Extension):
schema = config.ExtensionConfigSchema()
schema['hostname'] = config.Hostname()
schema['port'] = config.Port()
- schema['static_dir'] = config.String(optional=True)
+ schema['static_dir'] = config.Path(optional=True)
return schema
def validate_environment(self):
diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py
index 54085471..149cbc7f 100644
--- a/mopidy/frontends/http/actor.py
+++ b/mopidy/frontends/http/actor.py
@@ -6,7 +6,7 @@ import os
import pykka
-from mopidy import exceptions, models, settings
+from mopidy import exceptions, models
from mopidy.core import CoreListener
try:
@@ -25,6 +25,7 @@ logger = logging.getLogger('mopidy.frontends.http')
class HttpFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core):
super(HttpFrontend, self).__init__()
+ self.config = config
self.core = core
self._setup_server()
self._setup_websocket_plugin()
@@ -35,8 +36,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
cherrypy.config.update({
'engine.autoreload_on': False,
'server.socket_host': (
- settings.HTTP_SERVER_HOSTNAME.encode('utf-8')),
- 'server.socket_port': settings.HTTP_SERVER_PORT,
+ self.config['http']['hostname'].encode('utf-8')),
+ 'server.socket_port': self.config['http']['port'],
})
def _setup_websocket_plugin(self):
@@ -48,8 +49,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
root.mopidy = MopidyResource()
root.mopidy.ws = ws.WebSocketResource(self.core)
- if settings.HTTP_SERVER_STATIC_DIR:
- static_dir = settings.HTTP_SERVER_STATIC_DIR
+ if self.config['http']['static_dir']:
+ static_dir = self.config['http']['static_dir']
else:
static_dir = os.path.join(os.path.dirname(__file__), 'data')
logger.debug('HTTP server will serve "%s" at /', static_dir)
diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py
index 08bafd26..69297374 100644
--- a/mopidy/frontends/mpd/__init__.py
+++ b/mopidy/frontends/mpd/__init__.py
@@ -7,33 +7,11 @@ from mopidy.utils import config, formatting
default_config = """
[mpd]
-
-# If the MPD extension should be enabled or not
enabled = true
-
-# Which address the MPD server should bind to
-#
-# 127.0.0.1
-# Listens only on the IPv4 loopback interface
-# ::1
-# Listens only on the IPv6 loopback interface
-# 0.0.0.0
-# Listens on all IPv4 interfaces
-# ::
-# Listens on all interfaces, both IPv4 and IPv6
hostname = 127.0.0.1
-
-# Which TCP port the MPD server should listen to
port = 6600
-
-# The password required for connecting to the MPD server
password =
-
-# The maximum number of concurrent connections the MPD server will accept
max_connections = 20
-
-# Number of seconds an MPD client can stay inactive before the connection is
-# closed by the server
connection_timeout = 60
"""
@@ -51,6 +29,43 @@ https://github.com/mopidy/mopidy/issues?labels=MPD+frontend
None
+**Configuration**
+
+.. confval:: mpd/enabled
+
+ If the MPD extension should be enabled or not.
+
+.. confval:: mpd/hostname
+
+ Which address the MPD server should bind to.
+
+ ``127.0.0.1``
+ Listens only on the IPv4 loopback interface
+ ``::1``
+ Listens only on the IPv6 loopback interface
+ ``0.0.0.0``
+ Listens on all IPv4 interfaces
+ ``::``
+ Listens on all interfaces, both IPv4 and IPv6
+
+.. confval:: mpd/port
+
+ Which TCP port the MPD server should listen to.
+
+.. confval:: mpd/password
+
+ The password required for connecting to the MPD server. If blank, no
+ password is required.
+
+.. confval:: mpd/max_connections
+
+ The maximum number of concurrent connections the MPD server will accept.
+
+.. confval:: mpd/connection_timeout
+
+ Number of seconds an MPD client can stay inactive before the connection is
+ closed by the server.
+
**Default config**
.. code-block:: ini
diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py
index e288c24e..45ed753e 100644
--- a/mopidy/frontends/mpd/actor.py
+++ b/mopidy/frontends/mpd/actor.py
@@ -5,7 +5,6 @@ import sys
import pykka
-from mopidy import settings
from mopidy.core import CoreListener
from mopidy.frontends.mpd import session
from mopidy.utils import encoding, network, process
@@ -16,17 +15,21 @@ logger = logging.getLogger('mopidy.frontends.mpd')
class MpdFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core):
super(MpdFrontend, self).__init__()
- hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
- port = settings.MPD_SERVER_PORT
+ hostname = network.format_hostname(config['mpd']['hostname'])
+ port = config['mpd']['port']
# NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5
# See https://github.com/mopidy/mopidy/issues/302 for details.
try:
network.Server(
hostname, port,
- protocol=session.MpdSession, protocol_kwargs={b'core': core},
- max_connections=settings.MPD_SERVER_MAX_CONNECTIONS,
- timeout=settings.MPD_SERVER_CONNECTION_TIMEOUT)
+ protocol=session.MpdSession,
+ protocol_kwargs={
+ b'config': config,
+ b'core': core,
+ },
+ max_connections=config['mpd']['max_connections'],
+ timeout=config['mpd']['connection_timeout'])
except IOError as error:
logger.error(
'MPD server startup failed: %s',
diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py
index 660e1739..e39c140b 100644
--- a/mopidy/frontends/mpd/dispatcher.py
+++ b/mopidy/frontends/mpd/dispatcher.py
@@ -5,7 +5,6 @@ import re
import pykka
-from mopidy import settings
from mopidy.frontends.mpd import exceptions, protocol
logger = logging.getLogger('mopidy.frontends.mpd.dispatcher')
@@ -22,13 +21,15 @@ class MpdDispatcher(object):
_noidle = re.compile(r'^noidle$')
- def __init__(self, session=None, core=None):
+ def __init__(self, session=None, config=None, core=None):
+ self.config = config
self.authenticated = False
self.command_list_receiving = False
self.command_list_ok = False
self.command_list = []
self.command_list_index = None
- self.context = MpdContext(self, session=session, core=core)
+ self.context = MpdContext(
+ self, session=session, config=config, core=core)
def handle_request(self, request, current_command_list_index=None):
"""Dispatch incoming requests to the correct handler."""
@@ -82,7 +83,7 @@ class MpdDispatcher(object):
def _authenticate_filter(self, request, response, filter_chain):
if self.authenticated:
return self._call_next_filter(request, response, filter_chain)
- elif settings.MPD_SERVER_PASSWORD is None:
+ elif self.config['mpd']['password'] is None:
self.authenticated = True
return self._call_next_filter(request, response, filter_chain)
else:
@@ -223,6 +224,9 @@ class MpdContext(object):
#: The current :class:`mopidy.frontends.mpd.MpdSession`.
session = None
+ #: The Mopidy configuration.
+ config = None
+
#: The Mopidy core API. An instance of :class:`mopidy.core.Core`.
core = None
@@ -233,12 +237,12 @@ class MpdContext(object):
subscriptions = None
playlist_uri_from_name = None
-
playlist_name_from_uri = None
- def __init__(self, dispatcher, session=None, core=None):
+ def __init__(self, dispatcher, session=None, config=None, core=None):
self.dispatcher = dispatcher
self.session = session
+ self.config = config
self.core = core
self.events = set()
self.subscriptions = set()
diff --git a/mopidy/frontends/mpd/protocol/connection.py b/mopidy/frontends/mpd/protocol/connection.py
index f7898d21..44696705 100644
--- a/mopidy/frontends/mpd/protocol/connection.py
+++ b/mopidy/frontends/mpd/protocol/connection.py
@@ -1,6 +1,5 @@
from __future__ import unicode_literals
-from mopidy import settings
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import (
MpdPasswordError, MpdPermissionError)
@@ -40,7 +39,7 @@ def password_(context, password):
This is used for authentication with the server. ``PASSWORD`` is
simply the plaintext password.
"""
- if password == settings.MPD_SERVER_PASSWORD:
+ if password == context.config['mpd']['password']:
context.dispatcher.authenticated = True
else:
raise MpdPasswordError('incorrect password', command='password')
diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py
index 8a5deecd..14173308 100644
--- a/mopidy/frontends/mpd/session.py
+++ b/mopidy/frontends/mpd/session.py
@@ -18,9 +18,10 @@ class MpdSession(network.LineProtocol):
encoding = protocol.ENCODING
delimiter = r'\r?\n'
- def __init__(self, connection, core=None):
+ def __init__(self, connection, config=None, core=None):
super(MpdSession, self).__init__(connection)
- self.dispatcher = dispatcher.MpdDispatcher(session=self, core=core)
+ self.dispatcher = dispatcher.MpdDispatcher(
+ session=self, config=config, core=core)
def on_start(self):
logger.info('New MPD connection from [%s]:%s', self.host, self.port)
diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py
index 15ca181d..7cf5b0c6 100644
--- a/mopidy/frontends/mpd/translator.py
+++ b/mopidy/frontends/mpd/translator.py
@@ -5,7 +5,6 @@ import re
import shlex
import urllib
-from mopidy import settings
from mopidy.frontends.mpd import protocol
from mopidy.frontends.mpd.exceptions import MpdArgError
from mopidy.models import TlTrack
@@ -216,12 +215,14 @@ def query_from_mpd_search_format(mpd_query):
return query
-def tracks_to_tag_cache_format(tracks):
+def tracks_to_tag_cache_format(tracks, media_dir):
"""
Format list of tracks for output to MPD tag cache
:param tracks: the tracks
:type tracks: list of :class:`mopidy.models.Track`
+ :param media_dir: the path to the music dir
+ :type media_dir: string
:rtype: list of lists of two-tuples
"""
result = [
@@ -231,14 +232,15 @@ def tracks_to_tag_cache_format(tracks):
('info_end',)
]
tracks.sort(key=lambda t: t.uri)
- _add_to_tag_cache(result, *tracks_to_directory_tree(tracks))
+ dirs, files = tracks_to_directory_tree(tracks, media_dir)
+ _add_to_tag_cache(result, dirs, files, media_dir)
return result
-def _add_to_tag_cache(result, folders, files):
- base_path = settings.LOCAL_MUSIC_PATH.encode('utf-8')
+def _add_to_tag_cache(result, dirs, files, media_dir):
+ base_path = media_dir.encode('utf-8')
- for path, entry in folders.items():
+ for path, (entry_dirs, entry_files) in dirs.items():
try:
text_path = path.decode('utf-8')
except UnicodeDecodeError:
@@ -247,7 +249,7 @@ def _add_to_tag_cache(result, folders, files):
result.append(('directory', text_path))
result.append(('mtime', get_mtime(os.path.join(base_path, path))))
result.append(('begin', name))
- _add_to_tag_cache(result, *entry)
+ _add_to_tag_cache(result, entry_dirs, entry_files, media_dir)
result.append(('end', name))
result.append(('songList begin',))
@@ -273,7 +275,7 @@ def _add_to_tag_cache(result, folders, files):
result.append(('songList end',))
-def tracks_to_directory_tree(tracks):
+def tracks_to_directory_tree(tracks, media_dir):
directories = ({}, [])
for track in tracks:
@@ -282,8 +284,7 @@ def tracks_to_directory_tree(tracks):
absolute_track_dir_path = os.path.dirname(uri_to_path(track.uri))
relative_track_dir_path = re.sub(
- '^' + re.escape(settings.LOCAL_MUSIC_PATH), b'',
- absolute_track_dir_path)
+ '^' + re.escape(media_dir), b'', absolute_track_dir_path)
for part in split_path(relative_track_dir_path):
path = os.path.join(path, part)
diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py
index 79806c47..813e03a2 100644
--- a/mopidy/frontends/mpris/__init__.py
+++ b/mopidy/frontends/mpris/__init__.py
@@ -7,11 +7,7 @@ from mopidy.utils import formatting, config
default_config = """
[mpris]
-
-# If the MPRIS extension should be enabled or not
enabled = true
-
-# Location of the Mopidy .desktop file
desktop_file = /usr/share/applications/mopidy.desktop
"""
@@ -32,9 +28,19 @@ An example of an MPRIS client is the `Ubuntu Sound Menu
Ubuntu Sound Menu. The package is named ``python-indicate`` in
Ubuntu/Debian.
-- An ``.desktop`` file for Mopidy installed at the path set in
- :attr:`mopidy.settings.DESKTOP_FILE`. See :ref:`install-desktop-file` for
- details.
+- An ``.desktop`` file for Mopidy installed at the path set in the
+ :confval:`mpris/desktop_file` config value. See :ref:`install-desktop-file`
+ for details.
+
+**Configuration**
+
+.. confval:: mpris/enabled
+
+ If the MPRIS extension should be enabled or not.
+
+.. confval:: mpris/desktop_file
+
+ Location of the Mopidy ``.desktop`` file.
**Default config**
@@ -79,7 +85,7 @@ class Extension(ext.Extension):
def get_config_schema(self):
schema = config.ExtensionConfigSchema()
- schema['desktop_file'] = config.String()
+ schema['desktop_file'] = config.Path()
return schema
def validate_environment(self):
diff --git a/mopidy/frontends/mpris/actor.py b/mopidy/frontends/mpris/actor.py
index 11f87922..92805bd3 100644
--- a/mopidy/frontends/mpris/actor.py
+++ b/mopidy/frontends/mpris/actor.py
@@ -4,7 +4,6 @@ import logging
import pykka
-from mopidy import settings
from mopidy.core import CoreListener
from mopidy.frontends.mpris import objects
@@ -20,13 +19,14 @@ except ImportError as import_error:
class MprisFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core):
super(MprisFrontend, self).__init__()
+ self.config = config
self.core = core
self.indicate_server = None
self.mpris_object = None
def on_start(self):
try:
- self.mpris_object = objects.MprisObject(self.core)
+ self.mpris_object = objects.MprisObject(self.config, self.core)
self._send_startup_notification()
except Exception as e:
logger.error('MPRIS frontend setup failed (%s)', e)
@@ -53,7 +53,8 @@ class MprisFrontend(pykka.ThreadingActor, CoreListener):
logger.debug('Sending startup notification...')
self.indicate_server = indicate.Server()
self.indicate_server.set_type('music.mopidy')
- self.indicate_server.set_desktop_file(settings.DESKTOP_FILE)
+ self.indicate_server.set_desktop_file(
+ self.config['mpris']['desktop_file'])
self.indicate_server.show()
logger.debug('Startup notification sent')
diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py
index 04a72676..a7f049d2 100644
--- a/mopidy/frontends/mpris/objects.py
+++ b/mopidy/frontends/mpris/objects.py
@@ -13,7 +13,6 @@ except ImportError as import_error:
from mopidy.exceptions import OptionalDependencyError
raise OptionalDependencyError(import_error)
-from mopidy import settings
from mopidy.core import PlaybackState
from mopidy.utils.process import exit_process
@@ -36,7 +35,8 @@ class MprisObject(dbus.service.Object):
properties = None
- def __init__(self, core):
+ def __init__(self, config, core):
+ self.config = config
self.core = core
self.properties = {
ROOT_IFACE: self._get_root_iface_properties(),
@@ -93,7 +93,7 @@ class MprisObject(dbus.service.Object):
mainloop = dbus.mainloop.glib.DBusGMainLoop()
bus_name = dbus.service.BusName(
BUS_NAME, dbus.SessionBus(mainloop=mainloop))
- logger.info('Connected to D-Bus')
+ logger.info('MPRIS server connected to D-Bus')
return bus_name
def get_playlist_id(self, playlist_uri):
@@ -175,7 +175,8 @@ class MprisObject(dbus.service.Object):
### Root interface properties
def get_DesktopEntry(self):
- return os.path.splitext(os.path.basename(settings.DESKTOP_FILE))[0]
+ return os.path.splitext(os.path.basename(
+ self.config['mpris']['desktop_file']))[0]
def get_SupportedUriSchemes(self):
return dbus.Array(self.core.uri_schemes.get(), signature='s')
diff --git a/mopidy/frontends/scrobbler/__init__.py b/mopidy/frontends/scrobbler/__init__.py
index c33a5fa3..f3127040 100644
--- a/mopidy/frontends/scrobbler/__init__.py
+++ b/mopidy/frontends/scrobbler/__init__.py
@@ -7,20 +7,14 @@ from mopidy.utils import config, formatting
default_config = """
[scrobbler]
-
-# If the Last.fm extension should be enabled or not
enabled = true
-
-# Your Last.fm username
username =
-
-# Your Last.fm password
password =
"""
__doc__ = """
-Frontend which scrobbles the music you play to your `Last.fm
-`_ profile.
+Frontend which scrobbles the music you play to your
+`Last.fm `_ profile.
.. note::
@@ -30,6 +24,20 @@ Frontend which scrobbles the music you play to your `Last.fm
.. literalinclude:: ../../../requirements/scrobbler.txt
+**Configuration**
+
+.. confval:: scrobbler/enabled
+
+ If the scrobbler extension should be enabled or not.
+
+.. confval:: scrobbler/username
+
+ Your Last.fm username.
+
+.. confval:: scrobbler/password
+
+ Your Last.fm password.
+
**Default config**
.. code-block:: ini
diff --git a/mopidy/frontends/scrobbler/actor.py b/mopidy/frontends/scrobbler/actor.py
index eea088de..74a11f82 100644
--- a/mopidy/frontends/scrobbler/actor.py
+++ b/mopidy/frontends/scrobbler/actor.py
@@ -5,7 +5,7 @@ import time
import pykka
-from mopidy import exceptions, settings
+from mopidy import exceptions
from mopidy.core import CoreListener
try:
@@ -22,21 +22,17 @@ API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd'
class ScrobblerFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core):
super(ScrobblerFrontend, self).__init__()
+ self.config = config
self.lastfm = None
self.last_start_time = None
def on_start(self):
try:
- username = settings.LASTFM_USERNAME
- password_hash = pylast.md5(settings.LASTFM_PASSWORD)
self.lastfm = pylast.LastFMNetwork(
api_key=API_KEY, api_secret=API_SECRET,
- username=username, password_hash=password_hash)
- logger.info('Connected to Last.fm')
- except exceptions.SettingsError as e:
- logger.info('Last.fm scrobbler not started')
- logger.debug('Last.fm settings error: %s', e)
- self.stop()
+ username=self.config['scrobbler']['username'],
+ password_hash=pylast.md5(self.config['scrobbler']['password']))
+ logger.info('Scrobbler connected to Last.fm')
except (pylast.NetworkError, pylast.MalformedResponseError,
pylast.WSError) as e:
logger.error('Error during Last.fm setup: %s', e)
diff --git a/mopidy/scanner.py b/mopidy/scanner.py
index 4b057774..0c78839b 100644
--- a/mopidy/scanner.py
+++ b/mopidy/scanner.py
@@ -34,7 +34,6 @@ import pygst
pygst.require('0.10')
import gst
-from mopidy import settings
from mopidy.frontends.mpd import translator as mpd_translator
from mopidy.models import Track, Artist, Album
from mopidy.utils import log, path, versioning
@@ -42,6 +41,7 @@ from mopidy.utils import log, path, versioning
def main():
options = parse_options()
+ config = {} # TODO Read config from new config system
log.setup_root_logger()
log.setup_console_logging(options.verbosity_level)
@@ -57,9 +57,9 @@ def main():
logging.warning('Failed %s: %s', uri, error)
logging.debug('Debug info for %s: %s', uri, debug)
- logging.info('Scanning %s', settings.LOCAL_MUSIC_PATH)
+ logging.info('Scanning %s', config['local']['media_dir'])
- scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug)
+ scanner = Scanner(config['local']['media_dir'], store, debug)
try:
scanner.start()
except KeyboardInterrupt:
@@ -67,7 +67,8 @@ def main():
logging.info('Done scanning; writing tag cache...')
- for row in mpd_translator.tracks_to_tag_cache_format(tracks):
+ for row in mpd_translator.tracks_to_tag_cache_format(
+ tracks, config['mpd']['media_dir']):
if len(row) == 1:
print ('%s' % row).encode('utf-8')
else:
@@ -141,9 +142,9 @@ def translator(data):
class Scanner(object):
- def __init__(self, folder, data_callback, error_callback=None):
+ def __init__(self, base_dir, data_callback, error_callback=None):
self.data = {}
- self.files = path.find_files(folder)
+ self.files = path.find_files(base_dir)
self.data_callback = data_callback
self.error_callback = error_callback
self.loop = gobject.MainLoop()
diff --git a/mopidy/settings.py b/mopidy/settings.py
deleted file mode 100644
index cde6430a..00000000
--- a/mopidy/settings.py
+++ /dev/null
@@ -1,285 +0,0 @@
-"""
-All available settings and their default values.
-
-.. warning::
-
- Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a
- file called ``~/.config/mopidy/settings.py`` and redefine settings there.
-"""
-
-from __future__ import unicode_literals
-
-#: The log format used for informational logging.
-#:
-#: See http://docs.python.org/2/library/logging.html#formatter-objects for
-#: details on the format.
-CONSOLE_LOG_FORMAT = '%(levelname)-8s %(message)s'
-
-#: The log format used for debug logging.
-#:
-#: See http://docs.python.org/library/logging.html#formatter-objects for
-#: details on the format.
-DEBUG_LOG_FORMAT = '%(levelname)-8s %(asctime)s' + \
- ' [%(process)d:%(threadName)s] %(name)s\n %(message)s'
-
-#: The file to dump debug log data to when Mopidy is run with the
-#: :option:`--save-debug-log` option.
-#:
-#: Default::
-#:
-#: DEBUG_LOG_FILENAME = u'mopidy.log'
-DEBUG_LOG_FILENAME = 'mopidy.log'
-
-#: Location of the Mopidy .desktop file.
-#:
-#: Used by :mod:`mopidy.frontends.mpris`.
-#:
-#: Default::
-#:
-#: DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
-DESKTOP_FILE = '/usr/share/applications/mopidy.desktop'
-
-#: Which address Mopidy's HTTP server should bind to.
-#:
-#: Used by :mod:`mopidy.frontends.http`.
-#:
-#: Examples:
-#:
-#: ``127.0.0.1``
-#: Listens only on the IPv4 loopback interface. Default.
-#: ``::1``
-#: Listens only on the IPv6 loopback interface.
-#: ``0.0.0.0``
-#: Listens on all IPv4 interfaces.
-#: ``::``
-#: Listens on all interfaces, both IPv4 and IPv6.
-HTTP_SERVER_HOSTNAME = u'127.0.0.1'
-
-#: Which TCP port Mopidy's HTTP server should listen to.
-#:
-#: Used by :mod:`mopidy.frontends.http`.
-#:
-#: Default: 6680
-HTTP_SERVER_PORT = 6680
-
-#: Which directory Mopidy's HTTP server should serve at ``/``.
-#:
-#: Change this to have Mopidy serve e.g. files for your JavaScript client.
-#: ``/mopidy`` will continue to work as usual even if you change this setting.
-#:
-#: Used by :mod:`mopidy.frontends.http`.
-#:
-#: Default: None
-HTTP_SERVER_STATIC_DIR = None
-
-#: Your `Last.fm `_ username.
-#:
-#: Used by :mod:`mopidy.frontends.lastfm`.
-LASTFM_USERNAME = ''
-
-#: Your `Last.fm `_ password.
-#:
-#: Used by :mod:`mopidy.frontends.lastfm`.
-LASTFM_PASSWORD = ''
-
-#: Path to folder with local music.
-#:
-#: Used by :mod:`mopidy.backends.local`.
-#:
-#: Default::
-#:
-#: LOCAL_MUSIC_PATH = u'$XDG_MUSIC_DIR'
-LOCAL_MUSIC_PATH = '$XDG_MUSIC_DIR'
-
-#: Path to playlist folder with m3u files for local music.
-#:
-#: Used by :mod:`mopidy.backends.local`.
-#:
-#: Default::
-#:
-#: LOCAL_PLAYLIST_PATH = u'$XDG_DATA_DIR/mopidy/playlists'
-LOCAL_PLAYLIST_PATH = '$XDG_DATA_DIR/mopidy/playlists'
-
-#: Path to tag cache for local music.
-#:
-#: Used by :mod:`mopidy.backends.local`.
-#:
-#: Default::
-#:
-#: LOCAL_TAG_CACHE_FILE = u'$XDG_DATA_DIR/mopidy/tag_cache'
-LOCAL_TAG_CACHE_FILE = '$XDG_DATA_DIR/mopidy/tag_cache'
-
-#: Audio mixer to use.
-#:
-#: Expects a GStreamer mixer to use, typical values are:
-#: ``alsamixer``, ``pulsemixer``, ``ossmixer``, and ``oss4mixer``.
-#:
-#: Setting this to :class:`None` turns off volume control. ``software``
-#: can be used to force software mixing in the application.
-#:
-#: Default::
-#:
-#: MIXER = u'autoaudiomixer'
-MIXER = 'autoaudiomixer'
-
-#: Audio mixer track to use.
-#:
-#: Name of the mixer track to use. If this is not set we will try to find the
-#: master output track. As an example, using ``alsamixer`` you would
-#: typically set this to ``Master`` or ``PCM``.
-#:
-#: Default::
-#:
-#: MIXER_TRACK = None
-MIXER_TRACK = None
-
-#: Number of seconds an MPD client can stay inactive before the connection is
-#: closed by the server.
-#:
-#: Used by :mod:`mopidy.frontends.mpd`.
-#:
-#: Default::
-#:
-#: MPD_SERVER_CONNECTION_TIMEOUT = 60
-MPD_SERVER_CONNECTION_TIMEOUT = 60
-
-#: Which address Mopidy's MPD server should bind to.
-#:
-#: Used by :mod:`mopidy.frontends.mpd`.
-#:
-#: Examples:
-#:
-#: ``127.0.0.1``
-#: Listens only on the IPv4 loopback interface. Default.
-#: ``::1``
-#: Listens only on the IPv6 loopback interface.
-#: ``0.0.0.0``
-#: Listens on all IPv4 interfaces.
-#: ``::``
-#: Listens on all interfaces, both IPv4 and IPv6.
-MPD_SERVER_HOSTNAME = '127.0.0.1'
-
-#: Which TCP port Mopidy's MPD server should listen to.
-#:
-#: Used by :mod:`mopidy.frontends.mpd`.
-#:
-#: Default: 6600
-MPD_SERVER_PORT = 6600
-
-#: The password required for connecting to the MPD server.
-#:
-#: Used by :mod:`mopidy.frontends.mpd`.
-#:
-#: Default: :class:`None`, which means no password required.
-MPD_SERVER_PASSWORD = None
-
-#: The maximum number of concurrent connections the MPD server will accept.
-#:
-#: Used by :mod:`mopidy.frontends.mpd`.
-#:
-#: Default: 20
-MPD_SERVER_MAX_CONNECTIONS = 20
-
-#: Audio output to use.
-#:
-#: Expects a GStreamer sink. Typical values are ``autoaudiosink``,
-#: ``alsasink``, ``osssink``, ``oss4sink``, ``pulsesink``, and ``shout2send``,
-#: and additional arguments specific to each sink.
-#:
-#: Default::
-#:
-#: OUTPUT = u'autoaudiosink'
-OUTPUT = 'autoaudiosink'
-
-#: Path to the Spotify cache.
-#:
-#: Used by :mod:`mopidy.backends.spotify`.
-#:
-#: Default::
-#:
-#: SPOTIFY_CACHE_PATH = u'$XDG_CACHE_DIR/mopidy/spotify'
-SPOTIFY_CACHE_PATH = '$XDG_CACHE_DIR/mopidy/spotify'
-
-#: Your Spotify Premium username.
-#:
-#: Used by :mod:`mopidy.backends.spotify`.
-SPOTIFY_USERNAME = ''
-
-#: Your Spotify Premium password.
-#:
-#: Used by :mod:`mopidy.backends.spotify`.
-SPOTIFY_PASSWORD = ''
-
-#: Spotify preferred bitrate.
-#:
-#: Available values are 96, 160, and 320.
-#:
-#: Used by :mod:`mopidy.backends.spotify`.
-#:
-#: Default::
-#:
-#: SPOTIFY_BITRATE = 160
-SPOTIFY_BITRATE = 160
-
-#: Spotify proxy host.
-#:
-#: Used by :mod:`mopidy.backends.spotify`.
-#:
-#: Example::
-#:
-#: SPOTIFY_PROXY_HOST = u'protocol://host:port'
-#:
-#: Default::
-#:
-#: SPOTIFY_PROXY_HOST = None
-SPOTIFY_PROXY_HOST = None
-
-#: Spotify proxy username.
-#:
-#: Used by :mod:`mopidy.backends.spotify`.
-#:
-#: Default::
-#:
-#: SPOTIFY_PROXY_USERNAME = None
-SPOTIFY_PROXY_USERNAME = None
-
-#: Spotify proxy password.
-#:
-#: Used by :mod:`mopidy.backends.spotify`.
-#:
-#: Default::
-#:
-#: SPOTIFY_PROXY_PASSWORD = None
-SPOTIFY_PROXY_PASSWORD = None
-
-#: Max number of seconds to wait for Spotify operations to complete.
-#:
-#: Used by :mod:`mopidy.backends.spotify`.
-#:
-#: Default::
-#:
-#: SPOTIFY_TIMEOUT = 10
-SPOTIFY_TIMEOUT = 10
-
-#: Whitelist of URIs to support streaming from.
-#:
-#: Used by :mod:`mopidy.backends.stream`.
-#:
-#: Default::
-#:
-#: STREAM_PROTOCOLS = (
-#: u'http',
-#: u'https',
-#: u'mms',
-#: u'rtmp',
-#: u'rtmps',
-#: u'rtsp',
-#: )
-STREAM_PROTOCOLS = (
- 'http',
- 'https',
- 'mms',
- 'rtmp',
- 'rtmps',
- 'rtsp',
-)
diff --git a/mopidy/utils/config.py b/mopidy/utils/config.py
index aa1b06fd..09278535 100644
--- a/mopidy/utils/config.py
+++ b/mopidy/utils/config.py
@@ -5,6 +5,7 @@ import re
import socket
from mopidy import exceptions
+from mopidy.utils import path
def validate_required(value, required):
@@ -126,7 +127,7 @@ class ConfigValue(object):
class String(ConfigValue):
"""String values.
- Supports: optional choices and secret.
+ Supports: optional, choices and secret.
"""
def deserialize(self, value):
value = value.strip()
@@ -242,6 +243,34 @@ class Port(Integer):
self.maximum = 2 ** 16 - 1
+class ExpandedPath(bytes):
+ def __new__(self, value):
+ expanded = path.expand_path(value)
+ return super(ExpandedPath, self).__new__(self, expanded)
+
+ def __init__(self, value):
+ self.original = value
+
+
+class Path(ConfigValue):
+ """File system path that will be expanded with mopidy.utils.path.expand_path
+
+ Supports: optional, choices and secret.
+ """
+ def deserialize(self, value):
+ value = value.strip()
+ validate_required(value, not self.optional)
+ validate_choice(value, self.choices)
+ if not value:
+ return None
+ return ExpandedPath(value)
+
+ def serialize(self, value):
+ if isinstance(value, ExpandedPath):
+ return value.original
+ return value
+
+
class ConfigSchema(object):
"""Logical group of config values that correspond to a config section.
@@ -264,6 +293,8 @@ class ConfigSchema(object):
return self._schema[key]
def format(self, name, values):
+ # TODO: should the output be encoded utf-8 since we use that in
+ # serialize for strings?
lines = ['[%s]' % name]
for key in self._order:
value = values.get(key)
diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py
index 4e5a66cd..2ad51368 100644
--- a/mopidy/utils/path.py
+++ b/mopidy/utils/path.py
@@ -25,29 +25,27 @@ XDG_DIRS = {
'XDG_DATA_DIR': XDG_DATA_DIR,
'XDG_MUSIC_DIR': XDG_MUSIC_DIR,
}
-DATA_PATH = os.path.join(unicode(XDG_DATA_DIR), 'mopidy')
-SETTINGS_PATH = os.path.join(unicode(XDG_CONFIG_DIR), 'mopidy')
-SETTINGS_FILE = os.path.join(unicode(SETTINGS_PATH), 'settings.py')
-def get_or_create_folder(folder):
- folder = os.path.expanduser(folder)
- if os.path.isfile(folder):
+def get_or_create_dir(dir_path):
+ dir_path = expand_path(dir_path)
+ if os.path.isfile(dir_path):
raise OSError(
'A file with the same name as the desired dir, '
- '"%s", already exists.' % folder)
- elif not os.path.isdir(folder):
- logger.info('Creating dir %s', folder)
- os.makedirs(folder, 0755)
- return folder
+ '"%s", already exists.' % dir_path)
+ elif not os.path.isdir(dir_path):
+ logger.info('Creating dir %s', dir_path)
+ os.makedirs(dir_path, 0755)
+ return dir_path
-def get_or_create_file(filename):
- filename = os.path.expanduser(filename)
- if not os.path.isfile(filename):
- logger.info('Creating file %s', filename)
- open(filename, 'w')
- return filename
+def get_or_create_file(file_path):
+ file_path = expand_path(file_path)
+ get_or_create_dir(os.path.dirname(file_path))
+ if not os.path.isfile(file_path):
+ logger.info('Creating file %s', file_path)
+ open(file_path, 'w').close()
+ return file_path
def path_to_uri(*paths):
@@ -124,7 +122,7 @@ def find_files(path):
for dirpath, dirnames, filenames in os.walk(path, followlinks=True):
for dirname in dirnames:
if dirname.startswith(b'.'):
- # Skip hidden folders by modifying dirnames inplace
+ # Skip hidden dirs by modifying dirnames inplace
dirnames.remove(dirname)
for filename in filenames:
diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py
deleted file mode 100644
index f903a70d..00000000
--- a/mopidy/utils/settings.py
+++ /dev/null
@@ -1,173 +0,0 @@
-# Absolute import needed to import ~/.config/mopidy/settings.py and not
-# ourselves
-from __future__ import absolute_import, unicode_literals
-
-import copy
-import getpass
-import logging
-import os
-import pprint
-import sys
-
-from mopidy import exceptions
-from mopidy.utils import formatting, path
-
-logger = logging.getLogger('mopidy.utils.settings')
-
-
-class SettingsProxy(object):
- def __init__(self, default_settings_module):
- self.default = self._get_settings_dict_from_module(
- default_settings_module)
- self.local = self._get_local_settings()
- self.runtime = {}
-
- def _get_local_settings(self):
- if not os.path.isfile(path.SETTINGS_FILE):
- return {}
- sys.path.insert(0, path.SETTINGS_PATH)
- # pylint: disable = F0401
- import settings as local_settings_module
- # pylint: enable = F0401
- return self._get_settings_dict_from_module(local_settings_module)
-
- def _get_settings_dict_from_module(self, module):
- settings = filter(
- lambda (key, value): self._is_setting(key),
- module.__dict__.iteritems())
- return dict(settings)
-
- def _is_setting(self, name):
- return name.isupper()
-
- @property
- def current(self):
- current = copy.copy(self.default)
- current.update(self.local)
- current.update(self.runtime)
- return current
-
- def __getattr__(self, attr):
- if not self._is_setting(attr):
- return
-
- current = self.current # bind locally to avoid copying+updates
- if attr not in current:
- raise exceptions.SettingsError('Setting "%s" is not set.' % attr)
-
- value = current[attr]
- if isinstance(value, basestring) and len(value) == 0:
- raise exceptions.SettingsError('Setting "%s" is empty.' % attr)
- if not value:
- return value
- if attr.endswith('_PATH') or attr.endswith('_FILE'):
- value = path.expand_path(value)
- return value
-
- def __setattr__(self, attr, value):
- if self._is_setting(attr):
- self.runtime[attr] = value
- else:
- super(SettingsProxy, self).__setattr__(attr, value)
-
- def validate(self):
- if self.get_errors():
- logger.error(
- 'Settings validation errors: %s',
- formatting.indent(self.get_errors_as_string()))
- raise exceptions.SettingsError('Settings validation failed.')
-
- def _read_from_stdin(self, prompt):
- if '_PASSWORD' in prompt:
- return (
- getpass.getpass(prompt)
- .decode(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)
-
- def get_errors_as_string(self):
- lines = []
- for (setting, error) in self.get_errors().iteritems():
- lines.append('%s: %s' % (setting, error))
- return '\n'.join(lines)
-
-
-def validate_settings(defaults, settings):
- """
- Checks the settings for both errors like misspellings and against a set of
- rules for renamed settings, etc.
-
- Returns mapping from setting names to associated errors.
-
- :param defaults: Mopidy's default settings
- :type defaults: dict
- :param settings: the user's local settings
- :type settings: dict
- :rtype: dict
- """
- errors = {}
-
- changed = {
- 'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
- 'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
- 'GSTREAMER_AUDIO_SINK': 'OUTPUT',
- 'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH',
- 'LOCAL_OUTPUT_OVERRIDE': 'OUTPUT',
- 'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH',
- 'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE',
- 'MIXER_ALSA_CONTROL': None,
- 'MIXER_EXT_PORT': None,
- 'MIXER_EXT_SPEAKERS_A': None,
- 'MIXER_EXT_SPEAKERS_B': None,
- 'MIXER_MAX_VOLUME': None,
- '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',
- }
-
- must_be_iterable = [
- 'STREAM_PROTOCOLS',
- ]
-
- for setting, value in settings.iteritems():
- if setting in changed:
- if changed[setting] is None:
- errors[setting] = 'Deprecated setting. It may be removed.'
- else:
- errors[setting] = 'Deprecated setting. Use %s.' % (
- changed[setting],)
-
- elif setting == 'OUTPUTS':
- errors[setting] = (
- 'Deprecated setting, please change to OUTPUT. OUTPUT expects '
- 'a GStreamer bin description string for your desired output.')
-
- elif setting == 'SPOTIFY_BITRATE':
- if value not in (96, 160, 320):
- errors[setting] = (
- 'Unavailable Spotify bitrate. Available bitrates are 96, '
- '160, and 320.')
-
- elif setting.startswith('SHOUTCAST_OUTPUT_'):
- errors[setting] = (
- 'Deprecated setting, please set the value via the GStreamer '
- 'bin in OUTPUT.')
-
- elif setting in must_be_iterable and not hasattr(value, '__iter__'):
- errors[setting] = (
- 'Must be a tuple. '
- "Remember the comma after single values: (u'value',)")
-
- elif setting not in defaults and not setting.startswith('CUSTOM_'):
- errors[setting] = 'Unknown setting.'
-
- return errors
diff --git a/tests/__init__.py b/tests/__init__.py
index 7f7a9c36..b4e1d283 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -8,11 +8,6 @@ if sys.version_info < (2, 7):
else:
import unittest # noqa
-from mopidy import settings
-
-# Nuke any local settings to ensure same test env all over
-settings.local.clear()
-
def path_to_data_dir(name):
path = os.path.dirname(__file__)
diff --git a/tests/audio/actor_test.py b/tests/audio/actor_test.py
index 51786adb..a40b0572 100644
--- a/tests/audio/actor_test.py
+++ b/tests/audio/actor_test.py
@@ -6,7 +6,7 @@ import gst
import pykka
-from mopidy import audio, settings
+from mopidy import audio
from mopidy.utils.path import path_to_uri
from tests import unittest, path_to_data_dir
@@ -14,14 +14,18 @@ from tests import unittest, path_to_data_dir
class AudioTest(unittest.TestCase):
def setUp(self):
- settings.MIXER = 'fakemixer track_max_volume=65536'
- settings.OUTPUT = 'fakesink'
+ config = {
+ 'audio': {
+ 'mixer': 'fakemixer track_max_volume=65536',
+ 'mixer_track': None,
+ 'output': 'fakesink',
+ }
+ }
self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
- self.audio = audio.Audio.start(config=None).proxy()
+ self.audio = audio.Audio.start(config=config).proxy()
def tearDown(self):
pykka.ActorRegistry.stop_all()
- settings.runtime.clear()
def prepare_uri(self, uri):
self.audio.prepare_change()
@@ -59,8 +63,14 @@ class AudioTest(unittest.TestCase):
self.assertEqual(value, self.audio.get_volume().get())
def test_set_volume_with_mixer_max_below_100(self):
- settings.MIXER = 'fakemixer track_max_volume=40'
- self.audio = audio.Audio.start(config=None).proxy()
+ config = {
+ 'audio': {
+ 'mixer': 'fakemixer track_max_volume=40',
+ 'mixer_track': None,
+ 'output': 'fakesink',
+ }
+ }
+ self.audio = audio.Audio.start(config=config).proxy()
for value in range(0, 101):
self.assertTrue(self.audio.set_volume(value).get())
diff --git a/tests/backends/base/playlists.py b/tests/backends/base/playlists.py
index 00e32a6f..ad5648f9 100644
--- a/tests/backends/base/playlists.py
+++ b/tests/backends/base/playlists.py
@@ -1,25 +1,17 @@
from __future__ import unicode_literals
-import os
-import shutil
-import tempfile
-
import pykka
-from mopidy import audio, core, settings
+from mopidy import audio, core
from mopidy.models import Playlist
-from tests import unittest, path_to_data_dir
+from tests import unittest
class PlaylistsControllerTest(object):
config = {}
def setUp(self):
- settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp()
- settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')
- settings.LOCAL_MUSIC_PATH = path_to_data_dir('')
-
self.audio = audio.DummyAudio.start().proxy()
self.backend = self.backend_class.start(
config=self.config, audio=self.audio).proxy()
@@ -28,11 +20,6 @@ class PlaylistsControllerTest(object):
def tearDown(self):
pykka.ActorRegistry.stop_all()
- if os.path.exists(settings.LOCAL_PLAYLIST_PATH):
- shutil.rmtree(settings.LOCAL_PLAYLIST_PATH)
-
- settings.runtime.clear()
-
def test_create_returns_playlist_with_name_set(self):
playlist = self.core.playlists.create('test')
self.assertEqual(playlist.name, 'test')
diff --git a/tests/backends/local/events_test.py b/tests/backends/local/events_test.py
index 5ccf0886..e09bf4b9 100644
--- a/tests/backends/local/events_test.py
+++ b/tests/backends/local/events_test.py
@@ -1,4 +1,5 @@
-from mopidy import settings
+from __future__ import unicode_literals
+
from mopidy.backends.local import actor
from tests import unittest, path_to_data_dir
@@ -7,12 +8,10 @@ from tests.backends.base import events
class LocalBackendEventsTest(events.BackendEventsTest, unittest.TestCase):
backend_class = actor.LocalBackend
- # TODO: setup config
-
- def setUp(self):
- settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache')
- super(LocalBackendEventsTest, self).setUp()
-
- def tearDown(self):
- super(LocalBackendEventsTest, self).tearDown()
- settings.runtime.clear()
+ config = {
+ 'local': {
+ 'media_dir': path_to_data_dir(''),
+ 'playlists_dir': '',
+ 'tag_cache_file': path_to_data_dir('empty_tag_cache'),
+ }
+ }
diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py
index ca90e40b..74635b3e 100644
--- a/tests/backends/local/library_test.py
+++ b/tests/backends/local/library_test.py
@@ -1,6 +1,5 @@
from __future__ import unicode_literals
-from mopidy import settings
from mopidy.backends.local import actor
from tests import unittest, path_to_data_dir
@@ -9,15 +8,10 @@ from tests.backends.base.library import LibraryControllerTest
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
backend_class = actor.LocalBackend
- # TODO: setup config
-
- def setUp(self):
- settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')
- settings.LOCAL_MUSIC_PATH = path_to_data_dir('')
-
- super(LocalLibraryControllerTest, self).setUp()
-
- def tearDown(self):
- settings.runtime.clear()
-
- super(LocalLibraryControllerTest, self).tearDown()
+ config = {
+ 'local': {
+ 'media_dir': path_to_data_dir(''),
+ 'playlists_dir': '',
+ 'tag_cache_file': path_to_data_dir('library_tag_cache'),
+ }
+ }
diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py
index e9b3954c..16592539 100644
--- a/tests/backends/local/playback_test.py
+++ b/tests/backends/local/playback_test.py
@@ -1,6 +1,5 @@
from __future__ import unicode_literals
-from mopidy import settings
from mopidy.backends.local import actor
from mopidy.core import PlaybackState
from mopidy.models import Track
@@ -13,17 +12,15 @@ from tests.backends.local import generate_song
class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
backend_class = actor.LocalBackend
+ config = {
+ 'local': {
+ 'media_dir': path_to_data_dir(''),
+ 'playlists_dir': '',
+ 'tag_cache_file': path_to_data_dir('empty_tag_cache'),
+ }
+ }
tracks = [
Track(uri=generate_song(i), length=4464) for i in range(1, 4)]
- # TODO: setup config
-
- def setUp(self):
- settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache')
- super(LocalPlaybackControllerTest, self).setUp()
-
- def tearDown(self):
- super(LocalPlaybackControllerTest, self).tearDown()
- settings.runtime.clear()
def add_track(self, path):
uri = path_to_uri(path_to_data_dir(path))
diff --git a/tests/backends/local/playlists_test.py b/tests/backends/local/playlists_test.py
index 3dbc3a2a..acaac941 100644
--- a/tests/backends/local/playlists_test.py
+++ b/tests/backends/local/playlists_test.py
@@ -1,8 +1,9 @@
from __future__ import unicode_literals
import os
+import shutil
+import tempfile
-from mopidy import settings
from mopidy.backends.local import actor
from mopidy.models import Track
from mopidy.utils.path import path_to_uri
@@ -17,25 +18,34 @@ class LocalPlaylistsControllerTest(
PlaylistsControllerTest, unittest.TestCase):
backend_class = actor.LocalBackend
- # TODO: setup config
+ config = {
+ 'local': {
+ 'media_dir': path_to_data_dir(''),
+ 'tag_cache_file': path_to_data_dir('library_tag_cache'),
+ }
+ }
def setUp(self):
- settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache')
+ self.config['local']['playlists_dir'] = tempfile.mkdtemp()
+ self.playlists_dir = self.config['local']['playlists_dir']
+
super(LocalPlaylistsControllerTest, self).setUp()
def tearDown(self):
super(LocalPlaylistsControllerTest, self).tearDown()
- settings.runtime.clear()
+
+ if os.path.exists(self.playlists_dir):
+ shutil.rmtree(self.playlists_dir)
def test_created_playlist_is_persisted(self):
- path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
+ path = os.path.join(self.playlists_dir, 'test.m3u')
self.assertFalse(os.path.exists(path))
self.core.playlists.create('test')
self.assertTrue(os.path.exists(path))
def test_create_slugifies_playlist_name(self):
- path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test-foo-bar.m3u')
+ path = os.path.join(self.playlists_dir, 'test-foo-bar.m3u')
self.assertFalse(os.path.exists(path))
playlist = self.core.playlists.create('test FOO baR')
@@ -43,7 +53,7 @@ class LocalPlaylistsControllerTest(
self.assertTrue(os.path.exists(path))
def test_create_slugifies_names_which_tries_to_change_directory(self):
- path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test-foo-bar.m3u')
+ path = os.path.join(self.playlists_dir, 'test-foo-bar.m3u')
self.assertFalse(os.path.exists(path))
playlist = self.core.playlists.create('../../test FOO baR')
@@ -51,8 +61,8 @@ class LocalPlaylistsControllerTest(
self.assertTrue(os.path.exists(path))
def test_saved_playlist_is_persisted(self):
- path1 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test1.m3u')
- path2 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test2-foo-bar.m3u')
+ path1 = os.path.join(self.playlists_dir, 'test1.m3u')
+ path2 = os.path.join(self.playlists_dir, 'test2-foo-bar.m3u')
playlist = self.core.playlists.create('test1')
@@ -67,7 +77,7 @@ class LocalPlaylistsControllerTest(
self.assertTrue(os.path.exists(path2))
def test_deleted_playlist_is_removed(self):
- path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
+ path = os.path.join(self.playlists_dir, 'test.m3u')
self.assertFalse(os.path.exists(path))
playlist = self.core.playlists.create('test')
@@ -90,7 +100,7 @@ class LocalPlaylistsControllerTest(
self.assertEqual(track_path, contents.strip())
def test_playlists_are_loaded_at_startup(self):
- playlist_path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
+ playlist_path = os.path.join(self.playlists_dir, 'test.m3u')
track = Track(uri=path_to_uri(path_to_data_dir('uri2')))
playlist = self.core.playlists.create('test')
@@ -113,5 +123,5 @@ class LocalPlaylistsControllerTest(
pass
@unittest.SkipTest
- def test_playlist_folder_is_createad(self):
+ def test_playlist_dir_is_created(self):
pass
diff --git a/tests/backends/local/tracklist_test.py b/tests/backends/local/tracklist_test.py
index 24c400fa..2d8e87b6 100644
--- a/tests/backends/local/tracklist_test.py
+++ b/tests/backends/local/tracklist_test.py
@@ -1,6 +1,5 @@
from __future__ import unicode_literals
-from mopidy import settings
from mopidy.backends.local import actor
from mopidy.models import Track
@@ -11,14 +10,12 @@ from tests.backends.local import generate_song
class LocalTracklistControllerTest(TracklistControllerTest, unittest.TestCase):
backend_class = actor.LocalBackend
+ config = {
+ 'local': {
+ 'media_dir': path_to_data_dir(''),
+ 'playlists_dir': '',
+ 'tag_cache_file': path_to_data_dir('empty_tag_cache'),
+ }
+ }
tracks = [
Track(uri=generate_song(i), length=4464) for i in range(1, 4)]
- # TODO: setup config
-
- def setUp(self):
- settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache')
- super(LocalTracklistControllerTest, self).setUp()
-
- def tearDown(self):
- super(LocalTracklistControllerTest, self).tearDown()
- settings.runtime.clear()
diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py
index 61a86672..67907ff1 100644
--- a/tests/backends/local/translator_test.py
+++ b/tests/backends/local/translator_test.py
@@ -35,7 +35,7 @@ class M3UToUriTest(unittest.TestCase):
uris = parse_m3u(path_to_data_dir('comment.m3u'), data_dir)
self.assertEqual([song1_uri], uris)
- def test_file_is_relative_to_correct_folder(self):
+ def test_file_is_relative_to_correct_dir(self):
with tempfile.NamedTemporaryFile(delete=False) as tmp:
tmp.write('song1.mp3')
try:
diff --git a/tests/frontends/http/events_test.py b/tests/frontends/http/events_test.py
index 7661ac6e..c334eefa 100644
--- a/tests/frontends/http/events_test.py
+++ b/tests/frontends/http/events_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import json
try:
@@ -24,7 +26,14 @@ from tests import unittest
@mock.patch('cherrypy.engine.publish')
class HttpEventsTest(unittest.TestCase):
def setUp(self):
- self.http = actor.HttpFrontend(config=None, core=mock.Mock())
+ config = {
+ 'http': {
+ 'hostname': '127.0.0.1',
+ 'port': 6680,
+ 'static_dir': None,
+ }
+ }
+ self.http = actor.HttpFrontend(config=config, core=mock.Mock())
def test_track_playback_paused_is_broadcasted(self, publish):
publish.reset_mock()
diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py
index 3c32cd32..35e18c3b 100644
--- a/tests/frontends/mpd/dispatcher_test.py
+++ b/tests/frontends/mpd/dispatcher_test.py
@@ -13,9 +13,14 @@ from tests import unittest
class MpdDispatcherTest(unittest.TestCase):
def setUp(self):
+ config = {
+ 'mpd': {
+ 'password': None,
+ }
+ }
self.backend = dummy.create_dummy_backend_proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
- self.dispatcher = MpdDispatcher()
+ self.dispatcher = MpdDispatcher(config=config)
def tearDown(self):
pykka.ActorRegistry.stop_all()
diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py
index 9d24c3fa..21ba1c25 100644
--- a/tests/frontends/mpd/protocol/__init__.py
+++ b/tests/frontends/mpd/protocol/__init__.py
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import mock
import pykka
-from mopidy import core, settings
+from mopidy import core
from mopidy.backends import dummy
from mopidy.frontends.mpd import session
@@ -23,18 +23,25 @@ class MockConnection(mock.Mock):
class BaseTestCase(unittest.TestCase):
+ def get_config(self):
+ return {
+ 'mpd': {
+ 'password': None,
+ }
+ }
+
def setUp(self):
self.backend = dummy.create_dummy_backend_proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
self.connection = MockConnection()
- self.session = session.MpdSession(self.connection, core=self.core)
+ self.session = session.MpdSession(
+ self.connection, config=self.get_config(), core=self.core)
self.dispatcher = self.session.dispatcher
self.context = self.dispatcher.context
def tearDown(self):
pykka.ActorRegistry.stop_all()
- settings.runtime.clear()
def sendRequest(self, request):
self.connection.response = []
diff --git a/tests/frontends/mpd/protocol/authentication_test.py b/tests/frontends/mpd/protocol/authentication_test.py
index 26b03f45..2597ddef 100644
--- a/tests/frontends/mpd/protocol/authentication_test.py
+++ b/tests/frontends/mpd/protocol/authentication_test.py
@@ -1,63 +1,56 @@
from __future__ import unicode_literals
-from mopidy import settings
-
from tests.frontends.mpd import protocol
-class AuthenticationTest(protocol.BaseTestCase):
- def test_authentication_with_valid_password_is_accepted(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
+class AuthenticationActiveTest(protocol.BaseTestCase):
+ def get_config(self):
+ config = super(AuthenticationActiveTest, self).get_config()
+ config['mpd']['password'] = 'topsecret'
+ return config
+ def test_authentication_with_valid_password_is_accepted(self):
self.sendRequest('password "topsecret"')
self.assertTrue(self.dispatcher.authenticated)
self.assertInResponse('OK')
def test_authentication_with_invalid_password_is_not_accepted(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
-
self.sendRequest('password "secret"')
self.assertFalse(self.dispatcher.authenticated)
self.assertEqualResponse('ACK [3@0] {password} incorrect password')
- def test_authentication_with_anything_when_password_check_turned_off(self):
- settings.MPD_SERVER_PASSWORD = None
-
- self.sendRequest('any request at all')
- self.assertTrue(self.dispatcher.authenticated)
- self.assertEqualResponse('ACK [5@0] {} unknown command "any"')
-
def test_anything_when_not_authenticated_should_fail(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
-
self.sendRequest('any request at all')
self.assertFalse(self.dispatcher.authenticated)
self.assertEqualResponse(
u'ACK [4@0] {any} you don\'t have permission for "any"')
def test_close_is_allowed_without_authentication(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
-
self.sendRequest('close')
self.assertFalse(self.dispatcher.authenticated)
def test_commands_is_allowed_without_authentication(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
-
self.sendRequest('commands')
self.assertFalse(self.dispatcher.authenticated)
self.assertInResponse('OK')
def test_notcommands_is_allowed_without_authentication(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
-
self.sendRequest('notcommands')
self.assertFalse(self.dispatcher.authenticated)
self.assertInResponse('OK')
def test_ping_is_allowed_without_authentication(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
-
self.sendRequest('ping')
self.assertFalse(self.dispatcher.authenticated)
self.assertInResponse('OK')
+
+
+class AuthenticationInactiveTest(protocol.BaseTestCase):
+ def test_authentication_with_anything_when_password_check_turned_off(self):
+ self.sendRequest('any request at all')
+ self.assertTrue(self.dispatcher.authenticated)
+ self.assertEqualResponse('ACK [5@0] {} unknown command "any"')
+
+ def test_any_password_is_not_accepted_when_password_check_turned_off(self):
+ self.sendRequest('password "secret"')
+ self.assertEqualResponse('ACK [3@0] {password} incorrect password')
diff --git a/tests/frontends/mpd/protocol/connection_test.py b/tests/frontends/mpd/protocol/connection_test.py
index 840ce48f..01deb7a7 100644
--- a/tests/frontends/mpd/protocol/connection_test.py
+++ b/tests/frontends/mpd/protocol/connection_test.py
@@ -2,8 +2,6 @@ from __future__ import unicode_literals
from mock import patch
-from mopidy import settings
-
from tests.frontends.mpd import protocol
@@ -26,21 +24,6 @@ class ConnectionHandlerTest(protocol.BaseTestCase):
self.assertEqualResponse(
'ACK [4@0] {kill} you don\'t have permission for "kill"')
- def test_valid_password_is_accepted(self):
- settings.MPD_SERVER_PASSWORD = 'topsecret'
- self.sendRequest('password "topsecret"')
- self.assertEqualResponse('OK')
-
- def test_invalid_password_is_not_accepted(self):
- settings.MPD_SERVER_PASSWORD = 'topsecret'
- self.sendRequest('password "secret"')
- self.assertEqualResponse('ACK [3@0] {password} incorrect password')
-
- def test_any_password_is_not_accepted_when_password_check_turned_off(self):
- settings.MPD_SERVER_PASSWORD = None
- self.sendRequest('password "secret"')
- self.assertEqualResponse('ACK [3@0] {password} incorrect password')
-
def test_ping(self):
self.sendRequest('ping')
self.assertEqualResponse('OK')
diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py
index f2720473..16f4579f 100644
--- a/tests/frontends/mpd/protocol/reflection_test.py
+++ b/tests/frontends/mpd/protocol/reflection_test.py
@@ -1,7 +1,5 @@
from __future__ import unicode_literals
-from mopidy import settings
-
from tests.frontends.mpd import protocol
@@ -29,19 +27,6 @@ class ReflectionHandlerTest(protocol.BaseTestCase):
self.assertNotInResponse('command: sticker')
self.assertInResponse('OK')
- def test_commands_show_less_if_auth_required_and_not_authed(self):
- settings.MPD_SERVER_PASSWORD = u'secret'
- self.sendRequest('commands')
- # Not requiring auth
- self.assertInResponse('command: close')
- self.assertInResponse('command: commands')
- self.assertInResponse('command: notcommands')
- self.assertInResponse('command: password')
- self.assertInResponse('command: ping')
- # Requiring auth
- self.assertNotInResponse('command: play')
- self.assertNotInResponse('command: status')
-
def test_decoders(self):
self.sendRequest('decoders')
self.assertInResponse('OK')
@@ -53,8 +38,35 @@ class ReflectionHandlerTest(protocol.BaseTestCase):
self.assertInResponse('command: kill')
self.assertInResponse('OK')
+ def test_tagtypes(self):
+ self.sendRequest('tagtypes')
+ self.assertInResponse('OK')
+
+ def test_urlhandlers(self):
+ self.sendRequest('urlhandlers')
+ self.assertInResponse('OK')
+ self.assertInResponse('handler: dummy')
+
+
+class ReflectionWhenNotAuthedTest(protocol.BaseTestCase):
+ def get_config(self):
+ config = super(ReflectionWhenNotAuthedTest, self).get_config()
+ config['mpd']['password'] = 'topsecret'
+ return config
+
+ def test_commands_show_less_if_auth_required_and_not_authed(self):
+ self.sendRequest('commands')
+ # Not requiring auth
+ self.assertInResponse('command: close')
+ self.assertInResponse('command: commands')
+ self.assertInResponse('command: notcommands')
+ self.assertInResponse('command: password')
+ self.assertInResponse('command: ping')
+ # Requiring auth
+ self.assertNotInResponse('command: play')
+ self.assertNotInResponse('command: status')
+
def test_notcommands_returns_more_if_auth_required_and_not_authed(self):
- settings.MPD_SERVER_PASSWORD = u'secret'
self.sendRequest('notcommands')
# Not requiring auth
self.assertNotInResponse('command: close')
@@ -65,12 +77,3 @@ class ReflectionHandlerTest(protocol.BaseTestCase):
# Requiring auth
self.assertInResponse('command: play')
self.assertInResponse('command: status')
-
- def test_tagtypes(self):
- self.sendRequest('tagtypes')
- self.assertInResponse('OK')
-
- def test_urlhandlers(self):
- self.sendRequest('urlhandlers')
- self.assertInResponse('OK')
- self.assertInResponse('handler: dummy')
diff --git a/tests/frontends/mpd/translator_test.py b/tests/frontends/mpd/translator_test.py
index 088ae137..e4755d01 100644
--- a/tests/frontends/mpd/translator_test.py
+++ b/tests/frontends/mpd/translator_test.py
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
import datetime
import os
-from mopidy import settings
from mopidy.utils.path import mtime, uri_to_path
from mopidy.frontends.mpd import translator, protocol
from mopidy.models import Album, Artist, TlTrack, Playlist, Track
@@ -24,11 +23,10 @@ class TrackMpdFormatTest(unittest.TestCase):
)
def setUp(self):
- settings.LOCAL_MUSIC_PATH = '/dir/subdir'
+ self.media_dir = '/dir/subdir'
mtime.set_fake_time(1234567)
def tearDown(self):
- settings.runtime.clear()
mtime.undo_fake()
def test_track_to_mpd_format_for_empty_track(self):
@@ -137,15 +135,14 @@ class QueryFromMpdListFormatTest(unittest.TestCase):
class TracksToTagCacheFormatTest(unittest.TestCase):
def setUp(self):
- settings.LOCAL_MUSIC_PATH = '/dir/subdir'
+ self.media_dir = '/dir/subdir'
mtime.set_fake_time(1234567)
def tearDown(self):
- settings.runtime.clear()
mtime.undo_fake()
def translate(self, track):
- base_path = settings.LOCAL_MUSIC_PATH.encode('utf-8')
+ base_path = self.media_dir.encode('utf-8')
result = dict(translator.track_to_mpd_format(track))
result['file'] = uri_to_path(result['file'])[len(base_path) + 1:]
result['key'] = os.path.basename(result['file'])
@@ -177,11 +174,11 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
self.fail("Couldn't find end %s in result" % directory)
def test_empty_tag_cache_has_header(self):
- result = translator.tracks_to_tag_cache_format([])
+ result = translator.tracks_to_tag_cache_format([], self.media_dir)
result = self.consume_headers(result)
def test_empty_tag_cache_has_song_list(self):
- result = translator.tracks_to_tag_cache_format([])
+ result = translator.tracks_to_tag_cache_format([], self.media_dir)
result = self.consume_headers(result)
song_list, result = self.consume_song_list(result)
@@ -190,12 +187,12 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
def test_tag_cache_has_header(self):
track = Track(uri='file:///dir/subdir/song.mp3')
- result = translator.tracks_to_tag_cache_format([track])
+ result = translator.tracks_to_tag_cache_format([track], self.media_dir)
result = self.consume_headers(result)
def test_tag_cache_has_song_list(self):
track = Track(uri='file:///dir/subdir/song.mp3')
- result = translator.tracks_to_tag_cache_format([track])
+ result = translator.tracks_to_tag_cache_format([track], self.media_dir)
result = self.consume_headers(result)
song_list, result = self.consume_song_list(result)
@@ -205,7 +202,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
def test_tag_cache_has_formated_track(self):
track = Track(uri='file:///dir/subdir/song.mp3')
formated = self.translate(track)
- result = translator.tracks_to_tag_cache_format([track])
+ result = translator.tracks_to_tag_cache_format([track], self.media_dir)
result = self.consume_headers(result)
song_list, result = self.consume_song_list(result)
@@ -216,7 +213,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
def test_tag_cache_has_formated_track_with_key_and_mtime(self):
track = Track(uri='file:///dir/subdir/song.mp3')
formated = self.translate(track)
- result = translator.tracks_to_tag_cache_format([track])
+ result = translator.tracks_to_tag_cache_format([track], self.media_dir)
result = self.consume_headers(result)
song_list, result = self.consume_song_list(result)
@@ -224,50 +221,50 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
self.assertEqual(formated, song_list)
self.assertEqual(len(result), 0)
- def test_tag_cache_suports_directories(self):
+ def test_tag_cache_supports_directories(self):
track = Track(uri='file:///dir/subdir/folder/song.mp3')
formated = self.translate(track)
- result = translator.tracks_to_tag_cache_format([track])
+ result = translator.tracks_to_tag_cache_format([track], self.media_dir)
result = self.consume_headers(result)
- folder, result = self.consume_directory(result)
+ dir_data, result = self.consume_directory(result)
song_list, result = self.consume_song_list(result)
self.assertEqual(len(song_list), 0)
self.assertEqual(len(result), 0)
- song_list, result = self.consume_song_list(folder)
+ song_list, result = self.consume_song_list(dir_data)
self.assertEqual(len(result), 0)
self.assertEqual(formated, song_list)
def test_tag_cache_diretory_header_is_right(self):
track = Track(uri='file:///dir/subdir/folder/sub/song.mp3')
- result = translator.tracks_to_tag_cache_format([track])
+ result = translator.tracks_to_tag_cache_format([track], self.media_dir)
result = self.consume_headers(result)
- folder, result = self.consume_directory(result)
+ dir_data, result = self.consume_directory(result)
- self.assertEqual(('directory', 'folder/sub'), folder[0])
- self.assertEqual(('mtime', mtime('.')), folder[1])
- self.assertEqual(('begin', 'sub'), folder[2])
+ self.assertEqual(('directory', 'folder/sub'), dir_data[0])
+ self.assertEqual(('mtime', mtime('.')), dir_data[1])
+ self.assertEqual(('begin', 'sub'), dir_data[2])
def test_tag_cache_suports_sub_directories(self):
track = Track(uri='file:///dir/subdir/folder/sub/song.mp3')
formated = self.translate(track)
- result = translator.tracks_to_tag_cache_format([track])
+ result = translator.tracks_to_tag_cache_format([track], self.media_dir)
result = self.consume_headers(result)
- folder, result = self.consume_directory(result)
+ dir_data, result = self.consume_directory(result)
song_list, result = self.consume_song_list(result)
self.assertEqual(len(song_list), 0)
self.assertEqual(len(result), 0)
- folder, result = self.consume_directory(folder)
+ dir_data, result = self.consume_directory(dir_data)
song_list, result = self.consume_song_list(result)
self.assertEqual(len(result), 0)
self.assertEqual(len(song_list), 0)
- song_list, result = self.consume_song_list(folder)
+ song_list, result = self.consume_song_list(dir_data)
self.assertEqual(len(result), 0)
self.assertEqual(formated, song_list)
@@ -281,7 +278,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
formated.extend(self.translate(tracks[0]))
formated.extend(self.translate(tracks[1]))
- result = translator.tracks_to_tag_cache_format(tracks)
+ result = translator.tracks_to_tag_cache_format(tracks, self.media_dir)
result = self.consume_headers(result)
song_list, result = self.consume_song_list(result)
@@ -299,11 +296,11 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
formated.append(self.translate(tracks[0]))
formated.append(self.translate(tracks[1]))
- result = translator.tracks_to_tag_cache_format(tracks)
+ result = translator.tracks_to_tag_cache_format(tracks, self.media_dir)
result = self.consume_headers(result)
- folder, result = self.consume_directory(result)
- song_list, song_result = self.consume_song_list(folder)
+ dir_data, result = self.consume_directory(result)
+ song_list, song_result = self.consume_song_list(dir_data)
self.assertEqual(formated[1], song_list)
self.assertEqual(len(song_result), 0)
@@ -315,13 +312,10 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
class TracksToDirectoryTreeTest(unittest.TestCase):
def setUp(self):
- settings.LOCAL_MUSIC_PATH = '/root/'
-
- def tearDown(self):
- settings.runtime.clear()
+ self.media_dir = '/root'
def test_no_tracks_gives_emtpy_tree(self):
- tree = translator.tracks_to_directory_tree([])
+ tree = translator.tracks_to_directory_tree([], self.media_dir)
self.assertEqual(tree, ({}, []))
def test_top_level_files(self):
@@ -330,18 +324,18 @@ class TracksToDirectoryTreeTest(unittest.TestCase):
Track(uri='file:///root/file2.mp3'),
Track(uri='file:///root/file3.mp3'),
]
- tree = translator.tracks_to_directory_tree(tracks)
+ tree = translator.tracks_to_directory_tree(tracks, self.media_dir)
self.assertEqual(tree, ({}, tracks))
def test_single_file_in_subdir(self):
tracks = [Track(uri='file:///root/dir/file1.mp3')]
- tree = translator.tracks_to_directory_tree(tracks)
+ tree = translator.tracks_to_directory_tree(tracks, self.media_dir)
expected = ({'dir': ({}, tracks)}, [])
self.assertEqual(tree, expected)
def test_single_file_in_sub_subdir(self):
tracks = [Track(uri='file:///root/dir1/dir2/file1.mp3')]
- tree = translator.tracks_to_directory_tree(tracks)
+ tree = translator.tracks_to_directory_tree(tracks, self.media_dir)
expected = ({'dir1': ({'dir1/dir2': ({}, tracks)}, [])}, [])
self.assertEqual(tree, expected)
@@ -353,7 +347,7 @@ class TracksToDirectoryTreeTest(unittest.TestCase):
Track(uri='file:///root/dir2/file4.mp3'),
Track(uri='file:///root/dir2/sub/file5.mp3'),
]
- tree = translator.tracks_to_directory_tree(tracks)
+ tree = translator.tracks_to_directory_tree(tracks, self.media_dir)
expected = (
{
'dir1': ({}, [tracks[1], tracks[2]]),
diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py
index ec4a17a9..e1e13084 100644
--- a/tests/frontends/mpris/player_interface_test.py
+++ b/tests/frontends/mpris/player_interface_test.py
@@ -28,7 +28,7 @@ class PlayerInterfaceTest(unittest.TestCase):
objects.MprisObject._connect_to_dbus = mock.Mock()
self.backend = dummy.create_dummy_backend_proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
- self.mpris = objects.MprisObject(core=self.core)
+ self.mpris = objects.MprisObject(config={}, core=self.core)
def tearDown(self):
pykka.ActorRegistry.stop_all()
diff --git a/tests/frontends/mpris/playlists_interface_test.py b/tests/frontends/mpris/playlists_interface_test.py
index 745a858c..67f9e9be 100644
--- a/tests/frontends/mpris/playlists_interface_test.py
+++ b/tests/frontends/mpris/playlists_interface_test.py
@@ -25,7 +25,7 @@ class PlayerInterfaceTest(unittest.TestCase):
objects.MprisObject._connect_to_dbus = mock.Mock()
self.backend = dummy.create_dummy_backend_proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
- self.mpris = objects.MprisObject(core=self.core)
+ self.mpris = objects.MprisObject(config={}, core=self.core)
foo = self.core.playlists.create('foo').get()
foo = foo.copy(last_modified=datetime.datetime(2012, 3, 1, 6, 0, 0))
diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py
index 36d689a2..806b2162 100644
--- a/tests/frontends/mpris/root_interface_test.py
+++ b/tests/frontends/mpris/root_interface_test.py
@@ -5,7 +5,7 @@ import sys
import mock
import pykka
-from mopidy import core, exceptions, settings
+from mopidy import core, exceptions
from mopidy.backends import dummy
try:
@@ -19,11 +19,17 @@ from tests import unittest
@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
class RootInterfaceTest(unittest.TestCase):
def setUp(self):
+ config = {
+ 'mpris': {
+ 'desktop_file': '/tmp/foo.desktop',
+ }
+ }
+
objects.exit_process = mock.Mock()
objects.MprisObject._connect_to_dbus = mock.Mock()
self.backend = dummy.create_dummy_backend_proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
- self.mpris = objects.MprisObject(core=self.core)
+ self.mpris = objects.MprisObject(config=config, core=self.core)
def tearDown(self):
pykka.ActorRegistry.stop_all()
@@ -66,15 +72,9 @@ class RootInterfaceTest(unittest.TestCase):
result = self.mpris.Get(objects.ROOT_IFACE, 'Identity')
self.assertEquals(result, 'Mopidy')
- def test_desktop_entry_is_mopidy(self):
- result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry')
- self.assertEquals(result, 'mopidy')
-
def test_desktop_entry_is_based_on_DESKTOP_FILE_setting(self):
- settings.runtime['DESKTOP_FILE'] = '/tmp/foo.desktop'
result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry')
self.assertEquals(result, 'foo')
- settings.runtime.clear()
def test_supported_uri_schemes_includes_backend_uri_schemes(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedUriSchemes')
diff --git a/tests/scanner_test.py b/tests/scanner_test.py
index 617f2537..edcc2242 100644
--- a/tests/scanner_test.py
+++ b/tests/scanner_test.py
@@ -196,7 +196,7 @@ class ScannerTest(unittest.TestCase):
self.check('scanner/simple/song1.mp3', 'title', 'trackname')
self.check('scanner/simple/song1.ogg', 'title', 'trackname')
- def test_nonexistant_folder_does_not_fail(self):
+ def test_nonexistant_dir_does_not_fail(self):
self.scan('scanner/does-not-exist')
self.assert_(not self.errors)
diff --git a/tests/utils/config_test.py b/tests/utils/config_test.py
index ad86b961..bf26b2e7 100644
--- a/tests/utils/config_test.py
+++ b/tests/utils/config_test.py
@@ -298,6 +298,55 @@ class PortTest(unittest.TestCase):
self.assertRaises(ValueError, value.deserialize, '')
+class ExpandedPathTest(unittest.TestCase):
+ def test_is_bytes(self):
+ self.assertIsInstance(config.ExpandedPath('/tmp'), bytes)
+
+ @mock.patch('mopidy.utils.path.expand_path')
+ def test_defaults_to_expanded(self, expand_path_mock):
+ expand_path_mock.return_value = 'expanded_path'
+ self.assertEqual('expanded_path', config.ExpandedPath('~'))
+
+ @mock.patch('mopidy.utils.path.expand_path')
+ def test_orginal_stores_unexpanded(self, expand_path_mock):
+ self.assertEqual('~', config.ExpandedPath('~').original)
+
+
+class PathTest(unittest.TestCase):
+ def test_deserialize_conversion_success(self):
+ result = config.Path().deserialize('/foo')
+ self.assertEqual('/foo', result)
+ self.assertIsInstance(result, config.ExpandedPath)
+ self.assertIsInstance(result, bytes)
+
+ def test_deserialize_enforces_choices(self):
+ value = config.Path(choices=['/foo', '/bar', '/baz'])
+ self.assertEqual('/foo', value.deserialize('/foo'))
+ self.assertRaises(ValueError, value.deserialize, '/foobar')
+
+ def test_deserialize_enforces_required(self):
+ value = config.Path()
+ self.assertRaises(ValueError, value.deserialize, '')
+ self.assertRaises(ValueError, value.deserialize, ' ')
+
+ def test_deserialize_respects_optional(self):
+ value = config.Path(optional=True)
+ self.assertIsNone(value.deserialize(''))
+ self.assertIsNone(value.deserialize(' '))
+
+ @mock.patch('mopidy.utils.path.expand_path')
+ def test_serialize_uses_original(self, expand_path_mock):
+ expand_path_mock.return_value = 'expanded_path'
+ path = config.ExpandedPath('original_path')
+ value = config.Path()
+ self.assertEqual('expanded_path', path)
+ self.assertEqual('original_path', value.serialize(path))
+
+ def test_serialize_plain_string(self):
+ value = config.Path()
+ self.assertEqual('path', value.serialize('path'))
+
+
class ConfigSchemaTest(unittest.TestCase):
def setUp(self):
self.schema = config.ConfigSchema()
diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py
index 461f0809..9d1c16d3 100644
--- a/tests/utils/path_test.py
+++ b/tests/utils/path_test.py
@@ -13,7 +13,7 @@ from mopidy.utils import path
from tests import unittest, path_to_data_dir
-class GetOrCreateFolderTest(unittest.TestCase):
+class GetOrCreateDirTest(unittest.TestCase):
def setUp(self):
self.parent = tempfile.mkdtemp()
@@ -21,40 +21,78 @@ class GetOrCreateFolderTest(unittest.TestCase):
if os.path.isdir(self.parent):
shutil.rmtree(self.parent)
- def test_creating_folder(self):
- folder = os.path.join(self.parent, 'test')
- self.assert_(not os.path.exists(folder))
- self.assert_(not os.path.isdir(folder))
- created = path.get_or_create_folder(folder)
- self.assert_(os.path.exists(folder))
- self.assert_(os.path.isdir(folder))
- self.assertEqual(created, folder)
+ def test_creating_dir(self):
+ dir_path = os.path.join(self.parent, 'test')
+ self.assert_(not os.path.exists(dir_path))
+ created = path.get_or_create_dir(dir_path)
+ self.assert_(os.path.exists(dir_path))
+ self.assert_(os.path.isdir(dir_path))
+ self.assertEqual(created, dir_path)
- def test_creating_nested_folders(self):
- level2_folder = os.path.join(self.parent, 'test')
- level3_folder = os.path.join(self.parent, 'test', 'test')
- self.assert_(not os.path.exists(level2_folder))
- self.assert_(not os.path.isdir(level2_folder))
- self.assert_(not os.path.exists(level3_folder))
- self.assert_(not os.path.isdir(level3_folder))
- created = path.get_or_create_folder(level3_folder)
- self.assert_(os.path.exists(level2_folder))
- self.assert_(os.path.isdir(level2_folder))
- self.assert_(os.path.exists(level3_folder))
- self.assert_(os.path.isdir(level3_folder))
- self.assertEqual(created, level3_folder)
+ def test_creating_nested_dirs(self):
+ level2_dir = os.path.join(self.parent, 'test')
+ level3_dir = os.path.join(self.parent, 'test', 'test')
+ self.assert_(not os.path.exists(level2_dir))
+ self.assert_(not os.path.exists(level3_dir))
+ created = path.get_or_create_dir(level3_dir)
+ self.assert_(os.path.exists(level2_dir))
+ self.assert_(os.path.isdir(level2_dir))
+ self.assert_(os.path.exists(level3_dir))
+ self.assert_(os.path.isdir(level3_dir))
+ self.assertEqual(created, level3_dir)
- def test_creating_existing_folder(self):
- created = path.get_or_create_folder(self.parent)
+ def test_creating_existing_dir(self):
+ created = path.get_or_create_dir(self.parent)
self.assert_(os.path.exists(self.parent))
self.assert_(os.path.isdir(self.parent))
self.assertEqual(created, self.parent)
- def test_create_folder_with_name_of_existing_file_throws_oserror(self):
+ def test_create_dir_with_name_of_existing_file_throws_oserror(self):
conflicting_file = os.path.join(self.parent, 'test')
open(conflicting_file, 'w').close()
- folder = os.path.join(self.parent, 'test')
- self.assertRaises(OSError, path.get_or_create_folder, folder)
+ dir_path = os.path.join(self.parent, 'test')
+ self.assertRaises(OSError, path.get_or_create_dir, dir_path)
+
+
+class GetOrCreateFileTest(unittest.TestCase):
+ def setUp(self):
+ self.parent = tempfile.mkdtemp()
+
+ def tearDown(self):
+ if os.path.isdir(self.parent):
+ shutil.rmtree(self.parent)
+
+ def test_creating_file(self):
+ file_path = os.path.join(self.parent, 'test')
+ self.assert_(not os.path.exists(file_path))
+ created = path.get_or_create_file(file_path)
+ self.assert_(os.path.exists(file_path))
+ self.assert_(os.path.isfile(file_path))
+ self.assertEqual(created, file_path)
+
+ def test_creating_nested_file(self):
+ level2_dir = os.path.join(self.parent, 'test')
+ file_path = os.path.join(self.parent, 'test', 'test')
+ self.assert_(not os.path.exists(level2_dir))
+ self.assert_(not os.path.exists(file_path))
+ created = path.get_or_create_file(file_path)
+ self.assert_(os.path.exists(level2_dir))
+ self.assert_(os.path.isdir(level2_dir))
+ self.assert_(os.path.exists(file_path))
+ self.assert_(os.path.isfile(file_path))
+ self.assertEqual(created, file_path)
+
+ def test_creating_existing_file(self):
+ file_path = os.path.join(self.parent, 'test')
+ path.get_or_create_file(file_path)
+ created = path.get_or_create_file(file_path)
+ self.assert_(os.path.exists(file_path))
+ self.assert_(os.path.isfile(file_path))
+ self.assertEqual(created, file_path)
+
+ def test_create_file_with_name_of_existing_dir_throws_ioerror(self):
+ conflicting_dir = os.path.join(self.parent)
+ self.assertRaises(IOError, path.get_or_create_file, conflicting_dir)
class PathToFileURITest(unittest.TestCase):
@@ -66,7 +104,7 @@ class PathToFileURITest(unittest.TestCase):
result = path.path_to_uri('/etc/fstab')
self.assertEqual(result, 'file:///etc/fstab')
- def test_folder_and_path(self):
+ def test_dir_and_path(self):
if sys.platform == 'win32':
result = path.path_to_uri('C:/WINDOWS/', 'clock.avi')
self.assertEqual(result, 'file:///C://WINDOWS/clock.avi')
@@ -145,10 +183,10 @@ class SplitPathTest(unittest.TestCase):
def test_empty_path(self):
self.assertEqual([], path.split_path(''))
- def test_single_folder(self):
+ def test_single_dir(self):
self.assertEqual(['foo'], path.split_path('foo'))
- def test_folders(self):
+ def test_dirs(self):
self.assertEqual(['foo', 'bar', 'baz'], path.split_path('foo/bar/baz'))
def test_initial_slash_is_ignored(self):
@@ -190,10 +228,10 @@ class FindFilesTest(unittest.TestCase):
def find(self, value):
return list(path.find_files(path_to_data_dir(value)))
- def test_basic_folder(self):
+ def test_basic_dir(self):
self.assert_(self.find(''))
- def test_nonexistant_folder(self):
+ def test_nonexistant_dir(self):
self.assertEqual(self.find('does-not-exist'), [])
def test_file(self):
@@ -207,7 +245,7 @@ class FindFilesTest(unittest.TestCase):
self.assert_(
is_bytes(name), '%s is not bytes object' % repr(name))
- def test_ignores_hidden_folders(self):
+ def test_ignores_hidden_dirs(self):
self.assertEqual(self.find('.hidden'), [])
def test_ignores_hidden_files(self):
diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py
deleted file mode 100644
index ce763486..00000000
--- a/tests/utils/settings_test.py
+++ /dev/null
@@ -1,150 +0,0 @@
-from __future__ import unicode_literals
-
-import os
-
-from mopidy import exceptions, settings
-from mopidy.utils import settings as setting_utils
-
-from tests import unittest
-
-
-class ValidateSettingsTest(unittest.TestCase):
- def setUp(self):
- self.defaults = {
- 'MPD_SERVER_HOSTNAME': '::',
- 'MPD_SERVER_PORT': 6600,
- 'SPOTIFY_BITRATE': 160,
- }
-
- def test_no_errors_yields_empty_dict(self):
- result = setting_utils.validate_settings(self.defaults, {})
- self.assertEqual(result, {})
-
- def test_unknown_setting_returns_error(self):
- result = setting_utils.validate_settings(
- self.defaults, {'MPD_SERVER_HOSTNMAE': '127.0.0.1'})
- self.assertEqual(
- result['MPD_SERVER_HOSTNMAE'], 'Unknown setting.')
-
- def test_custom_settings_does_not_return_errors(self):
- result = setting_utils.validate_settings(
- self.defaults, {'CUSTOM_MYAPP_SETTING': 'foobar'})
- self.assertNotIn('CUSTOM_MYAPP_SETTING', result)
-
- def test_not_renamed_setting_returns_error(self):
- result = setting_utils.validate_settings(
- self.defaults, {'SERVER_HOSTNAME': '127.0.0.1'})
- self.assertEqual(
- result['SERVER_HOSTNAME'],
- 'Deprecated setting. Use MPD_SERVER_HOSTNAME.')
-
- def test_unneeded_settings_returns_error(self):
- result = setting_utils.validate_settings(
- self.defaults, {'SPOTIFY_LIB_APPKEY': '/tmp/foo'})
- self.assertEqual(
- result['SPOTIFY_LIB_APPKEY'],
- 'Deprecated setting. It may be removed.')
-
- def test_unavailable_bitrate_setting_returns_error(self):
- result = setting_utils.validate_settings(
- self.defaults, {'SPOTIFY_BITRATE': 50})
- self.assertEqual(
- result['SPOTIFY_BITRATE'],
- 'Unavailable Spotify bitrate. '
- 'Available bitrates are 96, 160, and 320.')
-
- def test_two_errors_are_both_reported(self):
- result = setting_utils.validate_settings(
- self.defaults, {'FOO': '', 'BAR': ''})
- self.assertEqual(len(result), 2)
-
-
-class SettingsProxyTest(unittest.TestCase):
- def setUp(self):
- self.settings = setting_utils.SettingsProxy(settings)
- self.settings.local.clear()
-
- def test_set_and_get_attr(self):
- self.settings.TEST = 'test'
- self.assertEqual(self.settings.TEST, 'test')
-
- def test_getattr_raises_error_on_missing_setting(self):
- try:
- self.settings.TEST
- self.fail('Should raise exception')
- except exceptions.SettingsError as e:
- self.assertEqual('Setting "TEST" is not set.', e.message)
-
- def test_getattr_raises_error_on_empty_setting(self):
- self.settings.TEST = ''
- try:
- self.settings.TEST
- self.fail('Should raise exception')
- except exceptions.SettingsError as e:
- self.assertEqual('Setting "TEST" is empty.', e.message)
-
- def test_getattr_does_not_raise_error_if_setting_is_false(self):
- self.settings.TEST = False
- self.assertEqual(False, self.settings.TEST)
-
- def test_getattr_does_not_raise_error_if_setting_is_none(self):
- self.settings.TEST = None
- self.assertEqual(None, self.settings.TEST)
-
- def test_getattr_does_not_raise_error_if_setting_is_zero(self):
- self.settings.TEST = 0
- self.assertEqual(0, self.settings.TEST)
-
- def test_setattr_updates_runtime_settings(self):
- self.settings.TEST = 'test'
- self.assertIn('TEST', self.settings.runtime)
-
- def test_setattr_updates_runtime_with_value(self):
- self.settings.TEST = 'test'
- self.assertEqual(self.settings.runtime['TEST'], 'test')
-
- def test_runtime_value_included_in_current(self):
- self.settings.TEST = 'test'
- self.assertEqual(self.settings.current['TEST'], 'test')
-
- def test_value_ending_in_path_is_expanded(self):
- self.settings.TEST_PATH = '~/test'
- actual = self.settings.TEST_PATH
- expected = os.path.expanduser('~/test')
- self.assertEqual(actual, expected)
-
- def test_value_ending_in_path_is_absolute(self):
- self.settings.TEST_PATH = './test'
- actual = self.settings.TEST_PATH
- expected = os.path.abspath('./test')
- self.assertEqual(actual, expected)
-
- def test_value_ending_in_file_is_expanded(self):
- self.settings.TEST_FILE = '~/test'
- actual = self.settings.TEST_FILE
- expected = os.path.expanduser('~/test')
- self.assertEqual(actual, expected)
-
- def test_value_ending_in_file_is_absolute(self):
- self.settings.TEST_FILE = './test'
- actual = self.settings.TEST_FILE
- expected = os.path.abspath('./test')
- self.assertEqual(actual, expected)
-
- def test_value_not_ending_in_path_or_file_is_not_expanded(self):
- self.settings.TEST = '~/test'
- actual = self.settings.TEST
- self.assertEqual(actual, '~/test')
-
- def test_value_not_ending_in_path_or_file_is_not_absolute(self):
- self.settings.TEST = './test'
- actual = self.settings.TEST
- self.assertEqual(actual, './test')
-
- def test_value_ending_in_file_can_be_none(self):
- self.settings.TEST_FILE = None
- self.assertEqual(self.settings.TEST_FILE, None)
-
- def test_value_ending_in_path_can_be_none(self):
- self.settings.TEST_PATH = None
- self.assertEqual(self.settings.TEST_PATH, None)