Merge branch 'develop' into feature/audio-message-handler
This commit is contained in:
commit
d049b07fa9
@ -50,6 +50,7 @@ To get started with Mopidy, check out
|
|||||||
`the installation docs <http://docs.mopidy.com/en/latest/installation/>`_.
|
`the installation docs <http://docs.mopidy.com/en/latest/installation/>`_.
|
||||||
|
|
||||||
- `Documentation <http://docs.mopidy.com/>`_
|
- `Documentation <http://docs.mopidy.com/>`_
|
||||||
|
- `Discuss <https://discuss.mopidy.com/>`_
|
||||||
- `Source code <https://github.com/mopidy/mopidy>`_
|
- `Source code <https://github.com/mopidy/mopidy>`_
|
||||||
- `Issue tracker <https://github.com/mopidy/mopidy/issues>`_
|
- `Issue tracker <https://github.com/mopidy/mopidy/issues>`_
|
||||||
- `Development branch tarball <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>`_
|
||||||
|
|||||||
@ -6,14 +6,15 @@ Architecture and concepts
|
|||||||
|
|
||||||
The overall architecture of Mopidy is organized around multiple frontends and
|
The overall architecture of Mopidy is organized around multiple frontends and
|
||||||
backends. The frontends use the core API. The core actor makes multiple backends
|
backends. The frontends use the core API. The core actor makes multiple backends
|
||||||
work as one. The backends connect to various music sources. Both the core actor
|
work as one. The backends connect to various music sources. The core actor use
|
||||||
and the backends use the audio actor to play audio and control audio volume.
|
the mixer actor to control volume, while the backends use the audio actor to
|
||||||
|
play audio.
|
||||||
|
|
||||||
.. digraph:: overall_architecture
|
.. digraph:: overall_architecture
|
||||||
|
|
||||||
"Multiple frontends" -> Core
|
"Multiple frontends" -> Core
|
||||||
Core -> "Multiple backends"
|
Core -> "Multiple backends"
|
||||||
Core -> Audio
|
Core -> Mixer
|
||||||
"Multiple backends" -> Audio
|
"Multiple backends" -> Audio
|
||||||
|
|
||||||
|
|
||||||
@ -93,7 +94,15 @@ Audio
|
|||||||
=====
|
=====
|
||||||
|
|
||||||
The audio actor is a thin wrapper around the parts of the GStreamer library we
|
The audio actor is a thin wrapper around the parts of the GStreamer library we
|
||||||
use. In addition to playback, it's responsible for volume control through both
|
use. If you implement an advanced backend, you may need to implement your own
|
||||||
GStreamer's own volume mixers, and mixers we've created ourselves. If you
|
playback provider using the :ref:`audio-api`.
|
||||||
implement an advanced backend, you may need to implement your own playback
|
|
||||||
provider using the :ref:`audio-api`.
|
|
||||||
|
Mixer
|
||||||
|
=====
|
||||||
|
|
||||||
|
The mixer actor is responsible for volume control and muting. The default
|
||||||
|
mixer use the audio actor to control volume in software. The alternative
|
||||||
|
implementations are typically independent of the audio actor, but instead use
|
||||||
|
some third party Python library or a serial interface to control other forms
|
||||||
|
of volume controls.
|
||||||
|
|||||||
@ -13,18 +13,42 @@ v0.20.0 (UNRELEASED)
|
|||||||
- Add cover URL to all scanned files with MusicBrainz album IDs. (Fixes:
|
- Add cover URL to all scanned files with MusicBrainz album IDs. (Fixes:
|
||||||
:issue:`697`, PR: :issue:`802`)
|
:issue:`697`, PR: :issue:`802`)
|
||||||
|
|
||||||
|
**MPD frontend**
|
||||||
|
|
||||||
v0.19.4 (UNRELEASED)
|
- In stored playlist names, replace "/", which are illegal, with "|" instead of
|
||||||
|
a whitespace. Pipes are more similar to forward slash.
|
||||||
|
|
||||||
|
|
||||||
|
v0.19.4 (2014-09-01)
|
||||||
====================
|
====================
|
||||||
|
|
||||||
Bug fix release.
|
Bug fix release.
|
||||||
|
|
||||||
|
- Configuration: :option:`mopidy --config` now supports directories.
|
||||||
|
|
||||||
|
- Logging: Fix that some loggers would be disabled if
|
||||||
|
:confval:`logging/config_file` was set. (Fixes: :issue:`740`)
|
||||||
|
|
||||||
|
- Quit process with exit code 1 when stopping because of a backend, frontend,
|
||||||
|
or mixer initialization error.
|
||||||
|
|
||||||
|
- Backend API: Update :meth:`mopidy.backend.LibraryProvider.browse` signature
|
||||||
|
and docs to match how the core use the backend's browse method. (Fixes:
|
||||||
|
:issue:`833`)
|
||||||
|
|
||||||
|
- Local library API: Add :attr:`mopidy.local.Library.ROOT_DIRECTORY_URI`
|
||||||
|
constant for use by implementors of :meth:`mopidy.local.Library.browse`.
|
||||||
|
(Related to: :issue:`833`)
|
||||||
|
|
||||||
|
- HTTP frontend: Guard against double close of WebSocket, which causes an
|
||||||
|
:exc:`AttributeError` on Tornado < 3.2.
|
||||||
|
|
||||||
- MPD frontend: Make the ``list`` command return albums when sending 3
|
- MPD frontend: Make the ``list`` command return albums when sending 3
|
||||||
arguments. This was incorrectly returning artists after the MPD command
|
arguments. This was incorrectly returning artists after the MPD command
|
||||||
changes in 0.19.0. (Fixes: :issue:`817`)
|
changes in 0.19.0. (Fixes: :issue:`817`)
|
||||||
|
|
||||||
- Logging: Fix that some loggers would be disabled if
|
- MPD frontend: Fix a race condition where two threads could try to free the
|
||||||
:confval:`logging/config_file` was set. (Fixes: :issue:`740`)
|
same data simultaneously. (Fixes: :issue:`781`)
|
||||||
|
|
||||||
|
|
||||||
v0.19.3 (2014-08-03)
|
v0.19.3 (2014-08-03)
|
||||||
|
|||||||
@ -50,11 +50,12 @@ Options
|
|||||||
Save debug log to the file specified in the :confval:`logging/debug_file`
|
Save debug log to the file specified in the :confval:`logging/debug_file`
|
||||||
config value, typically ``./mopidy.log``.
|
config value, typically ``./mopidy.log``.
|
||||||
|
|
||||||
.. cmdoption:: --config <file>
|
.. cmdoption:: --config <file|directory>
|
||||||
|
|
||||||
Specify config file to use. To use multiple config files, separate them
|
Specify config files and directories to use. To use multiple config files
|
||||||
with a colon. The later files override the earlier ones if there's a
|
or directories, separate them with a colon. The later files override the
|
||||||
conflict.
|
earlier ones if there's a conflict. When specifying a directory, all files
|
||||||
|
ending in .conf in the directory are used.
|
||||||
|
|
||||||
.. cmdoption:: --option <option>, -o <option>
|
.. cmdoption:: --option <option>, -o <option>
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,7 @@ MOCK_MODULES = [
|
|||||||
'glib',
|
'glib',
|
||||||
'gobject',
|
'gobject',
|
||||||
'gst',
|
'gst',
|
||||||
|
'gst.pbutils',
|
||||||
'pygst',
|
'pygst',
|
||||||
'pykka',
|
'pykka',
|
||||||
'pykka.actor',
|
'pykka.actor',
|
||||||
|
|||||||
@ -56,15 +56,20 @@ To get started with Mopidy, start by reading :ref:`installation`.
|
|||||||
|
|
||||||
**Getting help**
|
**Getting help**
|
||||||
|
|
||||||
If you get stuck, we usually hang around at ``#mopidy`` at `irc.freenode.net
|
If you get stuck, you can get help at the `Mopidy discussion forum
|
||||||
<http://freenode.net/>`_ (with `searchable logs
|
<https://discuss.mopidy.com/>`_. We also hang around at IRC on the ``#mopidy``
|
||||||
<https://botbot.me/freenode/mopidy/>`_) and also have a `mailing list at Google
|
channel at `irc.freenode.net <http://freenode.net/>`_. The IRC channel has
|
||||||
Groups <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_. If you
|
`public searchable logs <https://botbot.me/freenode/mopidy/>`_.
|
||||||
stumble into a bug or got a feature request, please create an issue in the
|
|
||||||
`issue tracker <https://github.com/mopidy/mopidy/issues>`_. The `source code
|
If you stumble into a bug or have a feature request, please create an issue in
|
||||||
<https://github.com/mopidy/mopidy>`_ may also be of help. If you want to stay
|
the `issue tracker <https://github.com/mopidy/mopidy/issues>`_. If you're
|
||||||
up to date on Mopidy developments, you can follow `@mopidy
|
unsure if its a bug or not, ask for help in the forum or at IRC first. The
|
||||||
<https://twitter.com/mopidy/>`_ on Twitter.
|
`source code <https://github.com/mopidy/mopidy>`_ may also be of help.
|
||||||
|
|
||||||
|
If you want to stay up to date on Mopidy developments, you can follow `@mopidy
|
||||||
|
<https://twitter.com/mopidy/>`_ on Twitter. There's also a `mailing list
|
||||||
|
<https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_ used for
|
||||||
|
announcements related to Mopidy and Mopidy extensions.
|
||||||
|
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
@ -118,6 +123,7 @@ About
|
|||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
authors
|
authors
|
||||||
|
sponsors
|
||||||
changelog
|
changelog
|
||||||
versioning
|
versioning
|
||||||
|
|
||||||
|
|||||||
38
docs/sponsors.rst
Normal file
38
docs/sponsors.rst
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
.. _sponsors:
|
||||||
|
|
||||||
|
********
|
||||||
|
Sponsors
|
||||||
|
********
|
||||||
|
|
||||||
|
The Mopidy project would like to thank the following sponsors for supporting
|
||||||
|
the project.
|
||||||
|
|
||||||
|
|
||||||
|
Rackspace
|
||||||
|
=========
|
||||||
|
|
||||||
|
`Rackspace <http://www.rackspace.com/>`_ lets Mopidy use their hosting services
|
||||||
|
for free. We use their services for the following sites:
|
||||||
|
|
||||||
|
- Hosting of the APT package repository at https://apt.mopidy.com.
|
||||||
|
|
||||||
|
- Hosting of the Discourse forum at https://discuss.mopidy.com.
|
||||||
|
|
||||||
|
- Mailgun for sending emails from the Discourse forum.
|
||||||
|
|
||||||
|
- Hosting of the Jenkins CI server at https://ci.mopidy.com.
|
||||||
|
|
||||||
|
- Hosting of a Linux worker for https://ci.mopidy.com.
|
||||||
|
|
||||||
|
- Hosting of a Windows worker for https://ci.mopidy.com.
|
||||||
|
|
||||||
|
- CDN hosting at http://dl.mopidy.com, which is used to distribute Pi Musicbox
|
||||||
|
images.
|
||||||
|
|
||||||
|
|
||||||
|
GlobalSign
|
||||||
|
==========
|
||||||
|
|
||||||
|
`GlobalSign <https://www.globalsign.com/>`_ provides Mopidy with a free
|
||||||
|
wildcard SSL certificate for mopidy.com, which we use to secure access to all
|
||||||
|
our web sites.
|
||||||
@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring)
|
|||||||
warnings.filterwarnings('ignore', 'could not open display')
|
warnings.filterwarnings('ignore', 'could not open display')
|
||||||
|
|
||||||
|
|
||||||
__version__ = '0.19.3'
|
__version__ = '0.19.4'
|
||||||
|
|||||||
@ -81,12 +81,12 @@ class LibraryProvider(object):
|
|||||||
def __init__(self, backend):
|
def __init__(self, backend):
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
|
||||||
def browse(self, path):
|
def browse(self, uri):
|
||||||
"""
|
"""
|
||||||
See :meth:`mopidy.core.LibraryController.browse`.
|
See :meth:`mopidy.core.LibraryController.browse`.
|
||||||
|
|
||||||
If you implement this method, make sure to also set
|
If you implement this method, make sure to also set
|
||||||
:attr:`root_directory_name`.
|
:attr:`root_directory`.
|
||||||
|
|
||||||
*MAY be implemented by subclass.*
|
*MAY be implemented by subclass.*
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -265,6 +265,7 @@ class RootCommand(Command):
|
|||||||
backend_classes = args.registry['backend']
|
backend_classes = args.registry['backend']
|
||||||
frontend_classes = args.registry['frontend']
|
frontend_classes = args.registry['frontend']
|
||||||
|
|
||||||
|
exit_status_code = 0
|
||||||
try:
|
try:
|
||||||
mixer = self.start_mixer(config, mixer_class)
|
mixer = self.start_mixer(config, mixer_class)
|
||||||
audio = self.start_audio(config, mixer)
|
audio = self.start_audio(config, mixer)
|
||||||
@ -276,8 +277,11 @@ class RootCommand(Command):
|
|||||||
exceptions.FrontendError,
|
exceptions.FrontendError,
|
||||||
exceptions.MixerError):
|
exceptions.MixerError):
|
||||||
logger.info('Initialization error. Exiting...')
|
logger.info('Initialization error. Exiting...')
|
||||||
|
exit_status_code = 1
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('Interrupted. Exiting...')
|
logger.info('Interrupted. Exiting...')
|
||||||
|
except Exception:
|
||||||
|
logger.exception('Uncaught exception')
|
||||||
finally:
|
finally:
|
||||||
loop.quit()
|
loop.quit()
|
||||||
self.stop_frontends(frontend_classes)
|
self.stop_frontends(frontend_classes)
|
||||||
@ -286,6 +290,7 @@ class RootCommand(Command):
|
|||||||
self.stop_audio()
|
self.stop_audio()
|
||||||
self.stop_mixer(mixer_class)
|
self.stop_mixer(mixer_class)
|
||||||
process.stop_remaining_actors()
|
process.stop_remaining_actors()
|
||||||
|
return exit_status_code
|
||||||
|
|
||||||
def get_mixer_class(self, config, mixer_classes):
|
def get_mixer_class(self, config, mixer_classes):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|||||||
@ -114,27 +114,14 @@ def _load(files, defaults, overrides):
|
|||||||
|
|
||||||
# Load config from a series of config files
|
# Load config from a series of config files
|
||||||
files = [path.expand_path(f) for f in files]
|
files = [path.expand_path(f) for f in files]
|
||||||
for filename in files:
|
for name in files:
|
||||||
if not os.path.exists(filename):
|
if os.path.isdir(name):
|
||||||
logger.debug(
|
for filename in os.listdir(name):
|
||||||
'Loading config from %s failed; it does not exist', filename)
|
filename = os.path.join(name, filename)
|
||||||
continue
|
if os.path.isfile(filename) and filename.endswith('.conf'):
|
||||||
|
_load_file(parser, filename)
|
||||||
try:
|
else:
|
||||||
logger.info('Loading config from %s', filename)
|
_load_file(parser, name)
|
||||||
with io.open(filename, 'rb') as filehandle:
|
|
||||||
parser.readfp(filehandle)
|
|
||||||
except configparser.MissingSectionHeaderError as e:
|
|
||||||
logger.warning('%s does not have a config section, not loaded.',
|
|
||||||
filename)
|
|
||||||
except configparser.ParsingError as e:
|
|
||||||
linenos = ', '.join(str(lineno) for lineno, line in e.errors)
|
|
||||||
logger.warning(
|
|
||||||
'%s has errors, line %s has been ignored.', filename, linenos)
|
|
||||||
except IOError:
|
|
||||||
# TODO: if this is the initial load of logging config we might not
|
|
||||||
# have a logger at this point, we might want to handle this better.
|
|
||||||
logger.debug('Config file %s not found; skipping', filename)
|
|
||||||
|
|
||||||
# If there have been parse errors there is a python bug that causes the
|
# If there have been parse errors there is a python bug that causes the
|
||||||
# values to be lists, this little trick coerces these into strings.
|
# values to be lists, this little trick coerces these into strings.
|
||||||
@ -151,6 +138,29 @@ def _load(files, defaults, overrides):
|
|||||||
return raw_config
|
return raw_config
|
||||||
|
|
||||||
|
|
||||||
|
def _load_file(parser, filename):
|
||||||
|
if not os.path.exists(filename):
|
||||||
|
logger.debug(
|
||||||
|
'Loading config from %s failed; it does not exist', filename)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info('Loading config from %s', filename)
|
||||||
|
with io.open(filename, 'rb') as filehandle:
|
||||||
|
parser.readfp(filehandle)
|
||||||
|
except configparser.MissingSectionHeaderError as e:
|
||||||
|
logger.warning('%s does not have a config section, not loaded.',
|
||||||
|
filename)
|
||||||
|
except configparser.ParsingError as e:
|
||||||
|
linenos = ', '.join(str(lineno) for lineno, line in e.errors)
|
||||||
|
logger.warning(
|
||||||
|
'%s has errors, line %s has been ignored.', filename, linenos)
|
||||||
|
except IOError:
|
||||||
|
# TODO: if this is the initial load of logging config we might not
|
||||||
|
# have a logger at this point, we might want to handle this better.
|
||||||
|
logger.debug('Config file %s not found; skipping', filename)
|
||||||
|
|
||||||
|
|
||||||
def _validate(raw_config, schemas):
|
def _validate(raw_config, schemas):
|
||||||
# Get validated config
|
# Get validated config
|
||||||
config = {}
|
config = {}
|
||||||
|
|||||||
@ -110,7 +110,10 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
|||||||
self.request.remote_ip, response)
|
self.request.remote_ip, response)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('WebSocket request error: %s', e)
|
logger.error('WebSocket request error: %s', e)
|
||||||
self.close()
|
if self.ws_connection:
|
||||||
|
# Tornado 3.2+ checks if self.ws_connection is None before
|
||||||
|
# using it, but not older versions.
|
||||||
|
self.close()
|
||||||
|
|
||||||
def check_origin(self, origin):
|
def check_origin(self, origin):
|
||||||
# Allow cross-origin WebSocket connections, like Tornado before 4.0
|
# Allow cross-origin WebSocket connections, like Tornado before 4.0
|
||||||
|
|||||||
@ -58,17 +58,28 @@ class Library(object):
|
|||||||
:param config: Config dictionary
|
:param config: Config dictionary
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
ROOT_DIRECTORY_URI = 'local:directory'
|
||||||
|
"""
|
||||||
|
URI of the local backend's root directory.
|
||||||
|
|
||||||
|
This constant should be used by libraries implementing the
|
||||||
|
:meth:`Library.browse` method.
|
||||||
|
"""
|
||||||
|
|
||||||
#: Name of the local library implementation, must be overriden.
|
#: Name of the local library implementation, must be overriden.
|
||||||
name = None
|
name = None
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
def browse(self, path):
|
def browse(self, uri):
|
||||||
"""
|
"""
|
||||||
Browse directories and tracks at the given path.
|
Browse directories and tracks at the given URI.
|
||||||
|
|
||||||
:param string path: path to browse or None for root.
|
The URI for the root directory is a constant available at
|
||||||
|
:attr:`Library.ROOT_DIRECTORY_URI`.
|
||||||
|
|
||||||
|
:param string path: URI to browse.
|
||||||
:rtype: List of :class:`~mopidy.models.Ref` tracks and directories.
|
:rtype: List of :class:`~mopidy.models.Ref` tracks and directories.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@ -61,8 +61,8 @@ class _BrowseCache(object):
|
|||||||
splitpath_re = re.compile(r'([^/]+)')
|
splitpath_re = re.compile(r'([^/]+)')
|
||||||
|
|
||||||
def __init__(self, uris):
|
def __init__(self, uris):
|
||||||
# TODO: local.ROOT_DIRECTORY_URI
|
self._cache = {
|
||||||
self._cache = {'local:directory': collections.OrderedDict()}
|
local.Library.ROOT_DIRECTORY_URI: collections.OrderedDict()}
|
||||||
|
|
||||||
for track_uri in uris:
|
for track_uri in uris:
|
||||||
path = translator.local_track_uri_to_path(track_uri, b'/')
|
path = translator.local_track_uri_to_path(track_uri, b'/')
|
||||||
@ -97,10 +97,11 @@ class _BrowseCache(object):
|
|||||||
else:
|
else:
|
||||||
# Loop completed, so final child needs to be added to root.
|
# Loop completed, so final child needs to be added to root.
|
||||||
if child:
|
if child:
|
||||||
self._cache['local:directory'][child.uri] = child
|
self._cache[
|
||||||
|
local.Library.ROOT_DIRECTORY_URI][child.uri] = child
|
||||||
# If no parent was set we belong in the root.
|
# If no parent was set we belong in the root.
|
||||||
if not parent_uri:
|
if not parent_uri:
|
||||||
parent_uri = 'local:directory'
|
parent_uri = local.Library.ROOT_DIRECTORY_URI
|
||||||
|
|
||||||
self._cache[parent_uri][track_uri] = track_ref
|
self._cache[parent_uri][track_uri] = track_ref
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from mopidy import backend, models
|
from mopidy import backend, local, models
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -10,18 +10,18 @@ logger = logging.getLogger(__name__)
|
|||||||
class LocalLibraryProvider(backend.LibraryProvider):
|
class LocalLibraryProvider(backend.LibraryProvider):
|
||||||
"""Proxy library that delegates work to our active local library."""
|
"""Proxy library that delegates work to our active local library."""
|
||||||
|
|
||||||
root_directory = models.Ref.directory(uri=b'local:directory',
|
root_directory = models.Ref.directory(
|
||||||
name='Local media')
|
uri=local.Library.ROOT_DIRECTORY_URI, name='Local media')
|
||||||
|
|
||||||
def __init__(self, backend, library):
|
def __init__(self, backend, library):
|
||||||
super(LocalLibraryProvider, self).__init__(backend)
|
super(LocalLibraryProvider, self).__init__(backend)
|
||||||
self._library = library
|
self._library = library
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def browse(self, path):
|
def browse(self, uri):
|
||||||
if not self._library:
|
if not self._library:
|
||||||
return []
|
return []
|
||||||
return self._library.browse(path)
|
return self._library.browse(uri)
|
||||||
|
|
||||||
def refresh(self, uri=None):
|
def refresh(self, uri=None):
|
||||||
if not self._library:
|
if not self._library:
|
||||||
|
|||||||
@ -269,7 +269,7 @@ class MpdContext(object):
|
|||||||
if not playlist.name:
|
if not playlist.name:
|
||||||
continue
|
continue
|
||||||
# TODO: add scheme to name perhaps 'foo (spotify)' etc.
|
# TODO: add scheme to name perhaps 'foo (spotify)' etc.
|
||||||
name = self._invalid_playlist_chars.sub(' ', playlist.name)
|
name = self._invalid_playlist_chars.sub('|', playlist.name)
|
||||||
self.insert_name_uri_mapping(name, playlist.uri)
|
self.insert_name_uri_mapping(name, playlist.uri)
|
||||||
|
|
||||||
def lookup_playlist_from_name(self, name):
|
def lookup_playlist_from_name(self, name):
|
||||||
|
|||||||
@ -268,8 +268,8 @@ class Connection(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
self.actor_ref.tell({'close': True})
|
|
||||||
self.disable_recv()
|
self.disable_recv()
|
||||||
|
self.actor_ref.tell({'close': True})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -60,6 +60,18 @@ class LoadConfigTest(unittest.TestCase):
|
|||||||
result = config._load([file1, file2], [], [])
|
result = config._load([file1, file2], [], [])
|
||||||
self.assertEqual(expected, result)
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_load_directory(self):
|
||||||
|
directory = path_to_data_dir('conf1.d')
|
||||||
|
expected = {'foo': {'bar': 'baz'}, 'foo2': {'bar': 'baz'}}
|
||||||
|
result = config._load([directory], [], [])
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_load_directory_only_conf_files(self):
|
||||||
|
directory = path_to_data_dir('conf2.d')
|
||||||
|
expected = {'foo': {'bar': 'baz'}}
|
||||||
|
result = config._load([directory], [], [])
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
def test_load_file_with_utf8(self):
|
def test_load_file_with_utf8(self):
|
||||||
expected = {'foo': {'bar': 'æøå'.encode('utf-8')}}
|
expected = {'foo': {'bar': 'æøå'.encode('utf-8')}}
|
||||||
result = config._load([path_to_data_dir('file3.conf')], [], [])
|
result = config._load([path_to_data_dir('file3.conf')], [], [])
|
||||||
|
|||||||
2
tests/data/conf1.d/file1.conf
Normal file
2
tests/data/conf1.d/file1.conf
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[foo]
|
||||||
|
bar = baz
|
||||||
2
tests/data/conf1.d/file2.conf
Normal file
2
tests/data/conf1.d/file2.conf
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[foo2]
|
||||||
|
bar = baz
|
||||||
2
tests/data/conf2.d/file1.conf
Normal file
2
tests/data/conf2.d/file1.conf
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[foo]
|
||||||
|
bar = baz
|
||||||
2
tests/data/conf2.d/file2.conf.disabled
Normal file
2
tests/data/conf2.d/file2.conf.disabled
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[foo2]
|
||||||
|
bar = baz
|
||||||
@ -121,12 +121,12 @@ class PlaylistsHandlerTest(protocol.BaseTestCase):
|
|||||||
self.assertNotInResponse('playlist: a\r')
|
self.assertNotInResponse('playlist: a\r')
|
||||||
self.assertInResponse('OK')
|
self.assertInResponse('OK')
|
||||||
|
|
||||||
def test_listplaylists_replaces_forward_slash_with_space(self):
|
def test_listplaylists_replaces_forward_slash_with_pipe(self):
|
||||||
self.backend.playlists.playlists = [
|
self.backend.playlists.playlists = [
|
||||||
Playlist(name='a/', uri='dummy:')]
|
Playlist(name='a/b', uri='dummy:')]
|
||||||
self.sendRequest('listplaylists')
|
self.sendRequest('listplaylists')
|
||||||
self.assertInResponse('playlist: a ')
|
self.assertInResponse('playlist: a|b')
|
||||||
self.assertNotInResponse('playlist: a/')
|
self.assertNotInResponse('playlist: a/b')
|
||||||
self.assertInResponse('OK')
|
self.assertInResponse('OK')
|
||||||
|
|
||||||
def test_load_appends_to_tracklist(self):
|
def test_load_appends_to_tracklist(self):
|
||||||
|
|||||||
@ -49,5 +49,6 @@ class VersionTest(unittest.TestCase):
|
|||||||
self.assertLess(SV('0.18.3'), SV('0.19.0'))
|
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.0'), SV('0.19.1'))
|
||||||
self.assertLess(SV('0.19.1'), SV('0.19.2'))
|
self.assertLess(SV('0.19.1'), SV('0.19.2'))
|
||||||
self.assertLess(SV('0.19.2'), SV(__version__))
|
self.assertLess(SV('0.19.2'), SV('0.19.3'))
|
||||||
self.assertLess(SV(__version__), SV('0.19.4'))
|
self.assertLess(SV('0.19.3'), SV(__version__))
|
||||||
|
self.assertLess(SV(__version__), SV('0.19.5'))
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import unittest
|
|||||||
|
|
||||||
import gobject
|
import gobject
|
||||||
|
|
||||||
from mock import Mock, patch, sentinel
|
from mock import Mock, call, patch, sentinel
|
||||||
|
|
||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
@ -418,8 +418,11 @@ class ConnectionTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertTrue(network.Connection.recv_callback(
|
self.assertTrue(network.Connection.recv_callback(
|
||||||
self.mock, sentinel.fd, gobject.IO_IN))
|
self.mock, sentinel.fd, gobject.IO_IN))
|
||||||
self.mock.actor_ref.tell.assert_called_once_with({'close': True})
|
self.assertEqual(self.mock.mock_calls, [
|
||||||
self.mock.disable_recv.assert_called_once_with()
|
call.sock.recv(any_int),
|
||||||
|
call.disable_recv(),
|
||||||
|
call.actor_ref.tell({'close': True}),
|
||||||
|
])
|
||||||
|
|
||||||
def test_recv_callback_recoverable_error(self):
|
def test_recv_callback_recoverable_error(self):
|
||||||
self.mock.sock = Mock(spec=socket.SocketType)
|
self.mock.sock = Mock(spec=socket.SocketType)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user