Merge branch 'develop' into feature/http-helpers
Conflicts: docs/changelog.rst
This commit is contained in:
commit
f6f445e4b6
@ -1,8 +1,8 @@
|
|||||||
.. _concepts:
|
.. _concepts:
|
||||||
|
|
||||||
*************************
|
************
|
||||||
Architecture and concepts
|
Architecture
|
||||||
*************************
|
************
|
||||||
|
|
||||||
The overall architecture of Mopidy is organized around multiple frontends and
|
The overall architecture of Mopidy is organized around multiple frontends and
|
||||||
backends. The frontends use the core API. The core actor makes multiple backends
|
backends. The frontends use the core API. The core actor makes multiple backends
|
||||||
@ -1,8 +1,8 @@
|
|||||||
.. _audio-api:
|
.. _audio-api:
|
||||||
|
|
||||||
*********
|
*********************************
|
||||||
Audio API
|
:mod:`mopidy.audio` --- Audio API
|
||||||
*********
|
*********************************
|
||||||
|
|
||||||
.. module:: mopidy.audio
|
.. module:: mopidy.audio
|
||||||
:synopsis: Thin wrapper around the parts of GStreamer we use
|
:synopsis: Thin wrapper around the parts of GStreamer we use
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
.. _backend-api:
|
.. _backend-api:
|
||||||
|
|
||||||
***********
|
*************************************
|
||||||
Backend API
|
:mod:`mopidy.backend` --- Backend API
|
||||||
***********
|
*************************************
|
||||||
|
|
||||||
.. module:: mopidy.backend
|
.. module:: mopidy.backend
|
||||||
:synopsis: The API implemented by backends
|
:synopsis: The API implemented by backends
|
||||||
@ -1,8 +1,8 @@
|
|||||||
.. _commands-api:
|
.. _commands-api:
|
||||||
|
|
||||||
************
|
***************************************
|
||||||
Commands API
|
:mod:`mopidy.commands` --- Commands API
|
||||||
************
|
***************************************
|
||||||
|
|
||||||
.. automodule:: mopidy.commands
|
.. automodule:: mopidy.commands
|
||||||
:synopsis: Commands API for Mopidy CLI.
|
:synopsis: Commands API for Mopidy CLI.
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
.. _config-api:
|
.. _config-api:
|
||||||
|
|
||||||
**********
|
***********************************
|
||||||
Config API
|
:mod:`mopidy.config` --- Config API
|
||||||
**********
|
***********************************
|
||||||
|
|
||||||
.. automodule:: mopidy.config
|
.. automodule:: mopidy.config
|
||||||
:synopsis: Config API for config loading and validation
|
:synopsis: Config API for config loading and validation
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
.. _core-api:
|
.. _core-api:
|
||||||
|
|
||||||
********
|
*******************************
|
||||||
Core API
|
:mod:`mopidy.core` --- Core API
|
||||||
********
|
*******************************
|
||||||
|
|
||||||
.. module:: mopidy.core
|
.. module:: mopidy.core
|
||||||
:synopsis: Core API for use by frontends
|
:synopsis: Core API for use by frontends
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
.. _ext-api:
|
.. _ext-api:
|
||||||
|
|
||||||
*************
|
**********************************
|
||||||
Extension API
|
:mod:`mopidy.ext` -- Extension API
|
||||||
*************
|
**********************************
|
||||||
|
|
||||||
If you want to learn how to make Mopidy extensions, read :ref:`extensiondev`.
|
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.
|
guide.
|
||||||
|
|
||||||
|
|
||||||
|
.. _static-web-client:
|
||||||
|
|
||||||
Static web client example
|
Static web client example
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|||||||
@ -4,9 +4,6 @@
|
|||||||
HTTP JSON-RPC API
|
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
|
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
|
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
|
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
|
please refer to the `JSON-RPC 2.0 spec
|
||||||
<http://www.jsonrpc.org/specification>`_.
|
<http://www.jsonrpc.org/specification>`_.
|
||||||
|
|
||||||
All methods (not attributes) in the :ref:`core-api` is made available through
|
All methods in the :ref:`core-api` is made available through JSON-RPC calls
|
||||||
JSON-RPC calls over the WebSocket. For example,
|
over the WebSocket. For example, :meth:`mopidy.core.PlaybackController.play` is
|
||||||
:meth:`mopidy.core.PlaybackController.play` is available as the JSON-RPC method
|
available as the JSON-RPC method ``core.playback.play``.
|
||||||
``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``.
|
|
||||||
|
|
||||||
Example JSON-RPC request::
|
Example JSON-RPC request::
|
||||||
|
|
||||||
|
|||||||
@ -4,26 +4,56 @@
|
|||||||
API reference
|
API reference
|
||||||
*************
|
*************
|
||||||
|
|
||||||
.. note:: What is public?
|
.. note::
|
||||||
|
|
||||||
Only APIs documented here are public and open for use by Mopidy
|
Only APIs documented here are public and open for use by Mopidy
|
||||||
extensions.
|
extensions.
|
||||||
|
|
||||||
|
|
||||||
.. toctree::
|
Concepts
|
||||||
:glob:
|
========
|
||||||
|
|
||||||
concepts
|
.. toctree::
|
||||||
|
|
||||||
|
architecture
|
||||||
models
|
models
|
||||||
backends
|
|
||||||
|
|
||||||
|
Basics
|
||||||
|
======
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
core
|
core
|
||||||
audio
|
frontend
|
||||||
mixer
|
backend
|
||||||
frontends
|
|
||||||
commands
|
|
||||||
ext
|
ext
|
||||||
config
|
|
||||||
zeroconf
|
|
||||||
|
Web/JavaScript
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
http-server
|
http-server
|
||||||
http
|
http
|
||||||
js
|
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.
|
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
|
Thus, if you use Mopidy to host your web client, like described in
|
||||||
load the latest version of Mopidy.js by adding the following script tag to your
|
:ref:`static-web-client`, you can load the latest version of Mopidy.js by
|
||||||
HTML file:
|
adding the following script tag to your HTML file:
|
||||||
|
|
||||||
.. code-block:: html
|
.. 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``
|
from the call, the errback will be called with a ``Mopidy.ConnectionError``
|
||||||
instance.
|
instance.
|
||||||
|
|
||||||
All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. The core
|
All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. For
|
||||||
API attributes is *not* available, but that shouldn't be a problem as we've
|
example, the :meth:`mopidy.core.PlaybackController.get_state` method is
|
||||||
added (undocumented) getters and setters for all of them, so you can access the
|
available in JSON-RPC as the method ``core.playback.get_state`` and in
|
||||||
attributes as well from JavaScript. For example, the
|
Mopidy.js as ``mopidy.playback.getState()``.
|
||||||
: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()``.
|
|
||||||
|
|
||||||
Both the WebSocket API and the JavaScript API are based on introspection of the
|
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
|
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
|
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-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
|
by JSON-RPC 2.0.
|
||||||
passing parameters by-position.
|
|
||||||
|
|
||||||
Obviously, you'll want to get a return value from many of your method calls.
|
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
|
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));
|
.done(printCurrentTrack, console.error.bind(console));
|
||||||
|
|
||||||
If you don't hook up an error handler function and never call ``done()`` on the
|
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
|
promise object, warnings will be logged to the console complaining that you
|
||||||
unhandled errors. In general, unhandled errors will not go silently missing.
|
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
|
The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A
|
||||||
<http://wiki.commonjs.org/wiki/Promises/A>`_ standard. We use the
|
<http://wiki.commonjs.org/wiki/Promises/A>`_ standard. We use the
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
.. _mixer-api:
|
.. _mixer-api:
|
||||||
|
|
||||||
***************
|
***************************************
|
||||||
Audio mixer API
|
:mod:`mopidy.mixer` --- Audio mixer API
|
||||||
***************
|
***************************************
|
||||||
|
|
||||||
.. module:: mopidy.mixer
|
.. module:: mopidy.mixer
|
||||||
:synopsis: The audio mixer API
|
: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
|
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
|
backends and between the backends and the MPD frontend. All fields are optional
|
||||||
@ -77,8 +77,29 @@ Data model helpers
|
|||||||
.. autoclass:: mopidy.models.ImmutableObject
|
.. autoclass:: mopidy.models.ImmutableObject
|
||||||
:members:
|
: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
|
.. 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:
|
||||||
|
|
||||||
************
|
***************************************
|
||||||
Zeroconf API
|
:mod:`mopidy.zeroconf` --- Zeroconf API
|
||||||
************
|
***************************************
|
||||||
|
|
||||||
.. module:: mopidy.zeroconf
|
.. module:: mopidy.zeroconf
|
||||||
:synopsis: Helper for publishing of services on Zeroconf
|
:synopsis: Helper for publishing of services on Zeroconf
|
||||||
|
|||||||
@ -12,10 +12,11 @@ Core API
|
|||||||
|
|
||||||
- Calling the following methods with ``kwargs`` is being deprecated.
|
- Calling the following methods with ``kwargs`` is being deprecated.
|
||||||
(PR: :issue:`1090`)
|
(PR: :issue:`1090`)
|
||||||
- :meth:`mopidy.core.library.LibraryController.search`
|
|
||||||
- :meth:`mopidy.core.library.PlaylistsController.filter`
|
- :meth:`mopidy.core.library.LibraryController.search`
|
||||||
- :meth:`mopidy.core.library.TracklistController.filter`
|
- :meth:`mopidy.core.library.PlaylistsController.filter`
|
||||||
- :meth:`mopidy.core.library.TracklistController.remove`
|
- :meth:`mopidy.core.library.TracklistController.filter`
|
||||||
|
- :meth:`mopidy.core.library.TracklistController.remove`
|
||||||
|
|
||||||
- Updated core controllers to handle backend exceptions in all calls that rely
|
- Updated core controllers to handle backend exceptions in all calls that rely
|
||||||
on multiple backends. (Issue: :issue:`667`)
|
on multiple backends. (Issue: :issue:`667`)
|
||||||
@ -27,6 +28,10 @@ Core API
|
|||||||
``tl_track`` versions of the calls. (Fixes: :issue:`1131` PR: :issue:`1136`,
|
``tl_track`` versions of the calls. (Fixes: :issue:`1131` PR: :issue:`1136`,
|
||||||
:issue:`1140`)
|
: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
|
Models
|
||||||
------
|
------
|
||||||
@ -58,6 +63,26 @@ Internal changes
|
|||||||
:issue:`1115`)
|
: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)
|
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
|
- 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
|
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`)
|
PR: :issue:`1154`)
|
||||||
|
|
||||||
|
|
||||||
@ -83,7 +108,7 @@ Bug fix release.
|
|||||||
|
|
||||||
- Audio: Fix for :issue:`1097` tuned down the buffer size in the queue. Turns
|
- 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
|
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.
|
- Audio: Make sure mute events get emitted by software mixer.
|
||||||
(Fixes: :issue:`1146`, PR: :issue:`1152`)
|
(Fixes: :issue:`1146`, PR: :issue:`1152`)
|
||||||
|
|||||||
@ -47,7 +47,6 @@ class Mock(object):
|
|||||||
return Mock()
|
return Mock()
|
||||||
|
|
||||||
MOCK_MODULES = [
|
MOCK_MODULES = [
|
||||||
'cherrypy',
|
|
||||||
'dbus',
|
'dbus',
|
||||||
'dbus.mainloop',
|
'dbus.mainloop',
|
||||||
'dbus.mainloop.glib',
|
'dbus.mainloop.glib',
|
||||||
@ -61,12 +60,6 @@ MOCK_MODULES = [
|
|||||||
'pykka.actor',
|
'pykka.actor',
|
||||||
'pykka.future',
|
'pykka.future',
|
||||||
'pykka.registry',
|
'pykka.registry',
|
||||||
'pylast',
|
|
||||||
'ws4py',
|
|
||||||
'ws4py.messaging',
|
|
||||||
'ws4py.server',
|
|
||||||
'ws4py.server.cherrypyserver',
|
|
||||||
'ws4py.websocket',
|
|
||||||
]
|
]
|
||||||
for mod_name in MOCK_MODULES:
|
for mod_name in MOCK_MODULES:
|
||||||
sys.modules[mod_name] = Mock()
|
sys.modules[mod_name] = Mock()
|
||||||
@ -102,7 +95,7 @@ master_doc = 'index'
|
|||||||
project = 'Mopidy'
|
project = 'Mopidy'
|
||||||
copyright = '2009-2015, Stein Magnus Jodal and contributors'
|
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()
|
release = get_version()
|
||||||
version = '.'.join(release.split('.')[:2])
|
version = '.'.join(release.split('.')[:2])
|
||||||
|
|
||||||
|
|||||||
@ -434,8 +434,8 @@ Use of Mopidy APIs
|
|||||||
==================
|
==================
|
||||||
|
|
||||||
When writing an extension, you should only use APIs documented at
|
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
|
:ref:`api-ref`. Other parts of Mopidy, like :mod:`mopidy.internal`, may change
|
||||||
any time and are not something extensions should use.
|
at any time and are not something extensions should use.
|
||||||
|
|
||||||
|
|
||||||
Logging in extensions
|
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`.
|
For details on how to use Mopidy's local backend, see :ref:`ext-local`.
|
||||||
|
|
||||||
.. automodule:: mopidy.local
|
.. automodule:: mopidy.local
|
||||||
:synopsis: Local backend
|
:synopsis: Local backend
|
||||||
|
|
||||||
|
|
||||||
|
Local library API
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. autoclass:: mopidy.local.Library
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
Translation utils
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. automodule:: mopidy.local.translator
|
||||||
|
:synopsis: Translators for local library extensions
|
||||||
:members:
|
: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`.
|
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')
|
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 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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ def main():
|
|||||||
extension.setup(registry)
|
extension.setup(registry)
|
||||||
|
|
||||||
# Anything that wants to exit after this point must use
|
# 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:
|
try:
|
||||||
return args.command.run(args, proxied_config)
|
return args.command.run(args, proxied_config)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
|
|||||||
@ -16,7 +16,7 @@ from mopidy import exceptions
|
|||||||
from mopidy.audio import playlists, utils
|
from mopidy.audio import playlists, utils
|
||||||
from mopidy.audio.constants import PlaybackState
|
from mopidy.audio.constants import PlaybackState
|
||||||
from mopidy.audio.listener import AudioListener
|
from mopidy.audio.listener import AudioListener
|
||||||
from mopidy.utils import deprecation, process
|
from mopidy.internal import deprecation, process
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -166,11 +166,7 @@ class _Outputs(gst.Bin):
|
|||||||
logger.info('Audio output set to "%s"', description)
|
logger.info('Audio output set to "%s"', description)
|
||||||
|
|
||||||
def _add(self, element):
|
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 = gst.element_factory_make('queue')
|
||||||
queue.set_property('max-size-time', 100 * gst.MSECOND)
|
|
||||||
|
|
||||||
self.add(element)
|
self.add(element)
|
||||||
self.add(queue)
|
self.add(queue)
|
||||||
queue.link(element)
|
queue.link(element)
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import gst.pbutils # noqa
|
|||||||
|
|
||||||
from mopidy import exceptions
|
from mopidy import exceptions
|
||||||
from mopidy.audio import utils
|
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
|
_missing_plugin_desc = gst.pbutils.missing_plugin_message_get_description
|
||||||
|
|
||||||
@ -182,7 +182,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
import gobject
|
import gobject
|
||||||
|
|
||||||
from mopidy.utils import path
|
from mopidy.internal import path
|
||||||
|
|
||||||
gobject.threads_init()
|
gobject.threads_init()
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import gobject
|
|||||||
from mopidy import config as config_lib, exceptions
|
from mopidy import config as config_lib, exceptions
|
||||||
from mopidy.audio import Audio
|
from mopidy.audio import Audio
|
||||||
from mopidy.core import Core
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from mopidy.compat import configparser
|
|||||||
from mopidy.config import keyring
|
from mopidy.config import keyring
|
||||||
from mopidy.config.schemas import * # noqa
|
from mopidy.config.schemas import * # noqa
|
||||||
from mopidy.config.types import * # noqa
|
from mopidy.config.types import * # noqa
|
||||||
from mopidy.utils import path, versioning
|
from mopidy.internal import path, versioning
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import socket
|
|||||||
|
|
||||||
from mopidy import compat
|
from mopidy import compat
|
||||||
from mopidy.config import validators
|
from mopidy.config import validators
|
||||||
from mopidy.utils import log, path
|
from mopidy.internal import log, path
|
||||||
|
|
||||||
|
|
||||||
def decode(value):
|
def decode(value):
|
||||||
|
|||||||
@ -14,8 +14,8 @@ from mopidy.core.mixer import MixerController
|
|||||||
from mopidy.core.playback import PlaybackController
|
from mopidy.core.playback import PlaybackController
|
||||||
from mopidy.core.playlists import PlaylistsController
|
from mopidy.core.playlists import PlaylistsController
|
||||||
from mopidy.core.tracklist import TracklistController
|
from mopidy.core.tracklist import TracklistController
|
||||||
from mopidy.utils import versioning
|
from mopidy.internal import versioning
|
||||||
from mopidy.utils.deprecation import deprecated_property
|
from mopidy.internal.deprecation import deprecated_property
|
||||||
|
|
||||||
|
|
||||||
class Core(
|
class Core(
|
||||||
|
|||||||
@ -1,16 +1,32 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
from mopidy.utils import deprecation, validation
|
from mopidy import compat, exceptions, models
|
||||||
|
from mopidy.internal import deprecation, validation
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class LibraryController(object):
|
||||||
pykka_traversable = True
|
pykka_traversable = True
|
||||||
|
|
||||||
@ -79,22 +95,24 @@ class LibraryController(object):
|
|||||||
backends = self.backends.with_library_browse.values()
|
backends = self.backends.with_library_browse.values()
|
||||||
futures = {b: b.library.root_directory for b in backends}
|
futures = {b: b.library.root_directory for b in backends}
|
||||||
for backend, future in futures.items():
|
for backend, future in futures.items():
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
directories.add(future.get())
|
root = future.get()
|
||||||
except Exception:
|
validation.check_instance(root, models.Ref)
|
||||||
logger.exception('%s backend caused an exception.',
|
directories.add(root)
|
||||||
backend.actor_ref.actor_class.__name__)
|
|
||||||
return sorted(directories, key=operator.attrgetter('name'))
|
return sorted(directories, key=operator.attrgetter('name'))
|
||||||
|
|
||||||
def _browse(self, uri):
|
def _browse(self, uri):
|
||||||
scheme = urlparse.urlparse(uri).scheme
|
scheme = urlparse.urlparse(uri).scheme
|
||||||
backend = self.backends.with_library_browse.get(scheme)
|
backend = self.backends.with_library_browse.get(scheme)
|
||||||
try:
|
|
||||||
if backend:
|
if not backend:
|
||||||
return backend.library.browse(uri).get()
|
return []
|
||||||
except Exception:
|
|
||||||
logger.exception('%s backend caused an exception.',
|
with _backend_error_handling(backend):
|
||||||
backend.actor_ref.actor_class.__name__)
|
result = backend.library.browse(uri).get()
|
||||||
|
validation.check_instances(result, models.Ref)
|
||||||
|
return result
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_distinct(self, field, query=None):
|
def get_distinct(self, field, query=None):
|
||||||
@ -106,7 +124,7 @@ class LibraryController(object):
|
|||||||
recommended to use this method.
|
recommended to use this method.
|
||||||
|
|
||||||
:param string field: One of ``artist``, ``albumartist``, ``album``,
|
: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
|
:param dict query: Query to use for limiting results, see
|
||||||
:meth:`search` for details about the query format.
|
:meth:`search` for details about the query format.
|
||||||
:rtype: set of values corresponding to the requested field type.
|
: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)
|
futures = {b: b.library.get_distinct(field, query)
|
||||||
for b in self.backends.with_library.values()}
|
for b in self.backends.with_library.values()}
|
||||||
for backend, future in futures.items():
|
for backend, future in futures.items():
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
result.update(future.get())
|
values = future.get()
|
||||||
except Exception:
|
if values is not None:
|
||||||
logger.exception('%s backend caused an exception.',
|
validation.check_instances(values, compat.text_type)
|
||||||
backend.actor_ref.actor_class.__name__)
|
result.update(values)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_images(self, uris):
|
def get_images(self, uris):
|
||||||
@ -152,12 +170,16 @@ class LibraryController(object):
|
|||||||
|
|
||||||
results = {uri: tuple() for uri in uris}
|
results = {uri: tuple() for uri in uris}
|
||||||
for backend, future in futures.items():
|
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():
|
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)
|
results[uri] += tuple(images)
|
||||||
except Exception:
|
|
||||||
logger.exception('%s backend caused an exception.',
|
|
||||||
backend.actor_ref.actor_class.__name__)
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def find_exact(self, query=None, uris=None, **kwargs):
|
def find_exact(self, query=None, uris=None, **kwargs):
|
||||||
@ -202,7 +224,7 @@ class LibraryController(object):
|
|||||||
uris = [uri]
|
uris = [uri]
|
||||||
|
|
||||||
futures = {}
|
futures = {}
|
||||||
result = {u: [] for u in uris}
|
results = {u: [] for u in uris}
|
||||||
|
|
||||||
# TODO: lookup(uris) to backend APIs
|
# TODO: lookup(uris) to backend APIs
|
||||||
for backend, backend_uris in self._get_backends_to_uris(uris).items():
|
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)
|
futures[(backend, u)] = backend.library.lookup(u)
|
||||||
|
|
||||||
for (backend, u), future in futures.items():
|
for (backend, u), future in futures.items():
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
result[u] = future.get()
|
result = future.get()
|
||||||
except Exception:
|
if result is not None:
|
||||||
logger.exception('%s backend caused an exception.',
|
validation.check_instances(result, models.Track)
|
||||||
backend.actor_ref.actor_class.__name__)
|
results[u] = result
|
||||||
|
|
||||||
if uri:
|
if uri:
|
||||||
return result[uri]
|
return results[uri]
|
||||||
return result
|
return results
|
||||||
|
|
||||||
def refresh(self, uri=None):
|
def refresh(self, uri=None):
|
||||||
"""
|
"""
|
||||||
@ -241,11 +263,8 @@ class LibraryController(object):
|
|||||||
futures[backend] = backend.library.refresh(uri)
|
futures[backend] = backend.library.refresh(uri)
|
||||||
|
|
||||||
for backend, future in futures.items():
|
for backend, future in futures.items():
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
future.get()
|
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):
|
def search(self, query=None, uris=None, exact=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -311,25 +330,26 @@ class LibraryController(object):
|
|||||||
futures[backend] = backend.library.search(
|
futures[backend] = backend.library.search(
|
||||||
query=query, uris=backend_uris, exact=exact)
|
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 = []
|
results = []
|
||||||
for backend, future in futures.items():
|
for backend, future in futures.items():
|
||||||
try:
|
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:
|
except TypeError:
|
||||||
backend_name = backend.actor_ref.actor_class.__name__
|
backend_name = backend.actor_ref.actor_class.__name__
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'%s does not implement library.search() with "exact" '
|
'%s does not implement library.search() with "exact" '
|
||||||
'support. Please upgrade it.', backend_name)
|
'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):
|
def _normalize_query(query):
|
||||||
|
|||||||
@ -1,13 +1,27 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from mopidy.utils import validation
|
from mopidy import exceptions
|
||||||
|
from mopidy.internal import validation
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class MixerController(object):
|
||||||
pykka_traversable = True
|
pykka_traversable = True
|
||||||
|
|
||||||
@ -21,8 +35,15 @@ class MixerController(object):
|
|||||||
|
|
||||||
The volume scale is linear.
|
The volume scale is linear.
|
||||||
"""
|
"""
|
||||||
if self._mixer is not None:
|
if self._mixer is None:
|
||||||
return self._mixer.get_volume().get()
|
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):
|
def set_volume(self, volume):
|
||||||
"""Set the volume.
|
"""Set the volume.
|
||||||
@ -36,9 +57,14 @@ class MixerController(object):
|
|||||||
validation.check_integer(volume, min=0, max=100)
|
validation.check_integer(volume, min=0, max=100)
|
||||||
|
|
||||||
if self._mixer is None:
|
if self._mixer is None:
|
||||||
return False
|
return False # TODO: 2.0 return None
|
||||||
else:
|
|
||||||
return self._mixer.set_volume(volume).get()
|
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):
|
def get_mute(self):
|
||||||
"""Get mute state.
|
"""Get mute state.
|
||||||
@ -46,8 +72,15 @@ class MixerController(object):
|
|||||||
:class:`True` if muted, :class:`False` unmuted, :class:`None` if
|
:class:`True` if muted, :class:`False` unmuted, :class:`None` if
|
||||||
unknown.
|
unknown.
|
||||||
"""
|
"""
|
||||||
if self._mixer is not None:
|
if self._mixer is None:
|
||||||
return self._mixer.get_mute().get()
|
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):
|
def set_mute(self, mute):
|
||||||
"""Set mute state.
|
"""Set mute state.
|
||||||
@ -58,6 +91,11 @@ class MixerController(object):
|
|||||||
"""
|
"""
|
||||||
validation.check_boolean(mute)
|
validation.check_boolean(mute)
|
||||||
if self._mixer is None:
|
if self._mixer is None:
|
||||||
return False
|
return False # TODO: 2.0 return None
|
||||||
else:
|
|
||||||
return self._mixer.set_mute(bool(mute)).get()
|
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 import models
|
||||||
from mopidy.audio import PlaybackState
|
from mopidy.audio import PlaybackState
|
||||||
from mopidy.core import listener
|
from mopidy.core import listener
|
||||||
from mopidy.utils import deprecation, validation
|
from mopidy.internal import deprecation, validation
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -61,9 +61,7 @@ class PlaybackController(object):
|
|||||||
|
|
||||||
Returns a :class:`mopidy.models.Track` or :class:`None`.
|
Returns a :class:`mopidy.models.Track` or :class:`None`.
|
||||||
"""
|
"""
|
||||||
tl_track = self.get_current_tl_track()
|
return getattr(self.get_current_tl_track(), 'track', None)
|
||||||
if tl_track is not None:
|
|
||||||
return tl_track.track
|
|
||||||
|
|
||||||
current_track = deprecation.deprecated_property(get_current_track)
|
current_track = deprecation.deprecated_property(get_current_track)
|
||||||
"""
|
"""
|
||||||
@ -71,6 +69,18 @@ class PlaybackController(object):
|
|||||||
Use :meth:`get_current_track` instead.
|
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):
|
def get_stream_title(self):
|
||||||
"""Get the current stream title or :class:`None`."""
|
"""Get the current stream title or :class:`None`."""
|
||||||
return self._stream_title
|
return self._stream_title
|
||||||
|
|||||||
@ -1,15 +1,31 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
|
from mopidy import exceptions
|
||||||
from mopidy.core import listener
|
from mopidy.core import listener
|
||||||
from mopidy.models import Playlist
|
from mopidy.internal import deprecation, validation
|
||||||
from mopidy.utils import deprecation, validation
|
from mopidy.models import Playlist, Ref
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class PlaylistsController(object):
|
||||||
pykka_traversable = True
|
pykka_traversable = True
|
||||||
|
|
||||||
@ -34,17 +50,18 @@ class PlaylistsController(object):
|
|||||||
for backend in set(self.backends.with_playlists.values())}
|
for backend in set(self.backends.with_playlists.values())}
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for backend, future in futures.items():
|
for b, future in futures.items():
|
||||||
try:
|
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:
|
except NotImplementedError:
|
||||||
backend_name = backend.actor_ref.actor_class.__name__
|
backend_name = b.actor_ref.actor_class.__name__
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'%s does not implement playlists.as_list(). '
|
'%s does not implement playlists.as_list(). '
|
||||||
'Please upgrade it.', backend_name)
|
'Please upgrade it.', backend_name)
|
||||||
except Exception:
|
|
||||||
logger.exception('%s backend caused an exception.',
|
|
||||||
backend.actor_ref.actor_class.__name__)
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@ -66,8 +83,16 @@ class PlaylistsController(object):
|
|||||||
|
|
||||||
uri_scheme = urlparse.urlparse(uri).scheme
|
uri_scheme = urlparse.urlparse(uri).scheme
|
||||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
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):
|
def get_playlists(self, include_tracks=True):
|
||||||
"""
|
"""
|
||||||
@ -120,16 +145,23 @@ class PlaylistsController(object):
|
|||||||
:type name: string
|
:type name: string
|
||||||
:param uri_scheme: use the backend matching the URI scheme
|
:param uri_scheme: use the backend matching the URI scheme
|
||||||
:type uri_scheme: string
|
: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:
|
if uri_scheme in self.backends.with_playlists:
|
||||||
backend = self.backends.with_playlists[uri_scheme]
|
backends = [self.backends.with_playlists[uri_scheme]]
|
||||||
else:
|
else:
|
||||||
# TODO: this fallback looks suspicious
|
backends = self.backends.with_playlists.values()
|
||||||
backend = list(self.backends.with_playlists.values())[0]
|
|
||||||
playlist = backend.playlists.create(name).get()
|
for backend in backends:
|
||||||
listener.CoreListener.send('playlist_changed', playlist=playlist)
|
with _backend_error_handling(backend):
|
||||||
return playlist
|
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):
|
def delete(self, uri):
|
||||||
"""
|
"""
|
||||||
@ -145,8 +177,14 @@ class PlaylistsController(object):
|
|||||||
|
|
||||||
uri_scheme = urlparse.urlparse(uri).scheme
|
uri_scheme = urlparse.urlparse(uri).scheme
|
||||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
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()
|
backend.playlists.delete(uri).get()
|
||||||
|
# TODO: emit playlist changed?
|
||||||
|
|
||||||
|
# TODO: return value?
|
||||||
|
|
||||||
def filter(self, criteria=None, **kwargs):
|
def filter(self, criteria=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -192,11 +230,16 @@ class PlaylistsController(object):
|
|||||||
"""
|
"""
|
||||||
uri_scheme = urlparse.urlparse(uri).scheme
|
uri_scheme = urlparse.urlparse(uri).scheme
|
||||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
backend = self.backends.with_playlists.get(uri_scheme, None)
|
||||||
if backend:
|
if not backend:
|
||||||
return backend.playlists.lookup(uri).get()
|
|
||||||
else:
|
|
||||||
return None
|
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
|
# TODO: there is an inconsistency between library.refresh(uri) and this
|
||||||
# call, not sure how to sort this out.
|
# call, not sure how to sort this out.
|
||||||
def refresh(self, uri_scheme=None):
|
def refresh(self, uri_scheme=None):
|
||||||
@ -225,12 +268,9 @@ class PlaylistsController(object):
|
|||||||
futures[backend] = backend.playlists.refresh()
|
futures[backend] = backend.playlists.refresh()
|
||||||
|
|
||||||
for backend, future in futures.items():
|
for backend, future in futures.items():
|
||||||
try:
|
with _backend_error_handling(backend):
|
||||||
future.get()
|
future.get()
|
||||||
playlists_loaded = True
|
playlists_loaded = True
|
||||||
except Exception:
|
|
||||||
logger.exception('%s backend caused an exception.',
|
|
||||||
backend.actor_ref.actor_class.__name__)
|
|
||||||
|
|
||||||
if playlists_loaded:
|
if playlists_loaded:
|
||||||
listener.CoreListener.send('playlists_loaded')
|
listener.CoreListener.send('playlists_loaded')
|
||||||
@ -264,7 +304,16 @@ class PlaylistsController(object):
|
|||||||
|
|
||||||
uri_scheme = urlparse.urlparse(playlist.uri).scheme
|
uri_scheme = urlparse.urlparse(playlist.uri).scheme
|
||||||
backend = self.backends.with_playlists.get(uri_scheme, None)
|
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()
|
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 playlist
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import logging
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from mopidy.core import listener
|
from mopidy.core import listener
|
||||||
|
from mopidy.internal import deprecation, validation
|
||||||
from mopidy.models import TlTrack, Track
|
from mopidy.models import TlTrack, Track
|
||||||
from mopidy.utils import deprecation, validation
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import tornado.websocket
|
|||||||
from mopidy import exceptions, models, zeroconf
|
from mopidy import exceptions, models, zeroconf
|
||||||
from mopidy.core import CoreListener
|
from mopidy.core import CoreListener
|
||||||
from mopidy.http import handlers
|
from mopidy.http import handlers
|
||||||
from mopidy.utils import encoding, formatting, network
|
from mopidy.internal import encoding, formatting, network
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import tornado.websocket
|
|||||||
|
|
||||||
import mopidy
|
import mopidy
|
||||||
from mopidy import core, models
|
from mopidy import core, models
|
||||||
from mopidy.utils import encoding, jsonrpc
|
from mopidy.internal import encoding, jsonrpc
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import pygst
|
|||||||
pygst.require('0.10')
|
pygst.require('0.10')
|
||||||
import gst # noqa
|
import gst # noqa
|
||||||
|
|
||||||
from mopidy.utils import formatting
|
from mopidy.internal import formatting
|
||||||
|
|
||||||
|
|
||||||
def format_dependency_list(adapters=None):
|
def format_dependency_list(adapters=None):
|
||||||
@ -11,7 +11,7 @@ import gobject
|
|||||||
|
|
||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
from mopidy.utils import encoding
|
from mopidy.internal import encoding
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -10,7 +10,7 @@ import urlparse
|
|||||||
|
|
||||||
from mopidy import compat, exceptions
|
from mopidy import compat, exceptions
|
||||||
from mopidy.compat import queue
|
from mopidy.compat import queue
|
||||||
from mopidy.utils import encoding, xdg
|
from mopidy.internal import encoding, xdg
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -7,8 +7,8 @@ import time
|
|||||||
|
|
||||||
from mopidy import commands, compat, exceptions
|
from mopidy import commands, compat, exceptions
|
||||||
from mopidy.audio import scan, utils
|
from mopidy.audio import scan, utils
|
||||||
|
from mopidy.internal import path
|
||||||
from mopidy.local import translator
|
from mopidy.local import translator
|
||||||
from mopidy.utils import path
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import tempfile
|
|||||||
|
|
||||||
import mopidy
|
import mopidy
|
||||||
from mopidy import compat, local, models
|
from mopidy import compat, local, models
|
||||||
|
from mopidy.internal import encoding, timer
|
||||||
from mopidy.local import search, storage, translator
|
from mopidy.local import search, storage, translator
|
||||||
from mopidy.utils import encoding, timer
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@ -7,5 +7,5 @@ from mopidy.local import translator
|
|||||||
class LocalPlaybackProvider(backend.PlaybackProvider):
|
class LocalPlaybackProvider(backend.PlaybackProvider):
|
||||||
|
|
||||||
def translate_uri(self, uri):
|
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'])
|
uri, self.backend.config['local']['media_dir'])
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from mopidy.utils import encoding, path
|
from mopidy.internal import encoding, path
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@ -5,25 +5,41 @@ import os
|
|||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
from mopidy import compat
|
from mopidy import compat
|
||||||
from mopidy.utils.path import path_to_uri, uri_to_path
|
from mopidy.internal import path
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def local_track_uri_to_file_uri(uri, media_dir):
|
def local_uri_to_file_uri(uri, media_dir):
|
||||||
return path_to_uri(local_track_uri_to_path(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):
|
def local_uri_to_path(uri, media_dir):
|
||||||
if not uri.startswith('local:track:'):
|
"""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.')
|
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)
|
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):
|
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):
|
if isinstance(relpath, compat.text_type):
|
||||||
relpath = relpath.encode('utf-8')
|
relpath = relpath.encode('utf-8')
|
||||||
return b'local:track:%s' % urllib.quote(relpath)
|
return b'local:track:%s' % urllib.quote(relpath)
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import logging
|
|||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
from mopidy import backend
|
from mopidy import backend
|
||||||
|
from mopidy.internal import encoding, path
|
||||||
from mopidy.m3u.library import M3ULibraryProvider
|
from mopidy.m3u.library import M3ULibraryProvider
|
||||||
from mopidy.m3u.playlists import M3UPlaylistsProvider
|
from mopidy.m3u.playlists import M3UPlaylistsProvider
|
||||||
from mopidy.utils import encoding, path
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@ -7,9 +7,8 @@ import urllib
|
|||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
from mopidy import compat
|
from mopidy import compat
|
||||||
|
from mopidy.internal import encoding, path
|
||||||
from mopidy.models import Track
|
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+),(.*)')
|
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):
|
def playlist_uri_to_path(uri, playlists_dir):
|
||||||
if not uri.startswith('m3u:'):
|
if not uri.startswith('m3u:'):
|
||||||
raise ValueError('Invalid URI %s' % uri)
|
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)
|
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:
|
with open(file_path) as m3u:
|
||||||
contents = m3u.readlines()
|
contents = m3u.readlines()
|
||||||
except IOError as error:
|
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
|
return tracks
|
||||||
|
|
||||||
if not contents:
|
if not contents:
|
||||||
@ -100,11 +99,11 @@ def parse_m3u(file_path, media_dir=None):
|
|||||||
if urlparse.urlsplit(line).scheme:
|
if urlparse.urlsplit(line).scheme:
|
||||||
tracks.append(track.replace(uri=line))
|
tracks.append(track.replace(uri=line))
|
||||||
elif os.path.normpath(line) == os.path.abspath(line):
|
elif os.path.normpath(line) == os.path.abspath(line):
|
||||||
path = path_to_uri(line)
|
uri = path.path_to_uri(line)
|
||||||
tracks.append(track.replace(uri=path))
|
tracks.append(track.replace(uri=uri))
|
||||||
elif media_dir is not None:
|
elif media_dir is not None:
|
||||||
path = path_to_uri(os.path.join(media_dir, line))
|
uri = path.path_to_uri(os.path.join(media_dir, line))
|
||||||
tracks.append(track.replace(uri=path))
|
tracks.append(track.replace(uri=uri))
|
||||||
|
|
||||||
track = Track()
|
track = Track()
|
||||||
return tracks
|
return tracks
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from mopidy.models import fields
|
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
|
from mopidy.models.serialize import ModelJSONEncoder, model_json_decoder
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ImmutableObject', 'Ref', 'Image', 'Artist', 'Album', 'track', 'TlTrack',
|
'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
|
Model to represent URI references with a human friendly name and type
|
||||||
@ -81,7 +82,7 @@ class Ref(ImmutableObject):
|
|||||||
return cls(**kwargs)
|
return cls(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Image(ImmutableObject):
|
class Image(ValidatedImmutableObject):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
:param string uri: URI of the image
|
:param string uri: URI of the image
|
||||||
@ -99,7 +100,7 @@ class Image(ImmutableObject):
|
|||||||
height = fields.Integer(min=0)
|
height = fields.Integer(min=0)
|
||||||
|
|
||||||
|
|
||||||
class Artist(ImmutableObject):
|
class Artist(ValidatedImmutableObject):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
:param uri: artist URI
|
:param uri: artist URI
|
||||||
@ -120,7 +121,7 @@ class Artist(ImmutableObject):
|
|||||||
musicbrainz_id = fields.Identifier()
|
musicbrainz_id = fields.Identifier()
|
||||||
|
|
||||||
|
|
||||||
class Album(ImmutableObject):
|
class Album(ValidatedImmutableObject):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
:param uri: album URI
|
:param uri: album URI
|
||||||
@ -169,7 +170,7 @@ class Album(ImmutableObject):
|
|||||||
# actual usage of this field with more than one image.
|
# actual usage of this field with more than one image.
|
||||||
|
|
||||||
|
|
||||||
class Track(ImmutableObject):
|
class Track(ValidatedImmutableObject):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
:param uri: track URI
|
:param uri: track URI
|
||||||
@ -253,7 +254,7 @@ class Track(ImmutableObject):
|
|||||||
last_modified = fields.Integer(min=0)
|
last_modified = fields.Integer(min=0)
|
||||||
|
|
||||||
|
|
||||||
class TlTrack(ImmutableObject):
|
class TlTrack(ValidatedImmutableObject):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A tracklist track. Wraps a regular track and it's tracklist ID.
|
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])
|
return iter([self.tlid, self.track])
|
||||||
|
|
||||||
|
|
||||||
class Playlist(ImmutableObject):
|
class Playlist(ValidatedImmutableObject):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
:param uri: playlist URI
|
:param uri: playlist URI
|
||||||
@ -329,7 +330,7 @@ class Playlist(ImmutableObject):
|
|||||||
return len(self.tracks)
|
return len(self.tracks)
|
||||||
|
|
||||||
|
|
||||||
class SearchResult(ImmutableObject):
|
class SearchResult(ValidatedImmutableObject):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
:param uri: search result URI
|
:param uri: search result URI
|
||||||
|
|||||||
@ -4,8 +4,9 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
class Field(object):
|
class Field(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Base field for use in :class:`ImmutableObject`. These fields are
|
Base field for use in
|
||||||
responsible for type checking and other data sanitation in our models.
|
: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
|
For simplicity fields use the Python descriptor protocol to store the
|
||||||
values in the instance dictionary. Also note that fields are mutable if
|
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):
|
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._choices = choices
|
||||||
self._default = default
|
self._default = default
|
||||||
self._type = type
|
self._type = type
|
||||||
@ -72,20 +73,41 @@ class String(Field):
|
|||||||
|
|
||||||
|
|
||||||
class Date(String):
|
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.
|
pass # TODO: make this check for YYYY-MM-DD, YYYY-MM, YYYY using strptime.
|
||||||
|
|
||||||
|
|
||||||
class Identifier(String):
|
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):
|
def validate(self, value):
|
||||||
return intern(str(super(Identifier, self).validate(value)))
|
return intern(str(super(Identifier, self).validate(value)))
|
||||||
|
|
||||||
|
|
||||||
class URI(Identifier):
|
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?
|
pass # TODO: validate URIs?
|
||||||
|
|
||||||
|
|
||||||
class Integer(Field):
|
class Integer(Field):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
:class:`Field` for storing integer numbers.
|
:class:`Field` for storing integer numbers.
|
||||||
|
|
||||||
@ -111,7 +133,6 @@ class Integer(Field):
|
|||||||
|
|
||||||
|
|
||||||
class Collection(Field):
|
class Collection(Field):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
:class:`Field` for storing collections of a given type.
|
:class:`Field` for storing collections of a given type.
|
||||||
|
|
||||||
|
|||||||
@ -1,81 +1,61 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import inspect
|
|
||||||
import itertools
|
import itertools
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.models.fields import Field
|
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):
|
class ImmutableObject(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Superclass for immutable objects whose fields can only be modified via the
|
Superclass for immutable objects whose fields can only be modified via the
|
||||||
constructor. Fields should be :class:`Field` instances to ensure type
|
constructor.
|
||||||
safety in our models.
|
|
||||||
|
|
||||||
Note that since these models can not be changed, we heavily memoize them
|
This version of this class has been retained to avoid breaking any clients
|
||||||
to save memory. So constructing a class with the same arguments twice will
|
relying on it's behavior. Internally in Mopidy we now use
|
||||||
give you the same instance twice.
|
:class:`ValidatedImmutableObject` for type safety and it's much smaller
|
||||||
|
memory footprint.
|
||||||
|
|
||||||
:param kwargs: kwargs to set as fields on the object
|
:param kwargs: kwargs to set as fields on the object
|
||||||
:type kwargs: any
|
: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):
|
def __init__(self, *args, **kwargs):
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
if key not in self._fields:
|
if not self._is_valid_field(key):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'__init__() got an unexpected keyword argument "%s"' %
|
'__init__() got an unexpected keyword argument "%s"' % key)
|
||||||
key)
|
self._set_field(key, value)
|
||||||
super(ImmutableObject, self).__setattr__(key, value)
|
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
if name in self.__slots__:
|
if name.startswith('_'):
|
||||||
return super(ImmutableObject, self).__setattr__(name, value)
|
object.__setattr__(self, name, value)
|
||||||
raise AttributeError('Object is immutable.')
|
else:
|
||||||
|
raise AttributeError('Object is immutable.')
|
||||||
|
|
||||||
def __delattr__(self, name):
|
def __delattr__(self, name):
|
||||||
if name in self.__slots__:
|
if name.startswith('_'):
|
||||||
return super(ImmutableObject, self).__delattr__(name)
|
object.__delattr__(self, name)
|
||||||
raise AttributeError('Object is immutable.')
|
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):
|
def _items(self):
|
||||||
for field, key in self._fields.items():
|
return self.__dict__.iteritems()
|
||||||
if hasattr(self, key):
|
|
||||||
yield field, getattr(self, key)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
kwarg_pairs = []
|
kwarg_pairs = []
|
||||||
@ -91,12 +71,10 @@ class ImmutableObject(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
if not hasattr(self, '_hash'):
|
hash_sum = 0
|
||||||
hash_sum = 0
|
for key, value in self._items():
|
||||||
for key, value in self._items():
|
hash_sum += hash(key) + hash(value)
|
||||||
hash_sum += hash(key) + hash(value)
|
return hash_sum
|
||||||
super(ImmutableObject, self).__setattr__('_hash', hash_sum)
|
|
||||||
return self._hash
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if not isinstance(other, self.__class__):
|
if not isinstance(other, self.__class__):
|
||||||
@ -110,11 +88,108 @@ class ImmutableObject(object):
|
|||||||
def copy(self, **values):
|
def copy(self, **values):
|
||||||
"""
|
"""
|
||||||
.. deprecated:: 1.1
|
.. deprecated:: 1.1
|
||||||
Use :meth:`replace` instead. Note that we no longer return copies.
|
Use :meth:`replace` instead.
|
||||||
"""
|
"""
|
||||||
deprecation.warn('model.immutable.copy')
|
deprecation.warn('model.immutable.copy')
|
||||||
return self.replace(**values)
|
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):
|
def replace(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Replace the fields in the model and return a new instance
|
Replace the fields in the model and return a new instance
|
||||||
@ -136,25 +211,7 @@ class ImmutableObject(object):
|
|||||||
"""
|
"""
|
||||||
if not kwargs:
|
if not kwargs:
|
||||||
return self
|
return self
|
||||||
other = copy.copy(self)
|
other = super(ValidatedImmutableObject, self).replace(**kwargs)
|
||||||
for key, value in kwargs.items():
|
if hasattr(self, '_hash'):
|
||||||
if key not in self._fields:
|
object.__delattr__(other, '_hash')
|
||||||
raise TypeError(
|
|
||||||
'copy() got an unexpected keyword argument "%s"' % key)
|
|
||||||
super(ImmutableObject, other).__setattr__(key, value)
|
|
||||||
super(ImmutableObject, other).__delattr__('_hash')
|
|
||||||
return self._instances.setdefault(weakref.ref(other), other)
|
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
|
import json
|
||||||
|
|
||||||
from mopidy.models.immutable import ImmutableObject
|
from mopidy.models import immutable
|
||||||
|
|
||||||
|
_MODELS = ['Ref', 'Artist', 'Album', 'Track', 'TlTrack', 'Playlist']
|
||||||
|
|
||||||
|
|
||||||
class ModelJSONEncoder(json.JSONEncoder):
|
class ModelJSONEncoder(json.JSONEncoder):
|
||||||
@ -19,7 +21,7 @@ class ModelJSONEncoder(json.JSONEncoder):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
if isinstance(obj, ImmutableObject):
|
if isinstance(obj, immutable.ImmutableObject):
|
||||||
return obj.serialize()
|
return obj.serialize()
|
||||||
return json.JSONEncoder.default(self, obj)
|
return json.JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
@ -38,8 +40,8 @@ def model_json_decoder(dct):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if '__model__' in dct:
|
if '__model__' in dct:
|
||||||
models = {c.__name__: c for c in ImmutableObject.__subclasses__()}
|
from mopidy import models
|
||||||
model_name = dct.pop('__model__')
|
model_name = dct.pop('__model__')
|
||||||
if model_name in models:
|
if model_name in _MODELS:
|
||||||
return models[model_name](**dct)
|
return getattr(models, model_name)(**dct)
|
||||||
return dct
|
return dct
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import pykka
|
|||||||
|
|
||||||
from mopidy import exceptions, zeroconf
|
from mopidy import exceptions, zeroconf
|
||||||
from mopidy.core import CoreListener
|
from mopidy.core import CoreListener
|
||||||
|
from mopidy.internal import encoding, network, process
|
||||||
from mopidy.mpd import session, uri_mapper
|
from mopidy.mpd import session, uri_mapper
|
||||||
from mopidy.utils import encoding, network, process
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@ -167,7 +167,8 @@ class MpdDispatcher(object):
|
|||||||
# TODO: check that blacklist items are valid commands?
|
# TODO: check that blacklist items are valid commands?
|
||||||
blacklist = self.config['mpd'].get('command_blacklist', [])
|
blacklist = self.config['mpd'].get('command_blacklist', [])
|
||||||
if tokens and tokens[0] in 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])
|
raise exceptions.MpdDisabled(command=tokens[0])
|
||||||
try:
|
try:
|
||||||
return protocol.commands.call(tokens, context=self.context)
|
return protocol.commands.call(tokens, context=self.context)
|
||||||
|
|||||||
@ -2,8 +2,8 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.mpd import exceptions, protocol, translator
|
from mopidy.mpd import exceptions, protocol, translator
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
|
|
||||||
@protocol.commands.add('add')
|
@protocol.commands.add('add')
|
||||||
|
|||||||
@ -3,9 +3,9 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.models import Track
|
from mopidy.models import Track
|
||||||
from mopidy.mpd import exceptions, protocol, translator
|
from mopidy.mpd import exceptions, protocol, translator
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
_SEARCH_MAPPING = {
|
_SEARCH_MAPPING = {
|
||||||
'album': 'album',
|
'album': 'album',
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from mopidy.core import PlaybackState
|
from mopidy.core import PlaybackState
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.mpd import exceptions, protocol
|
from mopidy.mpd import exceptions, protocol
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
|
|
||||||
@protocol.commands.add('consume', state=protocol.BOOL)
|
@protocol.commands.add('consume', state=protocol.BOOL)
|
||||||
|
|||||||
@ -2,8 +2,8 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from mopidy.internal import formatting, network
|
||||||
from mopidy.mpd import dispatcher, protocol
|
from mopidy.mpd import dispatcher, protocol
|
||||||
from mopidy.utils import formatting, network
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import pykka
|
|||||||
|
|
||||||
from mopidy import audio
|
from mopidy import audio
|
||||||
from mopidy.audio.constants import PlaybackState
|
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
|
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')),
|
uris = [path.path_to_uri(path_to_data_dir('song1.wav')),
|
||||||
path_to_uri(path_to_data_dir('song2.wav'))]
|
path.path_to_uri(path_to_data_dir('song2.wav'))]
|
||||||
|
|
||||||
audio_class = audio.Audio
|
audio_class = audio.Audio
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ class BaseTest(unittest.TestCase):
|
|||||||
'hostname': '',
|
'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()
|
self.audio = self.audio_class.start(config=config, mixer=None).proxy()
|
||||||
|
|
||||||
def tearDown(self): # noqa
|
def tearDown(self): # noqa
|
||||||
|
|||||||
@ -8,7 +8,7 @@ gobject.threads_init()
|
|||||||
|
|
||||||
from mopidy import exceptions
|
from mopidy import exceptions
|
||||||
from mopidy.audio import scan
|
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
|
from tests import path_to_data_dir
|
||||||
|
|
||||||
|
|||||||
@ -373,7 +373,7 @@ class ExpandedPathTest(unittest.TestCase):
|
|||||||
expanded = b'expanded_path'
|
expanded = b'expanded_path'
|
||||||
self.assertEqual(expanded, types.ExpandedPath(original, expanded))
|
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):
|
def test_orginal_stores_unexpanded(self, expand_path_mock):
|
||||||
original = b'~'
|
original = b'~'
|
||||||
expanded = b'expanded_path'
|
expanded = b'expanded_path'
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import mock
|
|||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
from mopidy.core import Core
|
from mopidy.core import Core
|
||||||
from mopidy.utils import versioning
|
from mopidy.internal import versioning
|
||||||
|
|
||||||
|
|
||||||
class CoreActorTest(unittest.TestCase):
|
class CoreActorTest(unittest.TestCase):
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import mock
|
|||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
from mopidy import core
|
from mopidy import core
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.models import Track
|
from mopidy.models import Track
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
from tests import dummy_backend
|
from tests import dummy_backend
|
||||||
|
|
||||||
@ -45,15 +45,12 @@ class BackendEventsTest(unittest.TestCase):
|
|||||||
self.assertEqual(send.call_args[1]['mute'], True)
|
self.assertEqual(send.call_args[1]['mute'], True)
|
||||||
|
|
||||||
def test_tracklist_add_sends_tracklist_changed_event(self, send):
|
def test_tracklist_add_sends_tracklist_changed_event(self, send):
|
||||||
send.reset_mock()
|
|
||||||
|
|
||||||
self.core.tracklist.add(uris=['dummy:a']).get()
|
self.core.tracklist.add(uris=['dummy:a']).get()
|
||||||
|
|
||||||
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
|
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
|
||||||
|
|
||||||
def test_tracklist_clear_sends_tracklist_changed_event(self, send):
|
def test_tracklist_clear_sends_tracklist_changed_event(self, send):
|
||||||
self.core.tracklist.add(uris=['dummy:a']).get()
|
self.core.tracklist.add(uris=['dummy:a']).get()
|
||||||
send.reset_mock()
|
|
||||||
|
|
||||||
self.core.tracklist.clear().get()
|
self.core.tracklist.clear().get()
|
||||||
|
|
||||||
@ -61,7 +58,6 @@ class BackendEventsTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_tracklist_move_sends_tracklist_changed_event(self, send):
|
def test_tracklist_move_sends_tracklist_changed_event(self, send):
|
||||||
self.core.tracklist.add(uris=['dummy:a', 'dummy:b']).get()
|
self.core.tracklist.add(uris=['dummy:a', 'dummy:b']).get()
|
||||||
send.reset_mock()
|
|
||||||
|
|
||||||
self.core.tracklist.move(0, 1, 1).get()
|
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):
|
def test_tracklist_remove_sends_tracklist_changed_event(self, send):
|
||||||
self.core.tracklist.add(uris=['dummy:a']).get()
|
self.core.tracklist.add(uris=['dummy:a']).get()
|
||||||
send.reset_mock()
|
|
||||||
|
|
||||||
self.core.tracklist.remove({'uri': ['dummy:a']}).get()
|
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):
|
def test_tracklist_shuffle_sends_tracklist_changed_event(self, send):
|
||||||
self.core.tracklist.add(uris=['dummy:a', 'dummy:b']).get()
|
self.core.tracklist.add(uris=['dummy:a', 'dummy:b']).get()
|
||||||
send.reset_mock()
|
|
||||||
|
|
||||||
self.core.tracklist.shuffle().get()
|
self.core.tracklist.shuffle().get()
|
||||||
|
|
||||||
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
|
self.assertEqual(send.call_args[0][0], 'tracklist_changed')
|
||||||
|
|
||||||
def test_playlists_refresh_sends_playlists_loaded_event(self, send):
|
def test_playlists_refresh_sends_playlists_loaded_event(self, send):
|
||||||
send.reset_mock()
|
|
||||||
|
|
||||||
self.core.playlists.refresh().get()
|
self.core.playlists.refresh().get()
|
||||||
|
|
||||||
self.assertEqual(send.call_args[0][0], 'playlists_loaded')
|
self.assertEqual(send.call_args[0][0], 'playlists_loaded')
|
||||||
|
|
||||||
def test_playlists_refresh_uri_sends_playlists_loaded_event(self, send):
|
def test_playlists_refresh_uri_sends_playlists_loaded_event(self, send):
|
||||||
send.reset_mock()
|
|
||||||
|
|
||||||
self.core.playlists.refresh(uri_scheme='dummy').get()
|
self.core.playlists.refresh(uri_scheme='dummy').get()
|
||||||
|
|
||||||
self.assertEqual(send.call_args[0][0], 'playlists_loaded')
|
self.assertEqual(send.call_args[0][0], 'playlists_loaded')
|
||||||
|
|
||||||
def test_playlists_create_sends_playlist_changed_event(self, send):
|
def test_playlists_create_sends_playlist_changed_event(self, send):
|
||||||
send.reset_mock()
|
|
||||||
|
|
||||||
self.core.playlists.create('foo').get()
|
self.core.playlists.create('foo').get()
|
||||||
|
|
||||||
self.assertEqual(send.call_args[0][0], 'playlist_changed')
|
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):
|
def test_playlists_save_sends_playlist_changed_event(self, send):
|
||||||
playlist = self.core.playlists.create('foo').get()
|
playlist = self.core.playlists.create('foo').get()
|
||||||
playlist = playlist.replace(name='bar')
|
playlist = playlist.replace(name='bar')
|
||||||
send.reset_mock()
|
|
||||||
|
|
||||||
self.core.playlists.save(playlist).get()
|
self.core.playlists.save(playlist).get()
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import unittest
|
|||||||
import mock
|
import mock
|
||||||
|
|
||||||
from mopidy import backend, core
|
from mopidy import backend, core
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.models import Image, Ref, SearchResult, Track
|
from mopidy.models import Image, Ref, SearchResult, Track
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
|
|
||||||
class BaseCoreLibraryTest(unittest.TestCase):
|
class BaseCoreLibraryTest(unittest.TestCase):
|
||||||
@ -15,24 +15,25 @@ class BaseCoreLibraryTest(unittest.TestCase):
|
|||||||
dummy1_root = Ref.directory(uri='dummy1:directory', name='dummy1')
|
dummy1_root = Ref.directory(uri='dummy1:directory', name='dummy1')
|
||||||
self.backend1 = mock.Mock()
|
self.backend1 = mock.Mock()
|
||||||
self.backend1.uri_schemes.get.return_value = ['dummy1']
|
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 = mock.Mock(spec=backend.LibraryProvider)
|
||||||
self.library1.get_images().get.return_value = {}
|
self.library1.get_images.return_value.get.return_value = {}
|
||||||
self.library1.get_images.reset_mock()
|
|
||||||
self.library1.root_directory.get.return_value = dummy1_root
|
self.library1.root_directory.get.return_value = dummy1_root
|
||||||
self.backend1.library = self.library1
|
self.backend1.library = self.library1
|
||||||
|
|
||||||
dummy2_root = Ref.directory(uri='dummy2:directory', name='dummy2')
|
dummy2_root = Ref.directory(uri='dummy2:directory', name='dummy2')
|
||||||
self.backend2 = mock.Mock()
|
self.backend2 = mock.Mock()
|
||||||
self.backend2.uri_schemes.get.return_value = ['dummy2', 'du2']
|
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 = mock.Mock(spec=backend.LibraryProvider)
|
||||||
self.library2.get_images().get.return_value = {}
|
self.library2.get_images.return_value.get.return_value = {}
|
||||||
self.library2.get_images.reset_mock()
|
|
||||||
self.library2.root_directory.get.return_value = dummy2_root
|
self.library2.root_directory.get.return_value = dummy2_root
|
||||||
self.backend2.library = self.library2
|
self.backend2.library = self.library2
|
||||||
|
|
||||||
# A backend without the optional library provider
|
# A backend without the optional library provider
|
||||||
self.backend3 = mock.Mock()
|
self.backend3 = mock.Mock()
|
||||||
self.backend3.uri_schemes.get.return_value = ['dummy3']
|
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().get.return_value = False
|
||||||
self.backend3.has_library_browse().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'])
|
self.library2.get_images.assert_called_once_with(['dummy2:track'])
|
||||||
|
|
||||||
def test_get_images_returns_images(self):
|
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')]}
|
'dummy1:track': [Image(uri='uri')]}
|
||||||
self.library1.get_images.reset_mock()
|
|
||||||
|
|
||||||
result = self.core.library.get_images(['dummy1:track'])
|
result = self.core.library.get_images(['dummy1:track'])
|
||||||
self.assertEqual({'dummy1:track': (Image(uri='uri'),)}, result)
|
self.assertEqual({'dummy1:track': (Image(uri='uri'),)}, result)
|
||||||
|
|
||||||
def test_get_images_merges_results(self):
|
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')]}
|
'dummy1:track': [Image(uri='uri1')]}
|
||||||
self.library1.get_images.reset_mock()
|
self.library2.get_images.return_value.get.return_value = {
|
||||||
self.library2.get_images().get.return_value = {
|
|
||||||
'dummy2:track': [Image(uri='uri2')]}
|
'dummy2:track': [Image(uri='uri2')]}
|
||||||
self.library2.get_images.reset_mock()
|
|
||||||
|
|
||||||
result = self.core.library.get_images(
|
result = self.core.library.get_images(
|
||||||
['dummy1:track', 'dummy2:track', 'dummy3:track', 'dummy4:track'])
|
['dummy1:track', 'dummy2:track', 'dummy3:track', 'dummy4:track'])
|
||||||
@ -106,11 +104,10 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
|||||||
self.assertFalse(self.library2.browse.called)
|
self.assertFalse(self.library2.browse.called)
|
||||||
|
|
||||||
def test_browse_dummy1_selects_dummy1_backend(self):
|
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.directory(uri='dummy1:directory:/foo/bar', name='bar'),
|
||||||
Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'),
|
Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'),
|
||||||
]
|
]
|
||||||
self.library1.browse.reset_mock()
|
|
||||||
|
|
||||||
self.core.library.browse('dummy1:directory:/foo')
|
self.core.library.browse('dummy1:directory:/foo')
|
||||||
|
|
||||||
@ -119,11 +116,10 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
|||||||
self.library1.browse.assert_called_with('dummy1:directory:/foo')
|
self.library1.browse.assert_called_with('dummy1:directory:/foo')
|
||||||
|
|
||||||
def test_browse_dummy2_selects_dummy2_backend(self):
|
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.directory(uri='dummy2:directory:/bar/baz', name='quux'),
|
||||||
Ref.track(uri='dummy2:track:/bar/foo.mp3', name='Baz'),
|
Ref.track(uri='dummy2:track:/bar/foo.mp3', name='Baz'),
|
||||||
]
|
]
|
||||||
self.library2.browse.reset_mock()
|
|
||||||
|
|
||||||
self.core.library.browse('dummy2:directory:/bar')
|
self.core.library.browse('dummy2:directory:/bar')
|
||||||
|
|
||||||
@ -139,11 +135,10 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
|||||||
self.assertEqual(self.library2.browse.call_count, 0)
|
self.assertEqual(self.library2.browse.call_count, 0)
|
||||||
|
|
||||||
def test_browse_dir_returns_subdirs_and_tracks(self):
|
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.directory(uri='dummy1:directory:/foo/bar', name='Bar'),
|
||||||
Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'),
|
Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'),
|
||||||
]
|
]
|
||||||
self.library1.browse.reset_mock()
|
|
||||||
|
|
||||||
result = self.core.library.browse('dummy1:directory:/foo')
|
result = self.core.library.browse('dummy1:directory:/foo')
|
||||||
self.assertEqual(result, [
|
self.assertEqual(result, [
|
||||||
@ -156,11 +151,14 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
|||||||
self.core.library.lookup('dummy1:a', ['dummy2:a'])
|
self.core.library.lookup('dummy1:a', ['dummy2:a'])
|
||||||
|
|
||||||
def test_lookup_can_handle_uris(self):
|
def test_lookup_can_handle_uris(self):
|
||||||
self.library1.lookup().get.return_value = [1234]
|
track1 = Track(name='abc')
|
||||||
self.library2.lookup().get.return_value = [5678]
|
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'])
|
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):
|
def test_lookup_uris_returns_empty_list_for_dummy3_track(self):
|
||||||
result = self.core.library.lookup(uris=['dummy3:a'])
|
result = self.core.library.lookup(uris=['dummy3:a'])
|
||||||
@ -199,10 +197,8 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
|||||||
result1 = SearchResult(tracks=[track1])
|
result1 = SearchResult(tracks=[track1])
|
||||||
result2 = SearchResult(tracks=[track2])
|
result2 = SearchResult(tracks=[track2])
|
||||||
|
|
||||||
self.library1.search().get.return_value = result1
|
self.library1.search.return_value.get.return_value = result1
|
||||||
self.library1.search.reset_mock()
|
self.library2.search.return_value.get.return_value = result2
|
||||||
self.library2.search().get.return_value = result2
|
|
||||||
self.library2.search.reset_mock()
|
|
||||||
|
|
||||||
result = self.core.library.search({'any': ['a']})
|
result = self.core.library.search({'any': ['a']})
|
||||||
|
|
||||||
@ -234,10 +230,8 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
|||||||
track1 = Track(uri='dummy1:a')
|
track1 = Track(uri='dummy1:a')
|
||||||
result1 = SearchResult(tracks=[track1])
|
result1 = SearchResult(tracks=[track1])
|
||||||
|
|
||||||
self.library1.search().get.return_value = result1
|
self.library1.search.return_value.get.return_value = result1
|
||||||
self.library1.search.reset_mock()
|
self.library2.search.return_value.get.return_value = None
|
||||||
self.library2.search().get.return_value = None
|
|
||||||
self.library2.search.reset_mock()
|
|
||||||
|
|
||||||
result = self.core.library.search({'any': ['a']})
|
result = self.core.library.search({'any': ['a']})
|
||||||
|
|
||||||
@ -254,10 +248,8 @@ class CoreLibraryTest(BaseCoreLibraryTest):
|
|||||||
result1 = SearchResult(tracks=[track1])
|
result1 = SearchResult(tracks=[track1])
|
||||||
result2 = SearchResult(tracks=[track2])
|
result2 = SearchResult(tracks=[track2])
|
||||||
|
|
||||||
self.library1.search().get.return_value = result1
|
self.library1.search.return_value.get.return_value = result1
|
||||||
self.library1.search.reset_mock()
|
self.library2.search.return_value.get.return_value = result2
|
||||||
self.library2.search().get.return_value = result2
|
|
||||||
self.library2.search.reset_mock()
|
|
||||||
|
|
||||||
result = self.core.library.search({'any': ['a']})
|
result = self.core.library.search({'any': ['a']})
|
||||||
|
|
||||||
@ -363,12 +355,14 @@ class DeprecatedLookupCoreLibraryTest(BaseCoreLibraryTest):
|
|||||||
return super(DeprecatedLookupCoreLibraryTest, self).run(result)
|
return super(DeprecatedLookupCoreLibraryTest, self).run(result)
|
||||||
|
|
||||||
def test_lookup_selects_dummy1_backend(self):
|
def test_lookup_selects_dummy1_backend(self):
|
||||||
|
self.library1.lookup.return_value.get.return_value = []
|
||||||
self.core.library.lookup('dummy1:a')
|
self.core.library.lookup('dummy1:a')
|
||||||
|
|
||||||
self.library1.lookup.assert_called_once_with('dummy1:a')
|
self.library1.lookup.assert_called_once_with('dummy1:a')
|
||||||
self.assertFalse(self.library2.lookup.called)
|
self.assertFalse(self.library2.lookup.called)
|
||||||
|
|
||||||
def test_lookup_selects_dummy2_backend(self):
|
def test_lookup_selects_dummy2_backend(self):
|
||||||
|
self.library2.lookup.return_value.get.return_value = []
|
||||||
self.core.library.lookup('dummy2:a')
|
self.core.library.lookup('dummy2:a')
|
||||||
|
|
||||||
self.assertFalse(self.library1.lookup.called)
|
self.assertFalse(self.library1.lookup.called)
|
||||||
@ -421,8 +415,7 @@ class LegacyFindExactToSearchLibraryTest(unittest.TestCase):
|
|||||||
# We are just testing that this doesn't fail.
|
# We are just testing that this doesn't fail.
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('mopidy.core.library.logger')
|
class MockBackendCoreLibraryBase(unittest.TestCase):
|
||||||
class BackendFailuresCoreLibraryTest(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self): # noqa: N802
|
def setUp(self): # noqa: N802
|
||||||
dummy_root = Ref.directory(uri='dummy:directory', name='dummy')
|
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])
|
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.
|
# Might happen if root_directory is a property for some weird reason.
|
||||||
self.library.root_directory.get.side_effect = Exception
|
self.library.root_directory.get.side_effect = Exception
|
||||||
self.assertEqual([], self.core.library.browse(None))
|
self.assertEqual([], self.core.library.browse(None))
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
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.library.browse.return_value.get.side_effect = Exception
|
||||||
self.assertEqual([], self.core.library.browse('dummy:directory'))
|
self.assertEqual([], self.core.library.browse('dummy:directory'))
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
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.library.get_distinct.return_value.get.side_effect = Exception
|
||||||
self.assertEqual(set(), self.core.library.get_distinct('artist'))
|
self.assertEqual(set(), self.core.library.get_distinct('artist'))
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
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.library.get_images.return_value.get.side_effect = Exception
|
||||||
self.assertEqual(
|
self.assertEqual({uri: tuple()}, self.core.library.get_images([uri]))
|
||||||
{'dummy:/1': tuple()}, self.core.library.get_images(['dummy:/1']))
|
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
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.library.lookup.return_value.get.side_effect = Exception
|
||||||
self.assertEqual(
|
self.assertEqual({uri: []}, self.core.library.lookup(uris=[uri]))
|
||||||
{'dummy:/1': []}, self.core.library.lookup(uris=['dummy:/1']))
|
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
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.library.refresh.return_value.get.side_effect = Exception
|
||||||
self.core.library.refresh()
|
self.core.library.refresh()
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
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.library.refresh.return_value.get.side_effect = Exception
|
||||||
self.core.library.refresh('dummy:/1')
|
self.core.library.refresh('dummy:/1')
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
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.library.search.return_value.get.side_effect = Exception
|
||||||
self.assertEqual([], self.core.library.search(query={'any': ['foo']}))
|
self.assertEqual([], self.core.library.search(query={'any': ['foo']}))
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
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
|
# TODO: is this behavior desired? Do we need to continue handling
|
||||||
# LookupError case specially.
|
# LookupError case specially.
|
||||||
self.library.search.return_value.get.side_effect = LookupError
|
self.library.search.return_value.get.side_effect = LookupError
|
||||||
with self.assertRaises(LookupError):
|
with self.assertRaises(LookupError):
|
||||||
self.core.library.search(query={'any': ['foo']})
|
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()
|
self.mixer.get_volume.assert_called_once_with()
|
||||||
|
|
||||||
def test_set_volume(self):
|
def test_set_volume(self):
|
||||||
|
self.mixer.set_volume.return_value.get.return_value = True
|
||||||
self.core.mixer.set_volume(30)
|
self.core.mixer.set_volume(30)
|
||||||
|
|
||||||
self.mixer.set_volume.assert_called_once_with(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()
|
self.mixer.get_mute.assert_called_once_with()
|
||||||
|
|
||||||
def test_set_mute(self):
|
def test_set_mute(self):
|
||||||
|
self.mixer.set_mute.return_value.get.return_value = True
|
||||||
self.core.mixer.set_mute(True)
|
self.core.mixer.set_mute(True)
|
||||||
|
|
||||||
self.mixer.set_mute.assert_called_once_with(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):
|
def test_forwards_mixer_mute_changed_event_to_frontends(self, send):
|
||||||
self.core.mixer.set_mute(mute=True)
|
self.core.mixer.set_mute(mute=True)
|
||||||
self.assertEqual(send.call_count, 0)
|
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
|
import pykka
|
||||||
|
|
||||||
from mopidy import backend, core
|
from mopidy import backend, core
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.models import Track
|
from mopidy.models import Track
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
from tests import dummy_audio as audio
|
from tests import dummy_audio as audio
|
||||||
|
|
||||||
@ -21,15 +21,13 @@ class CorePlaybackTest(unittest.TestCase):
|
|||||||
self.backend1 = mock.Mock()
|
self.backend1 = mock.Mock()
|
||||||
self.backend1.uri_schemes.get.return_value = ['dummy1']
|
self.backend1.uri_schemes.get.return_value = ['dummy1']
|
||||||
self.playback1 = mock.Mock(spec=backend.PlaybackProvider)
|
self.playback1 = mock.Mock(spec=backend.PlaybackProvider)
|
||||||
self.playback1.get_time_position().get.return_value = 1000
|
self.playback1.get_time_position.return_value.get.return_value = 1000
|
||||||
self.playback1.reset_mock()
|
|
||||||
self.backend1.playback = self.playback1
|
self.backend1.playback = self.playback1
|
||||||
|
|
||||||
self.backend2 = mock.Mock()
|
self.backend2 = mock.Mock()
|
||||||
self.backend2.uri_schemes.get.return_value = ['dummy2']
|
self.backend2.uri_schemes.get.return_value = ['dummy2']
|
||||||
self.playback2 = mock.Mock(spec=backend.PlaybackProvider)
|
self.playback2 = mock.Mock(spec=backend.PlaybackProvider)
|
||||||
self.playback2.get_time_position().get.return_value = 2000
|
self.playback2.get_time_position.return_value.get.return_value = 2000
|
||||||
self.playback2.reset_mock()
|
|
||||||
self.backend2.playback = self.playback2
|
self.backend2.playback = self.playback2
|
||||||
|
|
||||||
# A backend without the optional playback provider
|
# A backend without the optional playback provider
|
||||||
@ -123,6 +121,17 @@ class CorePlaybackTest(unittest.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.core.playback.get_current_track(), self.tracks[0])
|
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
|
# TODO Test state
|
||||||
|
|
||||||
def test_play_selects_dummy1_backend(self):
|
def test_play_selects_dummy1_backend(self):
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import unittest
|
|||||||
import mock
|
import mock
|
||||||
|
|
||||||
from mopidy import backend, core
|
from mopidy import backend, core
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.models import Playlist, Ref, Track
|
from mopidy.models import Playlist, Ref, Track
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
|
|
||||||
class BasePlaylistsTest(unittest.TestCase):
|
class BasePlaylistsTest(unittest.TestCase):
|
||||||
@ -90,8 +90,7 @@ class PlaylistTest(BasePlaylistsTest):
|
|||||||
|
|
||||||
def test_create_without_uri_scheme_uses_first_backend(self):
|
def test_create_without_uri_scheme_uses_first_backend(self):
|
||||||
playlist = Playlist()
|
playlist = Playlist()
|
||||||
self.sp1.create().get.return_value = playlist
|
self.sp1.create.return_value.get.return_value = playlist
|
||||||
self.sp1.reset_mock()
|
|
||||||
|
|
||||||
result = self.core.playlists.create('foo')
|
result = self.core.playlists.create('foo')
|
||||||
|
|
||||||
@ -99,10 +98,31 @@ class PlaylistTest(BasePlaylistsTest):
|
|||||||
self.sp1.create.assert_called_once_with('foo')
|
self.sp1.create.assert_called_once_with('foo')
|
||||||
self.assertFalse(self.sp2.create.called)
|
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):
|
def test_create_with_uri_scheme_selects_the_matching_backend(self):
|
||||||
playlist = Playlist()
|
playlist = Playlist()
|
||||||
self.sp2.create().get.return_value = playlist
|
self.sp2.create.return_value.get.return_value = playlist
|
||||||
self.sp2.reset_mock()
|
|
||||||
|
|
||||||
result = self.core.playlists.create('foo', uri_scheme='dummy2')
|
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):
|
def test_create_with_unsupported_uri_scheme_uses_first_backend(self):
|
||||||
playlist = Playlist()
|
playlist = Playlist()
|
||||||
self.sp1.create().get.return_value = playlist
|
self.sp1.create.return_value.get.return_value = playlist
|
||||||
self.sp1.reset_mock()
|
|
||||||
|
|
||||||
result = self.core.playlists.create('foo', uri_scheme='dummy3')
|
result = self.core.playlists.create('foo', uri_scheme='dummy3')
|
||||||
|
|
||||||
@ -190,8 +209,7 @@ class PlaylistTest(BasePlaylistsTest):
|
|||||||
|
|
||||||
def test_save_selects_the_dummy1_backend(self):
|
def test_save_selects_the_dummy1_backend(self):
|
||||||
playlist = Playlist(uri='dummy1:a')
|
playlist = Playlist(uri='dummy1:a')
|
||||||
self.sp1.save().get.return_value = playlist
|
self.sp1.save.return_value.get.return_value = playlist
|
||||||
self.sp1.reset_mock()
|
|
||||||
|
|
||||||
result = self.core.playlists.save(playlist)
|
result = self.core.playlists.save(playlist)
|
||||||
|
|
||||||
@ -201,8 +219,7 @@ class PlaylistTest(BasePlaylistsTest):
|
|||||||
|
|
||||||
def test_save_selects_the_dummy2_backend(self):
|
def test_save_selects_the_dummy2_backend(self):
|
||||||
playlist = Playlist(uri='dummy2:a')
|
playlist = Playlist(uri='dummy2:a')
|
||||||
self.sp2.save().get.return_value = playlist
|
self.sp2.save.return_value.get.return_value = playlist
|
||||||
self.sp2.reset_mock()
|
|
||||||
|
|
||||||
result = self.core.playlists.save(playlist)
|
result = self.core.playlists.save(playlist)
|
||||||
|
|
||||||
@ -281,8 +298,7 @@ class DeprecatedGetPlaylistsTest(BasePlaylistsTest):
|
|||||||
self.assertEqual(len(result[1].tracks), 0)
|
self.assertEqual(len(result[1].tracks), 0)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('mopidy.core.playlists.logger')
|
class MockBackendCorePlaylistsBase(unittest.TestCase):
|
||||||
class BackendFailuresCorePlaylistsTest(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self): # noqa: N802
|
def setUp(self): # noqa: N802
|
||||||
self.playlists = mock.Mock(spec=backend.PlaylistsProvider)
|
self.playlists = mock.Mock(spec=backend.PlaylistsProvider)
|
||||||
@ -294,27 +310,127 @@ class BackendFailuresCorePlaylistsTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.core = core.Core(mixer=None, backends=[self.backend])
|
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())
|
self.assertEqual([], self.core.playlists.as_list())
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
def test_get_items_backend_exception_gets_through(self, logger):
|
def test_backend_returns_none(self, logger):
|
||||||
# TODO: is this behavior desired?
|
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
|
self.playlists.get_items.return_value.get.side_effect = Exception
|
||||||
with self.assertRaises(Exception):
|
self.assertIsNone(self.core.playlists.get_items('dummy:/1'))
|
||||||
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')
|
@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.playlists.refresh.return_value.get.side_effect = Exception
|
||||||
self.core.playlists.refresh()
|
self.core.playlists.refresh()
|
||||||
self.assertFalse(send.called)
|
self.assertFalse(send.called)
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
||||||
|
|
||||||
@mock.patch('mopidy.core.listener.CoreListener.send')
|
@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.playlists.refresh.return_value.get.side_effect = Exception
|
||||||
self.core.playlists.refresh('dummy')
|
self.core.playlists.refresh('dummy')
|
||||||
self.assertFalse(send.called)
|
self.assertFalse(send.called)
|
||||||
logger.exception.assert_called_with(mock.ANY, 'DummyBackend')
|
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
|
import mock
|
||||||
|
|
||||||
from mopidy import backend, core
|
from mopidy import backend, core
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.models import TlTrack, Track
|
from mopidy.models import TlTrack, Track
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
|
|
||||||
class TracklistTest(unittest.TestCase):
|
class TracklistTest(unittest.TestCase):
|
||||||
|
|||||||
@ -12,8 +12,6 @@ from mopidy.http import actor
|
|||||||
class HttpEventsTest(unittest.TestCase):
|
class HttpEventsTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_track_playback_paused_is_broadcasted(self, broadcast):
|
def test_track_playback_paused_is_broadcasted(self, broadcast):
|
||||||
broadcast.reset_mock()
|
|
||||||
|
|
||||||
actor.on_event('track_playback_paused', foo='bar')
|
actor.on_event('track_playback_paused', foo='bar')
|
||||||
|
|
||||||
self.assertDictEqual(
|
self.assertDictEqual(
|
||||||
@ -23,8 +21,6 @@ class HttpEventsTest(unittest.TestCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def test_track_playback_resumed_is_broadcasted(self, broadcast):
|
def test_track_playback_resumed_is_broadcasted(self, broadcast):
|
||||||
broadcast.reset_mock()
|
|
||||||
|
|
||||||
actor.on_event('track_playback_resumed', foo='bar')
|
actor.on_event('track_playback_resumed', foo='bar')
|
||||||
|
|
||||||
self.assertDictEqual(
|
self.assertDictEqual(
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from mock import Mock, call, patch, sentinel
|
|||||||
|
|
||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
from mopidy.utils import network
|
from mopidy.internal import network
|
||||||
|
|
||||||
from tests import any_int, any_unicode
|
from tests import any_int, any_unicode
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ import unittest
|
|||||||
from mock import Mock, sentinel
|
from mock import Mock, sentinel
|
||||||
|
|
||||||
from mopidy import compat
|
from mopidy import compat
|
||||||
from mopidy.utils import network
|
from mopidy.internal import network
|
||||||
|
|
||||||
from tests import any_unicode
|
from tests import any_unicode
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ import gobject
|
|||||||
|
|
||||||
from mock import Mock, patch, sentinel
|
from mock import Mock, patch, sentinel
|
||||||
|
|
||||||
from mopidy.utils import network
|
from mopidy.internal import network
|
||||||
|
|
||||||
from tests import any_int
|
from tests import any_int
|
||||||
|
|
||||||
@ -5,18 +5,18 @@ import unittest
|
|||||||
|
|
||||||
from mock import Mock, patch
|
from mock import Mock, patch
|
||||||
|
|
||||||
from mopidy.utils import network
|
from mopidy.internal import network
|
||||||
|
|
||||||
|
|
||||||
class FormatHostnameTest(unittest.TestCase):
|
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):
|
def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self):
|
||||||
network.has_ipv6 = True
|
network.has_ipv6 = True
|
||||||
self.assertEqual(network.format_hostname('0.0.0.0'), '::ffff:0.0.0.0')
|
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')
|
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):
|
def test_format_hostname_does_nothing_when_only_ipv4_available(self):
|
||||||
network.has_ipv6 = False
|
network.has_ipv6 = False
|
||||||
self.assertEqual(network.format_hostname('0.0.0.0'), '0.0.0.0')
|
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):
|
class CreateSocketTest(unittest.TestCase):
|
||||||
|
|
||||||
@patch('mopidy.utils.network.has_ipv6', False)
|
@patch('mopidy.internal.network.has_ipv6', False)
|
||||||
@patch('socket.socket')
|
@patch('socket.socket')
|
||||||
def test_ipv4_socket(self, socket_mock):
|
def test_ipv4_socket(self, socket_mock):
|
||||||
network.create_socket()
|
network.create_socket()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
socket_mock.call_args[0], (socket.AF_INET, socket.SOCK_STREAM))
|
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')
|
@patch('socket.socket')
|
||||||
def test_ipv6_socket(self, socket_mock):
|
def test_ipv6_socket(self, socket_mock):
|
||||||
network.create_socket()
|
network.create_socket()
|
||||||
@ -12,7 +12,7 @@ import pygst
|
|||||||
pygst.require('0.10')
|
pygst.require('0.10')
|
||||||
import gst # noqa
|
import gst # noqa
|
||||||
|
|
||||||
from mopidy.utils import deps
|
from mopidy.internal import deps
|
||||||
|
|
||||||
|
|
||||||
class DepsTest(unittest.TestCase):
|
class DepsTest(unittest.TestCase):
|
||||||
@ -4,16 +4,16 @@ import unittest
|
|||||||
|
|
||||||
import mock
|
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):
|
class LocaleDecodeTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_can_decode_utf8_strings_with_french_content(self, mock):
|
def test_can_decode_utf8_strings_with_french_content(self, mock):
|
||||||
mock.return_value = 'UTF-8'
|
mock.return_value = 'UTF-8'
|
||||||
|
|
||||||
result = locale_decode(
|
result = encoding.locale_decode(
|
||||||
b'[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
|
b'[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
|
||||||
|
|
||||||
self.assertEqual('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
|
self.assertEqual('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
|
||||||
@ -22,7 +22,7 @@ class LocaleDecodeTest(unittest.TestCase):
|
|||||||
mock.return_value = 'UTF-8'
|
mock.return_value = 'UTF-8'
|
||||||
|
|
||||||
error = IOError(98, b'Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
|
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'
|
expected = '[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e'
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -33,13 +33,13 @@ class LocaleDecodeTest(unittest.TestCase):
|
|||||||
def test_does_not_use_locale_to_decode_unicode_strings(self, mock):
|
def test_does_not_use_locale_to_decode_unicode_strings(self, mock):
|
||||||
mock.return_value = 'UTF-8'
|
mock.return_value = 'UTF-8'
|
||||||
|
|
||||||
locale_decode('abc')
|
encoding.locale_decode('abc')
|
||||||
|
|
||||||
self.assertFalse(mock.called)
|
self.assertFalse(mock.called)
|
||||||
|
|
||||||
def test_does_not_use_locale_to_decode_ascii_bytestrings(self, mock):
|
def test_does_not_use_locale_to_decode_ascii_bytestrings(self, mock):
|
||||||
mock.return_value = 'UTF-8'
|
mock.return_value = 'UTF-8'
|
||||||
|
|
||||||
locale_decode('abc')
|
encoding.locale_decode('abc')
|
||||||
|
|
||||||
self.assertFalse(mock.called)
|
self.assertFalse(mock.called)
|
||||||
@ -8,7 +8,7 @@ import mock
|
|||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
from mopidy import core, models
|
from mopidy import core, models
|
||||||
from mopidy.utils import deprecation, jsonrpc
|
from mopidy.internal import deprecation, jsonrpc
|
||||||
|
|
||||||
from tests import dummy_backend
|
from tests import dummy_backend
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ import unittest
|
|||||||
import glib
|
import glib
|
||||||
|
|
||||||
from mopidy import compat, exceptions
|
from mopidy import compat, exceptions
|
||||||
from mopidy.utils import path
|
from mopidy.internal import path
|
||||||
|
|
||||||
import tests
|
import tests
|
||||||
|
|
||||||
@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
from pytest import raises
|
from pytest import raises
|
||||||
|
|
||||||
from mopidy import compat, exceptions
|
from mopidy import compat, exceptions
|
||||||
from mopidy.utils import validation
|
from mopidy.internal import validation
|
||||||
|
|
||||||
|
|
||||||
def test_check_boolean_with_valid_values():
|
def test_check_boolean_with_valid_values():
|
||||||
@ -6,7 +6,7 @@ import mock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from mopidy.utils import xdg
|
from mopidy.internal import xdg
|
||||||
|
|
||||||
|
|
||||||
@pytest.yield_fixture
|
@pytest.yield_fixture
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from mopidy.utils import deprecation
|
from mopidy.internal import deprecation
|
||||||
|
|
||||||
|
|
||||||
def generate_song(i):
|
def generate_song(i):
|
||||||
|
|||||||
@ -9,9 +9,9 @@ import pykka
|
|||||||
|
|
||||||
from mopidy import core
|
from mopidy import core
|
||||||
from mopidy.core import PlaybackState
|
from mopidy.core import PlaybackState
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.local import actor
|
from mopidy.local import actor
|
||||||
from mopidy.models import TlTrack, Track
|
from mopidy.models import TlTrack, Track
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
from tests import dummy_audio, path_to_data_dir
|
from tests import dummy_audio, path_to_data_dir
|
||||||
from tests.local import generate_song, populate_tracklist
|
from tests.local import generate_song, populate_tracklist
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import pykka
|
|||||||
|
|
||||||
from mopidy import core
|
from mopidy import core
|
||||||
from mopidy.core import PlaybackState
|
from mopidy.core import PlaybackState
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.local import actor
|
from mopidy.local import actor
|
||||||
from mopidy.models import Playlist, Track
|
from mopidy.models import Playlist, Track
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
from tests import dummy_audio, path_to_data_dir
|
from tests import dummy_audio, path_to_data_dir
|
||||||
from tests.local import generate_song, populate_tracklist
|
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
|
import pykka
|
||||||
|
|
||||||
from mopidy import core
|
from mopidy import core
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.m3u import actor
|
from mopidy.m3u import actor
|
||||||
from mopidy.m3u.translator import playlist_uri_to_path
|
from mopidy.m3u.translator import playlist_uri_to_path
|
||||||
from mopidy.models import Playlist, Track
|
from mopidy.models import Playlist, Track
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
from tests import dummy_audio, path_to_data_dir
|
from tests import dummy_audio, path_to_data_dir
|
||||||
from tests.m3u import generate_song
|
from tests.m3u import generate_song
|
||||||
|
|||||||
@ -6,9 +6,9 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from mopidy.internal import path
|
||||||
from mopidy.m3u import translator
|
from mopidy.m3u import translator
|
||||||
from mopidy.models import Track
|
from mopidy.models import Track
|
||||||
from mopidy.utils import path
|
|
||||||
|
|
||||||
from tests import path_to_data_dir
|
from tests import path_to_data_dir
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,14 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mopidy.models.fields import * # noqa: F403
|
from mopidy.models.fields import * # noqa: F403
|
||||||
from mopidy.models.immutable import ImmutableObjectMeta
|
|
||||||
|
|
||||||
|
|
||||||
def create_instance(field):
|
def create_instance(field):
|
||||||
"""Create an instance of a dummy class for testing fields."""
|
"""Create an instance of a dummy class for testing fields."""
|
||||||
|
|
||||||
class Dummy(object):
|
class Dummy(object):
|
||||||
__metaclass__ = ImmutableObjectMeta
|
|
||||||
attr = field
|
attr = field
|
||||||
|
attr._name = 'attr'
|
||||||
|
|
||||||
return Dummy()
|
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):
|
class Foo(Track):
|
||||||
pass
|
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):
|
class CachingTest(unittest.TestCase):
|
||||||
|
|
||||||
@ -1148,9 +1168,3 @@ class SearchResultTest(unittest.TestCase):
|
|||||||
self.assertDictEqual(
|
self.assertDictEqual(
|
||||||
{'__model__': 'SearchResult', 'uri': 'uri'},
|
{'__model__': 'SearchResult', 'uri': 'uri'},
|
||||||
SearchResult(uri='uri').serialize())
|
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
|
import pykka
|
||||||
|
|
||||||
from mopidy import core
|
from mopidy import core
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.mpd import session, uri_mapper
|
from mopidy.mpd import session, uri_mapper
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
from tests import dummy_backend, dummy_mixer
|
from tests import dummy_backend, dummy_mixer
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from mopidy.internal import deprecation
|
||||||
from mopidy.models import Ref, Track
|
from mopidy.models import Ref, Track
|
||||||
from mopidy.utils import deprecation
|
|
||||||
|
|
||||||
from tests.mpd import protocol
|
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