Merge branch 'develop' into feature/http-helpers
Conflicts: docs/changelog.rst
This commit is contained in:
commit
f6f445e4b6
@ -1,8 +1,8 @@
|
||||
.. _concepts:
|
||||
|
||||
*************************
|
||||
Architecture and concepts
|
||||
*************************
|
||||
************
|
||||
Architecture
|
||||
************
|
||||
|
||||
The overall architecture of Mopidy is organized around multiple frontends and
|
||||
backends. The frontends use the core API. The core actor makes multiple backends
|
||||
@ -1,8 +1,8 @@
|
||||
.. _audio-api:
|
||||
|
||||
*********
|
||||
Audio API
|
||||
*********
|
||||
*********************************
|
||||
:mod:`mopidy.audio` --- Audio API
|
||||
*********************************
|
||||
|
||||
.. module:: mopidy.audio
|
||||
:synopsis: Thin wrapper around the parts of GStreamer we use
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
.. _backend-api:
|
||||
|
||||
***********
|
||||
Backend API
|
||||
***********
|
||||
*************************************
|
||||
:mod:`mopidy.backend` --- Backend API
|
||||
*************************************
|
||||
|
||||
.. module:: mopidy.backend
|
||||
:synopsis: The API implemented by backends
|
||||
@ -1,8 +1,8 @@
|
||||
.. _commands-api:
|
||||
|
||||
************
|
||||
Commands API
|
||||
************
|
||||
***************************************
|
||||
:mod:`mopidy.commands` --- Commands API
|
||||
***************************************
|
||||
|
||||
.. automodule:: mopidy.commands
|
||||
:synopsis: Commands API for Mopidy CLI.
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
.. _config-api:
|
||||
|
||||
**********
|
||||
Config API
|
||||
**********
|
||||
***********************************
|
||||
:mod:`mopidy.config` --- Config API
|
||||
***********************************
|
||||
|
||||
.. automodule:: mopidy.config
|
||||
:synopsis: Config API for config loading and validation
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
.. _core-api:
|
||||
|
||||
********
|
||||
Core API
|
||||
********
|
||||
*******************************
|
||||
:mod:`mopidy.core` --- Core API
|
||||
*******************************
|
||||
|
||||
.. module:: mopidy.core
|
||||
:synopsis: Core API for use by frontends
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
.. _ext-api:
|
||||
|
||||
*************
|
||||
Extension API
|
||||
*************
|
||||
**********************************
|
||||
:mod:`mopidy.ext` -- Extension API
|
||||
**********************************
|
||||
|
||||
If you want to learn how to make Mopidy extensions, read :ref:`extensiondev`.
|
||||
|
||||
|
||||
@ -25,6 +25,8 @@ For details on how to make a Mopidy extension, see the :ref:`extensiondev`
|
||||
guide.
|
||||
|
||||
|
||||
.. _static-web-client:
|
||||
|
||||
Static web client example
|
||||
=========================
|
||||
|
||||
|
||||
@ -4,9 +4,6 @@
|
||||
HTTP JSON-RPC API
|
||||
*****************
|
||||
|
||||
.. module:: mopidy.http
|
||||
:synopsis: The HTTP frontend APIs
|
||||
|
||||
The :ref:`ext-http` extension makes Mopidy's :ref:`core-api` available using
|
||||
JSON-RPC over HTTP using HTTP POST and WebSockets. We also provide a JavaScript
|
||||
wrapper, called :ref:`Mopidy.js <mopidy-js>`, around the JSON-RPC over
|
||||
@ -65,14 +62,9 @@ JSON-RPC 2.0 messages can be recognized by checking for the key named
|
||||
please refer to the `JSON-RPC 2.0 spec
|
||||
<http://www.jsonrpc.org/specification>`_.
|
||||
|
||||
All methods (not attributes) in the :ref:`core-api` is made available through
|
||||
JSON-RPC calls over the WebSocket. For example,
|
||||
:meth:`mopidy.core.PlaybackController.play` is available as the JSON-RPC method
|
||||
``core.playback.play``.
|
||||
|
||||
The core API's attributes is made available through setters and getters. For
|
||||
example, the attribute :attr:`mopidy.core.PlaybackController.current_track` is
|
||||
available as the JSON-RPC method ``core.playback.get_current_track``.
|
||||
All methods in the :ref:`core-api` is made available through JSON-RPC calls
|
||||
over the WebSocket. For example, :meth:`mopidy.core.PlaybackController.play` is
|
||||
available as the JSON-RPC method ``core.playback.play``.
|
||||
|
||||
Example JSON-RPC request::
|
||||
|
||||
|
||||
@ -4,26 +4,56 @@
|
||||
API reference
|
||||
*************
|
||||
|
||||
.. note:: What is public?
|
||||
.. note::
|
||||
|
||||
Only APIs documented here are public and open for use by Mopidy
|
||||
extensions.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
Concepts
|
||||
========
|
||||
|
||||
concepts
|
||||
.. toctree::
|
||||
|
||||
architecture
|
||||
models
|
||||
backends
|
||||
|
||||
|
||||
Basics
|
||||
======
|
||||
|
||||
.. toctree::
|
||||
|
||||
core
|
||||
audio
|
||||
mixer
|
||||
frontends
|
||||
commands
|
||||
frontend
|
||||
backend
|
||||
ext
|
||||
config
|
||||
zeroconf
|
||||
|
||||
|
||||
Web/JavaScript
|
||||
==============
|
||||
|
||||
.. toctree::
|
||||
|
||||
http-server
|
||||
http
|
||||
js
|
||||
|
||||
|
||||
Audio
|
||||
=====
|
||||
|
||||
.. toctree::
|
||||
|
||||
audio
|
||||
mixer
|
||||
|
||||
|
||||
Utilities
|
||||
=========
|
||||
|
||||
.. toctree::
|
||||
|
||||
config
|
||||
commands
|
||||
zeroconf
|
||||
|
||||
@ -21,9 +21,9 @@ available at:
|
||||
|
||||
You may need to adjust hostname and port for your local setup.
|
||||
|
||||
Thus, if you use Mopidy to host your web client, like described above, you can
|
||||
load the latest version of Mopidy.js by adding the following script tag to your
|
||||
HTML file:
|
||||
Thus, if you use Mopidy to host your web client, like described in
|
||||
:ref:`static-web-client`, you can load the latest version of Mopidy.js by
|
||||
adding the following script tag to your HTML file:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
@ -189,13 +189,10 @@ you've hooked up an errback (more on that a bit later) to the promise returned
|
||||
from the call, the errback will be called with a ``Mopidy.ConnectionError``
|
||||
instance.
|
||||
|
||||
All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. The core
|
||||
API attributes is *not* available, but that shouldn't be a problem as we've
|
||||
added (undocumented) getters and setters for all of them, so you can access the
|
||||
attributes as well from JavaScript. For example, the
|
||||
:attr:`mopidy.core.PlaybackController.state` attribute is available in
|
||||
JSON-RPC as the method ``core.playback.get_state`` and in Mopidy.js as
|
||||
``mopidy.playback.getState()``.
|
||||
All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. For
|
||||
example, the :meth:`mopidy.core.PlaybackController.get_state` method is
|
||||
available in JSON-RPC as the method ``core.playback.get_state`` and in
|
||||
Mopidy.js as ``mopidy.playback.getState()``.
|
||||
|
||||
Both the WebSocket API and the JavaScript API are based on introspection of the
|
||||
core Python API. Thus, they will always be up to date and immediately reflect
|
||||
@ -218,8 +215,7 @@ by looking at the method's ``description`` and ``params`` attributes:
|
||||
|
||||
JSON-RPC 2.0 limits method parameters to be sent *either* by-position or
|
||||
by-name. Combinations of both, like we're used to from Python, isn't supported
|
||||
by JSON-RPC 2.0. To further limit this, Mopidy.js currently only supports
|
||||
passing parameters by-position.
|
||||
by JSON-RPC 2.0.
|
||||
|
||||
Obviously, you'll want to get a return value from many of your method calls.
|
||||
Since everything is happening across the WebSocket and maybe even across the
|
||||
@ -272,8 +268,9 @@ passing it as the second argument to ``done()``:
|
||||
.done(printCurrentTrack, console.error.bind(console));
|
||||
|
||||
If you don't hook up an error handler function and never call ``done()`` on the
|
||||
promise object, when.js will log warnings to the console that you have
|
||||
unhandled errors. In general, unhandled errors will not go silently missing.
|
||||
promise object, warnings will be logged to the console complaining that you
|
||||
have unhandled errors. In general, unhandled errors will not go silently
|
||||
missing.
|
||||
|
||||
The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A
|
||||
<http://wiki.commonjs.org/wiki/Promises/A>`_ standard. We use the
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
.. _mixer-api:
|
||||
|
||||
***************
|
||||
Audio mixer API
|
||||
***************
|
||||
***************************************
|
||||
:mod:`mopidy.mixer` --- Audio mixer API
|
||||
***************************************
|
||||
|
||||
.. module:: mopidy.mixer
|
||||
:synopsis: The audio mixer API
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
***********
|
||||
Data models
|
||||
***********
|
||||
************************************
|
||||
:mod:`mopidy.models` --- Data models
|
||||
************************************
|
||||
|
||||
These immutable data models are used for all data transfer within the Mopidy
|
||||
backends and between the backends and the MPD frontend. All fields are optional
|
||||
@ -77,8 +77,29 @@ Data model helpers
|
||||
.. autoclass:: mopidy.models.ImmutableObject
|
||||
:members:
|
||||
|
||||
.. autoclass:: mopidy.models.Field
|
||||
.. autoclass:: mopidy.models.ValidatedImmutableObject
|
||||
:members: replace
|
||||
|
||||
Data model (de)serialization
|
||||
----------------------------
|
||||
|
||||
.. autofunction:: mopidy.models.model_json_decoder
|
||||
|
||||
.. autoclass:: mopidy.models.ModelJSONEncoder
|
||||
|
||||
.. autofunction:: mopidy.models.model_json_decoder
|
||||
Data model field types
|
||||
----------------------
|
||||
|
||||
.. autoclass:: mopidy.models.fields.Field
|
||||
|
||||
.. autoclass:: mopidy.models.fields.String
|
||||
|
||||
.. autoclass:: mopidy.models.fields.Identifier
|
||||
|
||||
.. autoclass:: mopidy.models.fields.URI
|
||||
|
||||
.. autoclass:: mopidy.models.fields.Date
|
||||
|
||||
.. autoclass:: mopidy.models.fields.Integer
|
||||
|
||||
.. autoclass:: mopidy.models.fields.Collection
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
.. _zeroconf-api:
|
||||
|
||||
************
|
||||
Zeroconf API
|
||||
************
|
||||
***************************************
|
||||
:mod:`mopidy.zeroconf` --- Zeroconf API
|
||||
***************************************
|
||||
|
||||
.. module:: mopidy.zeroconf
|
||||
:synopsis: Helper for publishing of services on Zeroconf
|
||||
|
||||
@ -12,10 +12,11 @@ Core API
|
||||
|
||||
- Calling the following methods with ``kwargs`` is being deprecated.
|
||||
(PR: :issue:`1090`)
|
||||
- :meth:`mopidy.core.library.LibraryController.search`
|
||||
- :meth:`mopidy.core.library.PlaylistsController.filter`
|
||||
- :meth:`mopidy.core.library.TracklistController.filter`
|
||||
- :meth:`mopidy.core.library.TracklistController.remove`
|
||||
|
||||
- :meth:`mopidy.core.library.LibraryController.search`
|
||||
- :meth:`mopidy.core.library.PlaylistsController.filter`
|
||||
- :meth:`mopidy.core.library.TracklistController.filter`
|
||||
- :meth:`mopidy.core.library.TracklistController.remove`
|
||||
|
||||
- Updated core controllers to handle backend exceptions in all calls that rely
|
||||
on multiple backends. (Issue: :issue:`667`)
|
||||
@ -27,6 +28,10 @@ Core API
|
||||
``tl_track`` versions of the calls. (Fixes: :issue:`1131` PR: :issue:`1136`,
|
||||
:issue:`1140`)
|
||||
|
||||
- Add :meth:`mopidy.core.playback.PlaybackController.get_current_tlid`.
|
||||
(Part of: :issue:`1137`)
|
||||
|
||||
- Update core to handle backend crashes and bad data. (Fixes: :issue:`1161`)
|
||||
|
||||
Models
|
||||
------
|
||||
@ -58,6 +63,26 @@ Internal changes
|
||||
:issue:`1115`)
|
||||
|
||||
|
||||
v1.0.5 (UNRELEASED)
|
||||
===================
|
||||
|
||||
Bug fix release.
|
||||
|
||||
- Core: Add workaround for playlist providers that do not support
|
||||
creating playlists. (Fixes: :issue:`1162`, PR :issue:`1165`)
|
||||
|
||||
|
||||
v1.0.4 (2015-04-30)
|
||||
===================
|
||||
|
||||
Bug fix release.
|
||||
|
||||
- Audio: Since all previous attempts at tweaking the queuing for :issue:`1097`
|
||||
seems to break things in subtle ways for different users. We are giving up
|
||||
on tweaking the defaults and just going to live with a bit more lag on
|
||||
software volume changes. (Fixes: :issue:`1147`)
|
||||
|
||||
|
||||
v1.0.3 (2015-04-28)
|
||||
===================
|
||||
|
||||
@ -69,7 +94,7 @@ Bug fix release.
|
||||
|
||||
- Audio: Follow-up fix for :issue:`1097` still exhibits issues for certain
|
||||
setups. We are giving this get an other go by setting the buffer size to
|
||||
maximum 100ms instead of a fixed number of buffers. (Fixes: :issue:`1147`,
|
||||
maximum 100ms instead of a fixed number of buffers. (Addresses: :issue:`1147`,
|
||||
PR: :issue:`1154`)
|
||||
|
||||
|
||||
@ -83,7 +108,7 @@ Bug fix release.
|
||||
|
||||
- Audio: Fix for :issue:`1097` tuned down the buffer size in the queue. Turns
|
||||
out this can cause distortions in certain cases. Give this an other go with
|
||||
a more generous buffer size. (Fixes: :issue:`1147`, PR: :issue:`1152`)
|
||||
a more generous buffer size. (Addresses: :issue:`1147`, PR: :issue:`1152`)
|
||||
|
||||
- Audio: Make sure mute events get emitted by software mixer.
|
||||
(Fixes: :issue:`1146`, PR: :issue:`1152`)
|
||||
|
||||
@ -47,7 +47,6 @@ class Mock(object):
|
||||
return Mock()
|
||||
|
||||
MOCK_MODULES = [
|
||||
'cherrypy',
|
||||
'dbus',
|
||||
'dbus.mainloop',
|
||||
'dbus.mainloop.glib',
|
||||
@ -61,12 +60,6 @@ MOCK_MODULES = [
|
||||
'pykka.actor',
|
||||
'pykka.future',
|
||||
'pykka.registry',
|
||||
'pylast',
|
||||
'ws4py',
|
||||
'ws4py.messaging',
|
||||
'ws4py.server',
|
||||
'ws4py.server.cherrypyserver',
|
||||
'ws4py.websocket',
|
||||
]
|
||||
for mod_name in MOCK_MODULES:
|
||||
sys.modules[mod_name] = Mock()
|
||||
@ -102,7 +95,7 @@ master_doc = 'index'
|
||||
project = 'Mopidy'
|
||||
copyright = '2009-2015, Stein Magnus Jodal and contributors'
|
||||
|
||||
from mopidy.utils.versioning import get_version
|
||||
from mopidy.internal.versioning import get_version
|
||||
release = get_version()
|
||||
version = '.'.join(release.split('.')[:2])
|
||||
|
||||
|
||||
@ -434,8 +434,8 @@ 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 are not something extensions should use.
|
||||
:ref:`api-ref`. Other parts of Mopidy, like :mod:`mopidy.internal`, may change
|
||||
at any time and are not something extensions should use.
|
||||
|
||||
|
||||
Logging in extensions
|
||||
|
||||
@ -1,9 +1,23 @@
|
||||
************************************
|
||||
:mod:`mopidy.local` -- Local backend
|
||||
************************************
|
||||
*************************************
|
||||
:mod:`mopidy.local` --- Local backend
|
||||
*************************************
|
||||
|
||||
For details on how to use Mopidy's local backend, see :ref:`ext-local`.
|
||||
|
||||
.. automodule:: mopidy.local
|
||||
:synopsis: Local backend
|
||||
|
||||
|
||||
Local library API
|
||||
=================
|
||||
|
||||
.. autoclass:: mopidy.local.Library
|
||||
:members:
|
||||
|
||||
|
||||
Translation utils
|
||||
=================
|
||||
|
||||
.. automodule:: mopidy.local.translator
|
||||
:synopsis: Translators for local library extensions
|
||||
:members:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
*******************************
|
||||
:mod:`mopidy.mpd` -- MPD server
|
||||
*******************************
|
||||
********************************
|
||||
:mod:`mopidy.mpd` --- MPD server
|
||||
********************************
|
||||
|
||||
For details on how to use Mopidy's MPD server, see :ref:`ext-mpd`.
|
||||
|
||||
|
||||
@ -14,4 +14,4 @@ if not (2, 7) <= sys.version_info < (3,):
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '1.0.3'
|
||||
__version__ = '1.0.4'
|
||||
|
||||
@ -41,7 +41,7 @@ sys.argv[1:] = []
|
||||
|
||||
|
||||
from mopidy import commands, config as config_lib, ext
|
||||
from mopidy.utils import encoding, log, path, process, versioning
|
||||
from mopidy.internal import encoding, log, path, process, versioning
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -137,7 +137,7 @@ def main():
|
||||
extension.setup(registry)
|
||||
|
||||
# Anything that wants to exit after this point must use
|
||||
# mopidy.utils.process.exit_process as actors can have been started.
|
||||
# mopidy.internal.process.exit_process as actors can have been started.
|
||||
try:
|
||||
return args.command.run(args, proxied_config)
|
||||
except NotImplementedError:
|
||||
|
||||
@ -16,7 +16,7 @@ from mopidy import exceptions
|
||||
from mopidy.audio import playlists, utils
|
||||
from mopidy.audio.constants import PlaybackState
|
||||
from mopidy.audio.listener import AudioListener
|
||||
from mopidy.utils import deprecation, process
|
||||
from mopidy.internal import deprecation, process
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -166,11 +166,7 @@ class _Outputs(gst.Bin):
|
||||
logger.info('Audio output set to "%s"', description)
|
||||
|
||||
def _add(self, element):
|
||||
# All tee branches need a queue in front of them.
|
||||
# But keep the queue short so the volume change isn't to slow:
|
||||
queue = gst.element_factory_make('queue')
|
||||
queue.set_property('max-size-time', 100 * gst.MSECOND)
|
||||
|
||||
self.add(element)
|
||||
self.add(queue)
|
||||
queue.link(element)
|
||||
|
||||
@ -10,7 +10,7 @@ import gst.pbutils # noqa
|
||||
|
||||
from mopidy import exceptions
|
||||
from mopidy.audio import utils
|
||||
from mopidy.utils import encoding
|
||||
from mopidy.internal import encoding
|
||||
|
||||
_missing_plugin_desc = gst.pbutils.missing_plugin_message_get_description
|
||||
|
||||
@ -182,7 +182,7 @@ if __name__ == '__main__':
|
||||
|
||||
import gobject
|
||||
|
||||
from mopidy.utils import path
|
||||
from mopidy.internal import path
|
||||
|
||||
gobject.threads_init()
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import gobject
|
||||
from mopidy import config as config_lib, exceptions
|
||||
from mopidy.audio import Audio
|
||||
from mopidy.core import Core
|
||||
from mopidy.utils import deps, process, timer, versioning
|
||||
from mopidy.internal import deps, process, timer, versioning
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ from mopidy.compat import configparser
|
||||
from mopidy.config import keyring
|
||||
from mopidy.config.schemas import * # noqa
|
||||
from mopidy.config.types import * # noqa
|
||||
from mopidy.utils import path, versioning
|
||||
from mopidy.internal import path, versioning
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import socket
|
||||
|
||||
from mopidy import compat
|
||||
from mopidy.config import validators
|
||||
from mopidy.utils import log, path
|
||||
from mopidy.internal import log, path
|
||||
|
||||
|
||||
def decode(value):
|
||||
|
||||
@ -14,8 +14,8 @@ from mopidy.core.mixer import MixerController
|
||||
from mopidy.core.playback import PlaybackController
|
||||
from mopidy.core.playlists import PlaylistsController
|
||||
from mopidy.core.tracklist import TracklistController
|
||||
from mopidy.utils import versioning
|
||||
from mopidy.utils.deprecation import deprecated_property
|
||||
from mopidy.internal import versioning
|
||||
from mopidy.internal.deprecation import deprecated_property
|
||||
|
||||
|
||||
class Core(
|
||||
|
||||
@ -1,16 +1,32 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import logging
|
||||
import operator
|
||||
import urlparse
|
||||
|
||||
from mopidy.utils import deprecation, validation
|
||||
from mopidy import compat, exceptions, models
|
||||
from mopidy.internal import deprecation, validation
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _backend_error_handling(backend, reraise=None):
|
||||
try:
|
||||
yield
|
||||
except exceptions.ValidationError as e:
|
||||
logger.error('%s backend returned bad data: %s',
|
||||
backend.actor_ref.actor_class.__name__, e)
|
||||
except Exception as e:
|
||||
if reraise and isinstance(e, reraise):
|
||||
raise
|
||||
logger.exception('%s backend caused an exception.',
|
||||
backend.actor_ref.actor_class.__name__)
|
||||
|
||||
|
||||
class LibraryController(object):
|
||||
pykka_traversable = True
|
||||
|
||||
@ -79,22 +95,24 @@ class LibraryController(object):
|
||||
backends = self.backends.with_library_browse.values()
|
||||
futures = {b: b.library.root_directory for b in backends}
|
||||
for backend, future in futures.items():
|
||||
try:
|
||||
directories.add(future.get())
|
||||
except Exception:
|
||||
logger.exception('%s backend caused an exception.',
|
||||
backend.actor_ref.actor_class.__name__)
|
||||
with _backend_error_handling(backend):
|
||||
root = future.get()
|
||||
validation.check_instance(root, models.Ref)
|
||||
directories.add(root)
|
||||
return sorted(directories, key=operator.attrgetter('name'))
|
||||
|
||||
def _browse(self, uri):
|
||||
scheme = urlparse.urlparse(uri).scheme
|
||||
backend = self.backends.with_library_browse.get(scheme)
|
||||
try:
|
||||
if backend:
|
||||
return backend.library.browse(uri).get()
|
||||
except Exception:
|
||||
logger.exception('%s backend caused an exception.',
|
||||
backend.actor_ref.actor_class.__name__)
|
||||
|
||||
if not backend:
|
||||
return []
|
||||
|
||||
with _backend_error_handling(backend):
|
||||
result = backend.library.browse(uri).get()
|
||||
validation.check_instances(result, models.Ref)
|
||||
return result
|
||||
|
||||
return []
|
||||
|
||||
def get_distinct(self, field, query=None):
|
||||
@ -106,7 +124,7 @@ class LibraryController(object):
|
||||
recommended to use this method.
|
||||
|
||||
:param string field: One of ``artist``, ``albumartist``, ``album``,
|
||||
``composer``, ``performer``, ``date``or ``genre``.
|
||||
``composer``, ``performer``, ``date`` or ``genre``.
|
||||
:param dict query: Query to use for limiting results, see
|
||||
:meth:`search` for details about the query format.
|
||||
:rtype: set of values corresponding to the requested field type.
|
||||
@ -120,11 +138,11 @@ class LibraryController(object):
|
||||
futures = {b: b.library.get_distinct(field, query)
|
||||
for b in self.backends.with_library.values()}
|
||||
for backend, future in futures.items():
|
||||
try:
|
||||
result.update(future.get())
|
||||
except Exception:
|
||||
logger.exception('%s backend caused an exception.',
|
||||
backend.actor_ref.actor_class.__name__)
|
||||
with _backend_error_handling(backend):
|
||||
values = future.get()
|
||||
if values is not None:
|
||||
validation.check_instances(values, compat.text_type)
|
||||
result.update(values)
|
||||
return result
|
||||
|
||||
def get_images(self, uris):
|
||||
@ -152,12 +170,16 @@ class LibraryController(object):
|
||||
|
||||
results = {uri: tuple() for uri in uris}
|
||||
for backend, future in futures.items():
|
||||
try:
|
||||
with _backend_error_handling(backend):
|
||||
if future.get() is None:
|
||||
continue
|
||||
validation.check_instance(future.get(), collections.Mapping)
|
||||
for uri, images in future.get().items():
|
||||
if uri not in uris:
|
||||
raise exceptions.ValidationError(
|
||||
'Got unknown image URI: %s' % uri)
|
||||
validation.check_instances(images, models.Image)
|
||||
results[uri] += tuple(images)
|
||||
except Exception:
|
||||
logger.exception('%s backend caused an exception.',
|
||||
backend.actor_ref.actor_class.__name__)
|
||||
return results
|
||||
|
||||
def find_exact(self, query=None, uris=None, **kwargs):
|
||||
@ -202,7 +224,7 @@ class LibraryController(object):
|
||||
uris = [uri]
|
||||
|
||||
futures = {}
|
||||
result = {u: [] for u in uris}
|
||||
results = {u: [] for u in uris}
|
||||
|
||||
# TODO: lookup(uris) to backend APIs
|
||||
for backend, backend_uris in self._get_backends_to_uris(uris).items():
|
||||
@ -210,15 +232,15 @@ class LibraryController(object):
|
||||
futures[(backend, u)] = backend.library.lookup(u)
|
||||
|
||||
for (backend, u), future in futures.items():
|
||||
try:
|
||||
result[u] = future.get()
|
||||
except Exception:
|
||||
logger.exception('%s backend caused an exception.',
|
||||
backend.actor_ref.actor_class.__name__)
|
||||
with _backend_error_handling(backend):
|
||||
result = future.get()
|
||||
if result is not None:
|
||||
validation.check_instances(result, models.Track)
|
||||
results[u] = result
|
||||
|
||||
if uri:
|
||||
return result[uri]
|
||||
return result
|
||||
return results[uri]
|
||||
return results
|
||||
|
||||
def refresh(self, uri=None):
|
||||
"""
|
||||
@ -241,11 +263,8 @@ class LibraryController(object):
|
||||
futures[backend] = backend.library.refresh(uri)
|
||||
|
||||
for backend, future in futures.items():
|
||||
try:
|
||||
with _backend_error_handling(backend):
|
||||
future.get()
|
||||
except Exception:
|
||||
logger.exception('%s backend caused an exception.',
|
||||
backend.actor_ref.actor_class.__name__)
|
||||
|
||||
def search(self, query=None, uris=None, exact=False, **kwargs):
|
||||
"""
|
||||
@ -311,25 +330,26 @@ class LibraryController(object):
|
||||
futures[backend] = backend.library.search(
|
||||
query=query, uris=backend_uris, exact=exact)
|
||||
|
||||
# Some of our tests check for LookupError to catch bad queries. This is
|
||||
# silly and should be replaced with query validation before passing it
|
||||
# to the backends.
|
||||
reraise = (TypeError, LookupError)
|
||||
|
||||
results = []
|
||||
for backend, future in futures.items():
|
||||
try:
|
||||
results.append(future.get())
|
||||
with _backend_error_handling(backend, reraise=reraise):
|
||||
result = future.get()
|
||||
if result is not None:
|
||||
validation.check_instance(result, models.SearchResult)
|
||||
results.append(result)
|
||||
except TypeError:
|
||||
backend_name = backend.actor_ref.actor_class.__name__
|
||||
logger.warning(
|
||||
'%s does not implement library.search() with "exact" '
|
||||
'support. Please upgrade it.', backend_name)
|
||||
except LookupError:
|
||||
# Some of our tests check for this to catch bad queries. This
|
||||
# is silly and should be replaced with query validation before
|
||||
# passing it to the backends.
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception('%s backend caused an exception.',
|
||||
backend.actor_ref.actor_class.__name__)
|
||||
|
||||
return [r for r in results if r]
|
||||
return results
|
||||
|
||||
|
||||
def _normalize_query(query):
|
||||
|
||||
@ -1,13 +1,27 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
from mopidy.utils import validation
|
||||
from mopidy import exceptions
|
||||
from mopidy.internal import validation
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _mixer_error_handling(mixer):
|
||||
try:
|
||||
yield
|
||||
except exceptions.ValidationError as e:
|
||||
logger.error('%s mixer returned bad data: %s',
|
||||
mixer.actor_ref.actor_class.__name__, e)
|
||||
except Exception:
|
||||
logger.exception('%s mixer caused an exception.',
|
||||
mixer.actor_ref.actor_class.__name__)
|
||||
|
||||
|
||||
class MixerController(object):
|
||||
pykka_traversable = True
|
||||
|
||||
@ -21,8 +35,15 @@ class MixerController(object):
|
||||
|
||||
The volume scale is linear.
|
||||
"""
|
||||
if self._mixer is not None:
|
||||
return self._mixer.get_volume().get()
|
||||
if self._mixer is None:
|
||||
return None
|
||||
|
||||
with _mixer_error_handling(self._mixer):
|
||||
volume = self._mixer.get_volume().get()
|
||||
volume is None or validation.check_integer(volume, min=0, max=100)
|
||||
return volume
|
||||
|
||||
return None
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""Set the volume.
|
||||
@ -36,9 +57,14 @@ class MixerController(object):
|
||||
validation.check_integer(volume, min=0, max=100)
|
||||
|
||||
if self._mixer is None:
|
||||
return False
|
||||
else:
|
||||
return self._mixer.set_volume(volume).get()
|
||||
return False # TODO: 2.0 return None
|
||||
|
||||
with _mixer_error_handling(self._mixer):
|
||||
result = self._mixer.set_volume(volume).get()
|
||||
validation.check_instance(result, bool)
|
||||
return result
|
||||
|
||||
return False
|
||||
|
||||
def get_mute(self):
|
||||
"""Get mute state.
|
||||
@ -46,8 +72,15 @@ class MixerController(object):
|
||||
:class:`True` if muted, :class:`False` unmuted, :class:`None` if
|
||||
unknown.
|
||||
"""
|
||||
if self._mixer is not None:
|
||||
return self._mixer.get_mute().get()
|
||||
if self._mixer is None:
|
||||
return None
|
||||
|
||||
with _mixer_error_handling(self._mixer):
|
||||
mute = self._mixer.get_mute().get()
|
||||
mute is None or validation.check_instance(mute, bool)
|
||||
return mute
|
||||
|
||||
return None
|
||||
|
||||
def set_mute(self, mute):
|
||||
"""Set mute state.
|
||||
@ -58,6 +91,11 @@ class MixerController(object):
|
||||
"""
|
||||
validation.check_boolean(mute)
|
||||
if self._mixer is None:
|
||||
return False
|
||||
else:
|
||||
return self._mixer.set_mute(bool(mute)).get()
|
||||
return False # TODO: 2.0 return None
|
||||
|
||||
with _mixer_error_handling(self._mixer):
|
||||
result = self._mixer.set_mute(bool(mute)).get()
|
||||
validation.check_instance(result, bool)
|
||||
return result
|
||||
|
||||
return False
|
||||
|
||||
@ -6,7 +6,7 @@ import urlparse
|
||||
from mopidy import models
|
||||
from mopidy.audio import PlaybackState
|
||||
from mopidy.core import listener
|
||||
from mopidy.utils import deprecation, validation
|
||||
from mopidy.internal import deprecation, validation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -61,9 +61,7 @@ class PlaybackController(object):
|
||||
|
||||
Returns a :class:`mopidy.models.Track` or :class:`None`.
|
||||
"""
|
||||
tl_track = self.get_current_tl_track()
|
||||
if tl_track is not None:
|
||||
return tl_track.track
|
||||
return getattr(self.get_current_tl_track(), 'track', None)
|
||||
|
||||
current_track = deprecation.deprecated_property(get_current_track)
|
||||
"""
|
||||
@ -71,6 +69,18 @@ class PlaybackController(object):
|
||||
Use :meth:`get_current_track` instead.
|
||||
"""
|
||||
|
||||
def get_current_tlid(self):
|
||||
"""
|
||||
Get the currently playing or selected TLID.
|
||||
|
||||
Extracted from :meth:`get_current_tl_track` for convenience.
|
||||
|
||||
Returns a :class:`int` or :class:`None`.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
"""
|
||||
return getattr(self.get_current_tl_track(), 'tlid', None)
|
||||
|
||||
def get_stream_title(self):
|
||||
"""Get the current stream title or :class:`None`."""
|
||||
return self._stream_title
|
||||
|
||||
@ -1,15 +1,31 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import urlparse
|
||||
|
||||
from mopidy import exceptions
|
||||
from mopidy.core import listener
|
||||
from mopidy.models import Playlist
|
||||
from mopidy.utils import deprecation, validation
|
||||
from mopidy.internal import deprecation, validation
|
||||
from mopidy.models import Playlist, Ref
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _backend_error_handling(backend, reraise=None):
|
||||
try:
|
||||
yield
|
||||
except exceptions.ValidationError as e:
|
||||
logger.error('%s backend returned bad data: %s',
|
||||
backend.actor_ref.actor_class.__name__, e)
|
||||
except Exception as e:
|
||||
if reraise and isinstance(e, reraise):
|
||||
raise
|
||||
logger.exception('%s backend caused an exception.',
|
||||
backend.actor_ref.actor_class.__name__)
|
||||
|
||||
|
||||
class PlaylistsController(object):
|
||||
pykka_traversable = True
|
||||
|
||||
@ -34,17 +50,18 @@ class PlaylistsController(object):
|
||||
for backend in set(self.backends.with_playlists.values())}
|
||||
|
||||
results = []
|
||||
for backend, future in futures.items():
|
||||
for b, future in futures.items():
|
||||
try:
|
||||
results.extend(future.get())
|
||||
with _backend_error_handling(b, reraise=NotImplementedError):
|
||||
playlists = future.get()
|
||||
if playlists is not None:
|
||||
validation.check_instances(playlists, Ref)
|
||||
results.extend(playlists)
|
||||
except NotImplementedError:
|
||||
backend_name = backend.actor_ref.actor_class.__name__
|
||||
backend_name = b.actor_ref.actor_class.__name__
|
||||
logger.warning(
|
||||
'%s does not implement playlists.as_list(). '
|
||||
'Please upgrade it.', backend_name)
|
||||
except Exception:
|
||||
logger.exception('%s backend caused an exception.',
|
||||
backend.actor_ref.actor_class.__name__)
|
||||
|
||||
return results
|
||||
|
||||
@ -66,8 +83,16 @@ class PlaylistsController(object):
|
||||
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||
if backend:
|
||||
return backend.playlists.get_items(uri).get()
|
||||
|
||||
if not backend:
|
||||
return None
|
||||
|
||||
with _backend_error_handling(backend):
|
||||
items = backend.playlists.get_items(uri).get()
|
||||
items is None or validation.check_instances(items, Ref)
|
||||
return items
|
||||
|
||||
return None
|
||||
|
||||
def get_playlists(self, include_tracks=True):
|
||||
"""
|
||||
@ -120,16 +145,23 @@ class PlaylistsController(object):
|
||||
:type name: string
|
||||
:param uri_scheme: use the backend matching the URI scheme
|
||||
:type uri_scheme: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
:rtype: :class:`mopidy.models.Playlist` or :class:`None`
|
||||
"""
|
||||
if uri_scheme in self.backends.with_playlists:
|
||||
backend = self.backends.with_playlists[uri_scheme]
|
||||
backends = [self.backends.with_playlists[uri_scheme]]
|
||||
else:
|
||||
# TODO: this fallback looks suspicious
|
||||
backend = list(self.backends.with_playlists.values())[0]
|
||||
playlist = backend.playlists.create(name).get()
|
||||
listener.CoreListener.send('playlist_changed', playlist=playlist)
|
||||
return playlist
|
||||
backends = self.backends.with_playlists.values()
|
||||
|
||||
for backend in backends:
|
||||
with _backend_error_handling(backend):
|
||||
result = backend.playlists.create(name).get()
|
||||
if result is None:
|
||||
continue
|
||||
validation.check_instance(result, Playlist)
|
||||
listener.CoreListener.send('playlist_changed', playlist=result)
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
def delete(self, uri):
|
||||
"""
|
||||
@ -145,8 +177,14 @@ class PlaylistsController(object):
|
||||
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||
if backend:
|
||||
if not backend:
|
||||
return
|
||||
|
||||
with _backend_error_handling(backend):
|
||||
backend.playlists.delete(uri).get()
|
||||
# TODO: emit playlist changed?
|
||||
|
||||
# TODO: return value?
|
||||
|
||||
def filter(self, criteria=None, **kwargs):
|
||||
"""
|
||||
@ -192,11 +230,16 @@ class PlaylistsController(object):
|
||||
"""
|
||||
uri_scheme = urlparse.urlparse(uri).scheme
|
||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||
if backend:
|
||||
return backend.playlists.lookup(uri).get()
|
||||
else:
|
||||
if not backend:
|
||||
return None
|
||||
|
||||
with _backend_error_handling(backend):
|
||||
playlist = backend.playlists.lookup(uri).get()
|
||||
playlist is None or validation.check_instance(playlist, Playlist)
|
||||
return playlist
|
||||
|
||||
return None
|
||||
|
||||
# TODO: there is an inconsistency between library.refresh(uri) and this
|
||||
# call, not sure how to sort this out.
|
||||
def refresh(self, uri_scheme=None):
|
||||
@ -225,12 +268,9 @@ class PlaylistsController(object):
|
||||
futures[backend] = backend.playlists.refresh()
|
||||
|
||||
for backend, future in futures.items():
|
||||
try:
|
||||
with _backend_error_handling(backend):
|
||||
future.get()
|
||||
playlists_loaded = True
|
||||
except Exception:
|
||||
logger.exception('%s backend caused an exception.',
|
||||
backend.actor_ref.actor_class.__name__)
|
||||
|
||||
if playlists_loaded:
|
||||
listener.CoreListener.send('playlists_loaded')
|
||||
@ -264,7 +304,16 @@ class PlaylistsController(object):
|
||||
|
||||
uri_scheme = urlparse.urlparse(playlist.uri).scheme
|
||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||
if backend:
|
||||
if not backend:
|
||||
return None
|
||||
|
||||
# TODO: we let AssertionError error through due to legacy tests :/
|
||||
with _backend_error_handling(backend, reraise=AssertionError):
|
||||
playlist = backend.playlists.save(playlist).get()
|
||||
listener.CoreListener.send('playlist_changed', playlist=playlist)
|
||||
playlist is None or validation.check_instance(playlist, Playlist)
|
||||
if playlist:
|
||||
listener.CoreListener.send(
|
||||
'playlist_changed', playlist=playlist)
|
||||
return playlist
|
||||
|
||||
return None
|
||||
|
||||
@ -4,8 +4,8 @@ import logging
|
||||
import random
|
||||
|
||||
from mopidy.core import listener
|
||||
from mopidy.internal import deprecation, validation
|
||||
from mopidy.models import TlTrack, Track
|
||||
from mopidy.utils import deprecation, validation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ import tornado.websocket
|
||||
from mopidy import exceptions, models, zeroconf
|
||||
from mopidy.core import CoreListener
|
||||
from mopidy.http import handlers
|
||||
from mopidy.utils import encoding, formatting, network
|
||||
from mopidy.internal import encoding, formatting, network
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -12,7 +12,7 @@ import tornado.websocket
|
||||
|
||||
import mopidy
|
||||
from mopidy import core, models
|
||||
from mopidy.utils import encoding, jsonrpc
|
||||
from mopidy.internal import encoding, jsonrpc
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -11,7 +11,7 @@ import pygst
|
||||
pygst.require('0.10')
|
||||
import gst # noqa
|
||||
|
||||
from mopidy.utils import formatting
|
||||
from mopidy.internal import formatting
|
||||
|
||||
|
||||
def format_dependency_list(adapters=None):
|
||||
@ -11,7 +11,7 @@ import gobject
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.utils import encoding
|
||||
from mopidy.internal import encoding
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -10,7 +10,7 @@ import urlparse
|
||||
|
||||
from mopidy import compat, exceptions
|
||||
from mopidy.compat import queue
|
||||
from mopidy.utils import encoding, xdg
|
||||
from mopidy.internal import encoding, xdg
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -7,8 +7,8 @@ import time
|
||||
|
||||
from mopidy import commands, compat, exceptions
|
||||
from mopidy.audio import scan, utils
|
||||
from mopidy.internal import path
|
||||
from mopidy.local import translator
|
||||
from mopidy.utils import path
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -11,8 +11,8 @@ import tempfile
|
||||
|
||||
import mopidy
|
||||
from mopidy import compat, local, models
|
||||
from mopidy.internal import encoding, timer
|
||||
from mopidy.local import search, storage, translator
|
||||
from mopidy.utils import encoding, timer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -7,5 +7,5 @@ from mopidy.local import translator
|
||||
class LocalPlaybackProvider(backend.PlaybackProvider):
|
||||
|
||||
def translate_uri(self, uri):
|
||||
return translator.local_track_uri_to_file_uri(
|
||||
return translator.local_uri_to_file_uri(
|
||||
uri, self.backend.config['local']['media_dir'])
|
||||
|
||||
@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals
|
||||
import logging
|
||||
import os
|
||||
|
||||
from mopidy.utils import encoding, path
|
||||
from mopidy.internal import encoding, path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -5,25 +5,41 @@ import os
|
||||
import urllib
|
||||
|
||||
from mopidy import compat
|
||||
from mopidy.utils.path import path_to_uri, uri_to_path
|
||||
from mopidy.internal import path
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def local_track_uri_to_file_uri(uri, media_dir):
|
||||
return path_to_uri(local_track_uri_to_path(uri, media_dir))
|
||||
def local_uri_to_file_uri(uri, media_dir):
|
||||
"""Convert local track or directory URI to file URI."""
|
||||
return path_to_file_uri(local_uri_to_path(uri, media_dir))
|
||||
|
||||
|
||||
def local_track_uri_to_path(uri, media_dir):
|
||||
if not uri.startswith('local:track:'):
|
||||
def local_uri_to_path(uri, media_dir):
|
||||
"""Convert local track or directory URI to absolute path."""
|
||||
if (
|
||||
not uri.startswith('local:directory:') and
|
||||
not uri.startswith('local:track:')):
|
||||
raise ValueError('Invalid URI.')
|
||||
file_path = uri_to_path(uri).split(b':', 1)[1]
|
||||
file_path = path.uri_to_path(uri).split(b':', 1)[1]
|
||||
return os.path.join(media_dir, file_path)
|
||||
|
||||
|
||||
def local_track_uri_to_path(uri, media_dir):
|
||||
# Deprecated version to keep old versions of Mopidy-Local-Sqlite working.
|
||||
return local_uri_to_path(uri, media_dir)
|
||||
|
||||
|
||||
def path_to_file_uri(abspath):
|
||||
"""Convert absolute path to file URI."""
|
||||
# Re-export internal method for use by Mopidy-Local-* extensions.
|
||||
return path.path_to_uri(abspath)
|
||||
|
||||
|
||||
def path_to_local_track_uri(relpath):
|
||||
"""Convert path relative to media_dir to local track URI."""
|
||||
"""Convert path relative to :confval:`local/media_dir` to local track
|
||||
URI."""
|
||||
if isinstance(relpath, compat.text_type):
|
||||
relpath = relpath.encode('utf-8')
|
||||
return b'local:track:%s' % urllib.quote(relpath)
|
||||
|
||||
@ -5,9 +5,9 @@ import logging
|
||||
import pykka
|
||||
|
||||
from mopidy import backend
|
||||
from mopidy.internal import encoding, path
|
||||
from mopidy.m3u.library import M3ULibraryProvider
|
||||
from mopidy.m3u.playlists import M3UPlaylistsProvider
|
||||
from mopidy.utils import encoding, path
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -7,9 +7,8 @@ import urllib
|
||||
import urlparse
|
||||
|
||||
from mopidy import compat
|
||||
from mopidy.internal import encoding, path
|
||||
from mopidy.models import Track
|
||||
from mopidy.utils.encoding import locale_decode
|
||||
from mopidy.utils.path import path_to_uri, uri_to_path
|
||||
|
||||
|
||||
M3U_EXTINF_RE = re.compile(r'#EXTINF:(-1|\d+),(.*)')
|
||||
@ -20,7 +19,7 @@ logger = logging.getLogger(__name__)
|
||||
def playlist_uri_to_path(uri, playlists_dir):
|
||||
if not uri.startswith('m3u:'):
|
||||
raise ValueError('Invalid URI %s' % uri)
|
||||
file_path = uri_to_path(uri)
|
||||
file_path = path.uri_to_path(uri)
|
||||
return os.path.join(playlists_dir, file_path)
|
||||
|
||||
|
||||
@ -80,7 +79,7 @@ def parse_m3u(file_path, media_dir=None):
|
||||
with open(file_path) as m3u:
|
||||
contents = m3u.readlines()
|
||||
except IOError as error:
|
||||
logger.warning('Couldn\'t open m3u: %s', locale_decode(error))
|
||||
logger.warning('Couldn\'t open m3u: %s', encoding.locale_decode(error))
|
||||
return tracks
|
||||
|
||||
if not contents:
|
||||
@ -100,11 +99,11 @@ def parse_m3u(file_path, media_dir=None):
|
||||
if urlparse.urlsplit(line).scheme:
|
||||
tracks.append(track.replace(uri=line))
|
||||
elif os.path.normpath(line) == os.path.abspath(line):
|
||||
path = path_to_uri(line)
|
||||
tracks.append(track.replace(uri=path))
|
||||
uri = path.path_to_uri(line)
|
||||
tracks.append(track.replace(uri=uri))
|
||||
elif media_dir is not None:
|
||||
path = path_to_uri(os.path.join(media_dir, line))
|
||||
tracks.append(track.replace(uri=path))
|
||||
uri = path.path_to_uri(os.path.join(media_dir, line))
|
||||
tracks.append(track.replace(uri=uri))
|
||||
|
||||
track = Track()
|
||||
return tracks
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from mopidy.models import fields
|
||||
from mopidy.models.immutable import ImmutableObject
|
||||
from mopidy.models.immutable import ImmutableObject, ValidatedImmutableObject
|
||||
from mopidy.models.serialize import ModelJSONEncoder, model_json_decoder
|
||||
|
||||
__all__ = [
|
||||
'ImmutableObject', 'Ref', 'Image', 'Artist', 'Album', 'track', 'TlTrack',
|
||||
'Playlist', 'SearchResult', 'model_json_decoder', 'ModelJSONEncoder']
|
||||
'Playlist', 'SearchResult', 'model_json_decoder', 'ModelJSONEncoder',
|
||||
'ValidatedImmutableObject']
|
||||
|
||||
|
||||
class Ref(ImmutableObject):
|
||||
class Ref(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
Model to represent URI references with a human friendly name and type
|
||||
@ -81,7 +82,7 @@ class Ref(ImmutableObject):
|
||||
return cls(**kwargs)
|
||||
|
||||
|
||||
class Image(ImmutableObject):
|
||||
class Image(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param string uri: URI of the image
|
||||
@ -99,7 +100,7 @@ class Image(ImmutableObject):
|
||||
height = fields.Integer(min=0)
|
||||
|
||||
|
||||
class Artist(ImmutableObject):
|
||||
class Artist(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param uri: artist URI
|
||||
@ -120,7 +121,7 @@ class Artist(ImmutableObject):
|
||||
musicbrainz_id = fields.Identifier()
|
||||
|
||||
|
||||
class Album(ImmutableObject):
|
||||
class Album(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param uri: album URI
|
||||
@ -169,7 +170,7 @@ class Album(ImmutableObject):
|
||||
# actual usage of this field with more than one image.
|
||||
|
||||
|
||||
class Track(ImmutableObject):
|
||||
class Track(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param uri: track URI
|
||||
@ -253,7 +254,7 @@ class Track(ImmutableObject):
|
||||
last_modified = fields.Integer(min=0)
|
||||
|
||||
|
||||
class TlTrack(ImmutableObject):
|
||||
class TlTrack(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
A tracklist track. Wraps a regular track and it's tracklist ID.
|
||||
@ -292,7 +293,7 @@ class TlTrack(ImmutableObject):
|
||||
return iter([self.tlid, self.track])
|
||||
|
||||
|
||||
class Playlist(ImmutableObject):
|
||||
class Playlist(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param uri: playlist URI
|
||||
@ -329,7 +330,7 @@ class Playlist(ImmutableObject):
|
||||
return len(self.tracks)
|
||||
|
||||
|
||||
class SearchResult(ImmutableObject):
|
||||
class SearchResult(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param uri: search result URI
|
||||
|
||||
@ -4,8 +4,9 @@ from __future__ import absolute_import, unicode_literals
|
||||
class Field(object):
|
||||
|
||||
"""
|
||||
Base field for use in :class:`ImmutableObject`. These fields are
|
||||
responsible for type checking and other data sanitation in our models.
|
||||
Base field for use in
|
||||
:class:`~mopidy.models.immutable.ValidatedImmutableObject`. These fields
|
||||
are responsible for type checking and other data sanitation in our models.
|
||||
|
||||
For simplicity fields use the Python descriptor protocol to store the
|
||||
values in the instance dictionary. Also note that fields are mutable if
|
||||
@ -19,7 +20,7 @@ class Field(object):
|
||||
"""
|
||||
|
||||
def __init__(self, default=None, type=None, choices=None):
|
||||
self._name = None # Set by ImmutableObjectMeta
|
||||
self._name = None # Set by ValidatedImmutableObjectMeta
|
||||
self._choices = choices
|
||||
self._default = default
|
||||
self._type = type
|
||||
@ -72,20 +73,41 @@ class String(Field):
|
||||
|
||||
|
||||
class Date(String):
|
||||
"""
|
||||
:class:`Field` for storing ISO 8601 dates as a string.
|
||||
|
||||
Supported formats are ``YYYY-MM-DD``, ``YYYY-MM`` and ``YYYY``, currently
|
||||
not validated.
|
||||
|
||||
:param default: default value for field
|
||||
"""
|
||||
pass # TODO: make this check for YYYY-MM-DD, YYYY-MM, YYYY using strptime.
|
||||
|
||||
|
||||
class Identifier(String):
|
||||
"""
|
||||
:class:`Field` for storing ASCII values such as GUIDs or other identifiers.
|
||||
|
||||
Values will be interned.
|
||||
|
||||
:param default: default value for field
|
||||
"""
|
||||
def validate(self, value):
|
||||
return intern(str(super(Identifier, self).validate(value)))
|
||||
|
||||
|
||||
class URI(Identifier):
|
||||
"""
|
||||
:class`Field` for storing URIs
|
||||
|
||||
Values will be interned, currently not validated.
|
||||
|
||||
:param default: default value for field
|
||||
"""
|
||||
pass # TODO: validate URIs?
|
||||
|
||||
|
||||
class Integer(Field):
|
||||
|
||||
"""
|
||||
:class:`Field` for storing integer numbers.
|
||||
|
||||
@ -111,7 +133,6 @@ class Integer(Field):
|
||||
|
||||
|
||||
class Collection(Field):
|
||||
|
||||
"""
|
||||
:class:`Field` for storing collections of a given type.
|
||||
|
||||
|
||||
@ -1,81 +1,61 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import itertools
|
||||
import weakref
|
||||
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.models.fields import Field
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
|
||||
class ImmutableObjectMeta(type):
|
||||
|
||||
"""Helper to automatically assign field names to descriptors."""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
fields = {}
|
||||
for key, value in attrs.items():
|
||||
if isinstance(value, Field):
|
||||
fields[key] = '_' + key
|
||||
value._name = key
|
||||
|
||||
attrs['_fields'] = fields
|
||||
attrs['_instances'] = weakref.WeakValueDictionary()
|
||||
attrs['__slots__'] = ['_hash'] + fields.values()
|
||||
|
||||
for ancestor in [b for base in bases for b in inspect.getmro(base)]:
|
||||
if '__weakref__' in getattr(ancestor, '__slots__', []):
|
||||
break
|
||||
else:
|
||||
attrs['__slots__'].append('__weakref__')
|
||||
|
||||
return super(ImmutableObjectMeta, cls).__new__(cls, name, bases, attrs)
|
||||
|
||||
def __call__(cls, *args, **kwargs): # noqa: N805
|
||||
instance = super(ImmutableObjectMeta, cls).__call__(*args, **kwargs)
|
||||
return cls._instances.setdefault(weakref.ref(instance), instance)
|
||||
|
||||
|
||||
class ImmutableObject(object):
|
||||
|
||||
"""
|
||||
Superclass for immutable objects whose fields can only be modified via the
|
||||
constructor. Fields should be :class:`Field` instances to ensure type
|
||||
safety in our models.
|
||||
constructor.
|
||||
|
||||
Note that since these models can not be changed, we heavily memoize them
|
||||
to save memory. So constructing a class with the same arguments twice will
|
||||
give you the same instance twice.
|
||||
This version of this class has been retained to avoid breaking any clients
|
||||
relying on it's behavior. Internally in Mopidy we now use
|
||||
:class:`ValidatedImmutableObject` for type safety and it's much smaller
|
||||
memory footprint.
|
||||
|
||||
:param kwargs: kwargs to set as fields on the object
|
||||
:type kwargs: any
|
||||
"""
|
||||
|
||||
__metaclass__ = ImmutableObjectMeta
|
||||
# Any sub-classes that don't set slots won't be effected by the base using
|
||||
# slots as they will still get an instance dict.
|
||||
__slots__ = ['__weakref__']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
if key not in self._fields:
|
||||
if not self._is_valid_field(key):
|
||||
raise TypeError(
|
||||
'__init__() got an unexpected keyword argument "%s"' %
|
||||
key)
|
||||
super(ImmutableObject, self).__setattr__(key, value)
|
||||
'__init__() got an unexpected keyword argument "%s"' % key)
|
||||
self._set_field(key, value)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in self.__slots__:
|
||||
return super(ImmutableObject, self).__setattr__(name, value)
|
||||
raise AttributeError('Object is immutable.')
|
||||
if name.startswith('_'):
|
||||
object.__setattr__(self, name, value)
|
||||
else:
|
||||
raise AttributeError('Object is immutable.')
|
||||
|
||||
def __delattr__(self, name):
|
||||
if name in self.__slots__:
|
||||
return super(ImmutableObject, self).__delattr__(name)
|
||||
raise AttributeError('Object is immutable.')
|
||||
if name.startswith('_'):
|
||||
object.__delattr__(self, name)
|
||||
else:
|
||||
raise AttributeError('Object is immutable.')
|
||||
|
||||
def _is_valid_field(self, name):
|
||||
return hasattr(self, name) and not callable(getattr(self, name))
|
||||
|
||||
def _set_field(self, name, value):
|
||||
if value == getattr(self.__class__, name):
|
||||
self.__dict__.pop(name, None)
|
||||
else:
|
||||
self.__dict__[name] = value
|
||||
|
||||
def _items(self):
|
||||
for field, key in self._fields.items():
|
||||
if hasattr(self, key):
|
||||
yield field, getattr(self, key)
|
||||
return self.__dict__.iteritems()
|
||||
|
||||
def __repr__(self):
|
||||
kwarg_pairs = []
|
||||
@ -91,12 +71,10 @@ class ImmutableObject(object):
|
||||
}
|
||||
|
||||
def __hash__(self):
|
||||
if not hasattr(self, '_hash'):
|
||||
hash_sum = 0
|
||||
for key, value in self._items():
|
||||
hash_sum += hash(key) + hash(value)
|
||||
super(ImmutableObject, self).__setattr__('_hash', hash_sum)
|
||||
return self._hash
|
||||
hash_sum = 0
|
||||
for key, value in self._items():
|
||||
hash_sum += hash(key) + hash(value)
|
||||
return hash_sum
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
@ -110,11 +88,108 @@ class ImmutableObject(object):
|
||||
def copy(self, **values):
|
||||
"""
|
||||
.. deprecated:: 1.1
|
||||
Use :meth:`replace` instead. Note that we no longer return copies.
|
||||
Use :meth:`replace` instead.
|
||||
"""
|
||||
deprecation.warn('model.immutable.copy')
|
||||
return self.replace(**values)
|
||||
|
||||
def replace(self, **kwargs):
|
||||
"""
|
||||
Replace the fields in the model and return a new instance
|
||||
|
||||
Examples::
|
||||
|
||||
# Returns a track with a new name
|
||||
Track(name='foo').replace(name='bar')
|
||||
# Return an album with a new number of tracks
|
||||
Album(num_tracks=2).replace(num_tracks=5)
|
||||
|
||||
:param kwargs: kwargs to set as fields on the object
|
||||
:type kwargs: any
|
||||
:rtype: instance of the model with replaced fields
|
||||
"""
|
||||
other = copy.copy(self)
|
||||
for key, value in kwargs.items():
|
||||
if not self._is_valid_field(key):
|
||||
raise TypeError(
|
||||
'copy() got an unexpected keyword argument "%s"' % key)
|
||||
other._set_field(key, value)
|
||||
return other
|
||||
|
||||
def serialize(self):
|
||||
data = {}
|
||||
data['__model__'] = self.__class__.__name__
|
||||
for key, value in self._items():
|
||||
if isinstance(value, (set, frozenset, list, tuple)):
|
||||
value = [
|
||||
v.serialize() if isinstance(v, ImmutableObject) else v
|
||||
for v in value]
|
||||
elif isinstance(value, ImmutableObject):
|
||||
value = value.serialize()
|
||||
if not (isinstance(value, list) and len(value) == 0):
|
||||
data[key] = value
|
||||
return data
|
||||
|
||||
|
||||
class _ValidatedImmutableObjectMeta(type):
|
||||
|
||||
"""Helper that initializes fields, slots and memoizes instance creation."""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
fields = {}
|
||||
|
||||
for base in bases: # Copy parent fields over to our state
|
||||
fields.update(getattr(base, '_fields', {}))
|
||||
|
||||
for key, value in attrs.items(): # Add our own fields
|
||||
if isinstance(value, Field):
|
||||
fields[key] = '_' + key
|
||||
value._name = key
|
||||
|
||||
attrs['_fields'] = fields
|
||||
attrs['_instances'] = weakref.WeakValueDictionary()
|
||||
attrs['__slots__'] = list(attrs.get('__slots__', [])) + fields.values()
|
||||
|
||||
return super(_ValidatedImmutableObjectMeta, cls).__new__(
|
||||
cls, name, bases, attrs)
|
||||
|
||||
def __call__(cls, *args, **kwargs): # noqa: N805
|
||||
instance = super(_ValidatedImmutableObjectMeta, cls).__call__(
|
||||
*args, **kwargs)
|
||||
return cls._instances.setdefault(weakref.ref(instance), instance)
|
||||
|
||||
|
||||
class ValidatedImmutableObject(ImmutableObject):
|
||||
"""
|
||||
Superclass for immutable objects whose fields can only be modified via the
|
||||
constructor. Fields should be :class:`Field` instances to ensure type
|
||||
safety in our models.
|
||||
|
||||
Note that since these models can not be changed, we heavily memoize them
|
||||
to save memory. So constructing a class with the same arguments twice will
|
||||
give you the same instance twice.
|
||||
"""
|
||||
|
||||
__metaclass__ = _ValidatedImmutableObjectMeta
|
||||
__slots__ = ['_hash']
|
||||
|
||||
def __hash__(self):
|
||||
if not hasattr(self, '_hash'):
|
||||
hash_sum = super(ValidatedImmutableObject, self).__hash__()
|
||||
object.__setattr__(self, '_hash', hash_sum)
|
||||
return self._hash
|
||||
|
||||
def _is_valid_field(self, name):
|
||||
return name in self._fields
|
||||
|
||||
def _set_field(self, name, value):
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
def _items(self):
|
||||
for field, key in self._fields.items():
|
||||
if hasattr(self, key):
|
||||
yield field, getattr(self, key)
|
||||
|
||||
def replace(self, **kwargs):
|
||||
"""
|
||||
Replace the fields in the model and return a new instance
|
||||
@ -136,25 +211,7 @@ class ImmutableObject(object):
|
||||
"""
|
||||
if not kwargs:
|
||||
return self
|
||||
other = copy.copy(self)
|
||||
for key, value in kwargs.items():
|
||||
if key not in self._fields:
|
||||
raise TypeError(
|
||||
'copy() got an unexpected keyword argument "%s"' % key)
|
||||
super(ImmutableObject, other).__setattr__(key, value)
|
||||
super(ImmutableObject, other).__delattr__('_hash')
|
||||
other = super(ValidatedImmutableObject, self).replace(**kwargs)
|
||||
if hasattr(self, '_hash'):
|
||||
object.__delattr__(other, '_hash')
|
||||
return self._instances.setdefault(weakref.ref(other), other)
|
||||
|
||||
def serialize(self):
|
||||
data = {}
|
||||
data['__model__'] = self.__class__.__name__
|
||||
for key, value in self._items():
|
||||
if isinstance(value, (set, frozenset, list, tuple)):
|
||||
value = [
|
||||
v.serialize() if isinstance(v, ImmutableObject) else v
|
||||
for v in value]
|
||||
elif isinstance(value, ImmutableObject):
|
||||
value = value.serialize()
|
||||
if not (isinstance(value, list) and len(value) == 0):
|
||||
data[key] = value
|
||||
return data
|
||||
|
||||
@ -2,7 +2,9 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
from mopidy.models.immutable import ImmutableObject
|
||||
from mopidy.models import immutable
|
||||
|
||||
_MODELS = ['Ref', 'Artist', 'Album', 'Track', 'TlTrack', 'Playlist']
|
||||
|
||||
|
||||
class ModelJSONEncoder(json.JSONEncoder):
|
||||
@ -19,7 +21,7 @@ class ModelJSONEncoder(json.JSONEncoder):
|
||||
"""
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, ImmutableObject):
|
||||
if isinstance(obj, immutable.ImmutableObject):
|
||||
return obj.serialize()
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
@ -38,8 +40,8 @@ def model_json_decoder(dct):
|
||||
|
||||
"""
|
||||
if '__model__' in dct:
|
||||
models = {c.__name__: c for c in ImmutableObject.__subclasses__()}
|
||||
from mopidy import models
|
||||
model_name = dct.pop('__model__')
|
||||
if model_name in models:
|
||||
return models[model_name](**dct)
|
||||
if model_name in _MODELS:
|
||||
return getattr(models, model_name)(**dct)
|
||||
return dct
|
||||
|
||||
@ -6,8 +6,8 @@ import pykka
|
||||
|
||||
from mopidy import exceptions, zeroconf
|
||||
from mopidy.core import CoreListener
|
||||
from mopidy.internal import encoding, network, process
|
||||
from mopidy.mpd import session, uri_mapper
|
||||
from mopidy.utils import encoding, network, process
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -167,7 +167,8 @@ class MpdDispatcher(object):
|
||||
# TODO: check that blacklist items are valid commands?
|
||||
blacklist = self.config['mpd'].get('command_blacklist', [])
|
||||
if tokens and tokens[0] in blacklist:
|
||||
logger.warning('Client sent us blacklisted command: %s', tokens[0])
|
||||
logger.warning(
|
||||
'MPD client used blacklisted command: %s', tokens[0])
|
||||
raise exceptions.MpdDisabled(command=tokens[0])
|
||||
try:
|
||||
return protocol.commands.call(tokens, context=self.context)
|
||||
|
||||
@ -2,8 +2,8 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import urlparse
|
||||
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.mpd import exceptions, protocol, translator
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
|
||||
@protocol.commands.add('add')
|
||||
|
||||
@ -3,9 +3,9 @@ from __future__ import absolute_import, unicode_literals
|
||||
import functools
|
||||
import itertools
|
||||
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.models import Track
|
||||
from mopidy.mpd import exceptions, protocol, translator
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
_SEARCH_MAPPING = {
|
||||
'album': 'album',
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from mopidy.core import PlaybackState
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.mpd import exceptions, protocol
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
|
||||
@protocol.commands.add('consume', state=protocol.BOOL)
|
||||
|
||||
@ -2,8 +2,8 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from mopidy.internal import formatting, network
|
||||
from mopidy.mpd import dispatcher, protocol
|
||||
from mopidy.utils import formatting, network
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ import pykka
|
||||
|
||||
from mopidy import audio
|
||||
from mopidy.audio.constants import PlaybackState
|
||||
from mopidy.utils.path import path_to_uri
|
||||
from mopidy.internal import path
|
||||
|
||||
from tests import dummy_audio, path_to_data_dir
|
||||
|
||||
@ -36,8 +36,8 @@ class BaseTest(unittest.TestCase):
|
||||
}
|
||||
}
|
||||
|
||||
uris = [path_to_uri(path_to_data_dir('song1.wav')),
|
||||
path_to_uri(path_to_data_dir('song2.wav'))]
|
||||
uris = [path.path_to_uri(path_to_data_dir('song1.wav')),
|
||||
path.path_to_uri(path_to_data_dir('song2.wav'))]
|
||||
|
||||
audio_class = audio.Audio
|
||||
|
||||
@ -53,7 +53,7 @@ class BaseTest(unittest.TestCase):
|
||||
'hostname': '',
|
||||
},
|
||||
}
|
||||
self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
|
||||
self.song_uri = path.path_to_uri(path_to_data_dir('song1.wav'))
|
||||
self.audio = self.audio_class.start(config=config, mixer=None).proxy()
|
||||
|
||||
def tearDown(self): # noqa
|
||||
|
||||
@ -8,7 +8,7 @@ gobject.threads_init()
|
||||
|
||||
from mopidy import exceptions
|
||||
from mopidy.audio import scan
|
||||
from mopidy.utils import path as path_lib
|
||||
from mopidy.internal import path as path_lib
|
||||
|
||||
from tests import path_to_data_dir
|
||||
|
||||
|
||||
@ -373,7 +373,7 @@ class ExpandedPathTest(unittest.TestCase):
|
||||
expanded = b'expanded_path'
|
||||
self.assertEqual(expanded, types.ExpandedPath(original, expanded))
|
||||
|
||||
@mock.patch('mopidy.utils.path.expand_path')
|
||||
@mock.patch('mopidy.internal.path.expand_path')
|
||||
def test_orginal_stores_unexpanded(self, expand_path_mock):
|
||||
original = b'~'
|
||||
expanded = b'expanded_path'
|
||||
|
||||
@ -7,7 +7,7 @@ import mock
|
||||
import pykka
|
||||
|
||||
from mopidy.core import Core
|
||||
from mopidy.utils import versioning
|
||||
from mopidy.internal import versioning
|
||||
|
||||
|
||||
class CoreActorTest(unittest.TestCase):
|
||||
|
||||
@ -7,8 +7,8 @@ import mock
|
||||
import pykka
|
||||
|
||||
from mopidy import core
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.models import Track
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
from tests import dummy_backend
|
||||
|
||||
@ -45,15 +45,12 @@ class BackendEventsTest(unittest.TestCase):
|
||||
self.assertEqual(send.call_args[1]['mute'], True)
|
||||
|
||||
def test_tracklist_add_sends_tracklist_changed_event(self, send):
|
||||
send.reset_mock()
|
||||
|
||||
self.core.tracklist.add(uris=['dummy:a']).get()
|
||||
|
||||
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
|
||||
|
||||
def test_tracklist_clear_sends_tracklist_changed_event(self, send):
|
||||
self.core.tracklist.add(uris=['dummy:a']).get()
|
||||
send.reset_mock()
|
||||
|
||||
self.core.tracklist.clear().get()
|
||||
|
||||
@ -61,7 +58,6 @@ class BackendEventsTest(unittest.TestCase):
|
||||
|
||||
def test_tracklist_move_sends_tracklist_changed_event(self, send):
|
||||
self.core.tracklist.add(uris=['dummy:a', 'dummy:b']).get()
|
||||
send.reset_mock()
|
||||
|
||||
self.core.tracklist.move(0, 1, 1).get()
|
||||
|
||||
@ -69,7 +65,6 @@ class BackendEventsTest(unittest.TestCase):
|
||||
|
||||
def test_tracklist_remove_sends_tracklist_changed_event(self, send):
|
||||
self.core.tracklist.add(uris=['dummy:a']).get()
|
||||
send.reset_mock()
|
||||
|
||||
self.core.tracklist.remove({'uri': ['dummy:a']}).get()
|
||||
|
||||
@ -77,29 +72,22 @@ class BackendEventsTest(unittest.TestCase):
|
||||
|
||||
def test_tracklist_shuffle_sends_tracklist_changed_event(self, send):
|
||||
self.core.tracklist.add(uris=['dummy:a', 'dummy:b']).get()
|
||||
send.reset_mock()
|
||||
|
||||
self.core.tracklist.shuffle().get()
|
||||
|
||||
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
|
||||
|
||||
def test_playlists_refresh_sends_playlists_loaded_event(self, send):
|
||||
send.reset_mock()
|
||||
|
||||
self.core.playlists.refresh().get()
|
||||
|
||||
self.assertEqual(send.call_args[0][0], 'playlists_loaded')
|
||||
|
||||
def test_playlists_refresh_uri_sends_playlists_loaded_event(self, send):
|
||||
send.reset_mock()
|
||||
|
||||
self.core.playlists.refresh(uri_scheme='dummy').get()
|
||||
|
||||
self.assertEqual(send.call_args[0][0], 'playlists_loaded')
|
||||
|
||||
def test_playlists_create_sends_playlist_changed_event(self, send):
|
||||
send.reset_mock()
|
||||
|
||||
self.core.playlists.create('foo').get()
|
||||
|
||||
self.assertEqual(send.call_args[0][0], 'playlist_changed')
|
||||
@ -112,7 +100,6 @@ class BackendEventsTest(unittest.TestCase):
|
||||
def test_playlists_save_sends_playlist_changed_event(self, send):
|
||||
playlist = self.core.playlists.create('foo').get()
|
||||
playlist = playlist.replace(name='bar')
|
||||
send.reset_mock()
|
||||
|
||||
self.core.playlists.save(playlist).get()
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@ import unittest
|
||||
import mock
|
||||
|
||||
from mopidy import backend, core
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.models import Image, Ref, SearchResult, Track
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
|
||||
class BaseCoreLibraryTest(unittest.TestCase):
|
||||
@ -15,24 +15,25 @@ class BaseCoreLibraryTest(unittest.TestCase):
|
||||
dummy1_root = Ref.directory(uri='dummy1:directory', name='dummy1')
|
||||
self.backend1 = mock.Mock()
|
||||
self.backend1.uri_schemes.get.return_value = ['dummy1']
|
||||
self.backend1.actor_ref.actor_class.__name__ = 'DummyBackend1'
|
||||
self.library1 = mock.Mock(spec=backend.LibraryProvider)
|
||||
self.library1.get_images().get.return_value = {}
|
||||
self.library1.get_images.reset_mock()
|
||||
self.library1.get_images.return_value.get.return_value = {}
|
||||
self.library1.root_directory.get.return_value = dummy1_root
|
||||
self.backend1.library = self.library1
|
||||
|
||||
dummy2_root = Ref.directory(uri='dummy2:directory', name='dummy2')
|
||||
self.backend2 = mock.Mock()
|
||||
self.backend2.uri_schemes.get.return_value = ['dummy2', 'du2']
|
||||
self.backend2.actor_ref.actor_class.__name__ = 'DummyBackend2'
|
||||
self.library2 = mock.Mock(spec=backend.LibraryProvider)
|
||||
self.library2.get_images().get.return_value = {}
|
||||
self.library2.get_images.reset_mock()
|
||||
self.library2.get_images.return_value.get.return_value = {}
|
||||
self.library2.root_directory.get.return_value = dummy2_root
|
||||
self.backend2.library = self.library2
|
||||
|
||||
# A backend without the optional library provider
|
||||
self.backend3 = mock.Mock()
|
||||
self.backend3.uri_schemes.get.return_value = ['dummy3']
|
||||
self.backend3.actor_ref.actor_class.__name__ = 'DummyBackend3'
|
||||
self.backend3.has_library().get.return_value = False
|
||||
self.backend3.has_library_browse().get.return_value = False
|
||||
|
||||
@ -65,20 +66,17 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
||||
self.library2.get_images.assert_called_once_with(['dummy2:track'])
|
||||
|
||||
def test_get_images_returns_images(self):
|
||||
self.library1.get_images().get.return_value = {
|
||||
self.library1.get_images.return_value.get.return_value = {
|
||||
'dummy1:track': [Image(uri='uri')]}
|
||||
self.library1.get_images.reset_mock()
|
||||
|
||||
result = self.core.library.get_images(['dummy1:track'])
|
||||
self.assertEqual({'dummy1:track': (Image(uri='uri'),)}, result)
|
||||
|
||||
def test_get_images_merges_results(self):
|
||||
self.library1.get_images().get.return_value = {
|
||||
self.library1.get_images.return_value.get.return_value = {
|
||||
'dummy1:track': [Image(uri='uri1')]}
|
||||
self.library1.get_images.reset_mock()
|
||||
self.library2.get_images().get.return_value = {
|
||||
self.library2.get_images.return_value.get.return_value = {
|
||||
'dummy2:track': [Image(uri='uri2')]}
|
||||
self.library2.get_images.reset_mock()
|
||||
|
||||
result = self.core.library.get_images(
|
||||
['dummy1:track', 'dummy2:track', 'dummy3:track', 'dummy4:track'])
|
||||
@ -106,11 +104,10 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
||||
self.assertFalse(self.library2.browse.called)
|
||||
|
||||
def test_browse_dummy1_selects_dummy1_backend(self):
|
||||
self.library1.browse().get.return_value = [
|
||||
self.library1.browse.return_value.get.return_value = [
|
||||
Ref.directory(uri='dummy1:directory:/foo/bar', name='bar'),
|
||||
Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'),
|
||||
]
|
||||
self.library1.browse.reset_mock()
|
||||
|
||||
self.core.library.browse('dummy1:directory:/foo')
|
||||
|
||||
@ -119,11 +116,10 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
||||
self.library1.browse.assert_called_with('dummy1:directory:/foo')
|
||||
|
||||
def test_browse_dummy2_selects_dummy2_backend(self):
|
||||
self.library2.browse().get.return_value = [
|
||||
self.library2.browse.return_value.get.return_value = [
|
||||
Ref.directory(uri='dummy2:directory:/bar/baz', name='quux'),
|
||||
Ref.track(uri='dummy2:track:/bar/foo.mp3', name='Baz'),
|
||||
]
|
||||
self.library2.browse.reset_mock()
|
||||
|
||||
self.core.library.browse('dummy2:directory:/bar')
|
||||
|
||||
@ -139,11 +135,10 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
||||
self.assertEqual(self.library2.browse.call_count, 0)
|
||||
|
||||
def test_browse_dir_returns_subdirs_and_tracks(self):
|
||||
self.library1.browse().get.return_value = [
|
||||
self.library1.browse.return_value.get.return_value = [
|
||||
Ref.directory(uri='dummy1:directory:/foo/bar', name='Bar'),
|
||||
Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'),
|
||||
]
|
||||
self.library1.browse.reset_mock()
|
||||
|
||||
result = self.core.library.browse('dummy1:directory:/foo')
|
||||
self.assertEqual(result, [
|
||||
@ -156,11 +151,14 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
||||
self.core.library.lookup('dummy1:a', ['dummy2:a'])
|
||||
|
||||
def test_lookup_can_handle_uris(self):
|
||||
self.library1.lookup().get.return_value = [1234]
|
||||
self.library2.lookup().get.return_value = [5678]
|
||||
track1 = Track(name='abc')
|
||||
track2 = Track(name='def')
|
||||
|
||||
self.library1.lookup().get.return_value = [track1]
|
||||
self.library2.lookup().get.return_value = [track2]
|
||||
|
||||
result = self.core.library.lookup(uris=['dummy1:a', 'dummy2:a'])
|
||||
self.assertEqual(result, {'dummy2:a': [5678], 'dummy1:a': [1234]})
|
||||
self.assertEqual(result, {'dummy2:a': [track2], 'dummy1:a': [track1]})
|
||||
|
||||
def test_lookup_uris_returns_empty_list_for_dummy3_track(self):
|
||||
result = self.core.library.lookup(uris=['dummy3:a'])
|
||||
@ -199,10 +197,8 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
||||
result1 = SearchResult(tracks=[track1])
|
||||
result2 = SearchResult(tracks=[track2])
|
||||
|
||||
self.library1.search().get.return_value = result1
|
||||
self.library1.search.reset_mock()
|
||||
self.library2.search().get.return_value = result2
|
||||
self.library2.search.reset_mock()
|
||||
self.library1.search.return_value.get.return_value = result1
|
||||
self.library2.search.return_value.get.return_value = result2
|
||||
|
||||
result = self.core.library.search({'any': ['a']})
|
||||
|
||||
@ -234,10 +230,8 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
||||
track1 = Track(uri='dummy1:a')
|
||||
result1 = SearchResult(tracks=[track1])
|
||||
|
||||
self.library1.search().get.return_value = result1
|
||||
self.library1.search.reset_mock()
|
||||
self.library2.search().get.return_value = None
|
||||
self.library2.search.reset_mock()
|
||||
self.library1.search.return_value.get.return_value = result1
|
||||
self.library2.search.return_value.get.return_value = None
|
||||
|
||||
result = self.core.library.search({'any': ['a']})
|
||||
|
||||
@ -254,10 +248,8 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
||||
result1 = SearchResult(tracks=[track1])
|
||||
result2 = SearchResult(tracks=[track2])
|
||||
|
||||
self.library1.search().get.return_value = result1
|
||||
self.library1.search.reset_mock()
|
||||
self.library2.search().get.return_value = result2
|
||||
self.library2.search.reset_mock()
|
||||
self.library1.search.return_value.get.return_value = result1
|
||||
self.library2.search.return_value.get.return_value = result2
|
||||
|
||||
result = self.core.library.search({'any': ['a']})
|
||||
|
||||
@ -363,12 +355,14 @@ class DeprecatedLookupCoreLibraryTest(BaseCoreLibraryTest):
|
||||
return super(DeprecatedLookupCoreLibraryTest, self).run(result)
|
||||
|
||||
def test_lookup_selects_dummy1_backend(self):
|
||||
self.library1.lookup.return_value.get.return_value = []
|
||||
self.core.library.lookup('dummy1:a')
|
||||
|
||||
self.library1.lookup.assert_called_once_with('dummy1:a')
|
||||
self.assertFalse(self.library2.lookup.called)
|
||||
|
||||
def test_lookup_selects_dummy2_backend(self):
|
||||
self.library2.lookup.return_value.get.return_value = []
|
||||
self.core.library.lookup('dummy2:a')
|
||||
|
||||
self.assertFalse(self.library1.lookup.called)
|
||||
@ -421,8 +415,7 @@ class LegacyFindExactToSearchLibraryTest(unittest.TestCase):
|
||||
# We are just testing that this doesn't fail.
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.library.logger')
|
||||
class BackendFailuresCoreLibraryTest(unittest.TestCase):
|
||||
class MockBackendCoreLibraryBase(unittest.TestCase):
|
||||
|
||||
def setUp(self): # noqa: N802
|
||||
dummy_root = Ref.directory(uri='dummy:directory', name='dummy')
|
||||
@ -437,52 +430,182 @@ class BackendFailuresCoreLibraryTest(unittest.TestCase):
|
||||
|
||||
self.core = core.Core(mixer=None, backends=[self.backend])
|
||||
|
||||
def test_browse_backend_get_root_exception_gets_ignored(self, logger):
|
||||
|
||||
@mock.patch('mopidy.core.library.logger')
|
||||
class BrowseBadBackendTest(MockBackendCoreLibraryBase):
|
||||
|
||||
def test_backend_raises_exception_for_root(self, logger):
|
||||
# Might happen if root_directory is a property for some weird reason.
|
||||
self.library.root_directory.get.side_effect = Exception
|
||||
self.assertEqual([], self.core.library.browse(None))
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_browse_backend_browse_uri_exception_gets_ignored(self, logger):
|
||||
def test_backend_returns_none_for_root(self, logger):
|
||||
self.library.root_directory.get.return_value = None
|
||||
self.assertEqual([], self.core.library.browse(None))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
def test_backend_returns_wrong_type_for_root(self, logger):
|
||||
self.library.root_directory.get.return_value = 123
|
||||
self.assertEqual([], self.core.library.browse(None))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
def test_backend_raises_exception_for_browse(self, logger):
|
||||
self.library.browse.return_value.get.side_effect = Exception
|
||||
self.assertEqual([], self.core.library.browse('dummy:directory'))
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_get_distinct_backend_exception_gets_ignored(self, logger):
|
||||
def test_backend_returns_wrong_type_for_browse(self, logger):
|
||||
self.library.browse.return_value.get.return_value = [123]
|
||||
self.assertEqual([], self.core.library.browse('dummy:directory'))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.library.logger')
|
||||
class GetDistinctBadBackendTest(MockBackendCoreLibraryBase):
|
||||
|
||||
def test_backend_raises_exception(self, logger):
|
||||
self.library.get_distinct.return_value.get.side_effect = Exception
|
||||
self.assertEqual(set(), self.core.library.get_distinct('artist'))
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_get_images_backend_exception_get_ignored(self, logger):
|
||||
def test_backend_returns_none(self, logger):
|
||||
self.library.get_distinct.return_value.get.return_value = None
|
||||
self.assertEqual(set(), self.core.library.get_distinct('artist'))
|
||||
self.assertFalse(logger.error.called)
|
||||
|
||||
def test_backend_returns_wrong_type(self, logger):
|
||||
self.library.get_distinct.return_value.get.return_value = 'abc'
|
||||
self.assertEqual(set(), self.core.library.get_distinct('artist'))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
def test_backend_returns_iterable_containing_wrong_types(self, logger):
|
||||
self.library.get_distinct.return_value.get.return_value = [1, 2, 3]
|
||||
self.assertEqual(set(), self.core.library.get_distinct('artist'))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.library.logger')
|
||||
class GetImagesBadBackendTest(MockBackendCoreLibraryBase):
|
||||
|
||||
def test_backend_raises_exception(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.get_images.return_value.get.side_effect = Exception
|
||||
self.assertEqual(
|
||||
{'dummy:/1': tuple()}, self.core.library.get_images(['dummy:/1']))
|
||||
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_lookup_backend_exceptiosn_gets_ignores(self, logger):
|
||||
def test_backend_returns_none(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.get_images.return_value.get.return_value = None
|
||||
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||
self.assertFalse(logger.error.called)
|
||||
|
||||
def test_backend_returns_wrong_type(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.get_images.return_value.get.return_value = 'abc'
|
||||
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
def test_backend_returns_mapping_containing_wrong_types(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.get_images.return_value.get.return_value = {uri: 'abc'}
|
||||
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
def test_backend_returns_mapping_containing_none(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.get_images.return_value.get.return_value = {uri: None}
|
||||
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
def test_backend_returns_unknown_uri(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.get_images.return_value.get.return_value = {'foo': []}
|
||||
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.library.logger')
|
||||
class LookupByUrisBadBackendTest(MockBackendCoreLibraryBase):
|
||||
|
||||
def test_backend_raises_exception(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.lookup.return_value.get.side_effect = Exception
|
||||
self.assertEqual(
|
||||
{'dummy:/1': []}, self.core.library.lookup(uris=['dummy:/1']))
|
||||
self.assertEqual({uri: []}, self.core.library.lookup(uris=[uri]))
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_refresh_backend_exception_gets_ignored(self, logger):
|
||||
def test_backend_returns_none(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.lookup.return_value.get.return_value = None
|
||||
self.assertEqual({uri: []}, self.core.library.lookup(uris=[uri]))
|
||||
self.assertFalse(logger.error.called)
|
||||
|
||||
def test_backend_returns_wrong_type(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.lookup.return_value.get.return_value = 'abc'
|
||||
self.assertEqual({uri: []}, self.core.library.lookup(uris=[uri]))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
def test_backend_returns_iterable_containing_wrong_types(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.lookup.return_value.get.return_value = [123]
|
||||
self.assertEqual({uri: []}, self.core.library.lookup(uris=[uri]))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
def test_backend_returns_none_with_uri(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.lookup.return_value.get.return_value = None
|
||||
self.assertEqual([], self.core.library.lookup(uri))
|
||||
self.assertFalse(logger.error.called)
|
||||
|
||||
def test_backend_returns_wrong_type_with_uri(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.lookup.return_value.get.return_value = 'abc'
|
||||
self.assertEqual([], self.core.library.lookup(uri))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
def test_backend_returns_iterable_wrong_types_with_uri(self, logger):
|
||||
uri = 'dummy:/1'
|
||||
self.library.lookup.return_value.get.return_value = [123]
|
||||
self.assertEqual([], self.core.library.lookup(uri))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.library.logger')
|
||||
class RefreshBadBackendTest(MockBackendCoreLibraryBase):
|
||||
|
||||
def test_backend_raises_exception(self, logger):
|
||||
self.library.refresh.return_value.get.side_effect = Exception
|
||||
self.core.library.refresh()
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_refresh_uri_backend_exception_gets_ignored(self, logger):
|
||||
def test_backend_raises_exception_with_uri(self, logger):
|
||||
self.library.refresh.return_value.get.side_effect = Exception
|
||||
self.core.library.refresh('dummy:/1')
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_search_backend_exception_gets_ignored(self, logger):
|
||||
|
||||
@mock.patch('mopidy.core.library.logger')
|
||||
class SearchBadBackendTest(MockBackendCoreLibraryBase):
|
||||
|
||||
def test_backend_raises_exception(self, logger):
|
||||
self.library.search.return_value.get.side_effect = Exception
|
||||
self.assertEqual([], self.core.library.search(query={'any': ['foo']}))
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_search_backend_lookup_error_gets_through(self, logger):
|
||||
def test_backend_raises_lookuperror(self, logger):
|
||||
# TODO: is this behavior desired? Do we need to continue handling
|
||||
# LookupError case specially.
|
||||
self.library.search.return_value.get.side_effect = LookupError
|
||||
with self.assertRaises(LookupError):
|
||||
self.core.library.search(query={'any': ['foo']})
|
||||
|
||||
def test_backend_returns_none(self, logger):
|
||||
self.library.search.return_value.get.return_value = None
|
||||
self.assertEqual([], self.core.library.search(query={'any': ['foo']}))
|
||||
self.assertFalse(logger.error.called)
|
||||
|
||||
def test_backend_returns_wrong_type(self, logger):
|
||||
self.library.search.return_value.get.return_value = 'abc'
|
||||
self.assertEqual([], self.core.library.search(query={'any': ['foo']}))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
@ -23,6 +23,7 @@ class CoreMixerTest(unittest.TestCase):
|
||||
self.mixer.get_volume.assert_called_once_with()
|
||||
|
||||
def test_set_volume(self):
|
||||
self.mixer.set_volume.return_value.get.return_value = True
|
||||
self.core.mixer.set_volume(30)
|
||||
|
||||
self.mixer.set_volume.assert_called_once_with(30)
|
||||
@ -34,6 +35,7 @@ class CoreMixerTest(unittest.TestCase):
|
||||
self.mixer.get_mute.assert_called_once_with()
|
||||
|
||||
def test_set_mute(self):
|
||||
self.mixer.set_mute.return_value.get.return_value = True
|
||||
self.core.mixer.set_mute(True)
|
||||
|
||||
self.mixer.set_mute.assert_called_once_with(True)
|
||||
@ -92,3 +94,63 @@ class CoreNoneMixerListenerTest(unittest.TestCase):
|
||||
def test_forwards_mixer_mute_changed_event_to_frontends(self, send):
|
||||
self.core.mixer.set_mute(mute=True)
|
||||
self.assertEqual(send.call_count, 0)
|
||||
|
||||
|
||||
class MockBackendCoreMixerBase(unittest.TestCase):
|
||||
|
||||
def setUp(self): # noqa: N802
|
||||
self.mixer = mock.Mock()
|
||||
self.mixer.actor_ref.actor_class.__name__ = 'DummyMixer'
|
||||
self.core = core.Core(mixer=self.mixer, backends=[])
|
||||
|
||||
|
||||
class GetVolumeBadBackendTest(MockBackendCoreMixerBase):
|
||||
|
||||
def test_backend_raises_exception(self):
|
||||
self.mixer.get_volume.return_value.get.side_effect = Exception
|
||||
self.assertEqual(self.core.mixer.get_volume(), None)
|
||||
|
||||
def test_backend_returns_too_small_value(self):
|
||||
self.mixer.get_volume.return_value.get.return_value = -1
|
||||
self.assertEqual(self.core.mixer.get_volume(), None)
|
||||
|
||||
def test_backend_returns_too_large_value(self):
|
||||
self.mixer.get_volume.return_value.get.return_value = 1000
|
||||
self.assertEqual(self.core.mixer.get_volume(), None)
|
||||
|
||||
def test_backend_returns_wrong_type(self):
|
||||
self.mixer.get_volume.return_value.get.return_value = '12'
|
||||
self.assertEqual(self.core.mixer.get_volume(), None)
|
||||
|
||||
|
||||
class SetVolumeBadBackendTest(MockBackendCoreMixerBase):
|
||||
|
||||
def test_backend_raises_exception(self):
|
||||
self.mixer.set_volume.return_value.get.side_effect = Exception
|
||||
self.assertFalse(self.core.mixer.set_volume(30))
|
||||
|
||||
def test_backend_returns_wrong_type(self):
|
||||
self.mixer.set_volume.return_value.get.return_value = 'done'
|
||||
self.assertFalse(self.core.mixer.set_volume(30))
|
||||
|
||||
|
||||
class GetMuteBadBackendTest(MockBackendCoreMixerBase):
|
||||
|
||||
def test_backend_raises_exception(self):
|
||||
self.mixer.get_mute.return_value.get.side_effect = Exception
|
||||
self.assertEqual(self.core.mixer.get_mute(), None)
|
||||
|
||||
def test_backend_returns_wrong_type(self):
|
||||
self.mixer.get_mute.return_value.get.return_value = '12'
|
||||
self.assertEqual(self.core.mixer.get_mute(), None)
|
||||
|
||||
|
||||
class SetMuteBadBackendTest(MockBackendCoreMixerBase):
|
||||
|
||||
def test_backend_raises_exception(self):
|
||||
self.mixer.set_mute.return_value.get.side_effect = Exception
|
||||
self.assertFalse(self.core.mixer.set_mute(True))
|
||||
|
||||
def test_backend_returns_wrong_type(self):
|
||||
self.mixer.set_mute.return_value.get.return_value = 'done'
|
||||
self.assertFalse(self.core.mixer.set_mute(True))
|
||||
|
||||
@ -7,8 +7,8 @@ import mock
|
||||
import pykka
|
||||
|
||||
from mopidy import backend, core
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.models import Track
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
from tests import dummy_audio as audio
|
||||
|
||||
@ -21,15 +21,13 @@ class CorePlaybackTest(unittest.TestCase):
|
||||
self.backend1 = mock.Mock()
|
||||
self.backend1.uri_schemes.get.return_value = ['dummy1']
|
||||
self.playback1 = mock.Mock(spec=backend.PlaybackProvider)
|
||||
self.playback1.get_time_position().get.return_value = 1000
|
||||
self.playback1.reset_mock()
|
||||
self.playback1.get_time_position.return_value.get.return_value = 1000
|
||||
self.backend1.playback = self.playback1
|
||||
|
||||
self.backend2 = mock.Mock()
|
||||
self.backend2.uri_schemes.get.return_value = ['dummy2']
|
||||
self.playback2 = mock.Mock(spec=backend.PlaybackProvider)
|
||||
self.playback2.get_time_position().get.return_value = 2000
|
||||
self.playback2.reset_mock()
|
||||
self.playback2.get_time_position.return_value.get.return_value = 2000
|
||||
self.backend2.playback = self.playback2
|
||||
|
||||
# A backend without the optional playback provider
|
||||
@ -123,6 +121,17 @@ class CorePlaybackTest(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
self.core.playback.get_current_track(), self.tracks[0])
|
||||
|
||||
def test_get_current_tlid_none(self):
|
||||
self.set_current_tl_track(None)
|
||||
|
||||
self.assertEqual(self.core.playback.get_current_tlid(), None)
|
||||
|
||||
def test_get_current_tlid_play(self):
|
||||
self.core.playback.play(self.tl_tracks[0])
|
||||
|
||||
self.assertEqual(
|
||||
self.core.playback.get_current_tlid(), self.tl_tracks[0].tlid)
|
||||
|
||||
# TODO Test state
|
||||
|
||||
def test_play_selects_dummy1_backend(self):
|
||||
|
||||
@ -5,8 +5,8 @@ import unittest
|
||||
import mock
|
||||
|
||||
from mopidy import backend, core
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.models import Playlist, Ref, Track
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
|
||||
class BasePlaylistsTest(unittest.TestCase):
|
||||
@ -90,8 +90,7 @@ class PlaylistTest(BasePlaylistsTest):
|
||||
|
||||
def test_create_without_uri_scheme_uses_first_backend(self):
|
||||
playlist = Playlist()
|
||||
self.sp1.create().get.return_value = playlist
|
||||
self.sp1.reset_mock()
|
||||
self.sp1.create.return_value.get.return_value = playlist
|
||||
|
||||
result = self.core.playlists.create('foo')
|
||||
|
||||
@ -99,10 +98,31 @@ class PlaylistTest(BasePlaylistsTest):
|
||||
self.sp1.create.assert_called_once_with('foo')
|
||||
self.assertFalse(self.sp2.create.called)
|
||||
|
||||
def test_create_without_uri_scheme_ignores_none_result(self):
|
||||
playlist = Playlist()
|
||||
self.sp1.create.return_value.get.return_value = None
|
||||
self.sp2.create.return_value.get.return_value = playlist
|
||||
|
||||
result = self.core.playlists.create('foo')
|
||||
|
||||
self.assertEqual(playlist, result)
|
||||
self.sp1.create.assert_called_once_with('foo')
|
||||
self.sp2.create.assert_called_once_with('foo')
|
||||
|
||||
def test_create_without_uri_scheme_ignores_exception(self):
|
||||
playlist = Playlist()
|
||||
self.sp1.create.return_value.get.side_effect = Exception
|
||||
self.sp2.create.return_value.get.return_value = playlist
|
||||
|
||||
result = self.core.playlists.create('foo')
|
||||
|
||||
self.assertEqual(playlist, result)
|
||||
self.sp1.create.assert_called_once_with('foo')
|
||||
self.sp2.create.assert_called_once_with('foo')
|
||||
|
||||
def test_create_with_uri_scheme_selects_the_matching_backend(self):
|
||||
playlist = Playlist()
|
||||
self.sp2.create().get.return_value = playlist
|
||||
self.sp2.reset_mock()
|
||||
self.sp2.create.return_value.get.return_value = playlist
|
||||
|
||||
result = self.core.playlists.create('foo', uri_scheme='dummy2')
|
||||
|
||||
@ -112,8 +132,7 @@ class PlaylistTest(BasePlaylistsTest):
|
||||
|
||||
def test_create_with_unsupported_uri_scheme_uses_first_backend(self):
|
||||
playlist = Playlist()
|
||||
self.sp1.create().get.return_value = playlist
|
||||
self.sp1.reset_mock()
|
||||
self.sp1.create.return_value.get.return_value = playlist
|
||||
|
||||
result = self.core.playlists.create('foo', uri_scheme='dummy3')
|
||||
|
||||
@ -190,8 +209,7 @@ class PlaylistTest(BasePlaylistsTest):
|
||||
|
||||
def test_save_selects_the_dummy1_backend(self):
|
||||
playlist = Playlist(uri='dummy1:a')
|
||||
self.sp1.save().get.return_value = playlist
|
||||
self.sp1.reset_mock()
|
||||
self.sp1.save.return_value.get.return_value = playlist
|
||||
|
||||
result = self.core.playlists.save(playlist)
|
||||
|
||||
@ -201,8 +219,7 @@ class PlaylistTest(BasePlaylistsTest):
|
||||
|
||||
def test_save_selects_the_dummy2_backend(self):
|
||||
playlist = Playlist(uri='dummy2:a')
|
||||
self.sp2.save().get.return_value = playlist
|
||||
self.sp2.reset_mock()
|
||||
self.sp2.save.return_value.get.return_value = playlist
|
||||
|
||||
result = self.core.playlists.save(playlist)
|
||||
|
||||
@ -281,8 +298,7 @@ class DeprecatedGetPlaylistsTest(BasePlaylistsTest):
|
||||
self.assertEqual(len(result[1].tracks), 0)
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.playlists.logger')
|
||||
class BackendFailuresCorePlaylistsTest(unittest.TestCase):
|
||||
class MockBackendCorePlaylistsBase(unittest.TestCase):
|
||||
|
||||
def setUp(self): # noqa: N802
|
||||
self.playlists = mock.Mock(spec=backend.PlaylistsProvider)
|
||||
@ -294,27 +310,127 @@ class BackendFailuresCorePlaylistsTest(unittest.TestCase):
|
||||
|
||||
self.core = core.Core(mixer=None, backends=[self.backend])
|
||||
|
||||
def test_as_list_backend_exception_gets_ignored(self, logger):
|
||||
self.playlists.as_list.get.side_effect = Exception
|
||||
|
||||
@mock.patch('mopidy.core.playlists.logger')
|
||||
class AsListBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||
|
||||
def test_backend_raises_exception(self, logger):
|
||||
self.playlists.as_list.return_value.get.side_effect = Exception
|
||||
self.assertEqual([], self.core.playlists.as_list())
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_get_items_backend_exception_gets_through(self, logger):
|
||||
# TODO: is this behavior desired?
|
||||
def test_backend_returns_none(self, logger):
|
||||
self.playlists.as_list.return_value.get.return_value = None
|
||||
self.assertEqual([], self.core.playlists.as_list())
|
||||
self.assertFalse(logger.error.called)
|
||||
|
||||
def test_backend_returns_wrong_type(self, logger):
|
||||
self.playlists.as_list.return_value.get.return_value = 'abc'
|
||||
self.assertEqual([], self.core.playlists.as_list())
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.playlists.logger')
|
||||
class GetItemsBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||
|
||||
def test_backend_raises_exception(self, logger):
|
||||
self.playlists.get_items.return_value.get.side_effect = Exception
|
||||
with self.assertRaises(Exception):
|
||||
self.core.playlists.get_items('dummy:/1')
|
||||
self.assertIsNone(self.core.playlists.get_items('dummy:/1'))
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_backend_returns_none(self, logger):
|
||||
self.playlists.get_items.return_value.get.return_value = None
|
||||
self.assertIsNone(self.core.playlists.get_items('dummy:/1'))
|
||||
self.assertFalse(logger.error.called)
|
||||
|
||||
def test_backend_returns_wrong_type(self, logger):
|
||||
self.playlists.get_items.return_value.get.return_value = 'abc'
|
||||
self.assertIsNone(self.core.playlists.get_items('dummy:/1'))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.playlists.logger')
|
||||
class CreateBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||
|
||||
def test_backend_raises_exception(self, logger):
|
||||
self.playlists.create.return_value.get.side_effect = Exception
|
||||
self.assertIsNone(self.core.playlists.create('foobar'))
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_backend_returns_none(self, logger):
|
||||
self.playlists.create.return_value.get.return_value = None
|
||||
self.assertIsNone(self.core.playlists.create('foobar'))
|
||||
self.assertFalse(logger.error.called)
|
||||
|
||||
def test_backend_returns_wrong_type(self, logger):
|
||||
self.playlists.create.return_value.get.return_value = 'abc'
|
||||
self.assertIsNone(self.core.playlists.create('foobar'))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.playlists.logger')
|
||||
class DeleteBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||
|
||||
def test_backend_raises_exception(self, logger):
|
||||
self.playlists.delete.return_value.get.side_effect = Exception
|
||||
self.assertIsNone(self.core.playlists.delete('dummy:/1'))
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.playlists.logger')
|
||||
class LookupBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||
|
||||
def test_backend_raises_exception(self, logger):
|
||||
self.playlists.lookup.return_value.get.side_effect = Exception
|
||||
self.assertIsNone(self.core.playlists.lookup('dummy:/1'))
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_backend_returns_none(self, logger):
|
||||
self.playlists.lookup.return_value.get.return_value = None
|
||||
self.assertIsNone(self.core.playlists.lookup('dummy:/1'))
|
||||
self.assertFalse(logger.error.called)
|
||||
|
||||
def test_backend_returns_wrong_type(self, logger):
|
||||
self.playlists.lookup.return_value.get.return_value = 'abc'
|
||||
self.assertIsNone(self.core.playlists.lookup('dummy:/1'))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.playlists.logger')
|
||||
class RefreshBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||
|
||||
@mock.patch('mopidy.core.listener.CoreListener.send')
|
||||
def test_refresh_backend_exception_gets_ignored(self, send, logger):
|
||||
def test_backend_raises_exception(self, send, logger):
|
||||
self.playlists.refresh.return_value.get.side_effect = Exception
|
||||
self.core.playlists.refresh()
|
||||
self.assertFalse(send.called)
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
@mock.patch('mopidy.core.listener.CoreListener.send')
|
||||
def test_refresh_uri_backend_exception_gets_ignored(self, send, logger):
|
||||
def test_backend_raises_exception_called_with_uri(self, send, logger):
|
||||
self.playlists.refresh.return_value.get.side_effect = Exception
|
||||
self.core.playlists.refresh('dummy')
|
||||
self.assertFalse(send.called)
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
|
||||
@mock.patch('mopidy.core.playlists.logger')
|
||||
class SaveBadBackendsTest(MockBackendCorePlaylistsBase):
|
||||
|
||||
def test_backend_raises_exception(self, logger):
|
||||
playlist = Playlist(uri='dummy:/1')
|
||||
self.playlists.save.return_value.get.side_effect = Exception
|
||||
self.assertIsNone(self.core.playlists.save(playlist))
|
||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||
|
||||
def test_backend_returns_none(self, logger):
|
||||
playlist = Playlist(uri='dummy:/1')
|
||||
self.playlists.save.return_value.get.return_value = None
|
||||
self.assertIsNone(self.core.playlists.save(playlist))
|
||||
self.assertFalse(logger.error.called)
|
||||
|
||||
def test_backend_returns_wrong_type(self, logger):
|
||||
playlist = Playlist(uri='dummy:/1')
|
||||
self.playlists.save.return_value.get.return_value = 'abc'
|
||||
self.assertIsNone(self.core.playlists.save(playlist))
|
||||
logger.error.assert_called_with(mock.ANY, 'DummyBackend', mock.ANY)
|
||||
|
||||
@ -5,8 +5,8 @@ import unittest
|
||||
import mock
|
||||
|
||||
from mopidy import backend, core
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.models import TlTrack, Track
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
|
||||
class TracklistTest(unittest.TestCase):
|
||||
|
||||
@ -12,8 +12,6 @@ from mopidy.http import actor
|
||||
class HttpEventsTest(unittest.TestCase):
|
||||
|
||||
def test_track_playback_paused_is_broadcasted(self, broadcast):
|
||||
broadcast.reset_mock()
|
||||
|
||||
actor.on_event('track_playback_paused', foo='bar')
|
||||
|
||||
self.assertDictEqual(
|
||||
@ -23,8 +21,6 @@ class HttpEventsTest(unittest.TestCase):
|
||||
})
|
||||
|
||||
def test_track_playback_resumed_is_broadcasted(self, broadcast):
|
||||
broadcast.reset_mock()
|
||||
|
||||
actor.on_event('track_playback_resumed', foo='bar')
|
||||
|
||||
self.assertDictEqual(
|
||||
|
||||
@ -11,7 +11,7 @@ from mock import Mock, call, patch, sentinel
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy.utils import network
|
||||
from mopidy.internal import network
|
||||
|
||||
from tests import any_int, any_unicode
|
||||
|
||||
@ -8,7 +8,7 @@ import unittest
|
||||
from mock import Mock, sentinel
|
||||
|
||||
from mopidy import compat
|
||||
from mopidy.utils import network
|
||||
from mopidy.internal import network
|
||||
|
||||
from tests import any_unicode
|
||||
|
||||
@ -8,7 +8,7 @@ import gobject
|
||||
|
||||
from mock import Mock, patch, sentinel
|
||||
|
||||
from mopidy.utils import network
|
||||
from mopidy.internal import network
|
||||
|
||||
from tests import any_int
|
||||
|
||||
@ -5,18 +5,18 @@ import unittest
|
||||
|
||||
from mock import Mock, patch
|
||||
|
||||
from mopidy.utils import network
|
||||
from mopidy.internal import network
|
||||
|
||||
|
||||
class FormatHostnameTest(unittest.TestCase):
|
||||
|
||||
@patch('mopidy.utils.network.has_ipv6', True)
|
||||
@patch('mopidy.internal.network.has_ipv6', True)
|
||||
def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self):
|
||||
network.has_ipv6 = True
|
||||
self.assertEqual(network.format_hostname('0.0.0.0'), '::ffff:0.0.0.0')
|
||||
self.assertEqual(network.format_hostname('1.0.0.1'), '::ffff:1.0.0.1')
|
||||
|
||||
@patch('mopidy.utils.network.has_ipv6', False)
|
||||
@patch('mopidy.internal.network.has_ipv6', False)
|
||||
def test_format_hostname_does_nothing_when_only_ipv4_available(self):
|
||||
network.has_ipv6 = False
|
||||
self.assertEqual(network.format_hostname('0.0.0.0'), '0.0.0.0')
|
||||
@ -43,14 +43,14 @@ class TryIPv6SocketTest(unittest.TestCase):
|
||||
|
||||
class CreateSocketTest(unittest.TestCase):
|
||||
|
||||
@patch('mopidy.utils.network.has_ipv6', False)
|
||||
@patch('mopidy.internal.network.has_ipv6', False)
|
||||
@patch('socket.socket')
|
||||
def test_ipv4_socket(self, socket_mock):
|
||||
network.create_socket()
|
||||
self.assertEqual(
|
||||
socket_mock.call_args[0], (socket.AF_INET, socket.SOCK_STREAM))
|
||||
|
||||
@patch('mopidy.utils.network.has_ipv6', True)
|
||||
@patch('mopidy.internal.network.has_ipv6', True)
|
||||
@patch('socket.socket')
|
||||
def test_ipv6_socket(self, socket_mock):
|
||||
network.create_socket()
|
||||
@ -12,7 +12,7 @@ import pygst
|
||||
pygst.require('0.10')
|
||||
import gst # noqa
|
||||
|
||||
from mopidy.utils import deps
|
||||
from mopidy.internal import deps
|
||||
|
||||
|
||||
class DepsTest(unittest.TestCase):
|
||||
@ -4,16 +4,16 @@ import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from mopidy.utils.encoding import locale_decode
|
||||
from mopidy.internal import encoding
|
||||
|
||||
|
||||
@mock.patch('mopidy.utils.encoding.locale.getpreferredencoding')
|
||||
@mock.patch('mopidy.internal.encoding.locale.getpreferredencoding')
|
||||
class LocaleDecodeTest(unittest.TestCase):
|
||||
|
||||
def test_can_decode_utf8_strings_with_french_content(self, mock):
|
||||
mock.return_value = 'UTF-8'
|
||||
|
||||
result = locale_decode(
|
||||
result = encoding.locale_decode(
|
||||
b'[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
|
||||
|
||||
self.assertEqual('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
|
||||
@ -22,7 +22,7 @@ class LocaleDecodeTest(unittest.TestCase):
|
||||
mock.return_value = 'UTF-8'
|
||||
|
||||
error = IOError(98, b'Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
|
||||
result = locale_decode(error)
|
||||
result = encoding.locale_decode(error)
|
||||
expected = '[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e'
|
||||
|
||||
self.assertEqual(
|
||||
@ -33,13 +33,13 @@ class LocaleDecodeTest(unittest.TestCase):
|
||||
def test_does_not_use_locale_to_decode_unicode_strings(self, mock):
|
||||
mock.return_value = 'UTF-8'
|
||||
|
||||
locale_decode('abc')
|
||||
encoding.locale_decode('abc')
|
||||
|
||||
self.assertFalse(mock.called)
|
||||
|
||||
def test_does_not_use_locale_to_decode_ascii_bytestrings(self, mock):
|
||||
mock.return_value = 'UTF-8'
|
||||
|
||||
locale_decode('abc')
|
||||
encoding.locale_decode('abc')
|
||||
|
||||
self.assertFalse(mock.called)
|
||||
@ -8,7 +8,7 @@ import mock
|
||||
import pykka
|
||||
|
||||
from mopidy import core, models
|
||||
from mopidy.utils import deprecation, jsonrpc
|
||||
from mopidy.internal import deprecation, jsonrpc
|
||||
|
||||
from tests import dummy_backend
|
||||
|
||||
@ -10,7 +10,7 @@ import unittest
|
||||
import glib
|
||||
|
||||
from mopidy import compat, exceptions
|
||||
from mopidy.utils import path
|
||||
from mopidy.internal import path
|
||||
|
||||
import tests
|
||||
|
||||
@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals
|
||||
from pytest import raises
|
||||
|
||||
from mopidy import compat, exceptions
|
||||
from mopidy.utils import validation
|
||||
from mopidy.internal import validation
|
||||
|
||||
|
||||
def test_check_boolean_with_valid_values():
|
||||
@ -6,7 +6,7 @@ import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from mopidy.utils import xdg
|
||||
from mopidy.internal import xdg
|
||||
|
||||
|
||||
@pytest.yield_fixture
|
||||
@ -1,6 +1,6 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from mopidy.utils import deprecation
|
||||
from mopidy.internal import deprecation
|
||||
|
||||
|
||||
def generate_song(i):
|
||||
|
||||
@ -9,9 +9,9 @@ import pykka
|
||||
|
||||
from mopidy import core
|
||||
from mopidy.core import PlaybackState
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.local import actor
|
||||
from mopidy.models import TlTrack, Track
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
from tests import dummy_audio, path_to_data_dir
|
||||
from tests.local import generate_song, populate_tracklist
|
||||
|
||||
@ -7,9 +7,9 @@ import pykka
|
||||
|
||||
from mopidy import core
|
||||
from mopidy.core import PlaybackState
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.local import actor
|
||||
from mopidy.models import Playlist, Track
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
from tests import dummy_audio, path_to_data_dir
|
||||
from tests.local import generate_song, populate_tracklist
|
||||
|
||||
98
tests/local/test_translator.py
Normal file
98
tests/local/test_translator.py
Normal file
@ -0,0 +1,98 @@
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pytest
|
||||
|
||||
from mopidy.local import translator
|
||||
|
||||
|
||||
@pytest.mark.parametrize('local_uri,file_uri', [
|
||||
('local:directory:A/B', 'file:///home/alice/Music/A/B'),
|
||||
('local:directory:A%20B', 'file:///home/alice/Music/A%20B'),
|
||||
('local:directory:A+B', 'file:///home/alice/Music/A%2BB'),
|
||||
(
|
||||
'local:directory:%C3%A6%C3%B8%C3%A5',
|
||||
'file:///home/alice/Music/%C3%A6%C3%B8%C3%A5'),
|
||||
('local:track:A/B.mp3', 'file:///home/alice/Music/A/B.mp3'),
|
||||
('local:track:A%20B.mp3', 'file:///home/alice/Music/A%20B.mp3'),
|
||||
('local:track:A+B.mp3', 'file:///home/alice/Music/A%2BB.mp3'),
|
||||
(
|
||||
'local:track:%C3%A6%C3%B8%C3%A5.mp3',
|
||||
'file:///home/alice/Music/%C3%A6%C3%B8%C3%A5.mp3'),
|
||||
])
|
||||
def test_local_uri_to_file_uri(local_uri, file_uri):
|
||||
media_dir = b'/home/alice/Music'
|
||||
|
||||
assert translator.local_uri_to_file_uri(local_uri, media_dir) == file_uri
|
||||
|
||||
|
||||
@pytest.mark.parametrize('uri', [
|
||||
'A/B',
|
||||
'local:foo:A/B',
|
||||
])
|
||||
def test_local_uri_to_file_uri_errors(uri):
|
||||
media_dir = b'/home/alice/Music'
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
translator.local_uri_to_file_uri(uri, media_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('uri,path', [
|
||||
('local:directory:A/B', b'/home/alice/Music/A/B'),
|
||||
('local:directory:A%20B', b'/home/alice/Music/A B'),
|
||||
('local:directory:A+B', b'/home/alice/Music/A+B'),
|
||||
('local:directory:%C3%A6%C3%B8%C3%A5', b'/home/alice/Music/æøå'),
|
||||
('local:track:A/B.mp3', b'/home/alice/Music/A/B.mp3'),
|
||||
('local:track:A%20B.mp3', b'/home/alice/Music/A B.mp3'),
|
||||
('local:track:A+B.mp3', b'/home/alice/Music/A+B.mp3'),
|
||||
('local:track:%C3%A6%C3%B8%C3%A5.mp3', b'/home/alice/Music/æøå.mp3'),
|
||||
])
|
||||
def test_local_uri_to_path(uri, path):
|
||||
media_dir = b'/home/alice/Music'
|
||||
|
||||
assert translator.local_uri_to_path(uri, media_dir) == path
|
||||
|
||||
# Legacy version to keep old versions of Mopidy-Local-Sqlite working
|
||||
assert translator.local_track_uri_to_path(uri, media_dir) == path
|
||||
|
||||
|
||||
@pytest.mark.parametrize('uri', [
|
||||
'A/B',
|
||||
'local:foo:A/B',
|
||||
])
|
||||
def test_local_uri_to_path_errors(uri):
|
||||
media_dir = b'/home/alice/Music'
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
translator.local_uri_to_path(uri, media_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path,uri', [
|
||||
('/foo', 'file:///foo'),
|
||||
(b'/foo', 'file:///foo'),
|
||||
('/æøå', 'file:///%C3%A6%C3%B8%C3%A5'),
|
||||
(b'/\x00\x01\x02', 'file:///%00%01%02'),
|
||||
])
|
||||
def test_path_to_file_uri(path, uri):
|
||||
assert translator.path_to_file_uri(path) == uri
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path,uri', [
|
||||
('foo', 'local:track:foo'),
|
||||
(b'foo', 'local:track:foo'),
|
||||
('æøå', 'local:track:%C3%A6%C3%B8%C3%A5'),
|
||||
(b'\x00\x01\x02', 'local:track:%00%01%02'),
|
||||
])
|
||||
def test_path_to_local_track_uri(path, uri):
|
||||
assert translator.path_to_local_track_uri(path) == uri
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path,uri', [
|
||||
('foo', 'local:directory:foo'),
|
||||
(b'foo', 'local:directory:foo'),
|
||||
('æøå', 'local:directory:%C3%A6%C3%B8%C3%A5'),
|
||||
(b'\x00\x01\x02', 'local:directory:%00%01%02'),
|
||||
])
|
||||
def test_path_to_local_directory_uri(path, uri):
|
||||
assert translator.path_to_local_directory_uri(path) == uri
|
||||
@ -8,10 +8,10 @@ import unittest
|
||||
import pykka
|
||||
|
||||
from mopidy import core
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.m3u import actor
|
||||
from mopidy.m3u.translator import playlist_uri_to_path
|
||||
from mopidy.models import Playlist, Track
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
from tests import dummy_audio, path_to_data_dir
|
||||
from tests.m3u import generate_song
|
||||
|
||||
@ -6,9 +6,9 @@ import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from mopidy.internal import path
|
||||
from mopidy.m3u import translator
|
||||
from mopidy.models import Track
|
||||
from mopidy.utils import path
|
||||
|
||||
from tests import path_to_data_dir
|
||||
|
||||
|
||||
@ -3,15 +3,14 @@ from __future__ import absolute_import, unicode_literals
|
||||
import unittest
|
||||
|
||||
from mopidy.models.fields import * # noqa: F403
|
||||
from mopidy.models.immutable import ImmutableObjectMeta
|
||||
|
||||
|
||||
def create_instance(field):
|
||||
"""Create an instance of a dummy class for testing fields."""
|
||||
|
||||
class Dummy(object):
|
||||
__metaclass__ = ImmutableObjectMeta
|
||||
attr = field
|
||||
attr._name = 'attr'
|
||||
|
||||
return Dummy()
|
||||
|
||||
|
||||
164
tests/models/test_legacy.py
Normal file
164
tests/models/test_legacy.py
Normal file
@ -0,0 +1,164 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import unittest
|
||||
|
||||
from mopidy.models import ImmutableObject
|
||||
|
||||
|
||||
class Model(ImmutableObject):
|
||||
uri = None
|
||||
name = None
|
||||
models = frozenset()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__dict__['models'] = frozenset(kwargs.pop('models', None) or [])
|
||||
super(Model, self).__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
class SubModel(ImmutableObject):
|
||||
uri = None
|
||||
name = None
|
||||
|
||||
|
||||
class GenericCopyTest(unittest.TestCase):
|
||||
def compare(self, orig, other):
|
||||
self.assertEqual(orig, other)
|
||||
self.assertNotEqual(id(orig), id(other))
|
||||
|
||||
def test_copying_model(self):
|
||||
model = Model()
|
||||
self.compare(model, model.replace())
|
||||
|
||||
def test_copying_model_with_basic_values(self):
|
||||
model = Model(name='foo', uri='bar')
|
||||
other = model.replace(name='baz')
|
||||
self.assertEqual('baz', other.name)
|
||||
self.assertEqual('bar', other.uri)
|
||||
|
||||
def test_copying_model_with_missing_values(self):
|
||||
model = Model(uri='bar')
|
||||
other = model.replace(name='baz')
|
||||
self.assertEqual('baz', other.name)
|
||||
self.assertEqual('bar', other.uri)
|
||||
|
||||
def test_copying_model_with_private_internal_value(self):
|
||||
model = Model(models=[SubModel(name=123)])
|
||||
other = model.replace(models=[SubModel(name=345)])
|
||||
self.assertIn(SubModel(name=345), other.models)
|
||||
|
||||
def test_copying_model_with_invalid_key(self):
|
||||
with self.assertRaises(TypeError):
|
||||
Model().replace(invalid_key=True)
|
||||
|
||||
def test_copying_model_to_remove(self):
|
||||
model = Model(name='foo').replace(name=None)
|
||||
self.assertEqual(model, Model())
|
||||
|
||||
|
||||
class ModelTest(unittest.TestCase):
|
||||
def test_uri(self):
|
||||
uri = 'an_uri'
|
||||
model = Model(uri=uri)
|
||||
self.assertEqual(model.uri, uri)
|
||||
with self.assertRaises(AttributeError):
|
||||
model.uri = None
|
||||
|
||||
def test_name(self):
|
||||
name = 'a name'
|
||||
model = Model(name=name)
|
||||
self.assertEqual(model.name, name)
|
||||
with self.assertRaises(AttributeError):
|
||||
model.name = None
|
||||
|
||||
def test_submodels(self):
|
||||
models = [SubModel(name=123), SubModel(name=456)]
|
||||
model = Model(models=models)
|
||||
self.assertEqual(set(model.models), set(models))
|
||||
with self.assertRaises(AttributeError):
|
||||
model.models = None
|
||||
|
||||
def test_models_none(self):
|
||||
self.assertEqual(set(), Model(models=None).models)
|
||||
|
||||
def test_invalid_kwarg(self):
|
||||
with self.assertRaises(TypeError):
|
||||
Model(foo='baz')
|
||||
|
||||
def test_repr_without_models(self):
|
||||
self.assertEqual(
|
||||
"Model(name=u'name', uri=u'uri')",
|
||||
repr(Model(uri='uri', name='name')))
|
||||
|
||||
def test_repr_with_models(self):
|
||||
self.assertEqual(
|
||||
"Model(models=[SubModel(name=123)], name=u'name', uri=u'uri')",
|
||||
repr(Model(uri='uri', name='name', models=[SubModel(name=123)])))
|
||||
|
||||
def test_serialize_without_models(self):
|
||||
self.assertDictEqual(
|
||||
{'__model__': 'Model', 'uri': 'uri', 'name': 'name'},
|
||||
Model(uri='uri', name='name').serialize())
|
||||
|
||||
def test_serialize_with_models(self):
|
||||
submodel = SubModel(name=123)
|
||||
self.assertDictEqual(
|
||||
{'__model__': 'Model', 'uri': 'uri', 'name': 'name',
|
||||
'models': [submodel.serialize()]},
|
||||
Model(uri='uri', name='name', models=[submodel]).serialize())
|
||||
|
||||
def test_eq_uri(self):
|
||||
model1 = Model(uri='uri1')
|
||||
model2 = Model(uri='uri1')
|
||||
self.assertEqual(model1, model2)
|
||||
self.assertEqual(hash(model1), hash(model2))
|
||||
|
||||
def test_eq_name(self):
|
||||
model1 = Model(name='name1')
|
||||
model2 = Model(name='name1')
|
||||
self.assertEqual(model1, model2)
|
||||
self.assertEqual(hash(model1), hash(model2))
|
||||
|
||||
def test_eq_models(self):
|
||||
models = [SubModel()]
|
||||
model1 = Model(models=models)
|
||||
model2 = Model(models=models)
|
||||
self.assertEqual(model1, model2)
|
||||
self.assertEqual(hash(model1), hash(model2))
|
||||
|
||||
def test_eq_models_order(self):
|
||||
submodel1 = SubModel(name='name1')
|
||||
submodel2 = SubModel(name='name2')
|
||||
model1 = Model(models=[submodel1, submodel2])
|
||||
model2 = Model(models=[submodel2, submodel1])
|
||||
self.assertEqual(model1, model2)
|
||||
self.assertEqual(hash(model1), hash(model2))
|
||||
|
||||
def test_eq_none(self):
|
||||
self.assertNotEqual(Model(), None)
|
||||
|
||||
def test_eq_other(self):
|
||||
self.assertNotEqual(Model(), 'other')
|
||||
|
||||
def test_ne_uri(self):
|
||||
model1 = Model(uri='uri1')
|
||||
model2 = Model(uri='uri2')
|
||||
self.assertNotEqual(model1, model2)
|
||||
self.assertNotEqual(hash(model1), hash(model2))
|
||||
|
||||
def test_ne_name(self):
|
||||
model1 = Model(name='name1')
|
||||
model2 = Model(name='name2')
|
||||
self.assertNotEqual(model1, model2)
|
||||
self.assertNotEqual(hash(model1), hash(model2))
|
||||
|
||||
def test_ne_models(self):
|
||||
model1 = Model(models=[SubModel(name='name1')])
|
||||
model2 = Model(models=[SubModel(name='name2')])
|
||||
self.assertNotEqual(model1, model2)
|
||||
self.assertNotEqual(hash(model1), hash(model2))
|
||||
|
||||
def test_ignores_values_with_default_value_none(self):
|
||||
model1 = Model(name='name1')
|
||||
model2 = Model(name='name1', uri=None)
|
||||
self.assertEqual(model1, model2)
|
||||
self.assertEqual(hash(model1), hash(model2))
|
||||
@ -18,6 +18,26 @@ class InheritanceTest(unittest.TestCase):
|
||||
class Foo(Track):
|
||||
pass
|
||||
|
||||
def test_sub_class_can_have_its_own_slots(self):
|
||||
# Needed for things like SpotifyTrack in mopidy-spotify 1.x
|
||||
|
||||
class Foo(Track):
|
||||
__slots__ = ('_foo',)
|
||||
|
||||
f = Foo()
|
||||
f._foo = 123
|
||||
|
||||
def test_sub_class_can_be_initialized(self):
|
||||
# Fails with following error if fields are not handled across classes.
|
||||
# TypeError: __init__() got an unexpected keyword argument "type"
|
||||
# Essentially this is testing that sub-classes take parent _fields into
|
||||
# account.
|
||||
|
||||
class Foo(Ref):
|
||||
pass
|
||||
|
||||
Foo.directory()
|
||||
|
||||
|
||||
class CachingTest(unittest.TestCase):
|
||||
|
||||
@ -1148,9 +1168,3 @@ class SearchResultTest(unittest.TestCase):
|
||||
self.assertDictEqual(
|
||||
{'__model__': 'SearchResult', 'uri': 'uri'},
|
||||
SearchResult(uri='uri').serialize())
|
||||
|
||||
def test_to_json_and_back(self):
|
||||
result1 = SearchResult(uri='uri')
|
||||
serialized = json.dumps(result1, cls=ModelJSONEncoder)
|
||||
result2 = json.loads(serialized, object_hook=model_json_decoder)
|
||||
self.assertEqual(result1, result2)
|
||||
|
||||
@ -7,8 +7,8 @@ import mock
|
||||
import pykka
|
||||
|
||||
from mopidy import core
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.mpd import session, uri_mapper
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
from tests import dummy_backend, dummy_mixer
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from mopidy.internal import deprecation
|
||||
from mopidy.models import Ref, Track
|
||||
from mopidy.utils import deprecation
|
||||
|
||||
from tests.mpd import protocol
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user