Merge branch 'develop' into feature/config

This commit is contained in:
Thomas Adamcik 2013-04-01 21:25:07 +02:00
commit 50980723f8
40 changed files with 534 additions and 321 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
*.egg-info
*.pyc
*.swp
.coverage

View File

@ -18,3 +18,4 @@
- herrernst <herr.ernst@gmail.com>
- Nick Steel <kingosticks@gmail.com>
- Zan Dobersek <zandobersek@gmail.com>
- Thomas Refis <refis.thomas@gmail.com>

View File

@ -7,7 +7,7 @@ include mopidy/backends/spotify/spotify_appkey.key
include pylintrc
recursive-include docs *
prune docs/_build
recursive-include mopidy/frontends/http/data/
recursive-include mopidy/frontends/http/data *
recursive-include requirements *
recursive-include tests *.py
recursive-include tests/data *

View File

@ -18,9 +18,9 @@ platforms, including Windows, Mac OS X, Linux, Android and iOS.
To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
- `Documentation <http://docs.mopidy.com/>`_
- `Source code <http://github.com/mopidy/mopidy>`_
- `Issue tracker <http://github.com/mopidy/mopidy/issues>`_
- `CI server <http://travis-ci.org/mopidy/mopidy>`_
- `Source code <https://github.com/mopidy/mopidy>`_
- `Issue tracker <https://github.com/mopidy/mopidy/issues>`_
- `CI server <https://travis-ci.org/mopidy/mopidy>`_
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
- `Download development snapshot <http://github.com/mopidy/mopidy/tarball/develop#egg=mopidy-dev>`_
- `Download development snapshot <https://github.com/mopidy/mopidy/tarball/develop#egg=mopidy-dev>`_

View File

@ -1,5 +0,0 @@
#! /usr/bin/env python
if __name__ == '__main__':
from mopidy.__main__ import main
main()

View File

@ -1,5 +0,0 @@
#! /usr/bin/env python
if __name__ == '__main__':
from mopidy.scanner import main
main()

View File

@ -20,8 +20,8 @@ The following requirements applies to any frontend implementation:
- It MAY use additional actors to implement whatever it does, and using actors
in frontend implementations is encouraged.
- The frontend is activated by including its main actor in the
:attr:`mopidy.settings.FRONTENDS` setting.
- The frontend is enabled if the extension it is part of is enabled. See
:ref:`extensiondev` for more information.
- The main actor MUST be able to start and stop the frontend when the main
actor is started and stopped.

View File

@ -4,6 +4,20 @@ Changes
This change log is used to track all major changes to Mopidy.
v0.14.0 (UNRELEASED)
====================
**Dependencies**
- setuptools or distribute is now required. We've introduced this dependency to
use setuptools' entry points functionality to find installed Mopidy
extensions.
**Spotify backend**
- Add support for starred playlists, both your own and those owned by other
users. (Fixes: :issue:`326`)
v0.13.0 (2013-03-31)
====================

View File

@ -36,12 +36,11 @@ Mopidy executable. If this isn't in place, the sound menu will not detect that
Mopidy is running.
Next, Mopidy's MPRIS frontend must be running for the sound menu to be able to
control Mopidy. The frontend is activated by default, so unless you've changed
the :attr:`mopidy.settings.FRONTENDS` setting, you should be good to go. Keep
an eye out for warnings or errors from the MPRIS frontend when you start
Mopidy, since it may fail because of missing dependencies or because Mopidy is
started outside of X; the frontend won't work if ``$DISPLAY`` isn't set when
Mopidy is started.
control Mopidy. The frontend is enabled by default, so as long as you have all
its dependencies available, you should be good to go. Keep an eye out for
warnings or errors from the MPRIS frontend when you start Mopidy, since it may
fail because of missing dependencies or because Mopidy is started outside of X;
the frontend won't work if ``$DISPLAY`` isn't set when Mopidy is started.
Under normal use, if Mopidy isn't running and you open the menu and click on
"Mopidy Music Server", a terminal window will open and automatically start

View File

@ -281,12 +281,6 @@ settings file in the following way::
import os
profile = os.environ.get('PROFILE', '').split(',')
if 'spotify' in profile:
BACKENDS = (u'mopidy.backends.spotify.SpotifyBackend',)
elif 'local' in profile:
BACKENDS = (u'mopidy.backends.local.LocalBackend',)
LOCAL_MUSIC_PATH = u'~/music'
if 'shoutcast' in profile:
OUTPUT = u'lame ! shout2send mount="/stream"'
elif 'silent' in profile:
@ -296,7 +290,7 @@ settings file in the following way::
SPOTIFY_USERNAME = u'xxxxx'
SPOTIFY_PASSWORD = u'xxxxx'
Using this setup you can now run Mopidy with ``PROFILE=silent,spotify mopidy``
Using this setup you can now run Mopidy with ``PROFILE=silent mopidy``
if you for instance want to test Spotify without any actual audio output.
@ -359,7 +353,6 @@ Creating releases
#. Build package and upload to PyPI::
rm MANIFEST # Will be regenerated by setup.py
python setup.py sdist upload
#. Update the Debian package.

