Merge branch 'develop' into feature/audio-prep-work-for-gapless
This commit is contained in:
commit
e73159dc6c
1
.mailmap
1
.mailmap
@ -15,3 +15,4 @@ Janez Troha <janez.troha@gmail.com> <dz0ny@users.noreply.github.com>
|
||||
Janez Troha <janez.troha@gmail.com> <dz0ny@ubuntu.si>
|
||||
Luke Giuliani <luke@giuliani.com.au>
|
||||
Colin Montgomerie <kiteflyingmonkey@gmail.com>
|
||||
Ignasi Fosch <natx@y10k.ws> <ifosch@serenity-2.local>
|
||||
|
||||
@ -5,6 +5,8 @@ python:
|
||||
|
||||
env:
|
||||
- TOX_ENV=py27
|
||||
- TOX_ENV=tornado2.3
|
||||
- TOX_ENV=tornado3.2
|
||||
- TOX_ENV=docs
|
||||
- TOX_ENV=flake8
|
||||
|
||||
|
||||
1
AUTHORS
1
AUTHORS
@ -40,3 +40,4 @@
|
||||
- Pierpaolo Frasa <pfrasa@smail.uni-koeln.de>
|
||||
- Thomas Scholtes <thomas-scholtes@gmx.de>
|
||||
- Sam Willcocks <sam@wlcx.cc>
|
||||
- Ignasi Fosch <natx@y10k.ws>
|
||||
|
||||
57
README.rst
57
README.rst
@ -2,24 +2,57 @@
|
||||
Mopidy
|
||||
******
|
||||
|
||||
Mopidy is a music server which can play music both from multiple sources, like
|
||||
your local hard drive, radio streams, and from Spotify and SoundCloud. Searches
|
||||
combines results from all music sources, and you can mix tracks from all
|
||||
sources in your play queue. Your playlists from Spotify or SoundCloud are also
|
||||
available for use.
|
||||
Mopidy is an extensible music server written in Python.
|
||||
|
||||
To control your Mopidy music server, you can use one of Mopidy's web clients,
|
||||
the Ubuntu Sound Menu, any device on the same network which can control UPnP
|
||||
MediaRenderers, or any MPD client. MPD clients are available for many
|
||||
platforms, including Windows, OS X, Linux, Android and iOS.
|
||||
Mopidy plays music from local disk, Spotify, SoundCloud, Google Play Music, and
|
||||
more. You edit the playlist from any phone, tablet, or computer using a range
|
||||
of MPD and web clients.
|
||||
|
||||
To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
|
||||
**Stream music from the cloud**
|
||||
|
||||
Vanilla Mopidy only plays music from your local disk and radio streams.
|
||||
Through extensions, Mopidy can play music from cloud services like Spotify,
|
||||
SoundCloud, and Google Play Music. With Mopidy's extension support, backends
|
||||
for new music sources can be easily added.
|
||||
|
||||
**Mopidy is just a server**
|
||||
|
||||
Mopidy is a Python application that runs in a terminal or in the background on
|
||||
Linux computers or Macs that have network connectivity and audio output. Out of
|
||||
the box, Mopidy is an MPD and HTTP server. Additional frontends for controlling
|
||||
Mopidy can be installed from extensions.
|
||||
|
||||
**Everybody use their favorite client**
|
||||
|
||||
You and the people around you can all connect their favorite MPD or web client
|
||||
to the Mopidy server to search for music and manage the playlist together. With
|
||||
a browser or MPD client, which is available for all popular operating systems,
|
||||
you can control the music from any phone, tablet, or computer.
|
||||
|
||||
**Mopidy on Raspberry Pi**
|
||||
|
||||
The Raspberry Pi is a popular device to run Mopidy on, either using Raspbian or
|
||||
Arch Linux. It is quite slow, but it is very affordable. In fact, the
|
||||
Kickstarter funded Gramofon: Modern Cloud Jukebox project used Mopidy on a
|
||||
Raspberry Pi to prototype the Gramofon device. Mopidy is also a major building
|
||||
block in the Pi Musicbox integrated audio jukebox system for Raspberry Pi.
|
||||
|
||||
**Mopidy is hackable**
|
||||
|
||||
Mopidy's extension support and Python, JSON-RPC, and JavaScript APIs makes
|
||||
Mopidy perfect for building your own hacks. In one project, a Raspberry Pi was
|
||||
embedded in an old cassette player. The buttons and volume control are wired up
|
||||
with GPIO on the Raspberry Pi, and is used to control playback through a custom
|
||||
Mopidy extension. The cassettes have NFC tags used to select playlists from
|
||||
Spotify.
|
||||
|
||||
To get started with Mopidy, check out
|
||||
`the installation docs <http://docs.mopidy.com/en/latest/installation/>`_.
|
||||
|
||||
- `Documentation <http://docs.mopidy.com/>`_
|
||||
- `Source code <https://github.com/mopidy/mopidy>`_
|
||||
- `Issue tracker <https://github.com/mopidy/mopidy/issues>`_
|
||||
- `CI server <https://travis-ci.org/mopidy/mopidy>`_
|
||||
- `Download development snapshot <https://github.com/mopidy/mopidy/archive/develop.tar.gz#egg=mopidy-dev>`_
|
||||
- `Development branch tarball <https://github.com/mopidy/mopidy/archive/develop.tar.gz#egg=mopidy-dev>`_
|
||||
|
||||
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
|
||||
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
|
||||
|
||||
@ -4,20 +4,50 @@ Changelog
|
||||
|
||||
This changelog is used to track all major changes to Mopidy.
|
||||
|
||||
v0.19.1 (UNRELEASED)
|
||||
|
||||
v0.19.3 (UNRELEASED)
|
||||
====================
|
||||
|
||||
**Dependencies**
|
||||
Bug fix release.
|
||||
|
||||
- Mopidy now requires Tornado >= 2.3, instead of >= 3.1. This should make
|
||||
Mopidy continue to work on Debian/Raspbian stable, where Tornado 2.3 is the
|
||||
newest version available.
|
||||
- Audio: Fix negative track length for radio streams. (Fixes: :issue:`662`,
|
||||
PR: :issue:`796`)
|
||||
|
||||
**Development**
|
||||
- Zeroconf: Fix discovery by adding ``.local`` to the announced hostname. (PR:
|
||||
:issue:`795`)
|
||||
|
||||
- ``mopidy --version`` and :meth:`mopidy.core.Core.get_version` now returns the
|
||||
correct version when Mopidy is run from a Git repo other than Mopidy's own.
|
||||
(Related to :issue:`706`)
|
||||
|
||||
v0.19.2 (2014-07-26)
|
||||
====================
|
||||
|
||||
Bug fix release, directly from the Mopidy development sprint at EuroPython 2014
|
||||
in Berlin.
|
||||
|
||||
- Audio: Make :confval:`audio/mixer_volume` work on the software mixer again. This
|
||||
was broken with the mixer changes in 0.19.0. (Fixes: :issue:`791`)
|
||||
|
||||
- HTTP frontend: When using Tornado 4.0, allow WebSocket requests from other
|
||||
hosts. (Fixes: :issue:`788`)
|
||||
|
||||
- MPD frontend: Fix crash when MPD commands are called with the wrong number of
|
||||
arguments. This was broken with the MPD command changes in 0.19.0. (Fixes:
|
||||
:issue:`789`)
|
||||
|
||||
|
||||
v0.19.1 (2014-07-23)
|
||||
====================
|
||||
|
||||
Bug fix release.
|
||||
|
||||
- Dependencies: Mopidy now requires Tornado >= 2.3, instead of >= 3.1. This
|
||||
should make Mopidy continue to work on Debian/Raspbian stable, where Tornado
|
||||
2.3 is the newest version available.
|
||||
|
||||
- HTTP frontend: Add missing string interpolation placeholder.
|
||||
|
||||
- Development: ``mopidy --version`` and :meth:`mopidy.core.Core.get_version`
|
||||
now returns the correct version when Mopidy is run from a Git repo other than
|
||||
Mopidy's own. (Related to :issue:`706`)
|
||||
|
||||
|
||||
v0.19.0 (2014-07-21)
|
||||
|
||||
@ -5,9 +5,9 @@ Extension development
|
||||
*********************
|
||||
|
||||
Mopidy started as simply an MPD server that could play music from Spotify.
|
||||
Early on Mopidy got multiple "frontends" to expose Mopidy to more than just MPD
|
||||
clients: for example the scrobbler frontend what scrobbles what you've listened
|
||||
to to your Last.fm account, the MPRIS frontend that integrates Mopidy into the
|
||||
Early on, Mopidy got multiple "frontends" to expose Mopidy to more than just MPD
|
||||
clients: for example the scrobbler frontend that scrobbles your listening
|
||||
history to your Last.fm account, the MPRIS frontend that integrates Mopidy into the
|
||||
Ubuntu Sound Menu, and the HTTP server and JavaScript player API making web
|
||||
based Mopidy clients possible. In Mopidy 0.9 we added support for multiple
|
||||
music sources without stopping and reconfiguring Mopidy: for example the local
|
||||
@ -27,7 +27,7 @@ Anatomy of an extension
|
||||
|
||||
Extensions are located in a Python package called ``mopidy_something`` where
|
||||
"something" is the name of the application, library or web service you want to
|
||||
integrated with Mopidy. So for example if you plan to add support for a service
|
||||
integrate with Mopidy. So, for example, if you plan to add support for a service
|
||||
named Soundspot to Mopidy, you would name your extension's Python package
|
||||
``mopidy_soundspot``.
|
||||
|
||||
@ -37,10 +37,6 @@ be something like "Mopidy-Soundspot". Make sure to include the name "Mopidy"
|
||||
somewhere in that name and that you check the capitalization. This is the name
|
||||
users will use when they install your extension from PyPI.
|
||||
|
||||
Also make sure the development version link in your package details work so
|
||||
that people can easily install the development version into their virtualenv
|
||||
simply by running e.g. ``pip install Mopidy-Soundspot==dev``.
|
||||
|
||||
Mopidy extensions must be licensed under an Apache 2.0 (like Mopidy itself),
|
||||
BSD, MIT or more liberal license to be able to be enlisted in the Mopidy
|
||||
documentation. The license text should be included in the ``LICENSE`` file in
|
||||
@ -79,11 +75,11 @@ the readme of `cookiecutter-mopidy-ext
|
||||
Example README.rst
|
||||
==================
|
||||
|
||||
The README file should quickly tell what the extension does, how to install it,
|
||||
and how to configure it. The README should contain a development snapshot link
|
||||
to a tarball of the latest development version of the extension. It's important
|
||||
that the development snapshot link ends with ``#egg=Mopidy-Something-dev`` for
|
||||
installation using ``pip install Mopidy-Something==dev`` to work.
|
||||
The README file should quickly explain what the extension does, how to install
|
||||
it, and how to configure it. It should also contain a link to a tarball of the
|
||||
latest development version of the extension. It's important that this link ends
|
||||
with ``#egg=Mopidy-Something-dev`` for installation using
|
||||
``pip install Mopidy-Something==dev`` to work.
|
||||
|
||||
.. code-block:: rst
|
||||
|
||||
@ -124,7 +120,7 @@ installation using ``pip install Mopidy-Something==dev`` to work.
|
||||
|
||||
- `Source code <https://github.com/mopidy/mopidy-soundspot>`_
|
||||
- `Issue tracker <https://github.com/mopidy/mopidy-soundspot/issues>`_
|
||||
- `Download development snapshot <https://github.com/mopidy/mopidy-soundspot/tarball/master#egg=Mopidy-Soundspot-dev>`_
|
||||
- `Development branch tarball <https://github.com/mopidy/mopidy-soundspot/tarball/master#egg=Mopidy-Soundspot-dev>`_
|
||||
|
||||
|
||||
Changelog
|
||||
@ -239,9 +235,9 @@ The root of your Python package should have an ``__version__`` attribute with a
|
||||
class named ``Extension`` which inherits from Mopidy's extension base class,
|
||||
:class:`mopidy.ext.Extension`. This is the class referred to in the
|
||||
``entry_points`` part of ``setup.py``. Any imports of other files in your
|
||||
extension should be kept inside methods. This ensures that this file can be
|
||||
imported without raising :exc:`ImportError` exceptions for missing
|
||||
dependencies, etc.
|
||||
extension, outside of Mopidy and it's core requirements, should be kept inside
|
||||
methods. This ensures that this file can be imported without raising
|
||||
:exc:`ImportError` exceptions for missing dependencies, etc.
|
||||
|
||||
The default configuration for the extension is defined by the
|
||||
``get_default_config()`` method in the ``Extension`` class which returns a
|
||||
@ -252,10 +248,10 @@ an ``enabled`` config which normally 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 most secure configuration. Leave any
|
||||
configurations that doesn't have meaningful defaults blank, like ``username``
|
||||
configurations that don't have meaningful defaults blank, like ``username``
|
||||
and ``password``. In the example below, we've chosen to maintain the default
|
||||
config as a separate file named ``ext.conf``. This makes it easy to e.g.
|
||||
include the default config in documentation without duplicating it.
|
||||
config as a separate file named ``ext.conf``. This makes it easy to include the
|
||||
default config in documentation without duplicating it.
|
||||
|
||||
This is ``mopidy_soundspot/__init__.py``::
|
||||
|
||||
@ -321,6 +317,9 @@ This is ``mopidy_soundspot/__init__.py``::
|
||||
gobject.type_register(SoundspotMixer)
|
||||
gst.element_register(
|
||||
SoundspotMixer, 'soundspotmixer', gst.RANK_MARGINAL)
|
||||
|
||||
# Or nothing to register e.g. command extension
|
||||
pass
|
||||
|
||||
And this is ``mopidy_soundspot/ext.conf``:
|
||||
|
||||
@ -393,7 +392,7 @@ such as scanning for media, adding a command is the way to go. Your top level
|
||||
command name will always match your extension name, but you are free to add
|
||||
sub-commands with names of your choosing.
|
||||
|
||||
The skeleton of a commands would look like this. See :ref:`commands-api` for
|
||||
The skeleton of a command would look like this. See :ref:`commands-api` for
|
||||
more details.
|
||||
|
||||
::
|
||||
@ -409,14 +408,14 @@ more details.
|
||||
self.add_argument('--foo')
|
||||
|
||||
def run(self, args, config, extensions):
|
||||
# Your backend implementation
|
||||
# Your command implementation
|
||||
return 0
|
||||
|
||||
|
||||
Example web application
|
||||
=======================
|
||||
|
||||
As of Mopidy 0.19, extensions can use Mopidy's builtin web server to host
|
||||
As of Mopidy 0.19, extensions can use Mopidy's built-in web server to host
|
||||
static web clients as well as Tornado and WSGI web applications. For several
|
||||
examples, see the :ref:`http-server-api` docs or explore with
|
||||
:ref:`http-explore-extension` extension.
|
||||
@ -433,6 +432,17 @@ your :meth:`~mopidy.ext.Extension.setup` method register all your custom
|
||||
GStreamer elements.
|
||||
|
||||
|
||||
Running an extension
|
||||
====================
|
||||
|
||||
Once your extension is ready to go, to see it in action you'll need to register
|
||||
it with Mopidy. Typically this is done by running ``python setup.py install``
|
||||
from your extension's Git repo root directory. While developing your extension
|
||||
and to avoid doing this every time you make a change, you can instead run
|
||||
``python setup.py develop`` to effectively link Mopidy directly with your
|
||||
development files.
|
||||
|
||||
|
||||
Python conventions
|
||||
==================
|
||||
|
||||
@ -447,13 +457,13 @@ Use of Mopidy APIs
|
||||
|
||||
When writing an extension, you should only use APIs documented at
|
||||
:ref:`api-ref`. Other parts of Mopidy, like :mod:`mopidy.utils`, may change at
|
||||
any time, and is not something extensions should use.
|
||||
any time and are not something extensions should use.
|
||||
|
||||
|
||||
Logging in extensions
|
||||
=====================
|
||||
|
||||
When making servers like Mopidy, logging is essential for understanding what's
|
||||
For servers like Mopidy, logging is essential for understanding what's
|
||||
going on. We use the :mod:`logging` module from Python's standard library. When
|
||||
creating a logger, always namespace the logger using your Python package name
|
||||
as this will be visible in Mopidy's debug log::
|
||||
|
||||
@ -20,6 +20,9 @@ If you are running OS X, you can install everything needed with Homebrew.
|
||||
brew update
|
||||
brew upgrade
|
||||
|
||||
Notice that this will upgrade all software on your system that have been
|
||||
installed with Homebrew.
|
||||
|
||||
#. Mopidy works out of box if you have installed Python from Homebrew::
|
||||
|
||||
brew install python
|
||||
@ -36,6 +39,10 @@ If you are running OS X, you can install everything needed with Homebrew.
|
||||
|
||||
export PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages:$PYTHONPATH
|
||||
|
||||
And then reload the shell's init file or restart your terminal::
|
||||
|
||||
source ~/.bashrc
|
||||
|
||||
Or, you can prefix the Mopidy command every time you run it::
|
||||
|
||||
PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages mopidy
|
||||
|
||||
@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring)
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '0.19.0'
|
||||
__version__ = '0.19.2'
|
||||
|
||||
@ -221,6 +221,14 @@ class Audio(pykka.ThreadingActor):
|
||||
self._connect(self._playbin, 'notify::volume', self._on_mixer_change)
|
||||
self._connect(self._playbin, 'notify::mute', self._on_mixer_change)
|
||||
|
||||
# The Mopidy startup procedure will set the initial volume of a mixer,
|
||||
# but this happens before the audio actor is injected into the software
|
||||
# mixer and has no effect. Thus, we need to set the initial volume
|
||||
# again.
|
||||
initial_volume = self._config['audio']['mixer_volume']
|
||||
if initial_volume is not None:
|
||||
self._mixer.set_volume(initial_volume)
|
||||
|
||||
def _on_mixer_change(self, element, gparamspec):
|
||||
self._mixer.trigger_events_for_changed_values()
|
||||
|
||||
|
||||
@ -150,6 +150,18 @@ def _date(tags):
|
||||
return None
|
||||
|
||||
|
||||
def add_musicbrainz_cover_art(track):
|
||||
if track.album and track.album.musicbrainz_id:
|
||||
base = "http://coverartarchive.org/release"
|
||||
images = frozenset(
|
||||
["{}/{}/front".format(
|
||||
base,
|
||||
track.album.musicbrainz_id)])
|
||||
album = track.album.copy(images=images)
|
||||
track = track.copy(album=album)
|
||||
return track
|
||||
|
||||
|
||||
def audio_data_to_track(data):
|
||||
"""Convert taglist data + our extras to a track."""
|
||||
tags = data['tags']
|
||||
@ -186,7 +198,8 @@ def audio_data_to_track(data):
|
||||
|
||||
track_kwargs['date'] = _date(tags)
|
||||
track_kwargs['last_modified'] = int(data.get('mtime') or 0)
|
||||
track_kwargs['length'] = (data.get(gst.TAG_DURATION) or 0) // gst.MSECOND
|
||||
track_kwargs['length'] = max(
|
||||
0, (data.get(gst.TAG_DURATION) or 0)) // gst.MSECOND
|
||||
|
||||
# Clear out any empty values we found
|
||||
track_kwargs = {k: v for k, v in track_kwargs.items() if v}
|
||||
|
||||
@ -219,7 +219,7 @@ class Command(object):
|
||||
def run(self, *args, **kwargs):
|
||||
"""Run the command.
|
||||
|
||||
Must be implemented by sub-classes that are not simply and intermediate
|
||||
Must be implemented by sub-classes that are not simply an intermediate
|
||||
in the command namespace.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -58,10 +58,10 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
||||
if self.zeroconf_name:
|
||||
self.zeroconf_http = zeroconf.Zeroconf(
|
||||
stype='_http._tcp', name=self.zeroconf_name,
|
||||
host=self.hostname, port=self.port)
|
||||
port=self.port)
|
||||
self.zeroconf_mopidy_http = zeroconf.Zeroconf(
|
||||
stype='_mopidy-http._tcp', name=self.zeroconf_name,
|
||||
host=self.hostname, port=self.port)
|
||||
port=self.port)
|
||||
self.zeroconf_http.publish()
|
||||
self.zeroconf_mopidy_http.publish()
|
||||
|
||||
|
||||
@ -112,6 +112,11 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||
logger.error('WebSocket request error: %s', e)
|
||||
self.close()
|
||||
|
||||
def check_origin(self, origin):
|
||||
# Allow cross-origin WebSocket connections, like Tornado before 4.0
|
||||
# defaulted to.
|
||||
return True
|
||||
|
||||
|
||||
def set_mopidy_headers(request_handler):
|
||||
request_handler.set_header('Cache-Control', 'no-cache')
|
||||
@ -144,7 +149,7 @@ class JsonRpcHandler(tornado.web.RequestHandler):
|
||||
'Sent RPC message to %s: %r',
|
||||
self.request.remote_ip, response)
|
||||
except Exception as e:
|
||||
logger.error('HTTP JSON-RPC request error:', e)
|
||||
logger.error('HTTP JSON-RPC request error: %s', e)
|
||||
self.write_error(500)
|
||||
|
||||
def set_extra_headers(self):
|
||||
|
||||
@ -118,7 +118,8 @@ class ScanCommand(commands.Command):
|
||||
relpath = translator.local_track_uri_to_path(uri, media_dir)
|
||||
file_uri = path.path_to_uri(os.path.join(media_dir, relpath))
|
||||
data = scanner.scan(file_uri)
|
||||
track = scan.audio_data_to_track(data).copy(uri=uri)
|
||||
track = scan.add_musicbrainz_cover_art(
|
||||
scan.audio_data_to_track(data).copy(uri=uri)).copy(uri=uri)
|
||||
library.add(track)
|
||||
logger.debug('Added %s', track.uri)
|
||||
except exceptions.ScannerError as error:
|
||||
|
||||
@ -43,7 +43,7 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
|
||||
if self.zeroconf_name:
|
||||
self.zeroconf_service = zeroconf.Zeroconf(
|
||||
stype='_mpd._tcp', name=self.zeroconf_name,
|
||||
host=self.hostname, port=self.port)
|
||||
port=self.port)
|
||||
self.zeroconf_service.publish()
|
||||
|
||||
def on_stop(self):
|
||||
|
||||
@ -138,7 +138,13 @@ class Commands(object):
|
||||
def validate(*args, **kwargs):
|
||||
if varargs:
|
||||
return func(*args, **kwargs)
|
||||
callargs = inspect.getcallargs(func, *args, **kwargs)
|
||||
|
||||
try:
|
||||
callargs = inspect.getcallargs(func, *args, **kwargs)
|
||||
except TypeError:
|
||||
raise exceptions.MpdArgError(
|
||||
'wrong number of arguments for "%s"' % name)
|
||||
|
||||
for key, value in callargs.items():
|
||||
default = defaults.get(key, object())
|
||||
if key in validators and value != default:
|
||||
@ -146,6 +152,7 @@ class Commands(object):
|
||||
callargs[key] = validators[key](value)
|
||||
except ValueError:
|
||||
raise exceptions.MpdArgError('incorrect arguments')
|
||||
|
||||
return func(**callargs)
|
||||
|
||||
validate.auth_required = auth_required
|
||||
|
||||
@ -143,21 +143,23 @@ def _gstreamer_check_elements():
|
||||
# Spotify
|
||||
'appsrc',
|
||||
|
||||
# Mixers and sinks
|
||||
'alsamixer',
|
||||
# Audio sinks
|
||||
'alsasink',
|
||||
'ossmixer',
|
||||
'osssink',
|
||||
'oss4mixer',
|
||||
'oss4sink',
|
||||
'pulsemixer',
|
||||
'pulsesink',
|
||||
|
||||
# MP3 encoding and decoding
|
||||
'mp3parse',
|
||||
#
|
||||
# One of flump3dec, mad, and mpg123audiodec is required for MP3
|
||||
# playback.
|
||||
'flump3dec',
|
||||
'id3demux',
|
||||
'id3v2mux',
|
||||
'lame',
|
||||
'mad',
|
||||
'mp3parse',
|
||||
# 'mpg123audiodec', # Only available in GStreamer 1.x
|
||||
|
||||
# Ogg Vorbis encoding and decoding
|
||||
'vorbisdec',
|
||||
|
||||
@ -43,21 +43,17 @@ class Zeroconf(object):
|
||||
:type text: list of str
|
||||
"""
|
||||
|
||||
def __init__(self, name, port, stype=None, domain=None,
|
||||
host=None, text=None):
|
||||
def __init__(self, name, port, stype=None, domain=None, text=None):
|
||||
self.group = None
|
||||
self.stype = stype or '_http._tcp'
|
||||
self.domain = domain or ''
|
||||
self.port = port
|
||||
self.text = text or []
|
||||
if host in ('::', '0.0.0.0'):
|
||||
self.host = ''
|
||||
else:
|
||||
self.host = host
|
||||
|
||||
template = string.Template(name)
|
||||
self.name = template.safe_substitute(
|
||||
hostname=self.host or socket.getfqdn(), port=self.port)
|
||||
hostname=socket.getfqdn(), port=self.port)
|
||||
self.host = '%s.local' % socket.getfqdn()
|
||||
|
||||
def __str__(self):
|
||||
return 'Zeroconf service %s at [%s]:%d' % (
|
||||
|
||||
@ -71,6 +71,11 @@ class TranslatorTest(unittest.TestCase):
|
||||
actual = scan.audio_data_to_track(self.data)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def check_local(self, expected):
|
||||
actual = scan.add_musicbrainz_cover_art(
|
||||
scan.audio_data_to_track(self.data))
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_track(self):
|
||||
self.check(self.track)
|
||||
|
||||
@ -197,13 +202,20 @@ class TranslatorTest(unittest.TestCase):
|
||||
|
||||
def test_missing_album_musicbrainz_id(self):
|
||||
del self.data['tags']['musicbrainz-albumid']
|
||||
album = self.track.album.copy(musicbrainz_id=None)
|
||||
album = self.track.album.copy(musicbrainz_id=None,
|
||||
images=[])
|
||||
self.check(self.track.copy(album=album))
|
||||
|
||||
def test_multiple_album_musicbrainz_id(self):
|
||||
self.data['tags']['musicbrainz-albumid'].append('id')
|
||||
self.check(self.track)
|
||||
|
||||
def test_album_musicbrainz_id_cover(self):
|
||||
album = self.track.album.copy(
|
||||
images=frozenset(
|
||||
['http://coverartarchive.org/release/albumid/front']))
|
||||
self.check_local(self.track.copy(album=album))
|
||||
|
||||
def test_missing_album_num_tracks(self):
|
||||
del self.data['tags']['track-count']
|
||||
album = self.track.album.copy(num_tracks=None)
|
||||
|
||||
@ -19,6 +19,12 @@ class AuthenticationActiveTest(protocol.BaseTestCase):
|
||||
self.assertFalse(self.dispatcher.authenticated)
|
||||
self.assertEqualResponse('ACK [3@0] {password} incorrect password')
|
||||
|
||||
def test_authentication_without_password_fails(self):
|
||||
self.sendRequest('password')
|
||||
self.assertFalse(self.dispatcher.authenticated)
|
||||
self.assertEqualResponse(
|
||||
'ACK [2@0] {password} wrong number of arguments for "password"')
|
||||
|
||||
def test_anything_when_not_authenticated_should_fail(self):
|
||||
self.sendRequest('any request at all')
|
||||
self.assertFalse(self.dispatcher.authenticated)
|
||||
|
||||
@ -169,15 +169,15 @@ class TestCommands(unittest.TestCase):
|
||||
|
||||
def test_call_incorrect_args(self):
|
||||
self.commands.add('foo')(lambda context: context)
|
||||
with self.assertRaises(TypeError):
|
||||
with self.assertRaises(exceptions.MpdArgError):
|
||||
self.commands.call(['foo', 'bar'])
|
||||
|
||||
self.commands.add('bar')(lambda context, required: context)
|
||||
with self.assertRaises(TypeError):
|
||||
with self.assertRaises(exceptions.MpdArgError):
|
||||
self.commands.call(['bar', 'bar', 'baz'])
|
||||
|
||||
self.commands.add('baz')(lambda context, optional=None: context)
|
||||
with self.assertRaises(TypeError):
|
||||
with self.assertRaises(exceptions.MpdArgError):
|
||||
self.commands.call(['baz', 'bar', 'baz'])
|
||||
|
||||
def test_validator_gets_applied_to_required_arg(self):
|
||||
|
||||
@ -14,7 +14,10 @@ class HelpTest(unittest.TestCase):
|
||||
args = [sys.executable, mopidy_dir, '--help']
|
||||
process = subprocess.Popen(
|
||||
args,
|
||||
env={'PYTHONPATH': os.path.join(mopidy_dir, '..')},
|
||||
env={'PYTHONPATH': ':'.join([
|
||||
os.path.join(mopidy_dir, '..'),
|
||||
os.environ.get('PYTHONPATH', '')
|
||||
])},
|
||||
stdout=subprocess.PIPE)
|
||||
output = process.communicate()[0]
|
||||
self.assertIn('--version', output)
|
||||
|
||||
@ -46,5 +46,7 @@ class VersionTest(unittest.TestCase):
|
||||
self.assertLess(SV('0.18.0'), SV('0.18.1'))
|
||||
self.assertLess(SV('0.18.1'), SV('0.18.2'))
|
||||
self.assertLess(SV('0.18.2'), SV('0.18.3'))
|
||||
self.assertLess(SV('0.18.3'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.19.1'))
|
||||
self.assertLess(SV('0.18.3'), SV('0.19.0'))
|
||||
self.assertLess(SV('0.19.0'), SV('0.19.1'))
|
||||
self.assertLess(SV('0.19.1'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.19.3'))
|
||||
|
||||
16
tox.ini
16
tox.ini
@ -1,13 +1,25 @@
|
||||
[tox]
|
||||
envlist = py27, docs, flake8
|
||||
envlist = py27, py27-tornado23, py27-tornado31, docs, flake8
|
||||
|
||||
[testenv]
|
||||
sitepackages = true
|
||||
commands = nosetests -v --with-xunit --xunit-file=xunit-{envname}.xml --with-coverage --cover-package=mopidy
|
||||
deps =
|
||||
coverage
|
||||
mock
|
||||
nose
|
||||
commands = nosetests -v --with-xunit --xunit-file=xunit-{envname}.xml --with-coverage --cover-package=mopidy
|
||||
|
||||
[testenv:py27-tornado23]
|
||||
commands = nosetests -v tests/http
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
tornado==2.3
|
||||
|
||||
[testenv:py27-tornado31]
|
||||
commands = nosetests -v tests/http
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
tornado==3.1
|
||||
|
||||
[testenv:docs]
|
||||
deps = -r{toxinidir}/docs/requirements.txt
|
||||
|
||||
Loading…
Reference in New Issue
Block a user