Merge branch 'feature/split-controllers-and-providers' into develop

This commit is contained in:
Stein Magnus Jodal 2010-11-03 00:09:36 +01:00
commit b7a0d75372
38 changed files with 663 additions and 426 deletions

View File

@ -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`

View 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

View 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:

View 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`

View File

@ -5,4 +5,7 @@ API reference
.. toctree::
:glob:
**
backends/concepts
backends/controllers
backends/providers
*

View File

@ -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

View File

@ -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.

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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
"""

View File

@ -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

View File

@ -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
View 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

View File

@ -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')

View File

@ -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."""

View File

@ -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."""

View File

@ -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')

View File

@ -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."""

View File

@ -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`

View File

@ -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):

View File

@ -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]),

View File

@ -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()

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)]

View File

@ -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

View File

@ -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)