View File

@ -1,3 +1,5 @@
.. _extensiondev:
*********************
Extension development
*********************
@ -162,14 +164,13 @@ class that will connect the rest of the dots.
#py_modules=['mopidy_soundspot'],
zip_safe=False,
include_package_data=True,
platforms='any',
install_requires=[
'setuptools',
'Mopidy',
'pysoundspot',
],
entry_points={
'mopidy.extension': [
b'mopidy.extension': [
'soundspot = mopidy_soundspot:Extension',
],
},
@ -202,7 +203,8 @@ The default configuration for the extension is defined by the
``get_default_config()`` method in the ``Extension`` class which returns a
:mod:`ConfigParser` compatible config section. The config section's name should
be the same as the extension's short name, as defined in the ``entry_points``
part of ``setup.py``. All extensions should include an ``enabled`` config which
part of ``setup.py``, but prefixed with ``ext.``, for example
``ext.soundspot``. All extensions should include an ``enabled`` config which
should default to ``true``. Provide good defaults for all config values so that
as few users as possible will need to change them. The exception is if the
config value has security implications; in that case you should default to the
@ -234,7 +236,7 @@ meaningful defaults blank, like ``username`` and ``password``.
def get_default_config(self):
return """
[soundspot]
[ext.soundspot]
enabled = true
username =
password =
@ -265,13 +267,13 @@ meaningful defaults blank, like ``username`` and ``password``.
# You will typically only implement one of the next three methods
# in a single extension.
def get_frontend_class(self):
def get_frontend_classes(self):
from .frontend import SoundspotFrontend
return SoundspotFrontend
return [SoundspotFrontend]
def get_backend_class(self):
def get_backend_classes(self):
from .backend import SoundspotBackend
return SoundspotBackend
return [SoundspotBackend]
def register_gstreamer_elements(self):
from .mixer import SoundspotMixer

View File

@ -135,10 +135,10 @@ Pip.
sudo easy_install pip
#. Then get, build, and install the latest releast of pyspotify, pylast, pykka,
#. Then get, build, and install the latest release of pyspotify, pylast,
and Mopidy using Pip::
sudo pip install -U pyspotify pylast pykka mopidy
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>`.
@ -171,15 +171,7 @@ can install Mopidy from PyPI using Pip.
sudo yum install -y gcc python-devel python-pip
#. Then you'll need to install all of Mopidy's hard dependencies:
- Pykka >= 1.0::
sudo pip install -U pykka
On Fedora the binary is called ``pip-python``::
sudo pip-python install -U pykka
#. Then you'll need to install all of Mopidy's hard non-Python dependencies:
- GStreamer 0.10.x, with Python bindings. GStreamer is packaged for most
popular Linux distributions. Search for GStreamer in your package manager,
@ -235,7 +227,8 @@ can install Mopidy from PyPI using Pip.
sudo pip install -U pyspotify
# Fedora:
On Fedora the binary is called ``pip-python``::
sudo pip-python install -U pyspotify
#. Optional: If you want to scrobble your played tracks to Last.fm, you need
@ -243,7 +236,8 @@ can install Mopidy from PyPI using Pip.
sudo pip install -U pylast
# Fedora:
On Fedora the binary is called ``pip-python``::
sudo pip-python install -U pylast
#. Optional: To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound
@ -259,7 +253,8 @@ can install Mopidy from PyPI using Pip.
sudo pip install -U mopidy
# Fedora:
On Fedora the binary is called ``pip-python``::
sudo pip-python install -U mopidy
To upgrade Mopidy to future releases, just rerun this command.

View File

@ -15,9 +15,9 @@ if not (2, 6) <= sys.version_info < (3,):
'.'.join(map(str, sys.version_info[:3])))
if (isinstance(pykka.__version__, basestring)
and not SV('1.0') <= SV(pykka.__version__) < SV('2.0')):
and not SV('1.1') <= SV(pykka.__version__) < SV('2.0')):
sys.exit(
'Mopidy requires Pykka >= 1.0, < 2, but found %s' % pykka.__version__)
'Mopidy requires Pykka >= 1.1, < 2, but found %s' % pykka.__version__)
warnings.filterwarnings('ignore', 'could not open display')

View File

@ -9,6 +9,7 @@ import sys
import gobject
gobject.threads_init()
import pkg_resources
import pykka.debug
@ -36,8 +37,7 @@ from mopidy import exceptions, settings
from mopidy.audio import Audio
from mopidy.core import Core
from mopidy.utils import (
deps, importing, log, path, process, settings as settings_utils,
versioning)
deps, log, path, process, settings as settings_utils, versioning)
logger = logging.getLogger('mopidy.main')
@ -54,10 +54,11 @@ def main():
log.setup_logging(options.verbosity_level, options.save_debug_log)
check_old_folders()
setup_settings(options.interactive)
extensions = load_extensions()
audio = setup_audio()
backends = setup_backends(audio)
backends = setup_backends(extensions, audio)
core = setup_core(audio, backends)
setup_frontends(core)
setup_frontends(extensions, core)
loop.run()
except exceptions.SettingsError as ex:
logger.error(ex.message)
@ -67,9 +68,9 @@ def main():
logger.exception(ex)
finally:
loop.quit()
stop_frontends()
stop_frontends(extensions)
stop_core()
stop_backends()
stop_backends(extensions)
stop_audio()
process.stop_remaining_actors()
@ -138,51 +139,88 @@ def setup_settings(interactive):
sys.exit(1)
def load_extensions():
extensions = []
for entry_point in pkg_resources.iter_entry_points('mopidy.extension'):
logger.debug('Loading extension %s', entry_point.name)
# TODO Filter out disabled extensions
try:
extension_class = entry_point.load()
except pkg_resources.DistributionNotFound as ex:
logger.info(
'Disabled extension %s: Dependency %s not found',
entry_point.name, ex)
continue
extension = extension_class()
# TODO Validate configuration
try:
extension.validate_environment()
except exceptions.ExtensionError as ex:
logger.info(
'Disabled extension: %s (%s)', extension.name, ex.message)
continue
logger.info(
'Loaded extension %s: %s %s',
entry_point.name, extension.name, extension.version)
extensions.append(extension)
return extensions
def setup_audio():
logger.info('Starting Mopidy audio')
return Audio.start().proxy()
def stop_audio():
logger.info('Stopping Mopidy audio')
process.stop_actors_by_class(Audio)
def setup_backends(audio):
def setup_backends(extensions, audio):
logger.info('Starting Mopidy backends')
backends = []
for backend_class_name in settings.BACKENDS:
backend_class = importing.get_class(backend_class_name)
backend = backend_class.start(audio=audio).proxy()
backends.append(backend)
for extension in extensions:
for backend_class in extension.get_backend_classes():
backend = backend_class.start(audio=audio).proxy()
backends.append(backend)
return backends
def stop_backends():
for backend_class_name in settings.BACKENDS:
process.stop_actors_by_class(importing.get_class(backend_class_name))
def stop_backends(extensions):
logger.info('Stopping Mopidy backends')
for extension in extensions:
for backend_class in extension.get_backend_classes():
process.stop_actors_by_class(backend_class)
def setup_core(audio, backends):
logger.info('Starting Mopidy core')
return Core.start(audio=audio, backends=backends).proxy()
def stop_core():
logger.info('Stopping Mopidy core')
process.stop_actors_by_class(Core)
def setup_frontends(core):
for frontend_class_name in settings.FRONTENDS:
try:
importing.get_class(frontend_class_name).start(core=core)
except exceptions.OptionalDependencyError as ex:
logger.info('Disabled: %s (%s)', frontend_class_name, ex)
def setup_frontends(extensions, core):
logger.info('Starting Mopidy frontends')
for extension in extensions:
for frontend_class in extension.get_frontend_classes():
frontend_class.start(core=core)
def stop_frontends():
for frontend_class_name in settings.FRONTENDS:
try:
frontend_class = importing.get_class(frontend_class_name)
def stop_frontends(extensions):
logger.info('Stopping Mopidy frontends')
for extension in extensions:
for frontend_class in extension.get_frontend_classes():
process.stop_actors_by_class(frontend_class)
except exceptions.OptionalDependencyError:
pass
if __name__ == '__main__':

View File

@ -1,4 +1,10 @@
"""A backend for playing music from a local music archive.
from __future__ import unicode_literals
import mopidy
from mopidy import ext
__doc__ = """A backend for playing music from a local music archive.
This backend handles URIs starting with ``file:``.
@ -20,7 +26,21 @@ https://github.com/mopidy/mopidy/issues?labels=Local+backend
- :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE`
"""
from __future__ import unicode_literals
# flake8: noqa
from .actor import LocalBackend
class Extension(ext.Extension):
name = 'Mopidy-Local'
version = mopidy.__version__
def get_default_config(self):
return '[ext.local]'
def validate_config(self, config):
pass
def validate_environment(self):
pass
def get_backend_classes(self):
from .actor import LocalBackend
return [LocalBackend]

View File

@ -1,4 +1,39 @@
"""A backend for playing music from Spotify
from __future__ import unicode_literals
import mopidy
from mopidy import ext
from mopidy.exceptions import ExtensionError
from mopidy.utils.formatting import indent
config = """
[ext.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
# Connect to Spotify through a proxy
proxy_host =
proxy_username =
proxy_password =
"""
__doc__ = """A backend for playing music from Spotify
`Spotify <http://www.spotify.com/>`_ is a music streaming service. The backend
uses the official `libspotify
@ -22,14 +57,36 @@ https://github.com/mopidy/mopidy/issues?labels=Spotify+backend
.. literalinclude:: ../../../requirements/spotify.txt
**Settings:**
**Default config:**
- :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`
- :attr:`mopidy.settings.SPOTIFY_USERNAME`
- :attr:`mopidy.settings.SPOTIFY_PASSWORD`
"""
.. code-block:: ini
from __future__ import unicode_literals
%(config)s
""" % {'config': indent(config)}
# flake8: noqa
from .actor import SpotifyBackend
class Extension(ext.Extension):
name = 'Mopidy-Spotify'
version = mopidy.__version__
def get_default_config(self):
return config
def validate_config(self, config):
if not config.getboolean('spotify', 'enabled'):
return
if not config.get('spotify', 'username'):
raise ExtensionError('Config spotify.username not set')
if not config.get('spotify', 'password'):
raise ExtensionError('Config spotify.password not set')
def validate_environment(self):
try:
import spotify # noqa
except ImportError as e:
raise ExtensionError('pyspotify library not found', e)
def get_backend_classes(self):
from .actor import SpotifyBackend
return [SpotifyBackend]

View File

@ -169,6 +169,7 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
return
playlists = map(
translator.to_mopidy_playlist, self.session.playlist_container())
playlists.append(translator.to_mopidy_playlist(self.session.starred()))
playlists = filter(None, playlists)
self.backend.playlists.playlists = playlists
logger.info('Loaded %d Spotify playlist(s)', len(playlists))

View File

@ -71,16 +71,19 @@ def to_mopidy_playlist(spotify_playlist):
if not spotify_playlist.is_loaded():
return Playlist(uri=uri, name='[loading...]')
name = spotify_playlist.name()
tracks = [
to_mopidy_track(spotify_track)
for spotify_track in spotify_playlist
if not spotify_track.is_local()
]
if not name:
# Other user's "starred" playlists isn't handled properly by pyspotify
# See https://github.com/mopidy/pyspotify/issues/81
return
name = 'Starred'
# Tracks in the Starred playlist are in reverse order from the official
# client.
tracks.reverse()
if spotify_playlist.owner().canonical_name() != settings.SPOTIFY_USERNAME:
name += ' by ' + spotify_playlist.owner().canonical_name()
return Playlist(
uri=uri,
name=name,
tracks=[
to_mopidy_track(spotify_track)
for spotify_track in spotify_playlist
if not spotify_track.is_local()])
tracks=tracks)

View File

@ -1,4 +1,10 @@
"""A backend for playing music for streaming music.
from __future__ import unicode_literals
import mopidy
from mopidy import ext
__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
@ -17,7 +23,21 @@ https://github.com/mopidy/mopidy/issues?labels=Stream+backend
- :attr:`mopidy.settings.STREAM_PROTOCOLS`
"""
from __future__ import unicode_literals
# flake8: noqa
from .actor import StreamBackend
class Extension(ext.Extension):
name = 'Mopidy-Stream'
version = mopidy.__version__
def get_default_config(self):
return '[ext.stream]'
def validate_config(self, config):
pass
def validate_environment(self):
pass
def get_backend_classes(self):
from .actor import StreamBackend
return [StreamBackend]

View File

@ -37,3 +37,7 @@ class ConfigError(MopidyException):
class OptionalDependencyError(MopidyException):
pass
class ExtensionError(MopidyException):
pass

27
mopidy/ext.py Normal file
View File

@ -0,0 +1,27 @@
from __future__ import unicode_literals
class Extension(object):
name = None
version = None
def get_default_config(self):
raise NotImplementedError(
'Add at least a config section with "enabled = true"')
def validate_config(self, config):
raise NotImplementedError(
'You must explicitly pass config validation if not needed')
def validate_environment(self):
pass
def get_frontend_classes(self):
return []
def get_backend_classes(self):
return []
def register_gstreamer_elements(self):
pass

View File

@ -1,4 +1,11 @@
"""
from __future__ import unicode_literals
import mopidy
from mopidy import ext
from mopidy.exceptions import ExtensionError
__doc__ = """
The HTTP frontends lets you control Mopidy through HTTP and WebSockets, e.g.
from a web based client.
@ -18,8 +25,10 @@ from a web based client.
Setup
=====
When this frontend is included in :attr:`mopidy.settings.FRONTENDS`, it starts
a web server at the port specified by :attr:`mopidy.settings.HTTP_SERVER_PORT`.
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`.
.. warning:: Security
@ -357,15 +366,14 @@ event listeners, and delete the object like this:
Example to get started with
---------------------------
1. Create an empty directory for your web client.
1. Make sure that you've installed all dependencies required by the HTTP
frontend.
2. Change the setting :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point
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. Make sure that you've included
``mopidy.frontends.http.HttpFrontend`` in
:attr:`mopidy.settings.FRONTENDS`.
4. Start/restart Mopidy.
5. Create a file in the directory named ``index.html`` containing e.g. "Hello,
@ -477,5 +485,29 @@ Example to get started with
and all events that are emitted.
"""
# flake8: noqa
from .actor import HttpFrontend
class Extension(ext.Extension):
name = 'Mopidy-HTTP'
version = mopidy.__version__
def get_default_config(self):
return '[ext.http]'
def validate_config(self, config):
pass
def validate_environment(self):
try:
import cherrypy # noqa
except ImportError as e:
raise ExtensionError('Library cherrypy not found', e)
try:
import ws4py # noqa
except ImportError as e:
raise ExtensionError('Library ws4py not found', e)
def get_frontend_classes(self):
from .actor import HttpFrontend
return [HttpFrontend]

View File

@ -0,0 +1,50 @@
from __future__ import unicode_literals
import mopidy
from mopidy import ext
from mopidy.exceptions import ExtensionError
__doc__ = """
Frontend which scrobbles the music you play to your `Last.fm
<http://www.last.fm>`_ profile.
.. note::
This frontend requires a free user account at Last.fm.
**Dependencies:**
.. literalinclude:: ../../../requirements/lastfm.txt
**Settings:**
- :attr:`mopidy.settings.LASTFM_USERNAME`
- :attr:`mopidy.settings.LASTFM_PASSWORD`
**Usage:**
The frontend is enabled by default if all dependencies are available.
"""
class Extension(ext.Extension):
name = 'Mopidy-Lastfm'
version = mopidy.__version__
def get_default_config(self):
return '[ext.lastfm]'
def validate_config(self, config):
pass
def validate_environment(self):
try:
import pylast # noqa
except ImportError as e:
raise ExtensionError('pylast library not found', e)
def get_frontend_classes(self):
from .actor import LastfmFrontend
return [LastfmFrontend]

View File

@ -1,27 +1,3 @@
"""
Frontend which scrobbles the music you play to your `Last.fm
<http://www.last.fm>`_ profile.
.. note::
This frontend requires a free user account at Last.fm.
**Dependencies:**
.. literalinclude:: ../../../requirements/lastfm.txt
**Settings:**
- :attr:`mopidy.settings.LASTFM_USERNAME`
- :attr:`mopidy.settings.LASTFM_PASSWORD`
**Usage:**
Make sure :attr:`mopidy.settings.FRONTENDS` includes
``mopidy.frontends.lastfm.LastfmFrontend``. By default, the setting includes
the Last.fm frontend.
"""
from __future__ import unicode_literals
import logging

View File

@ -1,4 +1,10 @@
"""The MPD server frontend.
from __future__ import unicode_literals
import mopidy
from mopidy import ext
__doc__ = """The MPD server frontend.
MPD stands for Music Player Daemon. MPD is an independent project and server.
Mopidy implements the MPD protocol, and is thus compatible with clients for the
@ -16,9 +22,7 @@ original MPD server.
**Usage:**
Make sure :attr:`mopidy.settings.FRONTENDS` includes
``mopidy.frontends.mpd.MpdFrontend``. By default, the setting includes the MPD
frontend.
The frontend is enabled by default.
**Limitations:**
@ -44,7 +48,21 @@ near future:
- Live update of the music database is not supported
"""
from __future__ import unicode_literals
# flake8: noqa
from .actor import MpdFrontend
class Extension(ext.Extension):
name = 'Mopidy-MPD'
version = mopidy.__version__
def get_default_config(self):
return '[ext.mpd]'
def validate_config(self, config):
pass
def validate_environment(self):
pass
def get_frontend_classes(self):
from .actor import MpdFrontend
return [MpdFrontend]

View File

@ -1,4 +1,11 @@
"""
from __future__ import unicode_literals
import mopidy
from mopidy import ext
from mopidy.exceptions import ExtensionError
__doc__ = """
Frontend which lets you control Mopidy through the Media Player Remote
Interfacing Specification (`MPRIS <http://www.mpris.org/>`_) D-Bus
interface.
@ -25,9 +32,7 @@ An example of an MPRIS client is the `Ubuntu Sound Menu
**Usage:**
Make sure :attr:`mopidy.settings.FRONTENDS` includes
``mopidy.frontends.mpris.MprisFrontend``. By default, the setting includes the
MPRIS frontend.
The frontend is enabled by default if all dependencies are available.
**Testing the frontend**
@ -50,7 +55,24 @@ Now you can control Mopidy through the player object. Examples:
player.Quit(dbus_interface='org.mpris.MediaPlayer2')
"""
from __future__ import unicode_literals
# flake8: noqa
from .actor import MprisFrontend
class Extension(ext.Extension):
name = 'Mopidy-MPRIS'
version = mopidy.__version__
def get_default_config(self):
return '[ext.mpris]'
def validate_config(self, config):
pass
def validate_environment(self):
try:
import dbus # noqa
except ImportError as e:
raise ExtensionError('Library dbus not found', e)
def get_frontend_classes(self):
from .actor import MprisFrontend
return [MprisFrontend]

View File

@ -9,25 +9,6 @@ All available settings and their default values.
from __future__ import unicode_literals
#: List of playback backends to use. See :ref:`backend-implementations` for all
#: available backends.
#:
#: When results from multiple backends are combined, they are combined in the
#: order the backends are listed here.
#:
#: Default::
#:
#: BACKENDS = (
#: u'mopidy.backends.local.LocalBackend',
#: u'mopidy.backends.spotify.SpotifyBackend',
#: u'mopidy.backends.stream.StreamBackend',
#: )
BACKENDS = (
'mopidy.backends.local.LocalBackend',
'mopidy.backends.spotify.SpotifyBackend',
'mopidy.backends.stream.StreamBackend',
)
#: The log format used for informational logging.
#:
#: See http://docs.python.org/2/library/logging.html#formatter-objects for
@ -58,22 +39,6 @@ DEBUG_LOG_FILENAME = 'mopidy.log'
#: DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
DESKTOP_FILE = '/usr/share/applications/mopidy.desktop'
#: List of server frontends to use. See :ref:`frontend-implementations` for
#: available frontends.
#:
#: Default::
#:
#: FRONTENDS = (
#: u'mopidy.frontends.mpd.MpdFrontend',
#: u'mopidy.frontends.lastfm.LastfmFrontend',
#: u'mopidy.frontends.mpris.MprisFrontend',
#: )
FRONTENDS = (
'mopidy.frontends.mpd.MpdFrontend',
'mopidy.frontends.lastfm.LastfmFrontend',
'mopidy.frontends.mpris.MprisFrontend',
)
#: Which address Mopidy's HTTP server should bind to.
#:
#: Used by :mod:`mopidy.frontends.http`.

View File

@ -123,7 +123,6 @@ def validate_settings(defaults, settings):
changed = {
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
'FRONTEND': 'FRONTENDS',
'GSTREAMER_AUDIO_SINK': 'OUTPUT',
'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH',
'LOCAL_OUTPUT_OVERRIDE': 'OUTPUT',
@ -143,16 +142,9 @@ def validate_settings(defaults, settings):
}
must_be_iterable = [
'BACKENDS',
'FRONTENDS',
'STREAM_PROTOCOLS',
]
must_have_value_set = [
'BACKENDS',
'FRONTENDS',
]
for setting, value in settings.iteritems():
if setting in changed:
if changed[setting] is None:
@ -182,9 +174,6 @@ def validate_settings(defaults, settings):
'Must be a tuple. '
"Remember the comma after single values: (u'value',)")
elif setting in must_have_value_set and not value:
errors[setting] = 'Must be set.'
elif setting not in defaults and not setting.startswith('CUSTOM_'):
errors[setting] = 'Unknown setting.'
suggestion = did_you_mean(setting, defaults)

View File

@ -1,2 +1,5 @@
setuptools
# Available as python-setuptools in Debian/Ubuntu
Pykka >= 1.1
# Available as python-pykka from apt.mopidy.com

124
setup.py
View File

@ -1,101 +1,59 @@
"""
Most of this file is taken from the Django project, which is BSD licensed.
"""
from __future__ import unicode_literals
from distutils.core import setup
from distutils.command.install_data import install_data
from distutils.command.install import INSTALL_SCHEMES
import os
import re
import sys
from setuptools import setup, find_packages
def get_version():
init_py = open('mopidy/__init__.py').read()
def get_version(filename):
init_py = open(filename).read()
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", init_py))
return metadata['version']
class osx_install_data(install_data):
# On MacOS, the platform-specific lib dir is
# /System/Library/Framework/Python/.../ which is wrong. Python 2.5 supplied
# with MacOS 10.5 has an Apple-specific fix for this in
# distutils.command.install_data#306. It fixes install_lib but not
# install_data, which is why we roll our own install_data class.
def finalize_options(self):
# By the time finalize_options is called, install.install_lib is set to
# the fixed directory, so we set the installdir to install_lib. The
# install_data class uses ('install_data', 'install_dir') instead.
self.set_undefined_options('install', ('install_lib', 'install_dir'))
install_data.finalize_options(self)
if sys.platform == "darwin":
cmdclasses = {'install_data': osx_install_data}
else:
cmdclasses = {'install_data': install_data}
def fullsplit(path, result=None):
"""
Split a pathname into components (the opposite of os.path.join) in a
platform-neutral way.
"""
if result is None:
result = []
head, tail = os.path.split(path)
if head == '':
return [tail] + result
if head == path:
return result
return fullsplit(head, [tail] + result)
# Tell distutils to put the data_files in platform-specific installation
# locations. See here for an explanation:
# http://groups.google.com/group/comp.lang.python/browse_thread/
# thread/35ec7b2fed36eaec/2105ee4d9e8042cb
for scheme in INSTALL_SCHEMES.values():
scheme['data'] = scheme['purelib']
# Compile the list of packages available, because distutils doesn't have
# an easy way to do this.
packages, data_files = [], []
root_dir = os.path.dirname(__file__)
if root_dir != b'':
os.chdir(root_dir)
project_dir = b'mopidy'
for dirpath, dirnames, filenames in os.walk(project_dir):
# Ignore dirnames that start with '.'
for i, dirname in enumerate(dirnames):
if dirname.startswith(b'.'):
del dirnames[i]
if b'__init__.py' in filenames:
packages.append(b'.'.join(fullsplit(dirpath)))
elif filenames:
data_files.append([
dirpath, [os.path.join(dirpath, f) for f in filenames]])
setup(
name='Mopidy',
version=get_version(),
author='Stein Magnus Jodal',
author_email='stein.magnus@jodal.no',
packages=packages,
package_data={b'mopidy': ['backends/spotify/spotify_appkey.key']},
cmdclass=cmdclasses,
data_files=data_files,
scripts=['bin/mopidy', 'bin/mopidy-scan'],
version=get_version('mopidy/__init__.py'),
url='http://www.mopidy.com/',
license='Apache License, Version 2.0',
author='Stein Magnus Jodal',
author_email='stein.magnus@jodal.no',
description='Music server with MPD and Spotify support',
long_description=open('README.rst').read(),
packages=find_packages(exclude=['tests', 'tests.*']),
zip_safe=False,
include_package_data=True,
install_requires=[
'setuptools',
'Pykka >= 1.1',
],
extras_require={
b'spotify': ['pyspotify >= 1.9, < 1.11'],
b'lastfm': ['pylast >= 0.5.7'],
b'http': ['cherrypy >= 3.2.2', 'ws4py >= 0.2.3'],
b'external_mixers': ['pyserial'],
},
test_suite='nose.collector',
tests_require=[
'nose',
'mock >= 0.7',
'unittest2',
],
entry_points={
b'console_scripts': [
'mopidy = mopidy.__main__:main',
'mopidy-scan = mopidy.scanner:main',
],
b'mopidy.extension': [
'http = mopidy.frontends.http:Extension [http]',
'lastfm = mopidy.frontends.lastfm:Extension [lastfm]',
'local = mopidy.backends.local:Extension',
'mpd = mopidy.frontends.mpd:Extension',
'mpris = mopidy.frontends.mpris:Extension',
'spotify = mopidy.backends.spotify:Extension [spotify]',
'stream = mopidy.backends.stream:Extension',
],
},
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: No Input/Output (Daemon)',

View File

@ -1,12 +1,12 @@
from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.backends.local import actor
from tests import unittest, path_to_data_dir
from tests.backends.base import events
class LocalBackendEventsTest(events.BackendEventsTest, unittest.TestCase):
backend_class = LocalBackend
backend_class = actor.LocalBackend
def setUp(self):
settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache')

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals
from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.backends.local import actor
from tests import unittest, path_to_data_dir
from tests.backends.base.library import LibraryControllerTest
@ -9,7 +9,7 @@ from tests.backends.base.library import LibraryControllerTest
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
backend_class = LocalBackend
backend_class = actor.LocalBackend
def setUp(self):
settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals
from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.backends.local import actor
from mopidy.core import PlaybackState
from mopidy.models import Track
from mopidy.utils.path import path_to_uri
@ -12,12 +12,11 @@ from tests.backends.local import generate_song
class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
backend_class = LocalBackend
backend_class = actor.LocalBackend
tracks = [
Track(uri=generate_song(i), length=4464) for i in range(1, 4)]
def setUp(self):
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache')
super(LocalPlaybackControllerTest, self).setUp()

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
import os
from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.backends.local import actor
from mopidy.models import Track
from mopidy.utils.path import path_to_uri
@ -16,7 +16,7 @@ from tests.backends.local import generate_song
class LocalPlaylistsControllerTest(
PlaylistsControllerTest, unittest.TestCase):
backend_class = LocalBackend
backend_class = actor.LocalBackend
def setUp(self):
settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache')

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals
from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.backends.local import actor
from mopidy.models import Track
from tests import unittest, path_to_data_dir
@ -10,12 +10,11 @@ from tests.backends.local import generate_song
class LocalTracklistControllerTest(TracklistControllerTest, unittest.TestCase):
backend_class = LocalBackend
backend_class = actor.LocalBackend
tracks = [
Track(uri=generate_song(i), length=4464) for i in range(1, 4)]
def setUp(self):
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache')
super(LocalTracklistControllerTest, self).setUp()

25
tests/exceptions_test.py Normal file
View File

@ -0,0 +1,25 @@
from __future__ import unicode_literals
from mopidy import exceptions
from tests import unittest
class ExceptionsTest(unittest.TestCase):
def test_exception_can_include_message_string(self):
exc = exceptions.MopidyException('foo')
self.assertEqual(exc.message, 'foo')
self.assertEqual(str(exc), 'foo')
def test_settings_error_is_a_mopidy_exception(self):
self.assert_(issubclass(
exceptions.SettingsError, exceptions.MopidyException))
def test_optional_dependency_error_is_a_mopidy_exception(self):
self.assert_(issubclass(
exceptions.OptionalDependencyError, exceptions.MopidyException))
def test_extension_error_is_a_mopidy_exception(self):
self.assert_(issubclass(
exceptions.ExtensionError, exceptions.MopidyException))

34
tests/ext_test.py Normal file
View File

@ -0,0 +1,34 @@
from __future__ import unicode_literals
from mopidy.ext import Extension
from tests import unittest
class ExtensionTest(unittest.TestCase):
def setUp(self):
self.ext = Extension()
def test_name_is_none(self):
self.assertIsNone(self.ext.name)
def test_version_is_none(self):
self.assertIsNone(self.ext.version)
def test_get_default_config_raises_not_implemented(self):
self.assertRaises(NotImplementedError, self.ext.get_default_config)
def test_validate_config_raises_not_implemented(self):
self.assertRaises(NotImplementedError, self.ext.validate_config, None)
def test_validate_environment_does_nothing_by_default(self):
self.assertIsNone(self.ext.validate_environment())
def test_get_frontend_classes_returns_an_empty_list(self):
self.assertListEqual(self.ext.get_frontend_classes(), [])
def test_get_backend_classes_returns_an_empty_list(self):
self.assertListEqual(self.ext.get_backend_classes(), [])
def test_register_gstreamer_elements_does_nothing_by_default(self):
self.assertIsNone(self.ext.register_gstreamer_elements())

View File

@ -12,7 +12,7 @@ import mock
from mopidy.exceptions import OptionalDependencyError
try:
from mopidy.frontends.http import HttpFrontend
from mopidy.frontends.http import actor
except OptionalDependencyError:
pass
@ -24,7 +24,7 @@ from tests import unittest
@mock.patch('cherrypy.engine.publish')
class HttpEventsTest(unittest.TestCase):
def setUp(self):
self.http = HttpFrontend(core=mock.Mock())
self.http = actor.HttpFrontend(core=mock.Mock())
def test_track_playback_paused_is_broadcasted(self, publish):
publish.reset_mock()

View File

@ -8,7 +8,7 @@ from mopidy.exceptions import OptionalDependencyError
from mopidy.models import Playlist, TlTrack
try:
from mopidy.frontends.mpris import MprisFrontend, objects
from mopidy.frontends.mpris import actor, objects
except OptionalDependencyError:
pass
@ -19,7 +19,7 @@ from tests import unittest
class BackendEventsTest(unittest.TestCase):
def setUp(self):
# As a plain class, not an actor:
self.mpris_frontend = MprisFrontend(core=None)
self.mpris_frontend = actor.MprisFrontend(core=None)
self.mpris_object = mock.Mock(spec=objects.MprisObject)
self.mpris_frontend.mpris_object = self.mpris_object

View File

@ -11,8 +11,6 @@ from tests import unittest
class ValidateSettingsTest(unittest.TestCase):
def setUp(self):
self.defaults = {
'BACKENDS': ['a'],
'FRONTENDS': ['a'],
'MPD_SERVER_HOSTNAME': '::',
'MPD_SERVER_PORT': 6600,
'SPOTIFY_BITRATE': 160,
@ -75,26 +73,6 @@ class ValidateSettingsTest(unittest.TestCase):
'SPOTIFY_USERNAME', None)
self.assertEqual(None, not_secret)
def test_empty_frontends_list_returns_error(self):
result = setting_utils.validate_settings(
self.defaults, {'FRONTENDS': []})
self.assertEqual(
result['FRONTENDS'], 'Must be set.')
def test_empty_backends_list_returns_error(self):
result = setting_utils.validate_settings(
self.defaults, {'BACKENDS': []})
self.assertEqual(
result['BACKENDS'], 'Must be set.')
def test_noniterable_multivalue_setting_returns_error(self):
result = setting_utils.validate_settings(
self.defaults, {'FRONTENDS': ('this is not a tuple')})
self.assertEqual(
result['FRONTENDS'],
'Must be a tuple. '
"Remember the comma after single values: (u'value',)")
class SettingsProxyTest(unittest.TestCase):
def setUp(self):