Merge branch 'feature/split-controllers-and-providers' into develop
This commit is contained in:
commit
b7a0d75372
@ -1,90 +0,0 @@
|
||||
**********************
|
||||
:mod:`mopidy.backends`
|
||||
**********************
|
||||
|
||||
.. automodule:: mopidy.backends
|
||||
:synopsis: Backend API
|
||||
|
||||
|
||||
The backend and its controllers
|
||||
===============================
|
||||
|
||||
.. graph:: backend_relations
|
||||
|
||||
backend -- current_playlist
|
||||
backend -- library
|
||||
backend -- playback
|
||||
backend -- stored_playlists
|
||||
|
||||
|
||||
Backend API
|
||||
===========
|
||||
|
||||
.. note::
|
||||
|
||||
Currently this only documents the API that is available for use by
|
||||
frontends like :mod:`mopidy.frontends.mpd`, and not what is required to
|
||||
implement your own backend. :class:`mopidy.backends.base.BaseBackend` and
|
||||
its controllers implements many of these methods in a matter that should be
|
||||
independent of most concrete backend implementations, so you should
|
||||
generally just implement or override a few of these methods yourself to
|
||||
create a new backend with a complete feature set.
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BaseBackend
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Playback controller
|
||||
-------------------
|
||||
|
||||
Manages playback, with actions like play, pause, stop, next, previous, and
|
||||
seek.
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BasePlaybackController
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Mixer controller
|
||||
----------------
|
||||
|
||||
Manages volume. See :class:`mopidy.mixers.BaseMixer`.
|
||||
|
||||
|
||||
Current playlist controller
|
||||
---------------------------
|
||||
|
||||
Manages everything related to the currently loaded playlist.
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BaseCurrentPlaylistController
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Stored playlists controller
|
||||
---------------------------
|
||||
|
||||
Manages stored playlist.
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsController
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Library controller
|
||||
------------------
|
||||
|
||||
Manages the music library, e.g. searching for tracks to be added to a playlist.
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BaseLibraryController
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Backend implementations
|
||||
=======================
|
||||
|
||||
* :mod:`mopidy.backends.dummy`
|
||||
* :mod:`mopidy.backends.libspotify`
|
||||
* :mod:`mopidy.backends.local`
|
||||
28
docs/api/backends/concepts.rst
Normal file
28
docs/api/backends/concepts.rst
Normal file
@ -0,0 +1,28 @@
|
||||
**********************************************
|
||||
The backend, controller, and provider concepts
|
||||
**********************************************
|
||||
|
||||
Backend:
|
||||
The backend is mostly for convenience. It is a container that holds
|
||||
references to all the controllers.
|
||||
Controllers:
|
||||
Each controller has responsibility for a given part of the backend
|
||||
functionality. Most, but not all, controllers delegates some work to one or
|
||||
more providers. The controllers are responsible for choosing the right
|
||||
provider for any given task based upon i.e. the track's URI. See
|
||||
:ref:`backend-controller-api` for more details.
|
||||
Providers:
|
||||
Anything specific to i.e. Spotify integration or local storage is contained
|
||||
in the providers. To integrate with new music sources, you just add new
|
||||
providers. See :ref:`backend-provider-api` for more details.
|
||||
|
||||
.. digraph:: backend_relations
|
||||
|
||||
Backend -> "Current\nplaylist\ncontroller"
|
||||
Backend -> "Library\ncontroller"
|
||||
"Library\ncontroller" -> "Library\nproviders"
|
||||
Backend -> "Playback\ncontroller"
|
||||
"Playback\ncontroller" -> "Playback\nproviders"
|
||||
Backend -> "Stored\nplaylists\ncontroller"
|
||||
"Stored\nplaylists\ncontroller" -> "Stored\nplaylist\nproviders"
|
||||
Backend -> Mixer
|
||||
65
docs/api/backends/controllers.rst
Normal file
65
docs/api/backends/controllers.rst
Normal file
@ -0,0 +1,65 @@
|
||||
.. _backend-controller-api:
|
||||
|
||||
**********************
|
||||
Backend controller API
|
||||
**********************
|
||||
|
||||
|
||||
The backend controller API is the interface that is used by frontends like
|
||||
:mod:`mopidy.frontends.mpd`. If you want to implement your own backend, see the
|
||||
:ref:`backend-provider-api`.
|
||||
|
||||
|
||||
The backend
|
||||
===========
|
||||
|
||||
.. autoclass:: mopidy.backends.base.Backend
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Playback controller
|
||||
===================
|
||||
|
||||
Manages playback, with actions like play, pause, stop, next, previous, and
|
||||
seek.
|
||||
|
||||
.. autoclass:: mopidy.backends.base.PlaybackController
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Mixer controller
|
||||
================
|
||||
|
||||
Manages volume. See :class:`mopidy.mixers.BaseMixer`.
|
||||
|
||||
|
||||
Current playlist controller
|
||||
===========================
|
||||
|
||||
Manages everything related to the currently loaded playlist.
|
||||
|
||||
.. autoclass:: mopidy.backends.base.CurrentPlaylistController
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Stored playlists controller
|
||||
===========================
|
||||
|
||||
Manages stored playlist.
|
||||
|
||||
.. autoclass:: mopidy.backends.base.StoredPlaylistsController
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Library controller
|
||||
==================
|
||||
|
||||
Manages the music library, e.g. searching for tracks to be added to a playlist.
|
||||
|
||||
.. autoclass:: mopidy.backends.base.LibraryController
|
||||
:members:
|
||||
:undoc-members:
|
||||
41
docs/api/backends/providers.rst
Normal file
41
docs/api/backends/providers.rst
Normal file
@ -0,0 +1,41 @@
|
||||
.. _backend-provider-api:
|
||||
|
||||
********************
|
||||
Backend provider API
|
||||
********************
|
||||
|
||||
The backend provider API is the interface that must be implemented when you
|
||||
create a backend. If you are working on a frontend and need to access the
|
||||
backend, see the :ref:`backend-controller-api`.
|
||||
|
||||
|
||||
Playback provider
|
||||
=================
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BasePlaybackProvider
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Stored playlists provider
|
||||
=========================
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsProvider
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Library provider
|
||||
================
|
||||
|
||||
.. autoclass:: mopidy.backends.base.BaseLibraryProvider
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Backend provider implementations
|
||||
================================
|
||||
|
||||
* :mod:`mopidy.backends.dummy`
|
||||
* :mod:`mopidy.backends.libspotify`
|
||||
* :mod:`mopidy.backends.local`
|
||||
@ -5,4 +5,7 @@ API reference
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
**
|
||||
backends/concepts
|
||||
backends/controllers
|
||||
backends/providers
|
||||
*
|
||||
|
||||
@ -16,8 +16,8 @@ import sys, os
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
|
||||
sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/../'))
|
||||
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
||||
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/../'))
|
||||
|
||||
import mopidy
|
||||
|
||||
|
||||
@ -4,21 +4,19 @@ import random
|
||||
import time
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.base.current_playlist import BaseCurrentPlaylistController
|
||||
from mopidy.backends.base.library import BaseLibraryController
|
||||
from mopidy.backends.base.playback import BasePlaybackController
|
||||
from mopidy.backends.base.stored_playlists import BaseStoredPlaylistsController
|
||||
from mopidy.frontends.mpd import translator
|
||||
from mopidy.models import Playlist
|
||||
from mopidy.utils import get_class
|
||||
|
||||
from .current_playlist import CurrentPlaylistController
|
||||
from .library import LibraryController, BaseLibraryProvider
|
||||
from .playback import PlaybackController, BasePlaybackProvider
|
||||
from .stored_playlists import (StoredPlaylistsController,
|
||||
BaseStoredPlaylistsProvider)
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
__all__ = ['BaseBackend', 'BasePlaybackController',
|
||||
'BaseCurrentPlaylistController', 'BaseStoredPlaylistsController',
|
||||
'BaseLibraryController']
|
||||
|
||||
class BaseBackend(object):
|
||||
class Backend(object):
|
||||
"""
|
||||
:param core_queue: a queue for sending messages to
|
||||
:class:`mopidy.process.CoreProcess`
|
||||
@ -44,22 +42,22 @@ class BaseBackend(object):
|
||||
core_queue = None
|
||||
|
||||
#: The current playlist controller. An instance of
|
||||
#: :class:`mopidy.backends.base.BaseCurrentPlaylistController`.
|
||||
#: :class:`mopidy.backends.base.CurrentPlaylistController`.
|
||||
current_playlist = None
|
||||
|
||||
#: The library controller. An instance of
|
||||
# :class:`mopidy.backends.base.BaseLibraryController`.
|
||||
# :class:`mopidy.backends.base.LibraryController`.
|
||||
library = None
|
||||
|
||||
#: The sound mixer. An instance of :class:`mopidy.mixers.BaseMixer`.
|
||||
mixer = None
|
||||
|
||||
#: The playback controller. An instance of
|
||||
#: :class:`mopidy.backends.base.BasePlaybackController`.
|
||||
#: :class:`mopidy.backends.base.PlaybackController`.
|
||||
playback = None
|
||||
|
||||
#: The stored playlists controller. An instance of
|
||||
#: :class:`mopidy.backends.base.BaseStoredPlaylistsController`.
|
||||
#: :class:`mopidy.backends.base.StoredPlaylistsController`.
|
||||
stored_playlists = None
|
||||
|
||||
#: List of URI prefixes this backend can handle.
|
||||
|
||||
@ -6,10 +6,10 @@ from mopidy.frontends.mpd import translator
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
class BaseCurrentPlaylistController(object):
|
||||
class CurrentPlaylistController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
|
||||
@ -2,18 +2,21 @@ import logging
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
class BaseLibraryController(object):
|
||||
class LibraryController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
:param provider: provider the controller should use
|
||||
:type provider: instance of :class:`BaseLibraryProvider`
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
def __init__(self, backend, provider):
|
||||
self.backend = backend
|
||||
self.provider = provider
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup after component."""
|
||||
pass
|
||||
self.provider.destroy()
|
||||
|
||||
def find_exact(self, **query):
|
||||
"""
|
||||
@ -32,7 +35,7 @@ class BaseLibraryController(object):
|
||||
:type query: dict
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.provider.find_exact(**query)
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
@ -42,7 +45,7 @@ class BaseLibraryController(object):
|
||||
:type uri: string
|
||||
:rtype: :class:`mopidy.models.Track` or :class:`None`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.provider.lookup(uri)
|
||||
|
||||
def refresh(self, uri=None):
|
||||
"""
|
||||
@ -51,7 +54,7 @@ class BaseLibraryController(object):
|
||||
:param uri: directory or track URI
|
||||
:type uri: string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.provider.refresh(uri)
|
||||
|
||||
def search(self, **query):
|
||||
"""
|
||||
@ -70,4 +73,54 @@ class BaseLibraryController(object):
|
||||
:type query: dict
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
return self.provider.search(**query)
|
||||
|
||||
|
||||
class BaseLibraryProvider(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
|
||||
def destroy(self):
|
||||
"""
|
||||
Cleanup after component.
|
||||
|
||||
*MAY be implemented by subclasses.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_exact(self, **query):
|
||||
"""
|
||||
See :meth:`mopidy.backends.base.LibraryController.find_exact`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
See :meth:`mopidy.backends.base.LibraryController.lookup`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self, uri=None):
|
||||
"""
|
||||
See :meth:`mopidy.backends.base.LibraryController.refresh`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def search(self, **query):
|
||||
"""
|
||||
See :meth:`mopidy.backends.base.LibraryController.search`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -4,10 +4,12 @@ import time
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
class BasePlaybackController(object):
|
||||
class PlaybackController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
:param backend: the backend
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
:param provider: provider the controller should use
|
||||
:type provider: instance of :class:`BasePlaybackProvider`
|
||||
"""
|
||||
|
||||
# pylint: disable = R0902
|
||||
@ -54,8 +56,9 @@ class BasePlaybackController(object):
|
||||
#: Playback continues after current song.
|
||||
single = False
|
||||
|
||||
def __init__(self, backend):
|
||||
def __init__(self, backend, provider):
|
||||
self.backend = backend
|
||||
self.provider = provider
|
||||
self._state = self.STOPPED
|
||||
self._shuffled = []
|
||||
self._first_shuffle = True
|
||||
@ -65,10 +68,8 @@ class BasePlaybackController(object):
|
||||
def destroy(self):
|
||||
"""
|
||||
Cleanup after component.
|
||||
|
||||
May be overridden by subclasses.
|
||||
"""
|
||||
pass
|
||||
self.provider.destroy()
|
||||
|
||||
def _get_cpid(self, cp_track):
|
||||
if cp_track is None:
|
||||
@ -330,7 +331,7 @@ class BasePlaybackController(object):
|
||||
"""
|
||||
Tell the playback controller that the current playlist has changed.
|
||||
|
||||
Used by :class:`mopidy.backends.base.BaseCurrentPlaylistController`.
|
||||
Used by :class:`mopidy.backends.base.CurrentPlaylistController`.
|
||||
"""
|
||||
self._first_shuffle = True
|
||||
self._shuffled = []
|
||||
@ -353,18 +354,9 @@ class BasePlaybackController(object):
|
||||
|
||||
def pause(self):
|
||||
"""Pause playback."""
|
||||
if self.state == self.PLAYING and self._pause():
|
||||
if self.state == self.PLAYING and self.provider.pause():
|
||||
self.state = self.PAUSED
|
||||
|
||||
def _pause(self):
|
||||
"""
|
||||
To be overridden by subclass. Implement your backend's pause
|
||||
functionality here.
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def play(self, cp_track=None, on_error_step=1):
|
||||
"""
|
||||
Play the given track, or if the given track is :class:`None`, play the
|
||||
@ -391,7 +383,7 @@ class BasePlaybackController(object):
|
||||
self.state = self.STOPPED
|
||||
self.current_cp_track = cp_track
|
||||
self.state = self.PLAYING
|
||||
if not self._play(cp_track[1]):
|
||||
if not self.provider.play(cp_track[1]):
|
||||
# Track is not playable
|
||||
if self.random and self._shuffled:
|
||||
self._shuffled.remove(cp_track)
|
||||
@ -405,18 +397,6 @@ class BasePlaybackController(object):
|
||||
|
||||
self._trigger_started_playing_event()
|
||||
|
||||
def _play(self, track):
|
||||
"""
|
||||
To be overridden by subclass. Implement your backend's play
|
||||
functionality here.
|
||||
|
||||
:param track: the track to play
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def previous(self):
|
||||
"""Play the previous track."""
|
||||
if self.cp_track_at_previous is None:
|
||||
@ -428,18 +408,9 @@ class BasePlaybackController(object):
|
||||
|
||||
def resume(self):
|
||||
"""If paused, resume playing the current track."""
|
||||
if self.state == self.PAUSED and self._resume():
|
||||
if self.state == self.PAUSED and self.provider.resume():
|
||||
self.state = self.PLAYING
|
||||
|
||||
def _resume(self):
|
||||
"""
|
||||
To be overridden by subclass. Implement your backend's resume
|
||||
functionality here.
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def seek(self, time_position):
|
||||
"""
|
||||
Seeks to time position given in milliseconds.
|
||||
@ -465,18 +436,7 @@ class BasePlaybackController(object):
|
||||
self._play_time_started = self._current_wall_time
|
||||
self._play_time_accumulated = time_position
|
||||
|
||||
return self._seek(time_position)
|
||||
|
||||
def _seek(self, time_position):
|
||||
"""
|
||||
To be overridden by subclass. Implement your backend's seek
|
||||
functionality here.
|
||||
|
||||
:param time_position: time position in milliseconds
|
||||
:type time_position: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.provider.seek(time_position)
|
||||
|
||||
def stop(self, clear_current_track=False):
|
||||
"""
|
||||
@ -489,20 +449,11 @@ class BasePlaybackController(object):
|
||||
if self.state == self.STOPPED:
|
||||
return
|
||||
self._trigger_stopped_playing_event()
|
||||
if self._stop():
|
||||
if self.provider.stop():
|
||||
self.state = self.STOPPED
|
||||
if clear_current_track:
|
||||
self.current_cp_track = None
|
||||
|
||||
def _stop(self):
|
||||
"""
|
||||
To be overridden by subclass. Implement your backend's stop
|
||||
functionality here.
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _trigger_started_playing_event(self):
|
||||
"""
|
||||
Notifies frontends that a track has started playing.
|
||||
@ -532,3 +483,75 @@ class BasePlaybackController(object):
|
||||
'track': self.current_track,
|
||||
'stop_position': self.time_position,
|
||||
})
|
||||
|
||||
|
||||
class BasePlaybackProvider(object):
|
||||
"""
|
||||
:param backend: the backend
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
|
||||
def destroy(self):
|
||||
"""
|
||||
Cleanup after component.
|
||||
|
||||
*MAY be implemented by subclasses.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Pause playback.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def play(self, track):
|
||||
"""
|
||||
Play given track.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param track: the track to play
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resume playback at the same time position playback was paused.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def seek(self, time_position):
|
||||
"""
|
||||
Seek to a given time position.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param time_position: time position in milliseconds
|
||||
:type time_position: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop playback.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -3,28 +3,34 @@ import logging
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
class BaseStoredPlaylistsController(object):
|
||||
class StoredPlaylistsController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
:param provider: provider the controller should use
|
||||
:type provider: instance of :class:`BaseStoredPlaylistsProvider`
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
def __init__(self, backend, provider):
|
||||
self.backend = backend
|
||||
self._playlists = []
|
||||
self.provider = provider
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup after component."""
|
||||
pass
|
||||
self.provider.destroy()
|
||||
|
||||
@property
|
||||
def playlists(self):
|
||||
"""List of :class:`mopidy.models.Playlist`."""
|
||||
return copy(self._playlists)
|
||||
"""
|
||||
Currently stored playlists.
|
||||
|
||||
Read/write. List of :class:`mopidy.models.Playlist`.
|
||||
"""
|
||||
return self.provider.playlists
|
||||
|
||||
@playlists.setter
|
||||
def playlists(self, playlists):
|
||||
self._playlists = playlists
|
||||
self.provider.playlists = playlists
|
||||
|
||||
def create(self, name):
|
||||
"""
|
||||
@ -34,7 +40,7 @@ class BaseStoredPlaylistsController(object):
|
||||
:type name: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.provider.create(name)
|
||||
|
||||
def delete(self, playlist):
|
||||
"""
|
||||
@ -43,7 +49,7 @@ class BaseStoredPlaylistsController(object):
|
||||
:param playlist: the playlist to delete
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.provider.delete(playlist)
|
||||
|
||||
def get(self, **criteria):
|
||||
"""
|
||||
@ -55,13 +61,14 @@ class BaseStoredPlaylistsController(object):
|
||||
|
||||
get(name='a') # Returns track with name 'a'
|
||||
get(uri='xyz') # Returns track with URI 'xyz'
|
||||
get(name='a', uri='xyz') # Returns track with name 'a' and URI 'xyz'
|
||||
get(name='a', uri='xyz') # Returns track with name 'a' and URI
|
||||
# 'xyz'
|
||||
|
||||
:param criteria: one or more criteria to match by
|
||||
:type criteria: dict
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
matches = self._playlists
|
||||
matches = self.playlists
|
||||
for (key, value) in criteria.iteritems():
|
||||
matches = filter(lambda p: getattr(p, key) == value, matches)
|
||||
if len(matches) == 1:
|
||||
@ -82,11 +89,14 @@ class BaseStoredPlaylistsController(object):
|
||||
:type uri: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.provider.lookup(uri)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh stored playlists."""
|
||||
raise NotImplementedError
|
||||
"""
|
||||
Refresh the stored playlists in
|
||||
:attr:`mopidy.backends.base.StoredPlaylistsController.playlists`.
|
||||
"""
|
||||
return self.provider.refresh()
|
||||
|
||||
def rename(self, playlist, new_name):
|
||||
"""
|
||||
@ -97,7 +107,7 @@ class BaseStoredPlaylistsController(object):
|
||||
:param new_name: the new name
|
||||
:type new_name: string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.provider.rename(playlist, new_name)
|
||||
|
||||
def save(self, playlist):
|
||||
"""
|
||||
@ -106,4 +116,85 @@ class BaseStoredPlaylistsController(object):
|
||||
:param playlist: the playlist
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
return self.provider.save(playlist)
|
||||
|
||||
|
||||
class BaseStoredPlaylistsProvider(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self._playlists = []
|
||||
|
||||
def destroy(self):
|
||||
"""
|
||||
Cleanup after component.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def playlists(self):
|
||||
"""
|
||||
Currently stored playlists.
|
||||
|
||||
Read/write. List of :class:`mopidy.models.Playlist`.
|
||||
"""
|
||||
return copy(self._playlists)
|
||||
|
||||
@playlists.setter
|
||||
def playlists(self, playlists):
|
||||
self._playlists = playlists
|
||||
|
||||
def create(self, name):
|
||||
"""
|
||||
See :meth:`mopidy.backends.base.StoredPlaylistsController.create`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, playlist):
|
||||
"""
|
||||
See :meth:`mopidy.backends.base.StoredPlaylistsController.delete`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
See :meth:`mopidy.backends.base.StoredPlaylistsController.lookup`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
See :meth:`mopidy.backends.base.StoredPlaylistsController.refresh`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def rename(self, playlist, new_name):
|
||||
"""
|
||||
See :meth:`mopidy.backends.base.StoredPlaylistsController.rename`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, playlist):
|
||||
"""
|
||||
See :meth:`mopidy.backends.base.StoredPlaylistsController.save`.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@ -1,9 +1,19 @@
|
||||
from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController,
|
||||
BasePlaybackController, BaseLibraryController,
|
||||
BaseStoredPlaylistsController)
|
||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
||||
PlaybackController, BasePlaybackProvider, LibraryController,
|
||||
BaseLibraryProvider, StoredPlaylistsController,
|
||||
BaseStoredPlaylistsProvider)
|
||||
from mopidy.models import Playlist
|
||||
|
||||
class DummyBackend(BaseBackend):
|
||||
|
||||
class DummyQueue(object):
|
||||
def __init__(self):
|
||||
self.received_messages = []
|
||||
|
||||
def put(self, message):
|
||||
self.received_messages.append(message)
|
||||
|
||||
|
||||
class DummyBackend(Backend):
|
||||
"""
|
||||
A backend which implements the backend API in the simplest way possible.
|
||||
Used in tests of the frontends.
|
||||
@ -13,19 +23,30 @@ class DummyBackend(BaseBackend):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DummyBackend, self).__init__(*args, **kwargs)
|
||||
self.current_playlist = DummyCurrentPlaylistController(backend=self)
|
||||
self.library = DummyLibraryController(backend=self)
|
||||
self.playback = DummyPlaybackController(backend=self)
|
||||
self.stored_playlists = DummyStoredPlaylistsController(backend=self)
|
||||
|
||||
self.core_queue = DummyQueue()
|
||||
|
||||
self.current_playlist = CurrentPlaylistController(backend=self)
|
||||
|
||||
library_provider = DummyLibraryProvider(backend=self)
|
||||
self.library = LibraryController(backend=self,
|
||||
provider=library_provider)
|
||||
|
||||
playback_provider = DummyPlaybackProvider(backend=self)
|
||||
self.playback = PlaybackController(backend=self,
|
||||
provider=playback_provider)
|
||||
|
||||
stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self)
|
||||
self.stored_playlists = StoredPlaylistsController(backend=self,
|
||||
provider=stored_playlists_provider)
|
||||
|
||||
self.uri_handlers = [u'dummy:']
|
||||
|
||||
|
||||
class DummyCurrentPlaylistController(BaseCurrentPlaylistController):
|
||||
pass
|
||||
|
||||
|
||||
class DummyLibraryController(BaseLibraryController):
|
||||
_library = []
|
||||
class DummyLibraryProvider(BaseLibraryProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self._library = []
|
||||
|
||||
def find_exact(self, **query):
|
||||
return Playlist()
|
||||
@ -42,41 +63,25 @@ class DummyLibraryController(BaseLibraryController):
|
||||
return Playlist()
|
||||
|
||||
|
||||
class DummyPlaybackController(BasePlaybackController):
|
||||
def _next(self, track):
|
||||
class DummyPlaybackProvider(BasePlaybackProvider):
|
||||
def pause(self):
|
||||
return True
|
||||
|
||||
def play(self, track):
|
||||
"""Pass None as track to force failure"""
|
||||
return track is not None
|
||||
|
||||
def _pause(self):
|
||||
def resume(self):
|
||||
return True
|
||||
|
||||
def _play(self, track):
|
||||
"""Pass None as track to force failure"""
|
||||
return track is not None
|
||||
|
||||
def _previous(self, track):
|
||||
"""Pass None as track to force failure"""
|
||||
return track is not None
|
||||
|
||||
def _resume(self):
|
||||
def seek(self, time_position):
|
||||
return True
|
||||
|
||||
def _seek(self, time_position):
|
||||
def stop(self):
|
||||
return True
|
||||
|
||||
def _stop(self):
|
||||
return True
|
||||
|
||||
def _trigger_started_playing_event(self):
|
||||
pass # noop
|
||||
|
||||
def _trigger_stopped_playing_event(self):
|
||||
pass # noop
|
||||
|
||||
|
||||
class DummyStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
_playlists = []
|
||||
|
||||
class DummyStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
||||
def create(self, name):
|
||||
playlist = Playlist(name=name)
|
||||
self._playlists.append(playlist)
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import logging
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.base import BaseBackend, BaseCurrentPlaylistController
|
||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
||||
LibraryController, PlaybackController, StoredPlaylistsController)
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.libspotify')
|
||||
|
||||
ENCODING = 'utf-8'
|
||||
|
||||
class LibspotifyBackend(BaseBackend):
|
||||
class LibspotifyBackend(Backend):
|
||||
"""
|
||||
A `Spotify <http://www.spotify.com/>`_ backend which uses the official
|
||||
`libspotify <http://developer.spotify.com/en/libspotify/overview/>`_
|
||||
@ -33,18 +34,29 @@ class LibspotifyBackend(BaseBackend):
|
||||
# missing spotify dependencies.
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from .library import LibspotifyLibraryController
|
||||
from .playback import LibspotifyPlaybackController
|
||||
from .stored_playlists import LibspotifyStoredPlaylistsController
|
||||
from .library import LibspotifyLibraryProvider
|
||||
from .playback import LibspotifyPlaybackProvider
|
||||
from .stored_playlists import LibspotifyStoredPlaylistsProvider
|
||||
|
||||
super(LibspotifyBackend, self).__init__(*args, **kwargs)
|
||||
|
||||
self.current_playlist = BaseCurrentPlaylistController(backend=self)
|
||||
self.library = LibspotifyLibraryController(backend=self)
|
||||
self.playback = LibspotifyPlaybackController(backend=self)
|
||||
self.stored_playlists = LibspotifyStoredPlaylistsController(
|
||||
self.current_playlist = CurrentPlaylistController(backend=self)
|
||||
|
||||
library_provider = LibspotifyLibraryProvider(backend=self)
|
||||
self.library = LibraryController(backend=self,
|
||||
provider=library_provider)
|
||||
|
||||
playback_provider = LibspotifyPlaybackProvider(backend=self)
|
||||
self.playback = PlaybackController(backend=self,
|
||||
provider=playback_provider)
|
||||
|
||||
stored_playlists_provider = LibspotifyStoredPlaylistsProvider(
|
||||
backend=self)
|
||||
self.stored_playlists = StoredPlaylistsController(backend=self,
|
||||
provider=stored_playlists_provider)
|
||||
|
||||
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
|
||||
|
||||
self.spotify = self._connect()
|
||||
|
||||
def _connect(self):
|
||||
|
||||
@ -3,14 +3,14 @@ import multiprocessing
|
||||
|
||||
from spotify import Link, SpotifyError
|
||||
|
||||
from mopidy.backends.base import BaseLibraryController
|
||||
from mopidy.backends.base import BaseLibraryProvider
|
||||
from mopidy.backends.libspotify import ENCODING
|
||||
from mopidy.backends.libspotify.translator import LibspotifyTranslator
|
||||
from mopidy.models import Playlist
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.libspotify.library')
|
||||
|
||||
class LibspotifyLibraryController(BaseLibraryController):
|
||||
class LibspotifyLibraryProvider(BaseLibraryProvider):
|
||||
def find_exact(self, **query):
|
||||
return self.search(**query)
|
||||
|
||||
|
||||
@ -2,17 +2,17 @@ import logging
|
||||
|
||||
from spotify import Link, SpotifyError
|
||||
|
||||
from mopidy.backends.base import BasePlaybackController
|
||||
from mopidy.backends.base import BasePlaybackProvider
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.libspotify.playback')
|
||||
|
||||
class LibspotifyPlaybackController(BasePlaybackController):
|
||||
def _pause(self):
|
||||
class LibspotifyPlaybackProvider(BasePlaybackProvider):
|
||||
def pause(self):
|
||||
return self.backend.output.set_state('PAUSED')
|
||||
|
||||
def _play(self, track):
|
||||
def play(self, track):
|
||||
self.backend.output.set_state('READY')
|
||||
if self.state == self.PLAYING:
|
||||
if self.backend.playback.state == self.backend.playback.PLAYING:
|
||||
self.backend.spotify.session.play(0)
|
||||
if track.uri is None:
|
||||
return False
|
||||
@ -26,16 +26,16 @@ class LibspotifyPlaybackController(BasePlaybackController):
|
||||
logger.warning('Play %s failed: %s', track.uri, e)
|
||||
return False
|
||||
|
||||
def _resume(self):
|
||||
return self._seek(self.time_position)
|
||||
def resume(self):
|
||||
return self.seek(self.backend.playback.time_position)
|
||||
|
||||
def _seek(self, time_position):
|
||||
def seek(self, time_position):
|
||||
self.backend.output.set_state('READY')
|
||||
self.backend.spotify.session.seek(time_position)
|
||||
self.backend.output.set_state('PLAYING')
|
||||
return True
|
||||
|
||||
def _stop(self):
|
||||
def stop(self):
|
||||
result = self.backend.output.set_state('READY')
|
||||
self.backend.spotify.session.play(0)
|
||||
return result
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from mopidy.backends.base import BaseStoredPlaylistsController
|
||||
from mopidy.backends.base import BaseStoredPlaylistsProvider
|
||||
|
||||
class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
class LibspotifyStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
||||
def create(self, name):
|
||||
pass # TODO
|
||||
|
||||
|
||||
@ -5,9 +5,10 @@ import os
|
||||
import shutil
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.base import (BaseBackend, BaseLibraryController,
|
||||
BaseStoredPlaylistsController, BaseCurrentPlaylistController,
|
||||
BasePlaybackController)
|
||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
||||
LibraryController, BaseLibraryProvider, PlaybackController,
|
||||
BasePlaybackProvider, StoredPlaylistsController,
|
||||
BaseStoredPlaylistsProvider)
|
||||
from mopidy.models import Playlist, Track, Album
|
||||
from mopidy.utils.process import pickle_connection
|
||||
|
||||
@ -15,7 +16,7 @@ from .translator import parse_m3u, parse_mpd_tag_cache
|
||||
|
||||
logger = logging.getLogger(u'mopidy.backends.local')
|
||||
|
||||
class LocalBackend(BaseBackend):
|
||||
class LocalBackend(Backend):
|
||||
"""
|
||||
A backend for playing music from a local music archive.
|
||||
|
||||
@ -31,41 +32,55 @@ class LocalBackend(BaseBackend):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalBackend, self).__init__(*args, **kwargs)
|
||||
|
||||
self.library = LocalLibraryController(self)
|
||||
self.stored_playlists = LocalStoredPlaylistsController(self)
|
||||
self.current_playlist = BaseCurrentPlaylistController(self)
|
||||
self.playback = LocalPlaybackController(self)
|
||||
self.current_playlist = CurrentPlaylistController(backend=self)
|
||||
|
||||
library_provider = LocalLibraryProvider(backend=self)
|
||||
self.library = LibraryController(backend=self,
|
||||
provider=library_provider)
|
||||
|
||||
playback_provider = LocalPlaybackProvider(backend=self)
|
||||
self.playback = LocalPlaybackController(backend=self,
|
||||
provider=playback_provider)
|
||||
|
||||
stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self)
|
||||
self.stored_playlists = StoredPlaylistsController(backend=self,
|
||||
provider=stored_playlists_provider)
|
||||
|
||||
self.uri_handlers = [u'file://']
|
||||
|
||||
|
||||
class LocalPlaybackController(BasePlaybackController):
|
||||
def __init__(self, backend):
|
||||
super(LocalPlaybackController, self).__init__(backend)
|
||||
class LocalPlaybackController(PlaybackController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalPlaybackController, self).__init__(*args, **kwargs)
|
||||
|
||||
# XXX Why do we call stop()? Is it to set GStreamer state to 'READY'?
|
||||
self.stop()
|
||||
|
||||
def _play(self, track):
|
||||
return self.backend.output.play_uri(track.uri)
|
||||
|
||||
def _stop(self):
|
||||
return self.backend.output.set_state('READY')
|
||||
|
||||
def _pause(self):
|
||||
return self.backend.output.set_state('PAUSED')
|
||||
|
||||
def _resume(self):
|
||||
return self.backend.output.set_state('PLAYING')
|
||||
|
||||
def _seek(self, time_position):
|
||||
return self.backend.output.set_position(time_position)
|
||||
|
||||
@property
|
||||
def time_position(self):
|
||||
return self.backend.output.get_position()
|
||||
|
||||
|
||||
class LocalStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
def __init__(self, *args):
|
||||
super(LocalStoredPlaylistsController, self).__init__(*args)
|
||||
class LocalPlaybackProvider(BasePlaybackProvider):
|
||||
def pause(self):
|
||||
return self.backend.output.set_state('PAUSED')
|
||||
|
||||
def play(self, track):
|
||||
return self.backend.output.play_uri(track.uri)
|
||||
|
||||
def resume(self):
|
||||
return self.backend.output.set_state('PLAYING')
|
||||
|
||||
def seek(self, time_position):
|
||||
return self.backend.output.set_position(time_position)
|
||||
|
||||
def stop(self):
|
||||
return self.backend.output.set_state('READY')
|
||||
|
||||
|
||||
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs)
|
||||
self._folder = settings.LOCAL_PLAYLIST_PATH
|
||||
self.refresh()
|
||||
|
||||
@ -136,9 +151,9 @@ class LocalStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
self._playlists.append(playlist)
|
||||
|
||||
|
||||
class LocalLibraryController(BaseLibraryController):
|
||||
def __init__(self, backend):
|
||||
super(LocalLibraryController, self).__init__(backend)
|
||||
class LocalLibraryProvider(BaseLibraryProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self._uri_mapping = {}
|
||||
self.refresh()
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ class BaseFrontend(object):
|
||||
:param core_queue: queue for messaging the core
|
||||
:type core_queue: :class:`multiprocessing.Queue`
|
||||
:param backend: the backend
|
||||
:type backend: :class:`mopidy.backends.base.BaseBackend`
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
"""
|
||||
|
||||
def __init__(self, core_queue, backend):
|
||||
@ -13,17 +13,27 @@ class BaseFrontend(object):
|
||||
self.backend = backend
|
||||
|
||||
def start(self):
|
||||
"""Start the frontend."""
|
||||
"""
|
||||
Start the frontend.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
"""Destroy the frontend."""
|
||||
"""
|
||||
Destroy the frontend.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def process_message(self, message):
|
||||
"""
|
||||
Process messages for the frontend.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param message: the message
|
||||
:type message: dict
|
||||
"""
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
from mopidy import settings
|
||||
|
||||
class BaseMixer(object):
|
||||
"""
|
||||
:param backend: a backend instance
|
||||
:type mixer: :class:`mopidy.backends.base.BaseBackend`
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.MIXER_MAX_VOLUME`
|
||||
"""
|
||||
|
||||
def __init__(self, backend, *args, **kwargs):
|
||||
self.backend = backend
|
||||
self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
"""
|
||||
The audio volume
|
||||
|
||||
Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is
|
||||
equal to 0. Values above 100 is equal to 100.
|
||||
"""
|
||||
if self._get_volume() is None:
|
||||
return None
|
||||
return int(self._get_volume() / self.amplification_factor)
|
||||
|
||||
@volume.setter
|
||||
def volume(self, volume):
|
||||
volume = int(int(volume) * self.amplification_factor)
|
||||
if volume < 0:
|
||||
volume = 0
|
||||
elif volume > 100:
|
||||
volume = 100
|
||||
self._set_volume(volume)
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def _get_volume(self):
|
||||
"""
|
||||
Return volume as integer in range [0, 100]. :class:`None` if unknown.
|
||||
|
||||
*Must be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _set_volume(self, volume):
|
||||
"""
|
||||
Set volume as integer in range [0, 100].
|
||||
|
||||
*Must be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@ -2,7 +2,7 @@ import alsaaudio
|
||||
import logging
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.mixers import BaseMixer
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
|
||||
logger = logging.getLogger('mopidy.mixers.alsa')
|
||||
|
||||
|
||||
55
mopidy/mixers/base.py
Normal file
55
mopidy/mixers/base.py
Normal file
@ -0,0 +1,55 @@
|
||||
from mopidy import settings
|
||||
|
||||
class BaseMixer(object):
|
||||
"""
|
||||
:param backend: a backend instance
|
||||
:type backend: :class:`mopidy.backends.base.Backend`
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.MIXER_MAX_VOLUME`
|
||||
"""
|
||||
|
||||
def __init__(self, backend, *args, **kwargs):
|
||||
self.backend = backend
|
||||
self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
"""
|
||||
The audio volume
|
||||
|
||||
Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is
|
||||
equal to 0. Values above 100 is equal to 100.
|
||||
"""
|
||||
if self._get_volume() is None:
|
||||
return None
|
||||
return int(self._get_volume() / self.amplification_factor)
|
||||
|
||||
@volume.setter
|
||||
def volume(self, volume):
|
||||
volume = int(int(volume) * self.amplification_factor)
|
||||
if volume < 0:
|
||||
volume = 0
|
||||
elif volume > 100:
|
||||
volume = 100
|
||||
self._set_volume(volume)
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def _get_volume(self):
|
||||
"""
|
||||
Return volume as integer in range [0, 100]. :class:`None` if unknown.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _set_volume(self, volume):
|
||||
"""
|
||||
Set volume as integer in range [0, 100].
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@ -4,7 +4,7 @@ from threading import Lock
|
||||
from serial import Serial
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.mixers import BaseMixer
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
|
||||
logger = logging.getLogger(u'mopidy.mixers.denon')
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from mopidy.mixers import BaseMixer
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
|
||||
class DummyMixer(BaseMixer):
|
||||
"""Mixer which just stores and reports the chosen volume."""
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from mopidy.mixers import BaseMixer
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
|
||||
class GStreamerSoftwareMixer(BaseMixer):
|
||||
"""Mixer which uses GStreamer to control volume in software."""
|
||||
|
||||
@ -3,7 +3,7 @@ from serial import Serial
|
||||
from multiprocessing import Pipe
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.mixers import BaseMixer
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
from mopidy.utils.process import BaseThread
|
||||
|
||||
logger = logging.getLogger('mopidy.mixers.nad')
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from subprocess import Popen, PIPE
|
||||
import time
|
||||
|
||||
from mopidy.mixers import BaseMixer
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
|
||||
class OsaMixer(BaseMixer):
|
||||
"""Mixer which uses ``osascript`` on OS X to control volume."""
|
||||
|
||||
@ -7,21 +7,35 @@ class BaseOutput(object):
|
||||
self.core_queue = core_queue
|
||||
|
||||
def start(self):
|
||||
"""Start the output."""
|
||||
"""
|
||||
Start the output.
|
||||
|
||||
*MAY be implemented by subclasses.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
"""Destroy the output."""
|
||||
"""
|
||||
Destroy the output.
|
||||
|
||||
*MAY be implemented by subclasses.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def process_message(self, message):
|
||||
"""Process messages with the output as destination."""
|
||||
"""
|
||||
Process messages with the output as destination.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def play_uri(self, uri):
|
||||
"""
|
||||
Play URI.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param uri: the URI to play
|
||||
:type uri: string
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
@ -32,19 +46,27 @@ class BaseOutput(object):
|
||||
"""
|
||||
Deliver audio data to be played.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param capabilities: a GStreamer capabilities string
|
||||
:type capabilities: string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def end_of_data_stream(self):
|
||||
"""Signal that the last audio data has been delivered."""
|
||||
"""
|
||||
Signal that the last audio data has been delivered.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_position(self):
|
||||
"""
|
||||
Get position in milliseconds.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@ -53,6 +75,8 @@ class BaseOutput(object):
|
||||
"""
|
||||
Set position in milliseconds.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param position: the position in milliseconds
|
||||
:type volume: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
@ -63,6 +87,8 @@ class BaseOutput(object):
|
||||
"""
|
||||
Set playback state.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param state: the state
|
||||
:type state: string
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
@ -73,6 +99,8 @@ class BaseOutput(object):
|
||||
"""
|
||||
Get volume level for software mixer.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:rtype: int in range [0..100]
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@ -81,6 +109,8 @@ class BaseOutput(object):
|
||||
"""
|
||||
Set volume level for software mixer.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param volume: the volume in the range [0..100]
|
||||
:type volume: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
|
||||
@ -9,7 +9,7 @@ from mopidy.utils import get_class
|
||||
|
||||
from tests.backends.base import populate_playlist
|
||||
|
||||
class BaseCurrentPlaylistControllerTest(object):
|
||||
class CurrentPlaylistControllerTest(object):
|
||||
tracks = []
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@ -3,7 +3,7 @@ from mopidy.models import Playlist, Track, Album, Artist
|
||||
|
||||
from tests import SkipTest, data_folder
|
||||
|
||||
class BaseLibraryControllerTest(object):
|
||||
class LibraryControllerTest(object):
|
||||
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
|
||||
albums = [Album(name='album1', artists=artists[:1]),
|
||||
Album(name='album2', artists=artists[1:2]),
|
||||
|
||||
@ -13,7 +13,7 @@ from tests.backends.base import populate_playlist
|
||||
|
||||
# TODO Test 'playlist repeat', e.g. repeat=1,single=0
|
||||
|
||||
class BasePlaybackControllerTest(object):
|
||||
class PlaybackControllerTest(object):
|
||||
tracks = []
|
||||
|
||||
def setUp(self):
|
||||
@ -104,8 +104,8 @@ class BasePlaybackControllerTest(object):
|
||||
|
||||
@populate_playlist
|
||||
def test_play_skips_to_next_track_on_failure(self):
|
||||
# If _play() returns False, it is a failure.
|
||||
self.playback._play = lambda track: track != self.tracks[0]
|
||||
# If provider.play() returns False, it is a failure.
|
||||
self.playback.provider.play = lambda track: track != self.tracks[0]
|
||||
self.playback.play()
|
||||
self.assertNotEqual(self.playback.current_track, self.tracks[0])
|
||||
self.assertEqual(self.playback.current_track, self.tracks[1])
|
||||
@ -164,8 +164,8 @@ class BasePlaybackControllerTest(object):
|
||||
|
||||
@populate_playlist
|
||||
def test_previous_skips_to_previous_track_on_failure(self):
|
||||
# If _play() returns False, it is a failure.
|
||||
self.playback._play = lambda track: track != self.tracks[1]
|
||||
# If provider.play() returns False, it is a failure.
|
||||
self.playback.provider.play = lambda track: track != self.tracks[1]
|
||||
self.playback.play(self.current_playlist.cp_tracks[2])
|
||||
self.assertEqual(self.playback.current_track, self.tracks[2])
|
||||
self.playback.previous()
|
||||
@ -228,8 +228,8 @@ class BasePlaybackControllerTest(object):
|
||||
|
||||
@populate_playlist
|
||||
def test_next_skips_to_next_track_on_failure(self):
|
||||
# If _play() returns False, it is a failure.
|
||||
self.playback._play = lambda track: track != self.tracks[1]
|
||||
# If provider.play() returns False, it is a failure.
|
||||
self.playback.provider.play = lambda track: track != self.tracks[1]
|
||||
self.playback.play()
|
||||
self.assertEqual(self.playback.current_track, self.tracks[0])
|
||||
self.playback.next()
|
||||
@ -364,8 +364,8 @@ class BasePlaybackControllerTest(object):
|
||||
|
||||
@populate_playlist
|
||||
def test_end_of_track_skips_to_next_track_on_failure(self):
|
||||
# If _play() returns False, it is a failure.
|
||||
self.playback._play = lambda track: track != self.tracks[1]
|
||||
# If provider.play() returns False, it is a failure.
|
||||
self.playback.provider.play = lambda track: track != self.tracks[1]
|
||||
self.playback.play()
|
||||
self.assertEqual(self.playback.current_track, self.tracks[0])
|
||||
self.playback.on_end_of_track()
|
||||
|
||||
@ -8,7 +8,7 @@ from mopidy.models import Playlist
|
||||
|
||||
from tests import SkipTest, data_folder
|
||||
|
||||
class BaseStoredPlaylistsControllerTest(object):
|
||||
class StoredPlaylistsControllerTest(object):
|
||||
def setUp(self):
|
||||
settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp()
|
||||
settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache')
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
# TODO This integration test is work in progress.
|
||||
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.libspotify import LibspotifyBackend
|
||||
from mopidy.models import Track
|
||||
|
||||
from tests.backends.base.current_playlist import \
|
||||
BaseCurrentPlaylistControllerTest
|
||||
from tests.backends.base.library import BaseLibraryControllerTest
|
||||
from tests.backends.base.playback import BasePlaybackControllerTest
|
||||
from tests.backends.base.stored_playlists import \
|
||||
BaseStoredPlaylistsControllerTest
|
||||
|
||||
uris = [
|
||||
'spotify:track:6vqcpVcbI3Zu6sH3ieLDNt',
|
||||
'spotify:track:111sulhaZqgsnypz3MkiaW',
|
||||
'spotify:track:7t8oznvbeiAPMDRuK0R5ZT',
|
||||
]
|
||||
|
||||
class LibspotifyCurrentPlaylistControllerTest(
|
||||
BaseCurrentPlaylistControllerTest, unittest.TestCase):
|
||||
|
||||
backend_class = LibspotifyBackend
|
||||
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
|
||||
|
||||
|
||||
class LibspotifyPlaybackControllerTest(
|
||||
BasePlaybackControllerTest, unittest.TestCase):
|
||||
|
||||
backend_class = LibspotifyBackend
|
||||
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
|
||||
|
||||
|
||||
class LibspotifyStoredPlaylistsControllerTest(
|
||||
BaseStoredPlaylistsControllerTest, unittest.TestCase):
|
||||
|
||||
backend_class = LibspotifyBackend
|
||||
|
||||
|
||||
class LibspotifyLibraryControllerTest(
|
||||
BaseLibraryControllerTest, unittest.TestCase):
|
||||
|
||||
backend_class = LibspotifyBackend
|
||||
@ -10,11 +10,10 @@ from mopidy import settings
|
||||
from mopidy.backends.local import LocalBackend
|
||||
from mopidy.models import Track
|
||||
|
||||
from tests.backends.base.current_playlist import \
|
||||
BaseCurrentPlaylistControllerTest
|
||||
from tests.backends.base.current_playlist import CurrentPlaylistControllerTest
|
||||
from tests.backends.local import generate_song
|
||||
|
||||
class LocalCurrentPlaylistControllerTest(BaseCurrentPlaylistControllerTest,
|
||||
class LocalCurrentPlaylistControllerTest(CurrentPlaylistControllerTest,
|
||||
unittest.TestCase):
|
||||
|
||||
backend_class = LocalBackend
|
||||
|
||||
@ -10,9 +10,9 @@ from mopidy import settings
|
||||
from mopidy.backends.local import LocalBackend
|
||||
|
||||
from tests import data_folder
|
||||
from tests.backends.base.library import BaseLibraryControllerTest
|
||||
from tests.backends.base.library import LibraryControllerTest
|
||||
|
||||
class LocalLibraryControllerTest(BaseLibraryControllerTest, unittest.TestCase):
|
||||
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
|
||||
|
||||
backend_class = LocalBackend
|
||||
|
||||
|
||||
@ -12,12 +12,10 @@ from mopidy.models import Track
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
from tests import data_folder
|
||||
from tests.backends.base.playback import BasePlaybackControllerTest
|
||||
from tests.backends.base.playback import PlaybackControllerTest
|
||||
from tests.backends.local import generate_song
|
||||
|
||||
class LocalPlaybackControllerTest(BasePlaybackControllerTest,
|
||||
unittest.TestCase):
|
||||
|
||||
class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
|
||||
backend_class = LocalBackend
|
||||
tracks = [Track(uri=generate_song(i), length=4464)
|
||||
for i in range(1, 4)]
|
||||
|
||||
@ -16,10 +16,10 @@ from mopidy.utils.path import path_to_uri
|
||||
|
||||
from tests import data_folder
|
||||
from tests.backends.base.stored_playlists import \
|
||||
BaseStoredPlaylistsControllerTest
|
||||
StoredPlaylistsControllerTest
|
||||
from tests.backends.local import generate_song
|
||||
|
||||
class LocalStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest,
|
||||
class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
|
||||
unittest.TestCase):
|
||||
|
||||
backend_class = LocalBackend
|
||||
|
||||
@ -12,7 +12,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
|
||||
def test_add(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library._library = [Track(), Track(), needle, Track()]
|
||||
self.b.library.provider._library = [Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
@ -40,7 +40,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
|
||||
def test_addid_without_songpos(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library._library = [Track(), Track(), needle, Track()]
|
||||
self.b.library.provider._library = [Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
@ -58,7 +58,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
|
||||
def test_addid_with_songpos(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library._library = [Track(), Track(), needle, Track()]
|
||||
self.b.library.provider._library = [Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
@ -71,7 +71,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
|
||||
def test_addid_with_songpos_out_of_bounds_should_ack(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library._library = [Track(), Track(), needle, Track()]
|
||||
self.b.library.provider._library = [Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks), 5)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user