Merge branch 'develop' into pr/396

Conflicts:
	mopidy/frontends/mpd/dispatcher.py
This commit is contained in:
Stein Magnus Jodal 2013-04-09 23:37:17 +02:00
commit 3033b1db5f
62 changed files with 848 additions and 1203 deletions

View File

@ -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')

View File

@ -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

View File

@ -42,7 +42,7 @@ User documentation
installation/index
installation/raspberrypi
settings
config
running
clients/index
authors

View File

@ -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>`.

View File

@ -1,6 +1,6 @@
**********************************************
:mod:`mopidy.frontends.scrobble` -- Scrobbler
**********************************************
***********************************************
:mod:`mopidy.frontends.scrobbler` -- Scrobbler
***********************************************
.. automodule:: mopidy.frontends.scrobbler
:synopsis: Music scrobbler frontend

View File

@ -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)

View File

@ -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):

View File

@ -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():

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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):

View File

@ -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))

View File

@ -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]]

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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',

View File

@ -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()

View File

@ -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')

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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')

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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',
)

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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__)

View 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())

View File

@ -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')

View File

@ -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'),
}
}

View File

@ -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'),
}
}

View File

@ -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))

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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()

View File

@ -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()

View File

@ -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 = []

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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]]),

View File

@ -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()

View File

@ -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))

View File

@ -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')

View File

@ -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)

View File

@ -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()

View File

@ -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):

View File

@ -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)