Merge branch 'develop' into pr/396
Conflicts: mopidy/frontends/mpd/dispatcher.py
This commit is contained in:
commit
3033b1db5f
12
docs/conf.py
12
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')
|
||||
|
||||
@ -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
|
||||
<http://www.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 <http://www.mpris.org/>`_. 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
|
||||
@ -42,7 +42,7 @@ User documentation
|
||||
|
||||
installation/index
|
||||
installation/raspberrypi
|
||||
settings
|
||||
config
|
||||
running
|
||||
clients/index
|
||||
authors
|
||||
|
||||
@ -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 </settings>`, and then
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
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
|
||||
<https://aur.archlinux.org/packages/python2-pylast/>`_ from AUR.
|
||||
|
||||
#. Finally, you need to set a couple of :doc:`settings </settings>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
|
||||
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 </settings>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
|
||||
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 </settings>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
#. Finally, you need to set a couple of :doc:`config values </config>`, and
|
||||
then you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
**********************************************
|
||||
:mod:`mopidy.frontends.scrobble` -- Scrobbler
|
||||
**********************************************
|
||||
***********************************************
|
||||
:mod:`mopidy.frontends.scrobbler` -- Scrobbler
|
||||
***********************************************
|
||||
|
||||
.. automodule:: mopidy.frontends.scrobbler
|
||||
:synopsis: Music scrobbler frontend
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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 <http://gstreamer.freedesktop.org/>`_.
|
||||
|
||||
**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():
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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]]
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
<http://www.last.fm>`_ profile.
|
||||
Frontend which scrobbles the music you play to your
|
||||
`Last.fm <http://www.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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 <http://www.last.fm/>`_ username.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.lastfm`.
|
||||
LASTFM_USERNAME = ''
|
||||
|
||||
#: Your `Last.fm <http://www.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',
|
||||
)
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
@ -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__)
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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'),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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]]),
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
Loading…
Reference in New Issue
Block a user