Merge branch 'master' of git://github.com/jodal/mopidy into gstreamer
This commit is contained in:
commit
f098919aa0
@ -2,6 +2,10 @@
|
||||
:mod:`mopidy.backends`
|
||||
**********************
|
||||
|
||||
.. automodule:: mopidy.backends
|
||||
:synopsis: Backend API
|
||||
|
||||
|
||||
The backend and its controllers
|
||||
===============================
|
||||
|
||||
@ -16,20 +20,17 @@ The backend and its controllers
|
||||
Backend API
|
||||
===========
|
||||
|
||||
.. automodule:: mopidy.backends
|
||||
:synopsis: Backend interface.
|
||||
|
||||
.. note::
|
||||
|
||||
Currently this only documents the API that is available for use by
|
||||
frontends like :class:`mopidy.mpd.handler`, and not what is required to
|
||||
implement your own backend. :class:`mopidy.backends.BaseBackend` and its
|
||||
controllers implements many of these methods in a matter that should be
|
||||
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.BaseBackend
|
||||
.. autoclass:: mopidy.backends.base.BaseBackend
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
@ -40,7 +41,7 @@ Playback controller
|
||||
Manages playback, with actions like play, pause, stop, next, previous, and
|
||||
seek.
|
||||
|
||||
.. autoclass:: mopidy.backends.BasePlaybackController
|
||||
.. autoclass:: mopidy.backends.base.BasePlaybackController
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
@ -56,7 +57,7 @@ Current playlist controller
|
||||
|
||||
Manages everything related to the currently loaded playlist.
|
||||
|
||||
.. autoclass:: mopidy.backends.BaseCurrentPlaylistController
|
||||
.. autoclass:: mopidy.backends.base.BaseCurrentPlaylistController
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
@ -66,7 +67,7 @@ Stored playlists controller
|
||||
|
||||
Manages stored playlist.
|
||||
|
||||
.. autoclass:: mopidy.backends.BaseStoredPlaylistsController
|
||||
.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsController
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
@ -76,45 +77,39 @@ Library controller
|
||||
|
||||
Manages the music library, e.g. searching for tracks to be added to a playlist.
|
||||
|
||||
.. autoclass:: mopidy.backends.BaseLibraryController
|
||||
.. autoclass:: mopidy.backends.base.BaseLibraryController
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Spotify backends
|
||||
================
|
||||
|
||||
:mod:`mopidy.backends.despotify` -- Despotify backend
|
||||
-----------------------------------------------------
|
||||
=====================================================
|
||||
|
||||
.. automodule:: mopidy.backends.despotify
|
||||
:synopsis: Spotify backend using the despotify library.
|
||||
:synopsis: Spotify backend using the Despotify library
|
||||
:members:
|
||||
|
||||
|
||||
:mod:`mopidy.backends.libspotify` -- Libspotify backend
|
||||
-------------------------------------------------------
|
||||
|
||||
.. automodule:: mopidy.backends.libspotify
|
||||
:synopsis: Spotify backend using the libspotify library.
|
||||
:members:
|
||||
|
||||
|
||||
Other backends
|
||||
==============
|
||||
|
||||
:mod:`mopidy.backends.dummy` -- Dummy backend
|
||||
---------------------------------------------
|
||||
:mod:`mopidy.backends.dummy` -- Dummy backend for testing
|
||||
=========================================================
|
||||
|
||||
.. automodule:: mopidy.backends.dummy
|
||||
:synopsis: Dummy backend used for testing.
|
||||
:synopsis: Dummy backend used for testing
|
||||
:members:
|
||||
|
||||
|
||||
:mod:`mopidy.backends.gstreamer` -- GStreamer backend
|
||||
-----------------------------------------------------
|
||||
=====================================================
|
||||
|
||||
.. automodule:: mopidy.backends.gstreamer
|
||||
:synopsis: Backend for playing music from a local music archive using the
|
||||
GStreamer library.
|
||||
GStreamer library
|
||||
:members:
|
||||
|
||||
|
||||
:mod:`mopidy.backends.libspotify` -- Libspotify backend
|
||||
=======================================================
|
||||
|
||||
.. automodule:: mopidy.backends.libspotify
|
||||
:synopsis: Spotify backend using the libspotify library
|
||||
:members:
|
||||
|
||||
@ -13,6 +13,17 @@ simply instantiate a mixer and read/write to the ``volume`` attribute::
|
||||
>>> mixer.volume
|
||||
80
|
||||
|
||||
Most users will use one of the internal mixers which controls the volume on the
|
||||
computer running Mopidy. If you do not specify which mixer you want to use in
|
||||
the settings, Mopidy will choose one for you based upon what OS you run. See
|
||||
:attr:`mopidy.settings.MIXER` for the defaults.
|
||||
|
||||
Mopidy also supports controlling volume on other hardware devices instead of on
|
||||
the computer running Mopidy through the use of custom mixer implementations. To
|
||||
enable one of the hardware device mixers, you must the set
|
||||
:attr:`mopidy.settings.MIXER` setting to point to one of the classes found
|
||||
below, and possibly add some extra settings required by the mixer you choose.
|
||||
|
||||
|
||||
Mixer API
|
||||
=========
|
||||
@ -21,76 +32,56 @@ All mixers should subclass :class:`mopidy.mixers.BaseMixer` and override
|
||||
methods as described below.
|
||||
|
||||
.. automodule:: mopidy.mixers
|
||||
:synopsis: Sound mixer interface.
|
||||
:synopsis: Mixer API
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Internal mixers
|
||||
===============
|
||||
|
||||
Most users will use one of these internal mixers which controls the volume on
|
||||
the computer running Mopidy. If you do not specify which mixer you want to use
|
||||
in the settings, Mopidy will choose one for you based upon what OS you run. See
|
||||
:attr:`mopidy.settings.MIXER` for the defaults.
|
||||
|
||||
|
||||
:mod:`mopidy.mixers.alsa` -- ALSA mixer
|
||||
---------------------------------------
|
||||
:mod:`mopidy.mixers.alsa` -- ALSA mixer for Linux
|
||||
=================================================
|
||||
|
||||
.. automodule:: mopidy.mixers.alsa
|
||||
:synopsis: ALSA mixer for Linux.
|
||||
:synopsis: ALSA mixer for Linux
|
||||
:members:
|
||||
|
||||
.. inheritance-diagram:: mopidy.mixers.alsa.AlsaMixer
|
||||
|
||||
|
||||
:mod:`mopidy.mixers.dummy` -- Dummy mixer
|
||||
-----------------------------------------
|
||||
|
||||
.. automodule:: mopidy.mixers.dummy
|
||||
:synopsis: Dummy mixer for testing.
|
||||
:members:
|
||||
|
||||
.. inheritance-diagram:: mopidy.mixers.dummy
|
||||
|
||||
|
||||
:mod:`mopidy.mixers.osa` -- Osa mixer
|
||||
-------------------------------------
|
||||
|
||||
.. automodule:: mopidy.mixers.osa
|
||||
:synopsis: Osa mixer for OS X.
|
||||
:members:
|
||||
|
||||
.. inheritance-diagram:: mopidy.mixers.osa
|
||||
|
||||
|
||||
External device mixers
|
||||
======================
|
||||
|
||||
Mopidy supports controlling volume on external devices instead of on the
|
||||
computer running Mopidy through the use of custom mixer implementations. To
|
||||
enable one of the following mixers, you must the set
|
||||
:attr:`mopidy.settings.MIXER` setting to point to one of the classes
|
||||
found below, and possibly add some extra settings required by the mixer you
|
||||
choose.
|
||||
|
||||
|
||||
:mod:`mopidy.mixers.denon` -- Denon amplifier mixer
|
||||
---------------------------------------------------
|
||||
:mod:`mopidy.mixers.denon` -- Hardware mixer for Denon amplifiers
|
||||
=================================================================
|
||||
|
||||
.. automodule:: mopidy.mixers.denon
|
||||
:synopsis: Denon amplifier mixer.
|
||||
:synopsis: Hardware mixer for Denon amplifiers
|
||||
:members:
|
||||
|
||||
.. inheritance-diagram:: mopidy.mixers.denon
|
||||
|
||||
|
||||
:mod:`mopidy.mixers.nad` -- NAD amplifier mixer
|
||||
-----------------------------------------------
|
||||
:mod:`mopidy.mixers.dummy` -- Dummy mixer for testing
|
||||
=====================================================
|
||||
|
||||
.. automodule:: mopidy.mixers.dummy
|
||||
:synopsis: Dummy mixer for testing
|
||||
:members:
|
||||
|
||||
.. inheritance-diagram:: mopidy.mixers.dummy
|
||||
|
||||
|
||||
:mod:`mopidy.mixers.osa` -- Osa mixer for OS X
|
||||
==============================================
|
||||
|
||||
.. automodule:: mopidy.mixers.osa
|
||||
:synopsis: Osa mixer for OS X
|
||||
:members:
|
||||
|
||||
.. inheritance-diagram:: mopidy.mixers.osa
|
||||
|
||||
|
||||
:mod:`mopidy.mixers.nad` -- Hardware mixer for NAD amplifiers
|
||||
=============================================================
|
||||
|
||||
.. automodule:: mopidy.mixers.nad
|
||||
:synopsis: NAD amplifier mixer.
|
||||
:synopsis: Hardware mixer for NAD amplifiers
|
||||
:members:
|
||||
|
||||
.. inheritance-diagram:: mopidy.mixers.nad
|
||||
|
||||
@ -23,6 +23,6 @@ Data model API
|
||||
==============
|
||||
|
||||
.. automodule:: mopidy.models
|
||||
:synopsis: Immutable data models.
|
||||
:synopsis: Data model API
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
107
docs/api/mpd.rst
107
docs/api/mpd.rst
@ -1,22 +1,103 @@
|
||||
*****************
|
||||
:mod:`mopidy.mpd`
|
||||
*****************
|
||||
***************************
|
||||
:mod:`mopidy.frontends.mpd`
|
||||
***************************
|
||||
|
||||
MPD protocol implementation
|
||||
===========================
|
||||
.. automodule:: mopidy.frontends.mpd
|
||||
:synopsis: MPD frontend
|
||||
|
||||
.. automodule:: mopidy.mpd.frontend
|
||||
:synopsis: Our MPD protocol implementation.
|
||||
|
||||
MPD server
|
||||
==========
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.server
|
||||
:synopsis: MPD server
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. inheritance-diagram:: mopidy.frontends.mpd.server
|
||||
|
||||
|
||||
MPD frontend
|
||||
============
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.frontend
|
||||
:synopsis: MPD request dispatcher
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
MPD server implementation
|
||||
=========================
|
||||
MPD protocol
|
||||
============
|
||||
|
||||
.. automodule:: mopidy.mpd.server
|
||||
:synopsis: Our MPD server implementation.
|
||||
.. automodule:: mopidy.frontends.mpd.protocol
|
||||
:synopsis: MPD protocol
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. inheritance-diagram:: mopidy.mpd.server
|
||||
|
||||
Audio output
|
||||
------------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.audio_output
|
||||
:members:
|
||||
|
||||
|
||||
Command list
|
||||
------------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.command_list
|
||||
:members:
|
||||
|
||||
|
||||
Connection
|
||||
----------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.connection
|
||||
:members:
|
||||
|
||||
|
||||
Current playlist
|
||||
----------------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.current_playlist
|
||||
:members:
|
||||
|
||||
Music database
|
||||
--------------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.music_db
|
||||
:members:
|
||||
|
||||
|
||||
Playback
|
||||
--------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.playback
|
||||
:members:
|
||||
|
||||
|
||||
Reflection
|
||||
----------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.reflection
|
||||
:members:
|
||||
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.status
|
||||
:members:
|
||||
|
||||
|
||||
Stickers
|
||||
--------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.stickers
|
||||
:members:
|
||||
|
||||
|
||||
Stored playlists
|
||||
----------------
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.protocol.stored_playlists
|
||||
:members:
|
||||
|
||||
@ -22,6 +22,6 @@ Available settings
|
||||
==================
|
||||
|
||||
.. automodule:: mopidy.settings
|
||||
:synopsis: Available settings and their default values.
|
||||
:synopsis: Available settings and their default values
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
@ -17,16 +17,20 @@ Another great release.
|
||||
the packages created by ``setup.py`` for i.e. PyPI.
|
||||
- MPD frontend:
|
||||
|
||||
- Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`.
|
||||
- Split gigantic protocol implementation into eleven modules.
|
||||
- Search improvements, including support for multi-word search.
|
||||
- Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty.
|
||||
|
||||
- Backend API:
|
||||
|
||||
- Relocate from :mod:`mopidy.backends` to :mod:`mopidy.backends.base`.
|
||||
- The ``id`` field of :class:`mopidy.models.Track` has been removed, as it is
|
||||
no longer needed after the CPID refactoring.
|
||||
- :meth:`mopidy.backends.BaseLibraryController.find_exact()` now accepts
|
||||
- :meth:`mopidy.backends.base.BaseLibraryController.find_exact()` now accepts
|
||||
keyword arguments of the form ``find_exact(artist=['foo'],
|
||||
album=['bar'])``.
|
||||
- :meth:`mopidy.backends.BaseLibraryController.search()` now accepts
|
||||
- :meth:`mopidy.backends.base.BaseLibraryController.search()` now accepts
|
||||
keyword arguments of the form ``search(artist=['foo', 'fighters'],
|
||||
album=['bar', 'grooves'])``.
|
||||
|
||||
|
||||
@ -11,8 +11,9 @@ def get_mpd_protocol_version():
|
||||
return u'0.16.0'
|
||||
|
||||
class MopidyException(Exception):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
def __init__(self, message, *args, **kwargs):
|
||||
super(MopidyException, self).__init__(message, *args, **kwargs)
|
||||
self._message = message
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
|
||||
@ -1,837 +0,0 @@
|
||||
from copy import copy
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.models import Playlist
|
||||
from mopidy.mpd import serializer
|
||||
from mopidy.utils import get_class
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
__all__ = ['BaseBackend', 'BasePlaybackController',
|
||||
'BaseCurrentPlaylistController', 'BaseStoredPlaylistsController',
|
||||
'BaseLibraryController']
|
||||
|
||||
class BaseBackend(object):
|
||||
"""
|
||||
:param core_queue: a queue for sending messages to
|
||||
:class:`mopidy.process.CoreProcess`
|
||||
:type core_queue: :class:`multiprocessing.Queue`
|
||||
:param mixer: either a mixer instance, or :class:`None` to use the mixer
|
||||
defined in settings
|
||||
:type mixer: :class:`mopidy.mixers.BaseMixer` or :class:`None`
|
||||
"""
|
||||
|
||||
def __init__(self, core_queue=None, mixer=None):
|
||||
self.core_queue = core_queue
|
||||
if mixer is not None:
|
||||
self.mixer = mixer
|
||||
else:
|
||||
self.mixer = get_class(settings.MIXER)()
|
||||
|
||||
#: A :class:`multiprocessing.Queue` which can be used by e.g. library
|
||||
#: callbacks executing in other threads to send messages to the core
|
||||
#: thread, so that action may be taken in the correct thread.
|
||||
core_queue = None
|
||||
|
||||
#: The current playlist controller. An instance of
|
||||
#: :class:`BaseCurrentPlaylistController`.
|
||||
current_playlist = None
|
||||
|
||||
#: The library controller. An instance of :class:`BaseLibraryController`.
|
||||
library = None
|
||||
|
||||
#: The sound mixer. An instance of :class:`mopidy.mixers.BaseMixer`.
|
||||
mixer = None
|
||||
|
||||
#: The playback controller. An instance of :class:`BasePlaybackController`.
|
||||
playback = None
|
||||
|
||||
#: The stored playlists controller. An instance of
|
||||
#: :class:`BaseStoredPlaylistsController`.
|
||||
stored_playlists = None
|
||||
|
||||
#: List of URI prefixes this backend can handle.
|
||||
uri_handlers = []
|
||||
|
||||
def destroy(self):
|
||||
"""
|
||||
Call destroy on all sub-components in backend so that they can cleanup
|
||||
after themselves.
|
||||
"""
|
||||
|
||||
if self.current_playlist:
|
||||
self.current_playlist.destroy()
|
||||
|
||||
if self.library:
|
||||
self.library.destroy()
|
||||
|
||||
if self.mixer:
|
||||
self.mixer.destroy()
|
||||
|
||||
if self.playback:
|
||||
self.playback.destroy()
|
||||
|
||||
if self.stored_playlists:
|
||||
self.stored_playlists.destroy()
|
||||
|
||||
class BaseCurrentPlaylistController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
#: The current playlist version. Integer which is increased every time the
|
||||
#: current playlist is changed. Is not reset before Mopidy is restarted.
|
||||
version = 0
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self._cp_tracks = []
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup after component."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def cp_tracks(self):
|
||||
"""
|
||||
List of two-tuples of (CPID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
Read-only.
|
||||
"""
|
||||
return [copy(ct) for ct in self._cp_tracks]
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
"""
|
||||
List of :class:`mopidy.models.Track` in the current playlist.
|
||||
|
||||
Read-only.
|
||||
"""
|
||||
return [ct[1] for ct in self._cp_tracks]
|
||||
|
||||
def add(self, track, at_position=None):
|
||||
"""
|
||||
Add the track to the end of, or at the given position in the current
|
||||
playlist.
|
||||
|
||||
:param track: track to add
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:param at_position: position in current playlist to add track
|
||||
:type at_position: int or :class:`None`
|
||||
:rtype: two-tuple of (CPID integer, :class:`mopidy.models.Track`) that
|
||||
was added to the current playlist playlist
|
||||
"""
|
||||
assert at_position <= len(self._cp_tracks), \
|
||||
u'at_position can not be greater than playlist length'
|
||||
cp_track = (self.version, track)
|
||||
if at_position is not None:
|
||||
self._cp_tracks.insert(at_position, cp_track)
|
||||
else:
|
||||
self._cp_tracks.append(cp_track)
|
||||
self.version += 1
|
||||
return cp_track
|
||||
|
||||
def clear(self):
|
||||
"""Clear the current playlist."""
|
||||
self.backend.playback.stop()
|
||||
self.backend.playback.current_cp_track = None
|
||||
self._cp_tracks = []
|
||||
self.version += 1
|
||||
|
||||
def get(self, **criteria):
|
||||
"""
|
||||
Get track by given criterias from current playlist.
|
||||
|
||||
Raises :exc:`LookupError` if a unique match is not found.
|
||||
|
||||
Examples::
|
||||
|
||||
get(cpid=7) # Returns track with CPID 7
|
||||
# (current playlist ID)
|
||||
get(id=1) # Returns track with ID 1
|
||||
get(uri='xyz') # Returns track with URI 'xyz'
|
||||
get(id=1, uri='xyz') # Returns track with ID 1 and URI 'xyz'
|
||||
|
||||
:param criteria: on or more criteria to match by
|
||||
:type criteria: dict
|
||||
:rtype: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
||||
"""
|
||||
matches = self._cp_tracks
|
||||
for (key, value) in criteria.iteritems():
|
||||
if key == 'cpid':
|
||||
matches = filter(lambda ct: ct[0] == value, matches)
|
||||
else:
|
||||
matches = filter(lambda ct: getattr(ct[1], key) == value,
|
||||
matches)
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
criteria_string = ', '.join(
|
||||
['%s=%s' % (k, v) for (k, v) in criteria.iteritems()])
|
||||
if len(matches) == 0:
|
||||
raise LookupError(u'"%s" match no tracks' % criteria_string)
|
||||
else:
|
||||
raise LookupError(u'"%s" match multiple tracks' % criteria_string)
|
||||
|
||||
def load(self, tracks):
|
||||
"""
|
||||
Replace the tracks in the current playlist with the given tracks.
|
||||
|
||||
:param tracks: tracks to load
|
||||
:type tracks: list of :class:`mopidy.models.Track`
|
||||
"""
|
||||
self._cp_tracks = []
|
||||
self.version += 1
|
||||
for track in tracks:
|
||||
self.add(track)
|
||||
self.backend.playback.new_playlist_loaded_callback()
|
||||
|
||||
def move(self, start, end, to_position):
|
||||
"""
|
||||
Move the tracks in the slice ``[start:end]`` to ``to_position``.
|
||||
|
||||
:param start: position of first track to move
|
||||
:type start: int
|
||||
:param end: position after last track to move
|
||||
:type end: int
|
||||
:param to_position: new position for the tracks
|
||||
:type to_position: int
|
||||
"""
|
||||
if start == end:
|
||||
end += 1
|
||||
|
||||
cp_tracks = self._cp_tracks
|
||||
|
||||
assert start < end, 'start must be smaller than end'
|
||||
assert start >= 0, 'start must be at least zero'
|
||||
assert end <= len(cp_tracks), \
|
||||
'end can not be larger than playlist length'
|
||||
assert to_position >= 0, 'to_position must be at least zero'
|
||||
assert to_position <= len(cp_tracks), \
|
||||
'to_position can not be larger than playlist length'
|
||||
|
||||
new_cp_tracks = cp_tracks[:start] + cp_tracks[end:]
|
||||
for cp_track in cp_tracks[start:end]:
|
||||
new_cp_tracks.insert(to_position, cp_track)
|
||||
to_position += 1
|
||||
self._cp_tracks = new_cp_tracks
|
||||
self.version += 1
|
||||
|
||||
def remove(self, **criteria):
|
||||
"""
|
||||
Remove the track from the current playlist.
|
||||
|
||||
Uses :meth:`get()` to lookup the track to remove.
|
||||
|
||||
:param criteria: on or more criteria to match by
|
||||
:type criteria: dict
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
"""
|
||||
cp_track = self.get(**criteria)
|
||||
position = self._cp_tracks.index(cp_track)
|
||||
del self._cp_tracks[position]
|
||||
self.version += 1
|
||||
|
||||
def shuffle(self, start=None, end=None):
|
||||
"""
|
||||
Shuffles the entire playlist. If ``start`` and ``end`` is given only
|
||||
shuffles the slice ``[start:end]``.
|
||||
|
||||
:param start: position of first track to shuffle
|
||||
:type start: int or :class:`None`
|
||||
:param end: position after last track to shuffle
|
||||
:type end: int or :class:`None`
|
||||
"""
|
||||
cp_tracks = self._cp_tracks
|
||||
|
||||
if start is not None and end is not None:
|
||||
assert start < end, 'start must be smaller than end'
|
||||
|
||||
if start is not None:
|
||||
assert start >= 0, 'start must be at least zero'
|
||||
|
||||
if end is not None:
|
||||
assert end <= len(cp_tracks), 'end can not be larger than ' + \
|
||||
'playlist length'
|
||||
|
||||
before = cp_tracks[:start or 0]
|
||||
shuffled = cp_tracks[start:end]
|
||||
after = cp_tracks[end or len(cp_tracks):]
|
||||
random.shuffle(shuffled)
|
||||
self._cp_tracks = before + shuffled + after
|
||||
self.version += 1
|
||||
|
||||
def mpd_format(self, *args, **kwargs):
|
||||
"""Not a part of the generic backend API."""
|
||||
kwargs['cpids'] = [ct[0] for ct in self._cp_tracks]
|
||||
return serializer.tracks_to_mpd_format(self.tracks, *args, **kwargs)
|
||||
|
||||
|
||||
class BaseLibraryController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup after component."""
|
||||
pass
|
||||
|
||||
def find_exact(self, **query):
|
||||
"""
|
||||
Search the library for tracks where ``field`` is ``values``.
|
||||
|
||||
Examples::
|
||||
|
||||
# Returns results matching 'a'
|
||||
find_exact(any=['a'])
|
||||
# Returns results matching artist 'xyz'
|
||||
find_exact(artist=['xyz'])
|
||||
# Returns results matching 'a' and 'b' and artist 'xyz'
|
||||
find_exact(any=['a', 'b'], artist=['xyz'])
|
||||
|
||||
:param query: one or more queries to search for
|
||||
:type query: dict
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
Lookup track with given URI. Returns :class:`None` if not found.
|
||||
|
||||
:param uri: track URI
|
||||
:type uri: string
|
||||
:rtype: :class:`mopidy.models.Track` or :class:`None`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self, uri=None):
|
||||
"""
|
||||
Refresh library. Limit to URI and below if an URI is given.
|
||||
|
||||
:param uri: directory or track URI
|
||||
:type uri: string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def search(self, **query):
|
||||
"""
|
||||
Search the library for tracks where ``field`` contains ``values``.
|
||||
|
||||
Examples::
|
||||
|
||||
# Returns results matching 'a'
|
||||
search(any=['a'])
|
||||
# Returns results matching artist 'xyz'
|
||||
search(artist=['xyz'])
|
||||
# Returns results matching 'a' and 'b' and artist 'xyz'
|
||||
search(any=['a', 'b'], artist=['xyz'])
|
||||
|
||||
:param query: one or more queries to search for
|
||||
:type query: dict
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BasePlaybackController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
#: Constant representing the paused state.
|
||||
PAUSED = u'paused'
|
||||
|
||||
#: Constant representing the playing state.
|
||||
PLAYING = u'playing'
|
||||
|
||||
#: Constant representing the stopped state.
|
||||
STOPPED = u'stopped'
|
||||
|
||||
#: :class:`True`
|
||||
#: Tracks are removed from the playlist when they have been played.
|
||||
#: :class:`False`
|
||||
#: Tracks are not removed from the playlist.
|
||||
consume = False
|
||||
|
||||
#: The currently playing or selected track
|
||||
#:
|
||||
#: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or
|
||||
#: :class:`None`.
|
||||
current_cp_track = None
|
||||
|
||||
#: :class:`True`
|
||||
#: Tracks are selected at random from the playlist.
|
||||
#: :class:`False`
|
||||
#: Tracks are played in the order of the playlist.
|
||||
random = False
|
||||
|
||||
#: :class:`True`
|
||||
#: The current playlist is played repeatedly. To repeat a single track,
|
||||
#: select both :attr:`repeat` and :attr:`single`.
|
||||
#: :class:`False`
|
||||
#: The current playlist is played once.
|
||||
repeat = False
|
||||
|
||||
#: :class:`True`
|
||||
#: Playback is stopped after current song, unless in repeat mode.
|
||||
#: :class:`False`
|
||||
#: Playback continues after current song.
|
||||
single = False
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self._state = self.STOPPED
|
||||
self._shuffled = []
|
||||
self._first_shuffle = True
|
||||
self._play_time_accumulated = 0
|
||||
self._play_time_started = None
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup after component."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def current_cpid(self):
|
||||
"""
|
||||
The CPID (current playlist ID) of :attr:`current_track`.
|
||||
|
||||
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
||||
"""
|
||||
if self.current_cp_track is None:
|
||||
return None
|
||||
return self.current_cp_track[0]
|
||||
|
||||
@property
|
||||
def current_track(self):
|
||||
"""
|
||||
The currently playing or selected :class:`mopidy.models.Track`.
|
||||
|
||||
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
||||
"""
|
||||
if self.current_cp_track is None:
|
||||
return None
|
||||
return self.current_cp_track[1]
|
||||
|
||||
@property
|
||||
def current_playlist_position(self):
|
||||
"""The position of the current track in the current playlist."""
|
||||
if self.current_cp_track is None:
|
||||
return None
|
||||
try:
|
||||
return self.backend.current_playlist.cp_tracks.index(
|
||||
self.current_cp_track)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def next_track(self):
|
||||
"""
|
||||
The next track in the playlist.
|
||||
|
||||
A :class:`mopidy.models.Track` extracted from :attr:`next_cp_track` for
|
||||
convenience.
|
||||
"""
|
||||
next_cp_track = self.next_cp_track
|
||||
if next_cp_track is None:
|
||||
return None
|
||||
return next_cp_track[1]
|
||||
|
||||
@property
|
||||
def next_cp_track(self):
|
||||
"""
|
||||
The next track in the playlist.
|
||||
|
||||
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
For normal playback this is the next track in the playlist. If repeat
|
||||
is enabled the next track can loop around the playlist. When random is
|
||||
enabled this should be a random track, all tracks should be played once
|
||||
before the list repeats.
|
||||
"""
|
||||
cp_tracks = self.backend.current_playlist.cp_tracks
|
||||
|
||||
if not cp_tracks:
|
||||
return None
|
||||
|
||||
if self.random and not self._shuffled:
|
||||
if self.repeat or self._first_shuffle:
|
||||
logger.debug('Shuffling tracks')
|
||||
self._shuffled = cp_tracks
|
||||
random.shuffle(self._shuffled)
|
||||
self._first_shuffle = False
|
||||
|
||||
if self._shuffled:
|
||||
return self._shuffled[0]
|
||||
|
||||
if self.current_cp_track is None:
|
||||
return cp_tracks[0]
|
||||
|
||||
if self.repeat:
|
||||
return cp_tracks[
|
||||
(self.current_playlist_position + 1) % len(cp_tracks)]
|
||||
|
||||
try:
|
||||
return cp_tracks[self.current_playlist_position + 1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def previous_track(self):
|
||||
"""
|
||||
The previous track in the playlist.
|
||||
|
||||
A :class:`mopidy.models.Track` extracted from :attr:`previous_cp_track`
|
||||
for convenience.
|
||||
"""
|
||||
previous_cp_track = self.previous_cp_track
|
||||
if previous_cp_track is None:
|
||||
return None
|
||||
return previous_cp_track[1]
|
||||
|
||||
@property
|
||||
def previous_cp_track(self):
|
||||
"""
|
||||
The previous track in the playlist.
|
||||
|
||||
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
For normal playback this is the previous track in the playlist. If
|
||||
random and/or consume is enabled it should return the current track
|
||||
instead.
|
||||
"""
|
||||
if self.repeat or self.consume or self.random:
|
||||
return self.current_cp_track
|
||||
|
||||
if self.current_cp_track is None or self.current_playlist_position == 0:
|
||||
return None
|
||||
|
||||
return self.backend.current_playlist.cp_tracks[
|
||||
self.current_playlist_position - 1]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""
|
||||
The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or
|
||||
:attr:`STOPPED`.
|
||||
|
||||
Possible states and transitions:
|
||||
|
||||
.. digraph:: state_transitions
|
||||
|
||||
"STOPPED" -> "PLAYING" [ label="play" ]
|
||||
"PLAYING" -> "STOPPED" [ label="stop" ]
|
||||
"PLAYING" -> "PAUSED" [ label="pause" ]
|
||||
"PLAYING" -> "PLAYING" [ label="play" ]
|
||||
"PAUSED" -> "PLAYING" [ label="resume" ]
|
||||
"PAUSED" -> "STOPPED" [ label="stop" ]
|
||||
"""
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, new_state):
|
||||
(old_state, self._state) = (self.state, new_state)
|
||||
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
|
||||
# FIXME _play_time stuff assumes backend does not have a better way of
|
||||
# handeling this stuff :/
|
||||
if (old_state in (self.PLAYING, self.STOPPED)
|
||||
and new_state == self.PLAYING):
|
||||
self._play_time_start()
|
||||
elif old_state == self.PLAYING and new_state == self.PAUSED:
|
||||
self._play_time_pause()
|
||||
elif old_state == self.PAUSED and new_state == self.PLAYING:
|
||||
self._play_time_resume()
|
||||
|
||||
@property
|
||||
def time_position(self):
|
||||
"""Time position in milliseconds."""
|
||||
if self.state == self.PLAYING:
|
||||
time_since_started = (self._current_wall_time -
|
||||
self._play_time_started)
|
||||
return self._play_time_accumulated + time_since_started
|
||||
elif self.state == self.PAUSED:
|
||||
return self._play_time_accumulated
|
||||
elif self.state == self.STOPPED:
|
||||
return 0
|
||||
|
||||
def _play_time_start(self):
|
||||
self._play_time_accumulated = 0
|
||||
self._play_time_started = self._current_wall_time
|
||||
|
||||
def _play_time_pause(self):
|
||||
time_since_started = self._current_wall_time - self._play_time_started
|
||||
self._play_time_accumulated += time_since_started
|
||||
|
||||
def _play_time_resume(self):
|
||||
self._play_time_started = self._current_wall_time
|
||||
|
||||
@property
|
||||
def _current_wall_time(self):
|
||||
return int(time.time() * 1000)
|
||||
|
||||
def end_of_track_callback(self):
|
||||
"""
|
||||
Tell the playback controller that end of track is reached.
|
||||
|
||||
Typically called by :class:`mopidy.process.CoreProcess` after a message
|
||||
from a library thread is received.
|
||||
"""
|
||||
if self.next_cp_track is not None:
|
||||
self.next()
|
||||
else:
|
||||
self.stop()
|
||||
self.current_cp_track = None
|
||||
|
||||
def new_playlist_loaded_callback(self):
|
||||
"""
|
||||
Tell the playback controller that a new playlist has been loaded.
|
||||
|
||||
Typically called by :class:`mopidy.process.CoreProcess` after a message
|
||||
from a library thread is received.
|
||||
"""
|
||||
self.current_cp_track = None
|
||||
self._first_shuffle = True
|
||||
self._shuffled = []
|
||||
|
||||
if self.state == self.PLAYING:
|
||||
if len(self.backend.current_playlist.tracks) > 0:
|
||||
self.play()
|
||||
else:
|
||||
self.stop()
|
||||
elif self.state == self.PAUSED:
|
||||
self.stop()
|
||||
|
||||
def next(self):
|
||||
"""Play the next track."""
|
||||
original_cp_track = self.current_cp_track
|
||||
|
||||
if self.state == self.STOPPED:
|
||||
return
|
||||
elif self.next_cp_track is not None and self._next(self.next_track):
|
||||
self.current_cp_track = self.next_cp_track
|
||||
self.state = self.PLAYING
|
||||
elif self.next_cp_track is None:
|
||||
self.stop()
|
||||
self.current_cp_track = None
|
||||
|
||||
# FIXME handle in play aswell?
|
||||
if self.consume:
|
||||
self.backend.current_playlist.remove(cpid=original_cp_track[0])
|
||||
|
||||
if self.random and self.current_cp_track in self._shuffled:
|
||||
self._shuffled.remove(self.current_cp_track)
|
||||
|
||||
def _next(self, track):
|
||||
return self._play(track)
|
||||
|
||||
def pause(self):
|
||||
"""Pause playback."""
|
||||
if self.state == self.PLAYING and self._pause():
|
||||
self.state = self.PAUSED
|
||||
|
||||
def _pause(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def play(self, cp_track=None):
|
||||
"""
|
||||
Play the given track or the currently active track.
|
||||
|
||||
:param cp_track: track to play
|
||||
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
||||
or :class:`None`
|
||||
"""
|
||||
|
||||
if cp_track is not None:
|
||||
assert cp_track in self.backend.current_playlist.cp_tracks
|
||||
elif not self.current_cp_track:
|
||||
cp_track = self.next_cp_track
|
||||
|
||||
if self.state == self.PAUSED and cp_track is None:
|
||||
self.resume()
|
||||
elif cp_track is not None and self._play(cp_track[1]):
|
||||
self.current_cp_track = cp_track
|
||||
self.state = self.PLAYING
|
||||
|
||||
# TODO Do something sensible when _play() returns False, like calling
|
||||
# next(). Adding this todo instead of just implementing it as I want a
|
||||
# test case first.
|
||||
|
||||
if self.random and self.current_cp_track in self._shuffled:
|
||||
self._shuffled.remove(self.current_cp_track)
|
||||
|
||||
def _play(self, track):
|
||||
raise NotImplementedError
|
||||
|
||||
def previous(self):
|
||||
"""Play the previous track."""
|
||||
if (self.previous_cp_track is not None
|
||||
and self.state != self.STOPPED
|
||||
and self._previous(self.previous_track)):
|
||||
self.current_cp_track = self.previous_cp_track
|
||||
self.state = self.PLAYING
|
||||
|
||||
def _previous(self, track):
|
||||
return self._play(track)
|
||||
|
||||
def resume(self):
|
||||
"""If paused, resume playing the current track."""
|
||||
if self.state == self.PAUSED and self._resume():
|
||||
self.state = self.PLAYING
|
||||
|
||||
def _resume(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def seek(self, time_position):
|
||||
"""
|
||||
Seeks to time position given in milliseconds.
|
||||
|
||||
:param time_position: time position in milliseconds
|
||||
:type time_position: int
|
||||
"""
|
||||
if self.state == self.STOPPED:
|
||||
self.play()
|
||||
elif self.state == self.PAUSED:
|
||||
self.resume()
|
||||
|
||||
if time_position < 0:
|
||||
time_position = 0
|
||||
elif self.current_track and time_position > self.current_track.length:
|
||||
self.next()
|
||||
return
|
||||
|
||||
self._seek(time_position)
|
||||
|
||||
def _seek(self, time_position):
|
||||
raise NotImplementedError
|
||||
|
||||
def stop(self):
|
||||
"""Stop playing."""
|
||||
if self.state != self.STOPPED and self._stop():
|
||||
self.state = self.STOPPED
|
||||
|
||||
def _stop(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BaseStoredPlaylistsController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self._playlists = []
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup after component."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def playlists(self):
|
||||
"""List of :class:`mopidy.models.Playlist`."""
|
||||
return copy(self._playlists)
|
||||
|
||||
@playlists.setter
|
||||
def playlists(self, playlists):
|
||||
self._playlists = playlists
|
||||
|
||||
def create(self, name):
|
||||
"""
|
||||
Create a new playlist.
|
||||
|
||||
:param name: name of the new playlist
|
||||
:type name: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, playlist):
|
||||
"""
|
||||
Delete playlist.
|
||||
|
||||
:param playlist: the playlist to delete
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get(self, **criteria):
|
||||
"""
|
||||
Get playlist by given criterias from the set of stored playlists.
|
||||
|
||||
Raises :exc:`LookupError` if a unique match is not found.
|
||||
|
||||
Examples::
|
||||
|
||||
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'
|
||||
|
||||
:param criteria: one or more criteria to match by
|
||||
:type criteria: dict
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
matches = self._playlists
|
||||
for (key, value) in criteria.iteritems():
|
||||
matches = filter(lambda p: getattr(p, key) == value, matches)
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
criteria_string = ', '.join(
|
||||
['%s=%s' % (k, v) for (k, v) in criteria.iteritems()])
|
||||
if len(matches) == 0:
|
||||
raise LookupError('"%s" match no playlists' % criteria_string)
|
||||
else:
|
||||
raise LookupError('"%s" match multiple playlists' % criteria_string)
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
Lookup playlist with given URI in both the set of stored playlists and
|
||||
in any other playlist sources.
|
||||
|
||||
:param uri: playlist URI
|
||||
:type uri: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh stored playlists."""
|
||||
raise NotImplementedError
|
||||
|
||||
def rename(self, playlist, new_name):
|
||||
"""
|
||||
Rename playlist.
|
||||
|
||||
:param playlist: the playlist
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
:param new_name: the new name
|
||||
:type new_name: string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, playlist):
|
||||
"""
|
||||
Save the playlist to the set of stored playlists.
|
||||
|
||||
:param playlist: the playlist
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def search(self, query):
|
||||
"""
|
||||
Search for playlists whose name contains ``query``.
|
||||
|
||||
:param query: query to search for
|
||||
:type query: string
|
||||
:rtype: list of :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
return filter(lambda p: query in p.name, self._playlists)
|
||||
84
mopidy/backends/base/__init__.py
Normal file
84
mopidy/backends/base/__init__.py
Normal file
@ -0,0 +1,84 @@
|
||||
from copy import copy
|
||||
import logging
|
||||
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 serializer
|
||||
from mopidy.models import Playlist
|
||||
from mopidy.utils import get_class
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
__all__ = ['BaseBackend', 'BasePlaybackController',
|
||||
'BaseCurrentPlaylistController', 'BaseStoredPlaylistsController',
|
||||
'BaseLibraryController']
|
||||
|
||||
class BaseBackend(object):
|
||||
"""
|
||||
:param core_queue: a queue for sending messages to
|
||||
:class:`mopidy.process.CoreProcess`
|
||||
:type core_queue: :class:`multiprocessing.Queue`
|
||||
:param mixer: either a mixer instance, or :class:`None` to use the mixer
|
||||
defined in settings
|
||||
:type mixer: :class:`mopidy.mixers.BaseMixer` or :class:`None`
|
||||
"""
|
||||
|
||||
def __init__(self, core_queue=None, mixer=None):
|
||||
self.core_queue = core_queue
|
||||
if mixer is not None:
|
||||
self.mixer = mixer
|
||||
else:
|
||||
self.mixer = get_class(settings.MIXER)()
|
||||
|
||||
#: A :class:`multiprocessing.Queue` which can be used by e.g. library
|
||||
#: callbacks executing in other threads to send messages to the core
|
||||
#: thread, so that action may be taken in the correct thread.
|
||||
core_queue = None
|
||||
|
||||
#: The current playlist controller. An instance of
|
||||
#: :class:`mopidy.backends.base.BaseCurrentPlaylistController`.
|
||||
current_playlist = None
|
||||
|
||||
#: The library controller. An instance of
|
||||
# :class:`mopidy.backends.base.BaseLibraryController`.
|
||||
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`.
|
||||
playback = None
|
||||
|
||||
#: The stored playlists controller. An instance of
|
||||
#: :class:`mopidy.backends.base.BaseStoredPlaylistsController`.
|
||||
stored_playlists = None
|
||||
|
||||
#: List of URI prefixes this backend can handle.
|
||||
uri_handlers = []
|
||||
|
||||
def destroy(self):
|
||||
"""
|
||||
Call destroy on all sub-components in backend so that they can cleanup
|
||||
after themselves.
|
||||
"""
|
||||
|
||||
if self.current_playlist:
|
||||
self.current_playlist.destroy()
|
||||
|
||||
if self.library:
|
||||
self.library.destroy()
|
||||
|
||||
if self.mixer:
|
||||
self.mixer.destroy()
|
||||
|
||||
if self.playback:
|
||||
self.playback.destroy()
|
||||
|
||||
if self.stored_playlists:
|
||||
self.stored_playlists.destroy()
|
||||
199
mopidy/backends/base/current_playlist.py
Normal file
199
mopidy/backends/base/current_playlist.py
Normal file
@ -0,0 +1,199 @@
|
||||
from copy import copy
|
||||
import logging
|
||||
import random
|
||||
|
||||
from mopidy.frontends.mpd import serializer
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
class BaseCurrentPlaylistController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
#: The current playlist version. Integer which is increased every time the
|
||||
#: current playlist is changed. Is not reset before Mopidy is restarted.
|
||||
version = 0
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self._cp_tracks = []
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup after component."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def cp_tracks(self):
|
||||
"""
|
||||
List of two-tuples of (CPID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
Read-only.
|
||||
"""
|
||||
return [copy(ct) for ct in self._cp_tracks]
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
"""
|
||||
List of :class:`mopidy.models.Track` in the current playlist.
|
||||
|
||||
Read-only.
|
||||
"""
|
||||
return [ct[1] for ct in self._cp_tracks]
|
||||
|
||||
def add(self, track, at_position=None):
|
||||
"""
|
||||
Add the track to the end of, or at the given position in the current
|
||||
playlist.
|
||||
|
||||
:param track: track to add
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:param at_position: position in current playlist to add track
|
||||
:type at_position: int or :class:`None`
|
||||
:rtype: two-tuple of (CPID integer, :class:`mopidy.models.Track`) that
|
||||
was added to the current playlist playlist
|
||||
"""
|
||||
assert at_position <= len(self._cp_tracks), \
|
||||
u'at_position can not be greater than playlist length'
|
||||
cp_track = (self.version, track)
|
||||
if at_position is not None:
|
||||
self._cp_tracks.insert(at_position, cp_track)
|
||||
else:
|
||||
self._cp_tracks.append(cp_track)
|
||||
self.version += 1
|
||||
return cp_track
|
||||
|
||||
def clear(self):
|
||||
"""Clear the current playlist."""
|
||||
self.backend.playback.stop()
|
||||
self.backend.playback.current_cp_track = None
|
||||
self._cp_tracks = []
|
||||
self.version += 1
|
||||
|
||||
def get(self, **criteria):
|
||||
"""
|
||||
Get track by given criterias from current playlist.
|
||||
|
||||
Raises :exc:`LookupError` if a unique match is not found.
|
||||
|
||||
Examples::
|
||||
|
||||
get(cpid=7) # Returns track with CPID 7
|
||||
# (current playlist ID)
|
||||
get(id=1) # Returns track with ID 1
|
||||
get(uri='xyz') # Returns track with URI 'xyz'
|
||||
get(id=1, uri='xyz') # Returns track with ID 1 and URI 'xyz'
|
||||
|
||||
:param criteria: on or more criteria to match by
|
||||
:type criteria: dict
|
||||
:rtype: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
||||
"""
|
||||
matches = self._cp_tracks
|
||||
for (key, value) in criteria.iteritems():
|
||||
if key == 'cpid':
|
||||
matches = filter(lambda ct: ct[0] == value, matches)
|
||||
else:
|
||||
matches = filter(lambda ct: getattr(ct[1], key) == value,
|
||||
matches)
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
criteria_string = ', '.join(
|
||||
['%s=%s' % (k, v) for (k, v) in criteria.iteritems()])
|
||||
if len(matches) == 0:
|
||||
raise LookupError(u'"%s" match no tracks' % criteria_string)
|
||||
else:
|
||||
raise LookupError(u'"%s" match multiple tracks' % criteria_string)
|
||||
|
||||
def load(self, tracks):
|
||||
"""
|
||||
Replace the tracks in the current playlist with the given tracks.
|
||||
|
||||
:param tracks: tracks to load
|
||||
:type tracks: list of :class:`mopidy.models.Track`
|
||||
"""
|
||||
self._cp_tracks = []
|
||||
self.version += 1
|
||||
for track in tracks:
|
||||
self.add(track)
|
||||
self.backend.playback.new_playlist_loaded_callback()
|
||||
|
||||
def move(self, start, end, to_position):
|
||||
"""
|
||||
Move the tracks in the slice ``[start:end]`` to ``to_position``.
|
||||
|
||||
:param start: position of first track to move
|
||||
:type start: int
|
||||
:param end: position after last track to move
|
||||
:type end: int
|
||||
:param to_position: new position for the tracks
|
||||
:type to_position: int
|
||||
"""
|
||||
if start == end:
|
||||
end += 1
|
||||
|
||||
cp_tracks = self._cp_tracks
|
||||
|
||||
assert start < end, 'start must be smaller than end'
|
||||
assert start >= 0, 'start must be at least zero'
|
||||
assert end <= len(cp_tracks), \
|
||||
'end can not be larger than playlist length'
|
||||
assert to_position >= 0, 'to_position must be at least zero'
|
||||
assert to_position <= len(cp_tracks), \
|
||||
'to_position can not be larger than playlist length'
|
||||
|
||||
new_cp_tracks = cp_tracks[:start] + cp_tracks[end:]
|
||||
for cp_track in cp_tracks[start:end]:
|
||||
new_cp_tracks.insert(to_position, cp_track)
|
||||
to_position += 1
|
||||
self._cp_tracks = new_cp_tracks
|
||||
self.version += 1
|
||||
|
||||
def remove(self, **criteria):
|
||||
"""
|
||||
Remove the track from the current playlist.
|
||||
|
||||
Uses :meth:`get()` to lookup the track to remove.
|
||||
|
||||
:param criteria: on or more criteria to match by
|
||||
:type criteria: dict
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
"""
|
||||
cp_track = self.get(**criteria)
|
||||
position = self._cp_tracks.index(cp_track)
|
||||
del self._cp_tracks[position]
|
||||
self.version += 1
|
||||
|
||||
def shuffle(self, start=None, end=None):
|
||||
"""
|
||||
Shuffles the entire playlist. If ``start`` and ``end`` is given only
|
||||
shuffles the slice ``[start:end]``.
|
||||
|
||||
:param start: position of first track to shuffle
|
||||
:type start: int or :class:`None`
|
||||
:param end: position after last track to shuffle
|
||||
:type end: int or :class:`None`
|
||||
"""
|
||||
cp_tracks = self._cp_tracks
|
||||
|
||||
if start is not None and end is not None:
|
||||
assert start < end, 'start must be smaller than end'
|
||||
|
||||
if start is not None:
|
||||
assert start >= 0, 'start must be at least zero'
|
||||
|
||||
if end is not None:
|
||||
assert end <= len(cp_tracks), 'end can not be larger than ' + \
|
||||
'playlist length'
|
||||
|
||||
before = cp_tracks[:start or 0]
|
||||
shuffled = cp_tracks[start:end]
|
||||
after = cp_tracks[end or len(cp_tracks):]
|
||||
random.shuffle(shuffled)
|
||||
self._cp_tracks = before + shuffled + after
|
||||
self.version += 1
|
||||
|
||||
def mpd_format(self, *args, **kwargs):
|
||||
"""Not a part of the generic backend API."""
|
||||
kwargs['cpids'] = [ct[0] for ct in self._cp_tracks]
|
||||
return serializer.tracks_to_mpd_format(self.tracks, *args, **kwargs)
|
||||
73
mopidy/backends/base/library.py
Normal file
73
mopidy/backends/base/library.py
Normal file
@ -0,0 +1,73 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
class BaseLibraryController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup after component."""
|
||||
pass
|
||||
|
||||
def find_exact(self, **query):
|
||||
"""
|
||||
Search the library for tracks where ``field`` is ``values``.
|
||||
|
||||
Examples::
|
||||
|
||||
# Returns results matching 'a'
|
||||
find_exact(any=['a'])
|
||||
# Returns results matching artist 'xyz'
|
||||
find_exact(artist=['xyz'])
|
||||
# Returns results matching 'a' and 'b' and artist 'xyz'
|
||||
find_exact(any=['a', 'b'], artist=['xyz'])
|
||||
|
||||
:param query: one or more queries to search for
|
||||
:type query: dict
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
Lookup track with given URI. Returns :class:`None` if not found.
|
||||
|
||||
:param uri: track URI
|
||||
:type uri: string
|
||||
:rtype: :class:`mopidy.models.Track` or :class:`None`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self, uri=None):
|
||||
"""
|
||||
Refresh library. Limit to URI and below if an URI is given.
|
||||
|
||||
:param uri: directory or track URI
|
||||
:type uri: string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def search(self, **query):
|
||||
"""
|
||||
Search the library for tracks where ``field`` contains ``values``.
|
||||
|
||||
Examples::
|
||||
|
||||
# Returns results matching 'a'
|
||||
search(any=['a'])
|
||||
# Returns results matching artist 'xyz'
|
||||
search(artist=['xyz'])
|
||||
# Returns results matching 'a' and 'b' and artist 'xyz'
|
||||
search(any=['a', 'b'], artist=['xyz'])
|
||||
|
||||
:param query: one or more queries to search for
|
||||
:type query: dict
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
384
mopidy/backends/base/playback.py
Normal file
384
mopidy/backends/base/playback.py
Normal file
@ -0,0 +1,384 @@
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
class BasePlaybackController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
#: Constant representing the paused state.
|
||||
PAUSED = u'paused'
|
||||
|
||||
#: Constant representing the playing state.
|
||||
PLAYING = u'playing'
|
||||
|
||||
#: Constant representing the stopped state.
|
||||
STOPPED = u'stopped'
|
||||
|
||||
#: :class:`True`
|
||||
#: Tracks are removed from the playlist when they have been played.
|
||||
#: :class:`False`
|
||||
#: Tracks are not removed from the playlist.
|
||||
consume = False
|
||||
|
||||
#: The currently playing or selected track
|
||||
#:
|
||||
#: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or
|
||||
#: :class:`None`.
|
||||
current_cp_track = None
|
||||
|
||||
#: :class:`True`
|
||||
#: Tracks are selected at random from the playlist.
|
||||
#: :class:`False`
|
||||
#: Tracks are played in the order of the playlist.
|
||||
random = False
|
||||
|
||||
#: :class:`True`
|
||||
#: The current playlist is played repeatedly. To repeat a single track,
|
||||
#: select both :attr:`repeat` and :attr:`single`.
|
||||
#: :class:`False`
|
||||
#: The current playlist is played once.
|
||||
repeat = False
|
||||
|
||||
#: :class:`True`
|
||||
#: Playback is stopped after current song, unless in repeat mode.
|
||||
#: :class:`False`
|
||||
#: Playback continues after current song.
|
||||
single = False
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self._state = self.STOPPED
|
||||
self._shuffled = []
|
||||
self._first_shuffle = True
|
||||
self._play_time_accumulated = 0
|
||||
self._play_time_started = None
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup after component."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def current_cpid(self):
|
||||
"""
|
||||
The CPID (current playlist ID) of :attr:`current_track`.
|
||||
|
||||
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
||||
"""
|
||||
if self.current_cp_track is None:
|
||||
return None
|
||||
return self.current_cp_track[0]
|
||||
|
||||
@property
|
||||
def current_track(self):
|
||||
"""
|
||||
The currently playing or selected :class:`mopidy.models.Track`.
|
||||
|
||||
Read-only. Extracted from :attr:`current_cp_track` for convenience.
|
||||
"""
|
||||
if self.current_cp_track is None:
|
||||
return None
|
||||
return self.current_cp_track[1]
|
||||
|
||||
@property
|
||||
def current_playlist_position(self):
|
||||
"""The position of the current track in the current playlist."""
|
||||
if self.current_cp_track is None:
|
||||
return None
|
||||
try:
|
||||
return self.backend.current_playlist.cp_tracks.index(
|
||||
self.current_cp_track)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def next_track(self):
|
||||
"""
|
||||
The next track in the playlist.
|
||||
|
||||
A :class:`mopidy.models.Track` extracted from :attr:`next_cp_track` for
|
||||
convenience.
|
||||
"""
|
||||
next_cp_track = self.next_cp_track
|
||||
if next_cp_track is None:
|
||||
return None
|
||||
return next_cp_track[1]
|
||||
|
||||
@property
|
||||
def next_cp_track(self):
|
||||
"""
|
||||
The next track in the playlist.
|
||||
|
||||
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
For normal playback this is the next track in the playlist. If repeat
|
||||
is enabled the next track can loop around the playlist. When random is
|
||||
enabled this should be a random track, all tracks should be played once
|
||||
before the list repeats.
|
||||
"""
|
||||
cp_tracks = self.backend.current_playlist.cp_tracks
|
||||
|
||||
if not cp_tracks:
|
||||
return None
|
||||
|
||||
if self.random and not self._shuffled:
|
||||
if self.repeat or self._first_shuffle:
|
||||
logger.debug('Shuffling tracks')
|
||||
self._shuffled = cp_tracks
|
||||
random.shuffle(self._shuffled)
|
||||
self._first_shuffle = False
|
||||
|
||||
if self._shuffled:
|
||||
return self._shuffled[0]
|
||||
|
||||
if self.current_cp_track is None:
|
||||
return cp_tracks[0]
|
||||
|
||||
if self.repeat:
|
||||
return cp_tracks[
|
||||
(self.current_playlist_position + 1) % len(cp_tracks)]
|
||||
|
||||
try:
|
||||
return cp_tracks[self.current_playlist_position + 1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def previous_track(self):
|
||||
"""
|
||||
The previous track in the playlist.
|
||||
|
||||
A :class:`mopidy.models.Track` extracted from :attr:`previous_cp_track`
|
||||
for convenience.
|
||||
"""
|
||||
previous_cp_track = self.previous_cp_track
|
||||
if previous_cp_track is None:
|
||||
return None
|
||||
return previous_cp_track[1]
|
||||
|
||||
@property
|
||||
def previous_cp_track(self):
|
||||
"""
|
||||
The previous track in the playlist.
|
||||
|
||||
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
|
||||
|
||||
For normal playback this is the previous track in the playlist. If
|
||||
random and/or consume is enabled it should return the current track
|
||||
instead.
|
||||
"""
|
||||
if self.repeat or self.consume or self.random:
|
||||
return self.current_cp_track
|
||||
|
||||
if self.current_cp_track is None or self.current_playlist_position == 0:
|
||||
return None
|
||||
|
||||
return self.backend.current_playlist.cp_tracks[
|
||||
self.current_playlist_position - 1]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""
|
||||
The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or
|
||||
:attr:`STOPPED`.
|
||||
|
||||
Possible states and transitions:
|
||||
|
||||
.. digraph:: state_transitions
|
||||
|
||||
"STOPPED" -> "PLAYING" [ label="play" ]
|
||||
"PLAYING" -> "STOPPED" [ label="stop" ]
|
||||
"PLAYING" -> "PAUSED" [ label="pause" ]
|
||||
"PLAYING" -> "PLAYING" [ label="play" ]
|
||||
"PAUSED" -> "PLAYING" [ label="resume" ]
|
||||
"PAUSED" -> "STOPPED" [ label="stop" ]
|
||||
"""
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, new_state):
|
||||
(old_state, self._state) = (self.state, new_state)
|
||||
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
|
||||
# FIXME _play_time stuff assumes backend does not have a better way of
|
||||
# handeling this stuff :/
|
||||
if (old_state in (self.PLAYING, self.STOPPED)
|
||||
and new_state == self.PLAYING):
|
||||
self._play_time_start()
|
||||
elif old_state == self.PLAYING and new_state == self.PAUSED:
|
||||
self._play_time_pause()
|
||||
elif old_state == self.PAUSED and new_state == self.PLAYING:
|
||||
self._play_time_resume()
|
||||
|
||||
@property
|
||||
def time_position(self):
|
||||
"""Time position in milliseconds."""
|
||||
if self.state == self.PLAYING:
|
||||
time_since_started = (self._current_wall_time -
|
||||
self._play_time_started)
|
||||
return self._play_time_accumulated + time_since_started
|
||||
elif self.state == self.PAUSED:
|
||||
return self._play_time_accumulated
|
||||
elif self.state == self.STOPPED:
|
||||
return 0
|
||||
|
||||
def _play_time_start(self):
|
||||
self._play_time_accumulated = 0
|
||||
self._play_time_started = self._current_wall_time
|
||||
|
||||
def _play_time_pause(self):
|
||||
time_since_started = self._current_wall_time - self._play_time_started
|
||||
self._play_time_accumulated += time_since_started
|
||||
|
||||
def _play_time_resume(self):
|
||||
self._play_time_started = self._current_wall_time
|
||||
|
||||
@property
|
||||
def _current_wall_time(self):
|
||||
return int(time.time() * 1000)
|
||||
|
||||
def end_of_track_callback(self):
|
||||
"""
|
||||
Tell the playback controller that end of track is reached.
|
||||
|
||||
Typically called by :class:`mopidy.process.CoreProcess` after a message
|
||||
from a library thread is received.
|
||||
"""
|
||||
if self.next_cp_track is not None:
|
||||
self.next()
|
||||
else:
|
||||
self.stop()
|
||||
self.current_cp_track = None
|
||||
|
||||
def new_playlist_loaded_callback(self):
|
||||
"""
|
||||
Tell the playback controller that a new playlist has been loaded.
|
||||
|
||||
Typically called by :class:`mopidy.process.CoreProcess` after a message
|
||||
from a library thread is received.
|
||||
"""
|
||||
self.current_cp_track = None
|
||||
self._first_shuffle = True
|
||||
self._shuffled = []
|
||||
|
||||
if self.state == self.PLAYING:
|
||||
if len(self.backend.current_playlist.tracks) > 0:
|
||||
self.play()
|
||||
else:
|
||||
self.stop()
|
||||
elif self.state == self.PAUSED:
|
||||
self.stop()
|
||||
|
||||
def next(self):
|
||||
"""Play the next track."""
|
||||
original_cp_track = self.current_cp_track
|
||||
|
||||
if self.state == self.STOPPED:
|
||||
return
|
||||
elif self.next_cp_track is not None and self._next(self.next_track):
|
||||
self.current_cp_track = self.next_cp_track
|
||||
self.state = self.PLAYING
|
||||
elif self.next_cp_track is None:
|
||||
self.stop()
|
||||
self.current_cp_track = None
|
||||
|
||||
# FIXME handle in play aswell?
|
||||
if self.consume:
|
||||
self.backend.current_playlist.remove(cpid=original_cp_track[0])
|
||||
|
||||
if self.random and self.current_cp_track in self._shuffled:
|
||||
self._shuffled.remove(self.current_cp_track)
|
||||
|
||||
def _next(self, track):
|
||||
return self._play(track)
|
||||
|
||||
def pause(self):
|
||||
"""Pause playback."""
|
||||
if self.state == self.PLAYING and self._pause():
|
||||
self.state = self.PAUSED
|
||||
|
||||
def _pause(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def play(self, cp_track=None):
|
||||
"""
|
||||
Play the given track or the currently active track.
|
||||
|
||||
:param cp_track: track to play
|
||||
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
|
||||
or :class:`None`
|
||||
"""
|
||||
|
||||
if cp_track is not None:
|
||||
assert cp_track in self.backend.current_playlist.cp_tracks
|
||||
elif not self.current_cp_track:
|
||||
cp_track = self.next_cp_track
|
||||
|
||||
if self.state == self.PAUSED and cp_track is None:
|
||||
self.resume()
|
||||
elif cp_track is not None and self._play(cp_track[1]):
|
||||
self.current_cp_track = cp_track
|
||||
self.state = self.PLAYING
|
||||
|
||||
# TODO Do something sensible when _play() returns False, like calling
|
||||
# next(). Adding this todo instead of just implementing it as I want a
|
||||
# test case first.
|
||||
|
||||
if self.random and self.current_cp_track in self._shuffled:
|
||||
self._shuffled.remove(self.current_cp_track)
|
||||
|
||||
def _play(self, track):
|
||||
raise NotImplementedError
|
||||
|
||||
def previous(self):
|
||||
"""Play the previous track."""
|
||||
if (self.previous_cp_track is not None
|
||||
and self.state != self.STOPPED
|
||||
and self._previous(self.previous_track)):
|
||||
self.current_cp_track = self.previous_cp_track
|
||||
self.state = self.PLAYING
|
||||
|
||||
def _previous(self, track):
|
||||
return self._play(track)
|
||||
|
||||
def resume(self):
|
||||
"""If paused, resume playing the current track."""
|
||||
if self.state == self.PAUSED and self._resume():
|
||||
self.state = self.PLAYING
|
||||
|
||||
def _resume(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def seek(self, time_position):
|
||||
"""
|
||||
Seeks to time position given in milliseconds.
|
||||
|
||||
:param time_position: time position in milliseconds
|
||||
:type time_position: int
|
||||
"""
|
||||
if self.state == self.STOPPED:
|
||||
self.play()
|
||||
elif self.state == self.PAUSED:
|
||||
self.resume()
|
||||
|
||||
if time_position < 0:
|
||||
time_position = 0
|
||||
elif self.current_track and time_position > self.current_track.length:
|
||||
self.next()
|
||||
return
|
||||
|
||||
self._seek(time_position)
|
||||
|
||||
def _seek(self, time_position):
|
||||
raise NotImplementedError
|
||||
|
||||
def stop(self):
|
||||
"""Stop playing."""
|
||||
if self.state != self.STOPPED and self._stop():
|
||||
self.state = self.STOPPED
|
||||
|
||||
def _stop(self):
|
||||
raise NotImplementedError
|
||||
119
mopidy/backends/base/stored_playlists.py
Normal file
119
mopidy/backends/base/stored_playlists.py
Normal file
@ -0,0 +1,119 @@
|
||||
from copy import copy
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
class BaseStoredPlaylistsController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self._playlists = []
|
||||
|
||||
def destroy(self):
|
||||
"""Cleanup after component."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def playlists(self):
|
||||
"""List of :class:`mopidy.models.Playlist`."""
|
||||
return copy(self._playlists)
|
||||
|
||||
@playlists.setter
|
||||
def playlists(self, playlists):
|
||||
self._playlists = playlists
|
||||
|
||||
def create(self, name):
|
||||
"""
|
||||
Create a new playlist.
|
||||
|
||||
:param name: name of the new playlist
|
||||
:type name: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, playlist):
|
||||
"""
|
||||
Delete playlist.
|
||||
|
||||
:param playlist: the playlist to delete
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get(self, **criteria):
|
||||
"""
|
||||
Get playlist by given criterias from the set of stored playlists.
|
||||
|
||||
Raises :exc:`LookupError` if a unique match is not found.
|
||||
|
||||
Examples::
|
||||
|
||||
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'
|
||||
|
||||
:param criteria: one or more criteria to match by
|
||||
:type criteria: dict
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
matches = self._playlists
|
||||
for (key, value) in criteria.iteritems():
|
||||
matches = filter(lambda p: getattr(p, key) == value, matches)
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
criteria_string = ', '.join(
|
||||
['%s=%s' % (k, v) for (k, v) in criteria.iteritems()])
|
||||
if len(matches) == 0:
|
||||
raise LookupError('"%s" match no playlists' % criteria_string)
|
||||
else:
|
||||
raise LookupError('"%s" match multiple playlists' % criteria_string)
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
Lookup playlist with given URI in both the set of stored playlists and
|
||||
in any other playlist sources.
|
||||
|
||||
:param uri: playlist URI
|
||||
:type uri: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh stored playlists."""
|
||||
raise NotImplementedError
|
||||
|
||||
def rename(self, playlist, new_name):
|
||||
"""
|
||||
Rename playlist.
|
||||
|
||||
:param playlist: the playlist
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
:param new_name: the new name
|
||||
:type new_name: string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, playlist):
|
||||
"""
|
||||
Save the playlist to the set of stored playlists.
|
||||
|
||||
:param playlist: the playlist
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def search(self, query):
|
||||
"""
|
||||
Search for playlists whose name contains ``query``.
|
||||
|
||||
:param query: query to search for
|
||||
:type query: string
|
||||
:rtype: list of :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
return filter(lambda p: query in p.name, self._playlists)
|
||||
@ -5,7 +5,7 @@ import sys
|
||||
import spytify
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController,
|
||||
from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController,
|
||||
BaseLibraryController, BasePlaybackController,
|
||||
BaseStoredPlaylistsController)
|
||||
from mopidy.models import Artist, Album, Track, Playlist
|
||||
@ -60,6 +60,9 @@ class DespotifyLibraryController(BaseLibraryController):
|
||||
track = self.backend.spotify.lookup(uri.encode(ENCODING))
|
||||
return DespotifyTranslator.to_mopidy_track(track)
|
||||
|
||||
def refresh(self, uri=None):
|
||||
pass # TODO
|
||||
|
||||
def search(self, **query):
|
||||
spotify_query = []
|
||||
for (field, values) in query.iteritems():
|
||||
@ -106,6 +109,9 @@ class DespotifyPlaybackController(BasePlaybackController):
|
||||
logger.error(e)
|
||||
return False
|
||||
|
||||
def _seek(self, time_position):
|
||||
pass # TODO
|
||||
|
||||
def _stop(self):
|
||||
try:
|
||||
self.backend.spotify.stop()
|
||||
@ -116,6 +122,15 @@ class DespotifyPlaybackController(BasePlaybackController):
|
||||
|
||||
|
||||
class DespotifyStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
def create(self, name):
|
||||
pass # TODO
|
||||
|
||||
def delete(self, playlist):
|
||||
pass # TODO
|
||||
|
||||
def lookup(self, uri):
|
||||
pass # TODO
|
||||
|
||||
def refresh(self):
|
||||
logger.info(u'Caching stored playlists')
|
||||
playlists = []
|
||||
@ -127,6 +142,12 @@ class DespotifyStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
u', '.join([u'<%s>' % p.name for p in self.playlists]))
|
||||
logger.info(u'Done caching stored playlists')
|
||||
|
||||
def rename(self, playlist, new_name):
|
||||
pass # TODO
|
||||
|
||||
def save(self, playlist):
|
||||
pass # TODO
|
||||
|
||||
|
||||
class DespotifyTranslator(object):
|
||||
@classmethod
|
||||
@ -1,4 +1,4 @@
|
||||
from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController,
|
||||
from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController,
|
||||
BasePlaybackController, BaseLibraryController,
|
||||
BaseStoredPlaylistsController)
|
||||
from mopidy.models import Playlist
|
||||
@ -19,21 +19,28 @@ class DummyBackend(BaseBackend):
|
||||
self.stored_playlists = DummyStoredPlaylistsController(backend=self)
|
||||
self.uri_handlers = [u'dummy:']
|
||||
|
||||
|
||||
class DummyCurrentPlaylistController(BaseCurrentPlaylistController):
|
||||
pass
|
||||
|
||||
|
||||
class DummyLibraryController(BaseLibraryController):
|
||||
_library = []
|
||||
|
||||
def find_exact(self, **query):
|
||||
return Playlist()
|
||||
|
||||
def lookup(self, uri):
|
||||
matches = filter(lambda t: uri == t.uri, self._library)
|
||||
if matches:
|
||||
return matches[0]
|
||||
|
||||
def refresh(self, uri=None):
|
||||
pass
|
||||
|
||||
def search(self, **query):
|
||||
return Playlist()
|
||||
|
||||
find_exact = search
|
||||
|
||||
class DummyPlaybackController(BasePlaybackController):
|
||||
def _next(self, track):
|
||||
@ -51,6 +58,33 @@ class DummyPlaybackController(BasePlaybackController):
|
||||
def _resume(self):
|
||||
return True
|
||||
|
||||
def _seek(self, time_position):
|
||||
pass
|
||||
|
||||
def _stop(self):
|
||||
return True
|
||||
|
||||
|
||||
class DummyStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
def search(self, query):
|
||||
return [Playlist(name=query)]
|
||||
_playlists = []
|
||||
|
||||
def create(self, name):
|
||||
playlist = Playlist(name=name)
|
||||
self._playlists.append(playlist)
|
||||
return playlist
|
||||
|
||||
def delete(self, playlist):
|
||||
self._playlists.remove(playlist)
|
||||
|
||||
def lookup(self, uri):
|
||||
return filter(lambda p: p.uri == uri, self._playlists)
|
||||
|
||||
def refresh(self):
|
||||
pass
|
||||
|
||||
def rename(self, playlist, new_name):
|
||||
self._playlists[self._playlists.index(playlist)] = \
|
||||
playlist.with_(name=new_name)
|
||||
|
||||
def save(self, playlist):
|
||||
self._playlists.append(playlist)
|
||||
@ -1,7 +1,8 @@
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
# FIXME make sure we don't get hit by
|
||||
# http://jameswestby.net/weblog/tech/14-caution-python-multiprocessing-and-glib-dont-mix.html
|
||||
# http://jameswestby.net/
|
||||
# weblog/tech/14-caution-python-multiprocessing-and-glib-dont-mix.html
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
@ -13,7 +14,7 @@ import glob
|
||||
import shutil
|
||||
import threading
|
||||
|
||||
from mopidy.backends import *
|
||||
from mopidy.backends.base import *
|
||||
from mopidy.models import Playlist, Track, Album
|
||||
from mopidy import settings
|
||||
from mopidy.utils import parse_m3u, parse_mpd_tag_cache
|
||||
@ -71,9 +72,7 @@ class GStreamerPlaybackController(BasePlaybackController):
|
||||
|
||||
def _set_state(self, state):
|
||||
self._bin.set_state(state)
|
||||
|
||||
result, new, old = self._bin.get_state()
|
||||
|
||||
(_, new, _) = self._bin.get_state()
|
||||
return new == state
|
||||
|
||||
def _message(self, bus, message):
|
||||
@ -131,6 +130,9 @@ class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
self._folder = os.path.expanduser(settings.LOCAL_PLAYLIST_FOLDER)
|
||||
self.refresh()
|
||||
|
||||
def lookup(self, uri):
|
||||
pass # TODO
|
||||
|
||||
def refresh(self):
|
||||
playlists = []
|
||||
|
||||
@ -285,7 +287,7 @@ class GStreamerLibraryController(BaseLibraryController):
|
||||
return Playlist(tracks=result_tracks)
|
||||
|
||||
def _validate_query(self, query):
|
||||
for (field, values) in query.iteritems():
|
||||
for (_, values) in query.iteritems():
|
||||
if not values:
|
||||
raise LookupError('Missing query')
|
||||
for value in values:
|
||||
@ -10,7 +10,7 @@ from spotify.manager import SpotifySessionManager
|
||||
from spotify.alsahelper import AlsaController
|
||||
|
||||
from mopidy import get_version, settings
|
||||
from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController,
|
||||
from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController,
|
||||
BaseLibraryController, BasePlaybackController,
|
||||
BaseStoredPlaylistsController)
|
||||
from mopidy.models import Artist, Album, Track, Playlist
|
||||
@ -1,3 +1,5 @@
|
||||
import re
|
||||
|
||||
from mopidy import MopidyException
|
||||
|
||||
class MpdAckError(MopidyException):
|
||||
@ -19,6 +21,7 @@ class MpdAckError(MopidyException):
|
||||
"""
|
||||
|
||||
def __init__(self, message=u'', error_code=0, index=0, command=u''):
|
||||
super(MpdAckError, self).__init__(message, error_code, index, command)
|
||||
self.message = message
|
||||
self.error_code = error_code
|
||||
self.index = index
|
||||
@ -54,3 +57,37 @@ class MpdNotImplemented(MpdAckError):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MpdNotImplemented, self).__init__(*args, **kwargs)
|
||||
self.message = u'Not implemented'
|
||||
|
||||
mpd_commands = set()
|
||||
request_handlers = {}
|
||||
|
||||
def handle_pattern(pattern):
|
||||
"""
|
||||
Decorator for connecting command handlers to command patterns.
|
||||
|
||||
If you use named groups in the pattern, the decorated method will get the
|
||||
groups as keyword arguments. If the group is optional, remember to give the
|
||||
argument a default value.
|
||||
|
||||
For example, if the command is ``do that thing`` the ``what`` argument will
|
||||
be ``this thing``::
|
||||
|
||||
@handle_pattern('^do (?P<what>.+)$')
|
||||
def do(what):
|
||||
...
|
||||
|
||||
:param pattern: regexp pattern for matching commands
|
||||
:type pattern: string
|
||||
"""
|
||||
def decorator(func):
|
||||
match = re.search('([a-z_]+)', pattern)
|
||||
if match is not None:
|
||||
mpd_commands.add(match.group())
|
||||
if pattern in request_handlers:
|
||||
raise ValueError(u'Tried to redefine handler for %s with %s' % (
|
||||
pattern, func))
|
||||
request_handlers[pattern] = func
|
||||
func.__doc__ = ' - *Pattern:* ``%s``\n\n%s' % (
|
||||
pattern, func.__doc__ or '')
|
||||
return func
|
||||
return decorator
|
||||
79
mopidy/frontends/mpd/frontend.py
Normal file
79
mopidy/frontends/mpd/frontend.py
Normal file
@ -0,0 +1,79 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from mopidy.frontends.mpd import (mpd_commands, request_handlers,
|
||||
handle_pattern, MpdAckError, MpdArgError, MpdUnknownCommand)
|
||||
# Do not remove the following import. The protocol modules must be imported to
|
||||
# get them registered as request handlers.
|
||||
from mopidy.frontends.mpd.protocol import (audio_output, command_list,
|
||||
connection, current_playlist, music_db, playback, reflection, status,
|
||||
stickers, stored_playlists)
|
||||
from mopidy.utils import flatten
|
||||
|
||||
logger = logging.getLogger('mopidy.frontends.mpd.frontend')
|
||||
|
||||
class MpdFrontend(object):
|
||||
"""
|
||||
The MPD frontend dispatches MPD requests to the correct handler.
|
||||
"""
|
||||
|
||||
def __init__(self, backend=None):
|
||||
self.backend = backend
|
||||
self.command_list = False
|
||||
self.command_list_ok = False
|
||||
|
||||
def handle_request(self, request, command_list_index=None):
|
||||
"""Dispatch incoming requests to the correct handler."""
|
||||
if self.command_list is not False and request != u'command_list_end':
|
||||
self.command_list.append(request)
|
||||
return None
|
||||
try:
|
||||
(handler, kwargs) = self.find_handler(request)
|
||||
result = handler(self, **kwargs)
|
||||
except MpdAckError as e:
|
||||
if command_list_index is not None:
|
||||
e.index = command_list_index
|
||||
return self.handle_response(e.get_mpd_ack(), add_ok=False)
|
||||
if request in (u'command_list_begin', u'command_list_ok_begin'):
|
||||
return None
|
||||
if command_list_index is not None:
|
||||
return self.handle_response(result, add_ok=False)
|
||||
return self.handle_response(result)
|
||||
|
||||
def find_handler(self, request):
|
||||
"""Find the correct handler for a request."""
|
||||
for pattern in request_handlers:
|
||||
matches = re.match(pattern, request)
|
||||
if matches is not None:
|
||||
return (request_handlers[pattern], matches.groupdict())
|
||||
command = request.split(' ')[0]
|
||||
if command in mpd_commands:
|
||||
raise MpdArgError(u'incorrect arguments', command=command)
|
||||
raise MpdUnknownCommand(command=command)
|
||||
|
||||
def handle_response(self, result, add_ok=True):
|
||||
"""Format the response from a request handler."""
|
||||
response = []
|
||||
if result is None:
|
||||
result = []
|
||||
elif isinstance(result, set):
|
||||
result = list(result)
|
||||
elif not isinstance(result, list):
|
||||
result = [result]
|
||||
for line in flatten(result):
|
||||
if isinstance(line, dict):
|
||||
for (key, value) in line.items():
|
||||
response.append(u'%s: %s' % (key, value))
|
||||
elif isinstance(line, tuple):
|
||||
(key, value) = line
|
||||
response.append(u'%s: %s' % (key, value))
|
||||
else:
|
||||
response.append(line)
|
||||
if add_ok and (not response or not response[-1].startswith(u'ACK')):
|
||||
response.append(u'OK')
|
||||
return response
|
||||
|
||||
@handle_pattern(r'^$')
|
||||
def empty(frontend):
|
||||
"""The original MPD server returns ``OK`` on an empty request."""
|
||||
pass
|
||||
17
mopidy/frontends/mpd/protocol/__init__.py
Normal file
17
mopidy/frontends/mpd/protocol/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""
|
||||
This is Mopidy's MPD protocol implementation.
|
||||
|
||||
This is partly based upon the `MPD protocol documentation
|
||||
<http://www.musicpd.org/doc/protocol/>`_, which is a useful resource, but it is
|
||||
rather incomplete with regards to data formats, both for requests and
|
||||
responses. Thus, we have had to talk a great deal with the the original `MPD
|
||||
server <http://mpd.wikia.com/>`_ using telnet to get the details we need to
|
||||
implement our own MPD server which is compatible with the numerous existing
|
||||
`MPD clients <http://mpd.wikia.com/wiki/Clients>`_.
|
||||
"""
|
||||
|
||||
#: The MPD protocol uses UTF-8 for encoding all data.
|
||||
ENCODING = u'utf-8'
|
||||
|
||||
#: The MPD protocol uses ``\n`` as line terminator.
|
||||
LINE_TERMINATOR = u'\n'
|
||||
38
mopidy/frontends/mpd/protocol/audio_output.py
Normal file
38
mopidy/frontends/mpd/protocol/audio_output.py
Normal file
@ -0,0 +1,38 @@
|
||||
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
|
||||
|
||||
@handle_pattern(r'^disableoutput "(?P<outputid>\d+)"$')
|
||||
def disableoutput(frontend, outputid):
|
||||
"""
|
||||
*musicpd.org, audio output section:*
|
||||
|
||||
``disableoutput``
|
||||
|
||||
Turns an output off.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^enableoutput "(?P<outputid>\d+)"$')
|
||||
def enableoutput(frontend, outputid):
|
||||
"""
|
||||
*musicpd.org, audio output section:*
|
||||
|
||||
``enableoutput``
|
||||
|
||||
Turns an output on.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^outputs$')
|
||||
def outputs(frontend):
|
||||
"""
|
||||
*musicpd.org, audio output section:*
|
||||
|
||||
``outputs``
|
||||
|
||||
Shows information about all outputs.
|
||||
"""
|
||||
return [
|
||||
('outputid', 0),
|
||||
('outputname', frontend.backend.__class__.__name__),
|
||||
('outputenabled', 1),
|
||||
]
|
||||
47
mopidy/frontends/mpd/protocol/command_list.py
Normal file
47
mopidy/frontends/mpd/protocol/command_list.py
Normal file
@ -0,0 +1,47 @@
|
||||
from mopidy.frontends.mpd import handle_pattern, MpdUnknownCommand
|
||||
|
||||
@handle_pattern(r'^command_list_begin$')
|
||||
def command_list_begin(frontend):
|
||||
"""
|
||||
*musicpd.org, command list section:*
|
||||
|
||||
To facilitate faster adding of files etc. you can pass a list of
|
||||
commands all at once using a command list. The command list begins
|
||||
with ``command_list_begin`` or ``command_list_ok_begin`` and ends
|
||||
with ``command_list_end``.
|
||||
|
||||
It does not execute any commands until the list has ended. The
|
||||
return value is whatever the return for a list of commands is. On
|
||||
success for all commands, ``OK`` is returned. If a command fails,
|
||||
no more commands are executed and the appropriate ``ACK`` error is
|
||||
returned. If ``command_list_ok_begin`` is used, ``list_OK`` is
|
||||
returned for each successful command executed in the command list.
|
||||
"""
|
||||
frontend.command_list = []
|
||||
frontend.command_list_ok = False
|
||||
|
||||
@handle_pattern(r'^command_list_end$')
|
||||
def command_list_end(frontend):
|
||||
"""See :meth:`command_list_begin()`."""
|
||||
if frontend.command_list is False:
|
||||
# Test for False exactly, and not e.g. empty list
|
||||
raise MpdUnknownCommand(command='command_list_end')
|
||||
(command_list, frontend.command_list) = (frontend.command_list, False)
|
||||
(command_list_ok, frontend.command_list_ok) = (
|
||||
frontend.command_list_ok, False)
|
||||
result = []
|
||||
for i, command in enumerate(command_list):
|
||||
response = frontend.handle_request(command, command_list_index=i)
|
||||
if response is not None:
|
||||
result.append(response)
|
||||
if response and response[-1].startswith(u'ACK'):
|
||||
return result
|
||||
if command_list_ok:
|
||||
response.append(u'list_OK')
|
||||
return result
|
||||
|
||||
@handle_pattern(r'^command_list_ok_begin$')
|
||||
def command_list_ok_begin(frontend):
|
||||
"""See :meth:`command_list_begin()`."""
|
||||
frontend.command_list = []
|
||||
frontend.command_list_ok = True
|
||||
48
mopidy/frontends/mpd/protocol/connection.py
Normal file
48
mopidy/frontends/mpd/protocol/connection.py
Normal file
@ -0,0 +1,48 @@
|
||||
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
|
||||
|
||||
@handle_pattern(r'^close$')
|
||||
def close(frontend):
|
||||
"""
|
||||
*musicpd.org, connection section:*
|
||||
|
||||
``close``
|
||||
|
||||
Closes the connection to MPD.
|
||||
"""
|
||||
# TODO Does not work after multiprocessing branch merge
|
||||
#frontend.session.do_close()
|
||||
|
||||
@handle_pattern(r'^kill$')
|
||||
def kill(frontend):
|
||||
"""
|
||||
*musicpd.org, connection section:*
|
||||
|
||||
``kill``
|
||||
|
||||
Kills MPD.
|
||||
"""
|
||||
# TODO Does not work after multiprocessing branch merge
|
||||
#frontend.session.do_kill()
|
||||
|
||||
@handle_pattern(r'^password "(?P<password>[^"]+)"$')
|
||||
def password_(frontend, password):
|
||||
"""
|
||||
*musicpd.org, connection section:*
|
||||
|
||||
``password {PASSWORD}``
|
||||
|
||||
This is used for authentication with the server. ``PASSWORD`` is
|
||||
simply the plaintext password.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^ping$')
|
||||
def ping(frontend):
|
||||
"""
|
||||
*musicpd.org, connection section:*
|
||||
|
||||
``ping``
|
||||
|
||||
Does nothing but return ``OK``.
|
||||
"""
|
||||
pass
|
||||
351
mopidy/frontends/mpd/protocol/current_playlist.py
Normal file
351
mopidy/frontends/mpd/protocol/current_playlist.py
Normal file
@ -0,0 +1,351 @@
|
||||
from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError,
|
||||
MpdNotImplemented)
|
||||
|
||||
@handle_pattern(r'^add "(?P<uri>[^"]*)"$')
|
||||
def add(frontend, uri):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``add {URI}``
|
||||
|
||||
Adds the file ``URI`` to the playlist (directories add recursively).
|
||||
``URI`` can also be a single file.
|
||||
"""
|
||||
track = frontend.backend.library.lookup(uri)
|
||||
if track is None:
|
||||
raise MpdNoExistError(
|
||||
u'directory or file not found', command=u'add')
|
||||
frontend.backend.current_playlist.add(track)
|
||||
|
||||
@handle_pattern(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$')
|
||||
def addid(frontend, uri, songpos=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``addid {URI} [POSITION]``
|
||||
|
||||
Adds a song to the playlist (non-recursive) and returns the song id.
|
||||
|
||||
``URI`` is always a single file or URL. For example::
|
||||
|
||||
addid "foo.mp3"
|
||||
Id: 999
|
||||
OK
|
||||
"""
|
||||
if songpos is not None:
|
||||
songpos = int(songpos)
|
||||
track = frontend.backend.library.lookup(uri)
|
||||
if track is None:
|
||||
raise MpdNoExistError(u'No such song', command=u'addid')
|
||||
if songpos and songpos > len(frontend.backend.current_playlist.tracks):
|
||||
raise MpdArgError(u'Bad song index', command=u'addid')
|
||||
cp_track = frontend.backend.current_playlist.add(track, at_position=songpos)
|
||||
return ('Id', cp_track[0])
|
||||
|
||||
@handle_pattern(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
def delete_range(frontend, start, end=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``delete [{POS} | {START:END}]``
|
||||
|
||||
Deletes a song from the playlist.
|
||||
"""
|
||||
start = int(start)
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
else:
|
||||
end = len(frontend.backend.current_playlist.tracks)
|
||||
cp_tracks = frontend.backend.current_playlist.cp_tracks[start:end]
|
||||
if not cp_tracks:
|
||||
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||
for (cpid, _) in cp_tracks:
|
||||
frontend.backend.current_playlist.remove(cpid=cpid)
|
||||
|
||||
@handle_pattern(r'^delete "(?P<songpos>\d+)"$')
|
||||
def delete_songpos(frontend, songpos):
|
||||
"""See :meth:`delete_range`"""
|
||||
try:
|
||||
songpos = int(songpos)
|
||||
(cpid, _) = frontend.backend.current_playlist.cp_tracks[songpos]
|
||||
frontend.backend.current_playlist.remove(cpid=cpid)
|
||||
except IndexError:
|
||||
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||
|
||||
@handle_pattern(r'^deleteid "(?P<cpid>\d+)"$')
|
||||
def deleteid(frontend, cpid):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``deleteid {SONGID}``
|
||||
|
||||
Deletes the song ``SONGID`` from the playlist
|
||||
"""
|
||||
try:
|
||||
cpid = int(cpid)
|
||||
return frontend.backend.current_playlist.remove(cpid=cpid)
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'deleteid')
|
||||
|
||||
@handle_pattern(r'^clear$')
|
||||
def clear(frontend):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``clear``
|
||||
|
||||
Clears the current playlist.
|
||||
"""
|
||||
frontend.backend.current_playlist.clear()
|
||||
|
||||
@handle_pattern(r'^move "(?P<start>\d+):(?P<end>\d+)*" "(?P<to>\d+)"$')
|
||||
def move_range(frontend, start, to, end=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``move [{FROM} | {START:END}] {TO}``
|
||||
|
||||
Moves the song at ``FROM`` or range of songs at ``START:END`` to
|
||||
``TO`` in the playlist.
|
||||
"""
|
||||
if end is None:
|
||||
end = len(frontend.backend.current_playlist.tracks)
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
to = int(to)
|
||||
frontend.backend.current_playlist.move(start, end, to)
|
||||
|
||||
@handle_pattern(r'^move "(?P<songpos>\d+)" "(?P<to>\d+)"$')
|
||||
def move_songpos(frontend, songpos, to):
|
||||
"""See :meth:`move_range`."""
|
||||
songpos = int(songpos)
|
||||
to = int(to)
|
||||
frontend.backend.current_playlist.move(songpos, songpos + 1, to)
|
||||
|
||||
@handle_pattern(r'^moveid "(?P<cpid>\d+)" "(?P<to>\d+)"$')
|
||||
def moveid(frontend, cpid, to):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``moveid {FROM} {TO}``
|
||||
|
||||
Moves the song with ``FROM`` (songid) to ``TO`` (playlist index) in
|
||||
the playlist. If ``TO`` is negative, it is relative to the current
|
||||
song in the playlist (if there is one).
|
||||
"""
|
||||
cpid = int(cpid)
|
||||
to = int(to)
|
||||
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
|
||||
position = frontend.backend.current_playlist.cp_tracks.index(cp_track)
|
||||
frontend.backend.current_playlist.move(position, position + 1, to)
|
||||
|
||||
@handle_pattern(r'^playlist$')
|
||||
def playlist(frontend):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``playlist``
|
||||
|
||||
Displays the current playlist.
|
||||
|
||||
.. note::
|
||||
|
||||
Do not use this, instead use ``playlistinfo``.
|
||||
"""
|
||||
return playlistinfo(frontend)
|
||||
|
||||
@handle_pattern(r'^playlistfind (?P<tag>[^"]+) "(?P<needle>[^"]+)"$')
|
||||
@handle_pattern(r'^playlistfind "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
||||
def playlistfind(frontend, tag, needle):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``playlistfind {TAG} {NEEDLE}``
|
||||
|
||||
Finds songs in the current playlist with strict matching.
|
||||
|
||||
*GMPC:*
|
||||
|
||||
- does not add quotes around the tag.
|
||||
"""
|
||||
if tag == 'filename':
|
||||
try:
|
||||
cp_track = frontend.backend.current_playlist.get(uri=needle)
|
||||
return cp_track[1].mpd_format()
|
||||
except LookupError:
|
||||
return None
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^playlistid( "(?P<cpid>\d+)")*$')
|
||||
def playlistid(frontend, cpid=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``playlistid {SONGID}``
|
||||
|
||||
Displays a list of songs in the playlist. ``SONGID`` is optional
|
||||
and specifies a single song to display info for.
|
||||
"""
|
||||
if cpid is not None:
|
||||
try:
|
||||
cpid = int(cpid)
|
||||
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
|
||||
position = frontend.backend.current_playlist.cp_tracks.index(
|
||||
cp_track)
|
||||
return cp_track[1].mpd_format(position=position, cpid=cpid)
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'playlistid')
|
||||
else:
|
||||
return frontend.backend.current_playlist.mpd_format()
|
||||
|
||||
@handle_pattern(r'^playlistinfo$')
|
||||
@handle_pattern(r'^playlistinfo "(?P<songpos>-?\d+)"$')
|
||||
@handle_pattern(r'^playlistinfo "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
def playlistinfo(frontend, songpos=None,
|
||||
start=None, end=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``playlistinfo [[SONGPOS] | [START:END]]``
|
||||
|
||||
Displays a list of all songs in the playlist, or if the optional
|
||||
argument is given, displays information only for the song
|
||||
``SONGPOS`` or the range of songs ``START:END``.
|
||||
|
||||
*ncmpc and mpc:*
|
||||
|
||||
- uses negative indexes, like ``playlistinfo "-1"``, to request
|
||||
the entire playlist
|
||||
"""
|
||||
if songpos == "-1":
|
||||
songpos = None
|
||||
|
||||
if songpos is not None:
|
||||
songpos = int(songpos)
|
||||
start = songpos
|
||||
end = songpos + 1
|
||||
if start == -1:
|
||||
end = None
|
||||
return frontend.backend.current_playlist.mpd_format(start, end)
|
||||
else:
|
||||
if start is None:
|
||||
start = 0
|
||||
start = int(start)
|
||||
if not (0 <= start <= len(frontend.backend.current_playlist.tracks)):
|
||||
raise MpdArgError(u'Bad song index', command=u'playlistinfo')
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
if end > len(frontend.backend.current_playlist.tracks):
|
||||
end = None
|
||||
return frontend.backend.current_playlist.mpd_format(start, end)
|
||||
|
||||
@handle_pattern(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
||||
@handle_pattern(r'^playlistsearch (?P<tag>\S+) "(?P<needle>[^"]+)"$')
|
||||
def playlistsearch(frontend, tag, needle):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``playlistsearch {TAG} {NEEDLE}``
|
||||
|
||||
Searches case-sensitively for partial matches in the current
|
||||
playlist.
|
||||
|
||||
*GMPC:*
|
||||
|
||||
- does not add quotes around the tag
|
||||
- uses ``filename`` and ``any`` as tags
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^plchanges "(?P<version>\d+)"$')
|
||||
def plchanges(frontend, version):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``plchanges {VERSION}``
|
||||
|
||||
Displays changed songs currently in the playlist since ``VERSION``.
|
||||
|
||||
To detect songs that were deleted at the end of the playlist, use
|
||||
``playlistlength`` returned by status command.
|
||||
"""
|
||||
# XXX Naive implementation that returns all tracks as changed
|
||||
if int(version) < frontend.backend.current_playlist.version:
|
||||
return frontend.backend.current_playlist.mpd_format()
|
||||
|
||||
@handle_pattern(r'^plchangesposid "(?P<version>\d+)"$')
|
||||
def plchangesposid(frontend, version):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``plchangesposid {VERSION}``
|
||||
|
||||
Displays changed songs currently in the playlist since ``VERSION``.
|
||||
This function only returns the position and the id of the changed
|
||||
song, not the complete metadata. This is more bandwidth efficient.
|
||||
|
||||
To detect songs that were deleted at the end of the playlist, use
|
||||
``playlistlength`` returned by status command.
|
||||
"""
|
||||
# XXX Naive implementation that returns all tracks as changed
|
||||
if int(version) != frontend.backend.current_playlist.version:
|
||||
result = []
|
||||
for (position, (cpid, _)) in enumerate(
|
||||
frontend.backend.current_playlist.cp_tracks):
|
||||
result.append((u'cpos', position))
|
||||
result.append((u'Id', cpid))
|
||||
return result
|
||||
|
||||
@handle_pattern(r'^shuffle$')
|
||||
@handle_pattern(r'^shuffle "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
def shuffle(frontend, start=None, end=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``shuffle [START:END]``
|
||||
|
||||
Shuffles the current playlist. ``START:END`` is optional and
|
||||
specifies a range of songs.
|
||||
"""
|
||||
if start is not None:
|
||||
start = int(start)
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
frontend.backend.current_playlist.shuffle(start, end)
|
||||
|
||||
@handle_pattern(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$')
|
||||
def swap(frontend, songpos1, songpos2):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``swap {SONG1} {SONG2}``
|
||||
|
||||
Swaps the positions of ``SONG1`` and ``SONG2``.
|
||||
"""
|
||||
songpos1 = int(songpos1)
|
||||
songpos2 = int(songpos2)
|
||||
tracks = frontend.backend.current_playlist.tracks
|
||||
song1 = tracks[songpos1]
|
||||
song2 = tracks[songpos2]
|
||||
del tracks[songpos1]
|
||||
tracks.insert(songpos1, song2)
|
||||
del tracks[songpos2]
|
||||
tracks.insert(songpos2, song1)
|
||||
frontend.backend.current_playlist.load(tracks)
|
||||
|
||||
@handle_pattern(r'^swapid "(?P<cpid1>\d+)" "(?P<cpid2>\d+)"$')
|
||||
def swapid(frontend, cpid1, cpid2):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
``swapid {SONG1} {SONG2}``
|
||||
|
||||
Swaps the positions of ``SONG1`` and ``SONG2`` (both song ids).
|
||||
"""
|
||||
cpid1 = int(cpid1)
|
||||
cpid2 = int(cpid2)
|
||||
cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1)
|
||||
cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2)
|
||||
position1 = frontend.backend.current_playlist.cp_tracks.index(cp_track1)
|
||||
position2 = frontend.backend.current_playlist.cp_tracks.index(cp_track2)
|
||||
swap(frontend, position1, position2)
|
||||
254
mopidy/frontends/mpd/protocol/music_db.py
Normal file
254
mopidy/frontends/mpd/protocol/music_db.py
Normal file
@ -0,0 +1,254 @@
|
||||
import re
|
||||
|
||||
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
|
||||
from mopidy.frontends.mpd.protocol import stored_playlists
|
||||
|
||||
def _build_query(mpd_query):
|
||||
"""
|
||||
Parses a MPD query string and converts it to the Mopidy query format.
|
||||
"""
|
||||
query_pattern = (
|
||||
r'"?(?:[Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? "[^"]+"')
|
||||
query_parts = re.findall(query_pattern, mpd_query)
|
||||
query_part_pattern = (
|
||||
r'"?(?P<field>([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny))"? '
|
||||
r'"(?P<what>[^"]+)"')
|
||||
query = {}
|
||||
for query_part in query_parts:
|
||||
m = re.match(query_part_pattern, query_part)
|
||||
field = m.groupdict()['field'].lower()
|
||||
if field == u'title':
|
||||
field = u'track'
|
||||
field = str(field) # Needed for kwargs keys on OS X and Windows
|
||||
what = m.groupdict()['what'].lower()
|
||||
if field in query:
|
||||
query[field].append(what)
|
||||
else:
|
||||
query[field] = [what]
|
||||
return query
|
||||
|
||||
@handle_pattern(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
|
||||
def count(frontend, tag, needle):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
``count {TAG} {NEEDLE}``
|
||||
|
||||
Counts the number of songs and their total playtime in the db
|
||||
matching ``TAG`` exactly.
|
||||
"""
|
||||
return [('songs', 0), ('playtime', 0)] # TODO
|
||||
|
||||
@handle_pattern(r'^find '
|
||||
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
|
||||
' "[^"]+"\s?)+)$')
|
||||
def find(frontend, mpd_query):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
``find {TYPE} {WHAT}``
|
||||
|
||||
Finds songs in the db that are exactly ``WHAT``. ``TYPE`` should be
|
||||
``album``, ``artist``, or ``title``. ``WHAT`` is what to find.
|
||||
|
||||
*GMPC:*
|
||||
|
||||
- does not add quotes around the field argument.
|
||||
- also uses ``find album "[ALBUM]" artist "[ARTIST]"`` to list album
|
||||
tracks.
|
||||
|
||||
*ncmpc:*
|
||||
|
||||
- does not add quotes around the field argument.
|
||||
- capitalizes the type argument.
|
||||
"""
|
||||
query = _build_query(mpd_query)
|
||||
return frontend.backend.library.find_exact(**query).mpd_format()
|
||||
|
||||
@handle_pattern(r'^findadd '
|
||||
r'(?P<query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
|
||||
'"[^"]+"\s?)+)$')
|
||||
def findadd(frontend, query):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
``findadd {TYPE} {WHAT}``
|
||||
|
||||
Finds songs in the db that are exactly ``WHAT`` and adds them to
|
||||
current playlist. ``TYPE`` can be any tag supported by MPD.
|
||||
``WHAT`` is what to find.
|
||||
"""
|
||||
# TODO Add result to current playlist
|
||||
#result = frontend.find(query)
|
||||
|
||||
@handle_pattern(r'^list (?P<field>[Aa]rtist)$')
|
||||
@handle_pattern(r'^list "(?P<field>[Aa]rtist)"$')
|
||||
@handle_pattern(r'^list (?P<field>album( artist)?)'
|
||||
'( "(?P<artist>[^"]+)")*$')
|
||||
@handle_pattern(r'^list "(?P<field>album(" "artist)?)"'
|
||||
'( "(?P<artist>[^"]+)")*$')
|
||||
def list_(frontend, field, artist=None):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
``list {TYPE} [ARTIST]``
|
||||
|
||||
Lists all tags of the specified type. ``TYPE`` should be ``album``,
|
||||
``artist``, ``date``, or ``genre``.
|
||||
|
||||
``ARTIST`` is an optional parameter when type is ``album``,
|
||||
``date``, or ``genre``.
|
||||
|
||||
This filters the result list by an artist.
|
||||
|
||||
*GMPC:*
|
||||
|
||||
- does not add quotes around the field argument.
|
||||
- asks for "list artist" to get available artists and will not query
|
||||
for artist/album information if this is not retrived
|
||||
- asks for multiple fields, i.e.::
|
||||
|
||||
list album artist "an artist name"
|
||||
|
||||
returns the albums available for the asked artist::
|
||||
|
||||
list album artist "Tiesto"
|
||||
Album: Radio Trance Vol 4-Promo-CD
|
||||
Album: Ur A Tear in the Open CDR
|
||||
Album: Simple Trance 2004 Step One
|
||||
Album: In Concert 05-10-2003
|
||||
|
||||
*ncmpc:*
|
||||
|
||||
- does not add quotes around the field argument.
|
||||
- capitalizes the field argument.
|
||||
"""
|
||||
field = field.lower()
|
||||
if field == u'artist':
|
||||
return _list_artist(frontend)
|
||||
elif field == u'album artist':
|
||||
return _list_album_artist(frontend, artist)
|
||||
# TODO More to implement
|
||||
|
||||
def _list_artist(frontend):
|
||||
"""
|
||||
Since we don't know exactly all available artists, we respond with
|
||||
the artists we know for sure, which is all artists in our stored playlists.
|
||||
"""
|
||||
artists = set()
|
||||
for playlist in frontend.backend.stored_playlists.playlists:
|
||||
for track in playlist.tracks:
|
||||
for artist in track.artists:
|
||||
artists.add((u'Artist', artist.name))
|
||||
return artists
|
||||
|
||||
def _list_album_artist(frontend, artist):
|
||||
playlist = frontend.backend.library.find_exact(artist=[artist])
|
||||
albums = set()
|
||||
for track in playlist.tracks:
|
||||
albums.add((u'Album', track.album.name))
|
||||
return albums
|
||||
|
||||
@handle_pattern(r'^listall "(?P<uri>[^"]+)"')
|
||||
def listall(frontend, uri):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
``listall [URI]``
|
||||
|
||||
Lists all songs and directories in ``URI``.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^listallinfo "(?P<uri>[^"]+)"')
|
||||
def listallinfo(frontend, uri):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
``listallinfo [URI]``
|
||||
|
||||
Same as ``listall``, except it also returns metadata info in the
|
||||
same format as ``lsinfo``.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^lsinfo$')
|
||||
@handle_pattern(r'^lsinfo "(?P<uri>[^"]*)"$')
|
||||
def lsinfo(frontend, uri=None):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
``lsinfo [URI]``
|
||||
|
||||
Lists the contents of the directory ``URI``.
|
||||
|
||||
When listing the root directory, this currently returns the list of
|
||||
stored playlists. This behavior is deprecated; use
|
||||
``listplaylists`` instead.
|
||||
|
||||
MPD returns the same result, including both playlists and the files and
|
||||
directories located at the root level, for both ``lsinfo``, ``lsinfo
|
||||
""``, and ``lsinfo "/"``.
|
||||
"""
|
||||
if uri is None or uri == u'/' or uri == u'':
|
||||
return stored_playlists.listplaylists(frontend)
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^rescan( "(?P<uri>[^"]+)")*$')
|
||||
def rescan(frontend, uri=None):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
``rescan [URI]``
|
||||
|
||||
Same as ``update``, but also rescans unmodified files.
|
||||
"""
|
||||
return update(frontend, uri, rescan_unmodified_files=True)
|
||||
|
||||
@handle_pattern(r'^search '
|
||||
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
|
||||
' "[^"]+"\s?)+)$')
|
||||
def search(frontend, mpd_query):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
``search {TYPE} {WHAT}``
|
||||
|
||||
Searches for any song that contains ``WHAT``. ``TYPE`` can be
|
||||
``title``, ``artist``, ``album`` or ``filename``. Search is not
|
||||
case sensitive.
|
||||
|
||||
*GMPC:*
|
||||
|
||||
- does not add quotes around the field argument.
|
||||
- uses the undocumented field ``any``.
|
||||
- searches for multiple words like this::
|
||||
|
||||
search any "foo" any "bar" any "baz"
|
||||
|
||||
*ncmpc:*
|
||||
|
||||
- does not add quotes around the field argument.
|
||||
- capitalizes the field argument.
|
||||
"""
|
||||
query = _build_query(mpd_query)
|
||||
return frontend.backend.library.search(**query).mpd_format()
|
||||
|
||||
@handle_pattern(r'^update( "(?P<uri>[^"]+)")*$')
|
||||
def update(frontend, uri=None, rescan_unmodified_files=False):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
``update [URI]``
|
||||
|
||||
Updates the music database: find new files, remove deleted files,
|
||||
update modified files.
|
||||
|
||||
``URI`` is a particular directory or song/file to update. If you do
|
||||
not specify it, everything is updated.
|
||||
|
||||
Prints ``updating_db: JOBID`` where ``JOBID`` is a positive number
|
||||
identifying the update job. You can read the current job id in the
|
||||
``status`` response.
|
||||
"""
|
||||
return {'updating_db': 0} # TODO
|
||||
331
mopidy/frontends/mpd/protocol/playback.py
Normal file
331
mopidy/frontends/mpd/protocol/playback.py
Normal file
@ -0,0 +1,331 @@
|
||||
from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError,
|
||||
MpdNotImplemented)
|
||||
|
||||
@handle_pattern(r'^consume "(?P<state>[01])"$')
|
||||
def consume(frontend, state):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``consume {STATE}``
|
||||
|
||||
Sets consume state to ``STATE``, ``STATE`` should be 0 or
|
||||
1. When consume is activated, each song played is removed from
|
||||
playlist.
|
||||
"""
|
||||
if int(state):
|
||||
frontend.backend.playback.consume = True
|
||||
else:
|
||||
frontend.backend.playback.consume = False
|
||||
|
||||
@handle_pattern(r'^crossfade "(?P<seconds>\d+)"$')
|
||||
def crossfade(frontend, seconds):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``crossfade {SECONDS}``
|
||||
|
||||
Sets crossfading between songs.
|
||||
"""
|
||||
seconds = int(seconds)
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^next$')
|
||||
def next_(frontend):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``next``
|
||||
|
||||
Plays next song in the playlist.
|
||||
|
||||
*MPD's behaviour when affected by repeat/random/single/consume:*
|
||||
|
||||
Given a playlist of three tracks numbered 1, 2, 3, and a currently
|
||||
playing track ``c``. ``next_track`` is defined at the track that
|
||||
will be played upon calls to ``next``.
|
||||
|
||||
Tests performed on MPD 0.15.4-1ubuntu3.
|
||||
|
||||
====== ====== ====== ======= ===== ===== ===== =====
|
||||
Inputs next_track
|
||||
------------------------------- ------------------- -----
|
||||
repeat random single consume c = 1 c = 2 c = 3 Notes
|
||||
====== ====== ====== ======= ===== ===== ===== =====
|
||||
T T T T 2 3 EOPL
|
||||
T T T . Rand Rand Rand [1]
|
||||
T T . T Rand Rand Rand [4]
|
||||
T T . . Rand Rand Rand [4]
|
||||
T . T T 2 3 EOPL
|
||||
T . T . 2 3 1
|
||||
T . . T 3 3 EOPL
|
||||
T . . . 2 3 1
|
||||
. T T T Rand Rand Rand [3]
|
||||
. T T . Rand Rand Rand [3]
|
||||
. T . T Rand Rand Rand [2]
|
||||
. T . . Rand Rand Rand [2]
|
||||
. . T T 2 3 EOPL
|
||||
. . T . 2 3 EOPL
|
||||
. . . T 2 3 EOPL
|
||||
. . . . 2 3 EOPL
|
||||
====== ====== ====== ======= ===== ===== ===== =====
|
||||
|
||||
- When end of playlist (EOPL) is reached, the current track is
|
||||
unset.
|
||||
- [1] When *random* and *single* is combined, ``next`` selects
|
||||
a track randomly at each invocation, and not just the next track
|
||||
in an internal prerandomized playlist.
|
||||
- [2] When *random* is active, ``next`` will skip through
|
||||
all tracks in the playlist in random order, and finally EOPL is
|
||||
reached.
|
||||
- [3] *single* has no effect in combination with *random*
|
||||
alone, or *random* and *consume*.
|
||||
- [4] When *random* and *repeat* is active, EOPL is never
|
||||
reached, but the playlist is played again, in the same random
|
||||
order as the first time.
|
||||
|
||||
"""
|
||||
return frontend.backend.playback.next()
|
||||
|
||||
@handle_pattern(r'^pause "(?P<state>[01])"$')
|
||||
def pause(frontend, state):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``pause {PAUSE}``
|
||||
|
||||
Toggles pause/resumes playing, ``PAUSE`` is 0 or 1.
|
||||
"""
|
||||
if int(state):
|
||||
frontend.backend.playback.pause()
|
||||
else:
|
||||
frontend.backend.playback.resume()
|
||||
|
||||
@handle_pattern(r'^play$')
|
||||
def play(frontend):
|
||||
"""
|
||||
The original MPD server resumes from the paused state on ``play``
|
||||
without arguments.
|
||||
"""
|
||||
return frontend.backend.playback.play()
|
||||
|
||||
@handle_pattern(r'^playid "(?P<cpid>\d+)"$')
|
||||
@handle_pattern(r'^playid "(?P<cpid>-1)"$')
|
||||
def playid(frontend, cpid):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``playid [SONGID]``
|
||||
|
||||
Begins playing the playlist at song ``SONGID``.
|
||||
|
||||
*GMPC:*
|
||||
|
||||
- issues ``playid "-1"`` after playlist replacement to start playback
|
||||
at the first track.
|
||||
"""
|
||||
cpid = int(cpid)
|
||||
try:
|
||||
if cpid == -1:
|
||||
if not frontend.backend.current_playlist.cp_tracks:
|
||||
return # Fail silently
|
||||
cp_track = frontend.backend.current_playlist.cp_tracks[0]
|
||||
else:
|
||||
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
|
||||
return frontend.backend.playback.play(cp_track)
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'playid')
|
||||
|
||||
@handle_pattern(r'^play "(?P<songpos>\d+)"$')
|
||||
@handle_pattern(r'^play "(?P<songpos>-1)"$')
|
||||
def playpos(frontend, songpos):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``play [SONGPOS]``
|
||||
|
||||
Begins playing the playlist at song number ``SONGPOS``.
|
||||
|
||||
*MPoD:*
|
||||
|
||||
- issues ``play "-1"`` after playlist replacement to start playback at
|
||||
the first track.
|
||||
"""
|
||||
songpos = int(songpos)
|
||||
try:
|
||||
if songpos == -1:
|
||||
if not frontend.backend.current_playlist.cp_tracks:
|
||||
return # Fail silently
|
||||
cp_track = frontend.backend.current_playlist.cp_tracks[0]
|
||||
else:
|
||||
cp_track = frontend.backend.current_playlist.cp_tracks[songpos]
|
||||
return frontend.backend.playback.play(cp_track)
|
||||
except IndexError:
|
||||
raise MpdArgError(u'Bad song index', command=u'play')
|
||||
|
||||
@handle_pattern(r'^previous$')
|
||||
def previous(frontend):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``previous``
|
||||
|
||||
Plays previous song in the playlist.
|
||||
|
||||
*MPD's behaviour when affected by repeat/random/single/consume:*
|
||||
|
||||
Given a playlist of three tracks numbered 1, 2, 3, and a currently
|
||||
playing track ``c``. ``previous_track`` is defined at the track
|
||||
that will be played upon ``previous`` calls.
|
||||
|
||||
Tests performed on MPD 0.15.4-1ubuntu3.
|
||||
|
||||
====== ====== ====== ======= ===== ===== =====
|
||||
Inputs previous_track
|
||||
------------------------------- -------------------
|
||||
repeat random single consume c = 1 c = 2 c = 3
|
||||
====== ====== ====== ======= ===== ===== =====
|
||||
T T T T Rand? Rand? Rand?
|
||||
T T T . 3 1 2
|
||||
T T . T Rand? Rand? Rand?
|
||||
T T . . 3 1 2
|
||||
T . T T 3 1 2
|
||||
T . T . 3 1 2
|
||||
T . . T 3 1 2
|
||||
T . . . 3 1 2
|
||||
. T T T c c c
|
||||
. T T . c c c
|
||||
. T . T c c c
|
||||
. T . . c c c
|
||||
. . T T 1 1 2
|
||||
. . T . 1 1 2
|
||||
. . . T 1 1 2
|
||||
. . . . 1 1 2
|
||||
====== ====== ====== ======= ===== ===== =====
|
||||
|
||||
- If :attr:`time_position` of the current track is 15s or more,
|
||||
``previous`` should do a seek to time position 0.
|
||||
|
||||
"""
|
||||
return frontend.backend.playback.previous()
|
||||
|
||||
@handle_pattern(r'^random "(?P<state>[01])"$')
|
||||
def random(frontend, state):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``random {STATE}``
|
||||
|
||||
Sets random state to ``STATE``, ``STATE`` should be 0 or 1.
|
||||
"""
|
||||
if int(state):
|
||||
frontend.backend.playback.random = True
|
||||
else:
|
||||
frontend.backend.playback.random = False
|
||||
|
||||
@handle_pattern(r'^repeat "(?P<state>[01])"$')
|
||||
def repeat(frontend, state):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``repeat {STATE}``
|
||||
|
||||
Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1.
|
||||
"""
|
||||
if int(state):
|
||||
frontend.backend.playback.repeat = True
|
||||
else:
|
||||
frontend.backend.playback.repeat = False
|
||||
|
||||
@handle_pattern(r'^replay_gain_mode "(?P<mode>(off|track|album))"$')
|
||||
def replay_gain_mode(frontend, mode):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``replay_gain_mode {MODE}``
|
||||
|
||||
Sets the replay gain mode. One of ``off``, ``track``, ``album``.
|
||||
|
||||
Changing the mode during playback may take several seconds, because
|
||||
the new settings does not affect the buffered data.
|
||||
|
||||
This command triggers the options idle event.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^replay_gain_status$')
|
||||
def replay_gain_status(frontend):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``replay_gain_status``
|
||||
|
||||
Prints replay gain options. Currently, only the variable
|
||||
``replay_gain_mode`` is returned.
|
||||
"""
|
||||
return u'off' # TODO
|
||||
|
||||
@handle_pattern(r'^seek "(?P<songpos>\d+)" "(?P<seconds>\d+)"$')
|
||||
def seek(frontend, songpos, seconds):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``seek {SONGPOS} {TIME}``
|
||||
|
||||
Seeks to the position ``TIME`` (in seconds) of entry ``SONGPOS`` in
|
||||
the playlist.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^seekid "(?P<cpid>\d+)" "(?P<seconds>\d+)"$')
|
||||
def seekid(frontend, cpid, seconds):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``seekid {SONGID} {TIME}``
|
||||
|
||||
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^setvol "(?P<volume>[-+]*\d+)"$')
|
||||
def setvol(frontend, volume):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``setvol {VOL}``
|
||||
|
||||
Sets volume to ``VOL``, the range of volume is 0-100.
|
||||
"""
|
||||
volume = int(volume)
|
||||
if volume < 0:
|
||||
volume = 0
|
||||
if volume > 100:
|
||||
volume = 100
|
||||
frontend.backend.mixer.volume = volume
|
||||
|
||||
@handle_pattern(r'^single "(?P<state>[01])"$')
|
||||
def single(frontend, state):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``single {STATE}``
|
||||
|
||||
Sets single state to ``STATE``, ``STATE`` should be 0 or 1. When
|
||||
single is activated, playback is stopped after current song, or
|
||||
song is repeated if the ``repeat`` mode is enabled.
|
||||
"""
|
||||
if int(state):
|
||||
frontend.backend.playback.single = True
|
||||
else:
|
||||
frontend.backend.playback.single = False
|
||||
|
||||
@handle_pattern(r'^stop$')
|
||||
def stop(frontend):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
``stop``
|
||||
|
||||
Stops playing.
|
||||
"""
|
||||
frontend.backend.playback.stop()
|
||||
79
mopidy/frontends/mpd/protocol/reflection.py
Normal file
79
mopidy/frontends/mpd/protocol/reflection.py
Normal file
@ -0,0 +1,79 @@
|
||||
from mopidy.frontends.mpd import (handle_pattern, mpd_commands,
|
||||
MpdNotImplemented)
|
||||
|
||||
@handle_pattern(r'^commands$')
|
||||
def commands(frontend):
|
||||
"""
|
||||
*musicpd.org, reflection section:*
|
||||
|
||||
``commands``
|
||||
|
||||
Shows which commands the current user has access to.
|
||||
|
||||
As permissions is not implemented, any user has access to all commands.
|
||||
"""
|
||||
sorted_commands = sorted(list(mpd_commands))
|
||||
|
||||
# Not shown by MPD in its command list
|
||||
sorted_commands.remove('command_list_begin')
|
||||
sorted_commands.remove('command_list_ok_begin')
|
||||
sorted_commands.remove('command_list_end')
|
||||
sorted_commands.remove('idle')
|
||||
sorted_commands.remove('noidle')
|
||||
sorted_commands.remove('sticker')
|
||||
|
||||
return [('command', c) for c in sorted_commands]
|
||||
|
||||
@handle_pattern(r'^decoders$')
|
||||
def decoders(frontend):
|
||||
"""
|
||||
*musicpd.org, reflection section:*
|
||||
|
||||
``decoders``
|
||||
|
||||
Print a list of decoder plugins, followed by their supported
|
||||
suffixes and MIME types. Example response::
|
||||
|
||||
plugin: mad
|
||||
suffix: mp3
|
||||
suffix: mp2
|
||||
mime_type: audio/mpeg
|
||||
plugin: mpcdec
|
||||
suffix: mpc
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^notcommands$')
|
||||
def notcommands(frontend):
|
||||
"""
|
||||
*musicpd.org, reflection section:*
|
||||
|
||||
``notcommands``
|
||||
|
||||
Shows which commands the current user does not have access to.
|
||||
|
||||
As permissions is not implemented, any user has access to all commands.
|
||||
"""
|
||||
pass
|
||||
|
||||
@handle_pattern(r'^tagtypes$')
|
||||
def tagtypes(frontend):
|
||||
"""
|
||||
*musicpd.org, reflection section:*
|
||||
|
||||
``tagtypes``
|
||||
|
||||
Shows a list of available song metadata.
|
||||
"""
|
||||
pass # TODO
|
||||
|
||||
@handle_pattern(r'^urlhandlers$')
|
||||
def urlhandlers(frontend):
|
||||
"""
|
||||
*musicpd.org, reflection section:*
|
||||
|
||||
``urlhandlers``
|
||||
|
||||
Gets a list of available URL handlers.
|
||||
"""
|
||||
return [(u'handler', uri) for uri in frontend.backend.uri_handlers]
|
||||
216
mopidy/frontends/mpd/protocol/status.py
Normal file
216
mopidy/frontends/mpd/protocol/status.py
Normal file
@ -0,0 +1,216 @@
|
||||
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
|
||||
|
||||
@handle_pattern(r'^clearerror$')
|
||||
def clearerror(frontend):
|
||||
"""
|
||||
*musicpd.org, status section:*
|
||||
|
||||
``clearerror``
|
||||
|
||||
Clears the current error message in status (this is also
|
||||
accomplished by any command that starts playback).
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^currentsong$')
|
||||
def currentsong(frontend):
|
||||
"""
|
||||
*musicpd.org, status section:*
|
||||
|
||||
``currentsong``
|
||||
|
||||
Displays the song info of the current song (same song that is
|
||||
identified in status).
|
||||
"""
|
||||
if frontend.backend.playback.current_track is not None:
|
||||
return frontend.backend.playback.current_track.mpd_format(
|
||||
position=frontend.backend.playback.current_playlist_position,
|
||||
cpid=frontend.backend.playback.current_cpid)
|
||||
|
||||
@handle_pattern(r'^idle$')
|
||||
@handle_pattern(r'^idle (?P<subsystems>.+)$')
|
||||
def idle(frontend, subsystems=None):
|
||||
"""
|
||||
*musicpd.org, status section:*
|
||||
|
||||
``idle [SUBSYSTEMS...]``
|
||||
|
||||
Waits until there is a noteworthy change in one or more of MPD's
|
||||
subsystems. As soon as there is one, it lists all changed systems
|
||||
in a line in the format ``changed: SUBSYSTEM``, where ``SUBSYSTEM``
|
||||
is one of the following:
|
||||
|
||||
- ``database``: the song database has been modified after update.
|
||||
- ``update``: a database update has started or finished. If the
|
||||
database was modified during the update, the database event is
|
||||
also emitted.
|
||||
- ``stored_playlist``: a stored playlist has been modified,
|
||||
renamed, created or deleted
|
||||
- ``playlist``: the current playlist has been modified
|
||||
- ``player``: the player has been started, stopped or seeked
|
||||
- ``mixer``: the volume has been changed
|
||||
- ``output``: an audio output has been enabled or disabled
|
||||
- ``options``: options like repeat, random, crossfade, replay gain
|
||||
|
||||
While a client is waiting for idle results, the server disables
|
||||
timeouts, allowing a client to wait for events as long as MPD runs.
|
||||
The idle command can be canceled by sending the command ``noidle``
|
||||
(no other commands are allowed). MPD will then leave idle mode and
|
||||
print results immediately; might be empty at this time.
|
||||
|
||||
If the optional ``SUBSYSTEMS`` argument is used, MPD will only send
|
||||
notifications when something changed in one of the specified
|
||||
subsystems.
|
||||
"""
|
||||
pass # TODO
|
||||
|
||||
@handle_pattern(r'^noidle$')
|
||||
def noidle(frontend):
|
||||
"""See :meth:`_status_idle`."""
|
||||
pass # TODO
|
||||
|
||||
@handle_pattern(r'^stats$')
|
||||
def stats(frontend):
|
||||
"""
|
||||
*musicpd.org, status section:*
|
||||
|
||||
``stats``
|
||||
|
||||
Displays statistics.
|
||||
|
||||
- ``artists``: number of artists
|
||||
- ``songs``: number of albums
|
||||
- ``uptime``: daemon uptime in seconds
|
||||
- ``db_playtime``: sum of all song times in the db
|
||||
- ``db_update``: last db update in UNIX time
|
||||
- ``playtime``: time length of music played
|
||||
"""
|
||||
return {
|
||||
'artists': 0, # TODO
|
||||
'albums': 0, # TODO
|
||||
'songs': 0, # TODO
|
||||
# TODO Does not work after multiprocessing branch merge
|
||||
'uptime': 0, # frontend.session.stats_uptime(),
|
||||
'db_playtime': 0, # TODO
|
||||
'db_update': 0, # TODO
|
||||
'playtime': 0, # TODO
|
||||
}
|
||||
|
||||
@handle_pattern(r'^status$')
|
||||
def status(frontend):
|
||||
"""
|
||||
*musicpd.org, status section:*
|
||||
|
||||
``status``
|
||||
|
||||
Reports the current status of the player and the volume level.
|
||||
|
||||
- ``volume``: 0-100
|
||||
- ``repeat``: 0 or 1
|
||||
- ``single``: 0 or 1
|
||||
- ``consume``: 0 or 1
|
||||
- ``playlist``: 31-bit unsigned integer, the playlist version
|
||||
number
|
||||
- ``playlistlength``: integer, the length of the playlist
|
||||
- ``state``: play, stop, or pause
|
||||
- ``song``: playlist song number of the current song stopped on or
|
||||
playing
|
||||
- ``songid``: playlist songid of the current song stopped on or
|
||||
playing
|
||||
- ``nextsong``: playlist song number of the next song to be played
|
||||
- ``nextsongid``: playlist songid of the next song to be played
|
||||
- ``time``: total time elapsed (of current playing/paused song)
|
||||
- ``elapsed``: Total time elapsed within the current song, but with
|
||||
higher resolution.
|
||||
- ``bitrate``: instantaneous bitrate in kbps
|
||||
- ``xfade``: crossfade in seconds
|
||||
- ``audio``: sampleRate``:bits``:channels
|
||||
- ``updatings_db``: job id
|
||||
- ``error``: if there is an error, returns message here
|
||||
"""
|
||||
result = [
|
||||
('volume', _status_volume(frontend)),
|
||||
('repeat', _status_repeat(frontend)),
|
||||
('random', _status_random(frontend)),
|
||||
('single', _status_single(frontend)),
|
||||
('consume', _status_consume(frontend)),
|
||||
('playlist', _status_playlist_version(frontend)),
|
||||
('playlistlength', _status_playlist_length(frontend)),
|
||||
('xfade', _status_xfade(frontend)),
|
||||
('state', _status_state(frontend)),
|
||||
]
|
||||
if frontend.backend.playback.current_track is not None:
|
||||
result.append(('song', _status_songpos(frontend)))
|
||||
result.append(('songid', _status_songid(frontend)))
|
||||
if frontend.backend.playback.state in (frontend.backend.playback.PLAYING,
|
||||
frontend.backend.playback.PAUSED):
|
||||
result.append(('time', _status_time(frontend)))
|
||||
result.append(('elapsed', _status_time_elapsed(frontend)))
|
||||
result.append(('bitrate', _status_bitrate(frontend)))
|
||||
return result
|
||||
|
||||
def _status_bitrate(frontend):
|
||||
if frontend.backend.playback.current_track is not None:
|
||||
return frontend.backend.playback.current_track.bitrate
|
||||
|
||||
def _status_consume(frontend):
|
||||
if frontend.backend.playback.consume:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _status_playlist_length(frontend):
|
||||
return len(frontend.backend.current_playlist.tracks)
|
||||
|
||||
def _status_playlist_version(frontend):
|
||||
return frontend.backend.current_playlist.version
|
||||
|
||||
def _status_random(frontend):
|
||||
return int(frontend.backend.playback.random)
|
||||
|
||||
def _status_repeat(frontend):
|
||||
return int(frontend.backend.playback.repeat)
|
||||
|
||||
def _status_single(frontend):
|
||||
return int(frontend.backend.playback.single)
|
||||
|
||||
def _status_songid(frontend):
|
||||
if frontend.backend.playback.current_cpid is not None:
|
||||
return frontend.backend.playback.current_cpid
|
||||
else:
|
||||
return _status_songpos(frontend)
|
||||
|
||||
def _status_songpos(frontend):
|
||||
return frontend.backend.playback.current_playlist_position
|
||||
|
||||
def _status_state(frontend):
|
||||
if frontend.backend.playback.state == frontend.backend.playback.PLAYING:
|
||||
return u'play'
|
||||
elif frontend.backend.playback.state == frontend.backend.playback.STOPPED:
|
||||
return u'stop'
|
||||
elif frontend.backend.playback.state == frontend.backend.playback.PAUSED:
|
||||
return u'pause'
|
||||
|
||||
def _status_time(frontend):
|
||||
return u'%s:%s' % (_status_time_elapsed(frontend) // 1000,
|
||||
_status_time_total(frontend) // 1000)
|
||||
|
||||
def _status_time_elapsed(frontend):
|
||||
return frontend.backend.playback.time_position
|
||||
|
||||
def _status_time_total(frontend):
|
||||
if frontend.backend.playback.current_track is None:
|
||||
return 0
|
||||
elif frontend.backend.playback.current_track.length is None:
|
||||
return 0
|
||||
else:
|
||||
return frontend.backend.playback.current_track.length
|
||||
|
||||
def _status_volume(frontend):
|
||||
if frontend.backend.mixer.volume is not None:
|
||||
return frontend.backend.mixer.volume
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _status_xfade(frontend):
|
||||
return 0 # TODO
|
||||
64
mopidy/frontends/mpd/protocol/stickers.py
Normal file
64
mopidy/frontends/mpd/protocol/stickers.py
Normal file
@ -0,0 +1,64 @@
|
||||
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
|
||||
|
||||
@handle_pattern(r'^sticker delete "(?P<field>[^"]+)" '
|
||||
r'"(?P<uri>[^"]+)"( "(?P<name>[^"]+)")*$')
|
||||
def sticker_delete(frontend, field, uri, name=None):
|
||||
"""
|
||||
*musicpd.org, sticker section:*
|
||||
|
||||
``sticker delete {TYPE} {URI} [NAME]``
|
||||
|
||||
Deletes a sticker value from the specified object. If you do not
|
||||
specify a sticker name, all sticker values are deleted.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^sticker find "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||
r'"(?P<name>[^"]+)"$')
|
||||
def sticker_find(frontend, field, uri, name):
|
||||
"""
|
||||
*musicpd.org, sticker section:*
|
||||
|
||||
``sticker find {TYPE} {URI} {NAME}``
|
||||
|
||||
Searches the sticker database for stickers with the specified name,
|
||||
below the specified directory (``URI``). For each matching song, it
|
||||
prints the ``URI`` and that one sticker's value.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^sticker get "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||
r'"(?P<name>[^"]+)"$')
|
||||
def sticker_get(frontend, field, uri, name):
|
||||
"""
|
||||
*musicpd.org, sticker section:*
|
||||
|
||||
``sticker get {TYPE} {URI} {NAME}``
|
||||
|
||||
Reads a sticker value for the specified object.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^sticker list "(?P<field>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||
def sticker_list(frontend, field, uri):
|
||||
"""
|
||||
*musicpd.org, sticker section:*
|
||||
|
||||
``sticker list {TYPE} {URI}``
|
||||
|
||||
Lists the stickers for the specified object.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^sticker set "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||
r'"(?P<name>[^"]+)" "(?P<value>[^"]+)"$')
|
||||
def sticker_set(frontend, field, uri, name, value):
|
||||
"""
|
||||
*musicpd.org, sticker section:*
|
||||
|
||||
``sticker set {TYPE} {URI} {NAME} {VALUE}``
|
||||
|
||||
Adds a sticker value to the specified object. If a sticker item
|
||||
with that name already exists, it is replaced.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
180
mopidy/frontends/mpd/protocol/stored_playlists.py
Normal file
180
mopidy/frontends/mpd/protocol/stored_playlists.py
Normal file
@ -0,0 +1,180 @@
|
||||
import datetime as dt
|
||||
|
||||
from mopidy.frontends.mpd import (handle_pattern, MpdNoExistError,
|
||||
MpdNotImplemented)
|
||||
|
||||
@handle_pattern(r'^listplaylist "(?P<name>[^"]+)"$')
|
||||
def listplaylist(frontend, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
``listplaylist {NAME}``
|
||||
|
||||
Lists the files in the playlist ``NAME.m3u``.
|
||||
|
||||
Output format::
|
||||
|
||||
file: relative/path/to/file1.flac
|
||||
file: relative/path/to/file2.ogg
|
||||
file: relative/path/to/file3.mp3
|
||||
"""
|
||||
try:
|
||||
return ['file: %s' % t.uri
|
||||
for t in frontend.backend.stored_playlists.get(name=name).tracks]
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such playlist', command=u'listplaylist')
|
||||
|
||||
@handle_pattern(r'^listplaylistinfo "(?P<name>[^"]+)"$')
|
||||
def listplaylistinfo(frontend, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
``listplaylistinfo {NAME}``
|
||||
|
||||
Lists songs in the playlist ``NAME.m3u``.
|
||||
|
||||
Output format:
|
||||
|
||||
Standard track listing, with fields: file, Time, Title, Date,
|
||||
Album, Artist, Track
|
||||
"""
|
||||
try:
|
||||
return frontend.backend.stored_playlists.get(name=name).mpd_format()
|
||||
except LookupError:
|
||||
raise MpdNoExistError(
|
||||
u'No such playlist', command=u'listplaylistinfo')
|
||||
|
||||
@handle_pattern(r'^listplaylists$')
|
||||
def listplaylists(frontend):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
``listplaylists``
|
||||
|
||||
Prints a list of the playlist directory.
|
||||
|
||||
After each playlist name the server sends its last modification
|
||||
time as attribute ``Last-Modified`` in ISO 8601 format. To avoid
|
||||
problems due to clock differences between clients and the server,
|
||||
clients should not compare this value with their local clock.
|
||||
|
||||
Output format::
|
||||
|
||||
playlist: a
|
||||
Last-Modified: 2010-02-06T02:10:25Z
|
||||
playlist: b
|
||||
Last-Modified: 2010-02-06T02:11:08Z
|
||||
"""
|
||||
result = []
|
||||
for playlist in frontend.backend.stored_playlists.playlists:
|
||||
result.append((u'playlist', playlist.name))
|
||||
last_modified = (playlist.last_modified or
|
||||
dt.datetime.now()).isoformat()
|
||||
# Remove microseconds
|
||||
last_modified = last_modified.split('.')[0]
|
||||
# Add time zone information
|
||||
# TODO Convert to UTC before adding Z
|
||||
last_modified = last_modified + 'Z'
|
||||
result.append((u'Last-Modified', last_modified))
|
||||
return result
|
||||
|
||||
@handle_pattern(r'^load "(?P<name>[^"]+)"$')
|
||||
def load(frontend, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
``load {NAME}``
|
||||
|
||||
Loads the playlist ``NAME.m3u`` from the playlist directory.
|
||||
"""
|
||||
matches = frontend.backend.stored_playlists.search(name)
|
||||
if matches:
|
||||
frontend.backend.current_playlist.load(matches[0].tracks)
|
||||
|
||||
@handle_pattern(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||
def playlistadd(frontend, name, uri):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
``playlistadd {NAME} {URI}``
|
||||
|
||||
Adds ``URI`` to the playlist ``NAME.m3u``.
|
||||
|
||||
``NAME.m3u`` will be created if it does not exist.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^playlistclear "(?P<name>[^"]+)"$')
|
||||
def playlistclear(frontend, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
``playlistclear {NAME}``
|
||||
|
||||
Clears the playlist ``NAME.m3u``.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^playlistdelete "(?P<name>[^"]+)" "(?P<songpos>\d+)"$')
|
||||
def playlistdelete(frontend, name, songpos):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
``playlistdelete {NAME} {SONGPOS}``
|
||||
|
||||
Deletes ``SONGPOS`` from the playlist ``NAME.m3u``.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^playlistmove "(?P<name>[^"]+)" '
|
||||
r'"(?P<from_pos>\d+)" "(?P<to_pos>\d+)"$')
|
||||
def playlistmove(frontend, name, from_pos, to_pos):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
``playlistmove {NAME} {SONGID} {SONGPOS}``
|
||||
|
||||
Moves ``SONGID`` in the playlist ``NAME.m3u`` to the position
|
||||
``SONGPOS``.
|
||||
|
||||
*Clarifications:*
|
||||
|
||||
- The second argument is not a ``SONGID`` as used elsewhere in the
|
||||
protocol documentation, but just the ``SONGPOS`` to move *from*,
|
||||
i.e. ``playlistmove {NAME} {FROM_SONGPOS} {TO_SONGPOS}``.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^rename "(?P<old_name>[^"]+)" "(?P<new_name>[^"]+)"$')
|
||||
def rename(frontend, old_name, new_name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
``rename {NAME} {NEW_NAME}``
|
||||
|
||||
Renames the playlist ``NAME.m3u`` to ``NEW_NAME.m3u``.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^rm "(?P<name>[^"]+)"$')
|
||||
def rm(frontend, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
``rm {NAME}``
|
||||
|
||||
Removes the playlist ``NAME.m3u`` from the playlist directory.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^save "(?P<name>[^"]+)"$')
|
||||
def save(frontend, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
``save {NAME}``
|
||||
|
||||
Saves the current playlist to ``NAME.m3u`` in the playlist
|
||||
directory.
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
@ -58,7 +58,7 @@ def tracks_to_mpd_format(tracks, start=0, end=None, cpids=None):
|
||||
end = len(tracks)
|
||||
tracks = tracks[start:end]
|
||||
positions = range(start, end)
|
||||
cpids = cpids and cpids[start:end] or [None for t in tracks]
|
||||
cpids = cpids and cpids[start:end] or [None for _ in tracks]
|
||||
assert len(tracks) == len(positions) == len(cpids)
|
||||
result = []
|
||||
for track, position, cpid in zip(tracks, positions, cpids):
|
||||
@ -1,7 +1,3 @@
|
||||
"""
|
||||
This is our MPD server implementation.
|
||||
"""
|
||||
|
||||
import asynchat
|
||||
import asyncore
|
||||
import logging
|
||||
@ -11,15 +7,10 @@ import socket
|
||||
import sys
|
||||
|
||||
from mopidy import get_mpd_protocol_version, settings
|
||||
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR
|
||||
from mopidy.utils import indent, pickle_connection
|
||||
|
||||
logger = logging.getLogger('mopidy.mpd.server')
|
||||
|
||||
#: The MPD protocol uses UTF-8 for encoding all data.
|
||||
ENCODING = u'utf-8'
|
||||
|
||||
#: The MPD protocol uses ``\n`` as line terminator.
|
||||
LINE_TERMINATOR = u'\n'
|
||||
logger = logging.getLogger('mopidy.frontends.mpd.server')
|
||||
|
||||
class MpdServer(asyncore.dispatcher):
|
||||
"""
|
||||
@ -31,6 +22,7 @@ class MpdServer(asyncore.dispatcher):
|
||||
self.core_queue = core_queue
|
||||
|
||||
def start(self):
|
||||
"""Start MPD server."""
|
||||
try:
|
||||
if socket.has_ipv6:
|
||||
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
@ -49,6 +41,7 @@ class MpdServer(asyncore.dispatcher):
|
||||
sys.exit('MPD server startup failed: %s' % e)
|
||||
|
||||
def handle_accept(self):
|
||||
"""Handle new client connection."""
|
||||
(client_socket, client_socket_address) = self.accept()
|
||||
logger.info(u'MPD client connection from [%s]:%s',
|
||||
client_socket_address[0], client_socket_address[1])
|
||||
@ -56,6 +49,7 @@ class MpdServer(asyncore.dispatcher):
|
||||
self.core_queue).start()
|
||||
|
||||
def handle_close(self):
|
||||
"""Handle end of client connection."""
|
||||
self.close()
|
||||
|
||||
def _format_hostname(self, hostname):
|
||||
@ -67,7 +61,8 @@ class MpdServer(asyncore.dispatcher):
|
||||
|
||||
class MpdSession(asynchat.async_chat):
|
||||
"""
|
||||
The MPD client session. Dispatches MPD requests to the frontend.
|
||||
The MPD client session. Keeps track of a single client and dispatches its
|
||||
MPD requests to the frontend.
|
||||
"""
|
||||
|
||||
def __init__(self, server, client_socket, client_socket_address,
|
||||
@ -81,12 +76,15 @@ class MpdSession(asynchat.async_chat):
|
||||
self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
|
||||
|
||||
def start(self):
|
||||
"""Start a new client session."""
|
||||
self.send_response(u'OK MPD %s' % get_mpd_protocol_version())
|
||||
|
||||
def collect_incoming_data(self, data):
|
||||
"""Collect incoming data into buffer until a terminator is found."""
|
||||
self.input_buffer.append(data)
|
||||
|
||||
def found_terminator(self):
|
||||
"""Handle request when a terminator is found."""
|
||||
data = ''.join(self.input_buffer).strip()
|
||||
self.input_buffer = []
|
||||
try:
|
||||
@ -98,6 +96,7 @@ class MpdSession(asynchat.async_chat):
|
||||
logger.warning(u'Received invalid data: %s', e)
|
||||
|
||||
def handle_request(self, request):
|
||||
"""Handle request by sending it to the MPD frontend."""
|
||||
my_end, other_end = multiprocessing.Pipe()
|
||||
self.core_queue.put({
|
||||
'command': 'mpd_request',
|
||||
@ -110,9 +109,11 @@ class MpdSession(asynchat.async_chat):
|
||||
self.handle_response(response)
|
||||
|
||||
def handle_response(self, response):
|
||||
"""Handle response from the MPD frontend."""
|
||||
self.send_response(LINE_TERMINATOR.join(response))
|
||||
|
||||
def send_response(self, output):
|
||||
"""Send a response to the client."""
|
||||
logger.debug(u'Output to [%s]:%s: %s', self.client_address,
|
||||
self.client_port, indent(output))
|
||||
output = u'%s%s' % (output, LINE_TERMINATOR)
|
||||
@ -26,7 +26,8 @@ class AlsaMixer(BaseMixer):
|
||||
return control
|
||||
else:
|
||||
logger.debug(u'Mixer control not found, skipping: %s', control)
|
||||
logger.warning(u'No working mixer controls found. Tried: %s', candidates)
|
||||
logger.warning(u'No working mixer controls found. Tried: %s',
|
||||
candidates)
|
||||
|
||||
def _get_mixer_control_candidates(self):
|
||||
"""
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from copy import copy
|
||||
|
||||
from mopidy.mpd import serializer
|
||||
from mopidy.frontends.mpd import serializer
|
||||
|
||||
class ImmutableObject(object):
|
||||
"""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -41,8 +41,8 @@ DUMP_LOG_FILENAME = u'dump.log'
|
||||
|
||||
#: Protocol frontend to use. Default::
|
||||
#:
|
||||
#: FRONTEND = u'mopidy.mpd.frontend.MpdFrontend'
|
||||
FRONTEND = u'mopidy.mpd.frontend.MpdFrontend'
|
||||
#: FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend'
|
||||
FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend'
|
||||
|
||||
#: Path to folder with local music. Default::
|
||||
#:
|
||||
@ -107,8 +107,8 @@ MIXER_EXT_SPEAKERS_B = None
|
||||
|
||||
#: Server to use. Default::
|
||||
#:
|
||||
#: SERVER = u'mopidy.mpd.server.MpdServer'
|
||||
SERVER = u'mopidy.mpd.server.MpdServer'
|
||||
#: SERVER = u'mopidy.frontends.mpd.server.MpdServer'
|
||||
SERVER = u'mopidy.frontends.mpd.server.MpdServer'
|
||||
|
||||
#: Which address Mopidy should bind to. Examples:
|
||||
#:
|
||||
|
||||
@ -18,11 +18,15 @@ def flatten(the_list):
|
||||
result.append(element)
|
||||
return result
|
||||
|
||||
def import_module(name):
|
||||
__import__(name)
|
||||
return sys.modules[name]
|
||||
|
||||
def get_class(name):
|
||||
module_name = name[:name.rindex('.')]
|
||||
class_name = name[name.rindex('.') + 1:]
|
||||
logger.debug('Loading: %s', name)
|
||||
module = __import__(module_name, globals(), locals(), [class_name], -1)
|
||||
module = import_module(module_name)
|
||||
class_object = getattr(module, class_name)
|
||||
return class_object
|
||||
|
||||
|
||||
0
tests/frontends/__init__.py
Normal file
0
tests/frontends/__init__.py
Normal file
0
tests/frontends/mpd/__init__.py
Normal file
0
tests/frontends/mpd/__init__.py
Normal file
@ -1,8 +1,8 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import frontend
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.mpd import frontend
|
||||
|
||||
class AudioOutputHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -1,8 +1,8 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import frontend
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.mpd import frontend
|
||||
|
||||
class CommandListsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -1,8 +1,8 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import frontend
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.mpd import frontend
|
||||
|
||||
class ConnectionHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -1,9 +1,9 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import frontend
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.models import Track
|
||||
from mopidy.mpd import frontend
|
||||
|
||||
class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -1,6 +1,7 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.mpd import MpdAckError, MpdUnknownCommand, MpdNotImplemented
|
||||
from mopidy.frontends.mpd import (MpdAckError, MpdUnknownCommand,
|
||||
MpdNotImplemented)
|
||||
|
||||
class MpdExceptionsTest(unittest.TestCase):
|
||||
def test_key_error_wrapped_in_mpd_ack_error(self):
|
||||
@ -1,8 +1,8 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import frontend
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.mpd import frontend
|
||||
|
||||
class MusicDatabaseHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -1,9 +1,9 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import frontend
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.models import Track
|
||||
from mopidy.mpd import frontend
|
||||
|
||||
class PlaybackOptionsHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -180,6 +180,13 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||
self.assertEqual(self.b.playback.current_track, track)
|
||||
|
||||
def test_play_minus_one_on_empty_playlist_does_not_ack(self):
|
||||
self.b.current_playlist.clear()
|
||||
result = self.h.handle_request(u'play "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(self.b.playback.STOPPED, self.b.playback.state)
|
||||
self.assertEqual(self.b.playback.current_track, None)
|
||||
|
||||
def test_playid(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
result = self.h.handle_request(u'playid "1"')
|
||||
@ -194,6 +201,13 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||
self.assertEqual(self.b.playback.current_track, track)
|
||||
|
||||
def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
|
||||
self.b.current_playlist.clear()
|
||||
result = self.h.handle_request(u'playid "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(self.b.playback.STOPPED, self.b.playback.state)
|
||||
self.assertEqual(self.b.playback.current_track, None)
|
||||
|
||||
def test_playid_which_does_not_exist(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
result = self.h.handle_request(u'playid "12345"')
|
||||
@ -1,8 +1,8 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import frontend
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.mpd import frontend
|
||||
|
||||
class ReflectionHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -1,8 +1,8 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import frontend, MpdAckError
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.mpd import frontend, MpdAckError
|
||||
|
||||
class RequestHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -29,7 +29,7 @@ class RequestHandlerTest(unittest.TestCase):
|
||||
|
||||
def test_finding_handler_for_known_command_returns_handler_and_kwargs(self):
|
||||
expected_handler = lambda x: None
|
||||
frontend._request_handlers['known_command (?P<arg1>.+)'] = \
|
||||
frontend.request_handlers['known_command (?P<arg1>.+)'] = \
|
||||
expected_handler
|
||||
(handler, kwargs) = self.h.find_handler('known_command an_arg')
|
||||
self.assertEqual(handler, expected_handler)
|
||||
@ -42,7 +42,7 @@ class RequestHandlerTest(unittest.TestCase):
|
||||
|
||||
def test_handling_known_request(self):
|
||||
expected = 'magic'
|
||||
frontend._request_handlers['known request'] = lambda x: expected
|
||||
frontend.request_handlers['known request'] = lambda x: expected
|
||||
result = self.h.handle_request('known request')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(expected in result)
|
||||
@ -1,8 +1,8 @@
|
||||
import datetime as dt
|
||||
import unittest
|
||||
|
||||
from mopidy.frontends.mpd import serializer
|
||||
from mopidy.models import Album, Artist, Playlist, Track
|
||||
from mopidy.mpd import serializer
|
||||
|
||||
class TrackMpdFormatTest(unittest.TestCase):
|
||||
def test_mpd_format_for_empty_track(self):
|
||||
@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.mpd import server
|
||||
from mopidy.frontends.mpd import server
|
||||
|
||||
class MpdServerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -1,9 +1,9 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import frontend
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.models import Track
|
||||
from mopidy.mpd import frontend
|
||||
|
||||
class StatusHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -52,7 +52,7 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_stats_method(self):
|
||||
result = self.h._status_stats()
|
||||
result = frontend.status.stats(self.h)
|
||||
self.assert_('artists' in result)
|
||||
self.assert_(int(result['artists']) >= 0)
|
||||
self.assert_('albums' in result)
|
||||
@ -73,106 +73,106 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_status_method_contains_volume_which_defaults_to_0(self):
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('volume' in result)
|
||||
self.assertEqual(int(result['volume']), 0)
|
||||
|
||||
def test_status_method_contains_volume(self):
|
||||
self.b.mixer.volume = 17
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('volume' in result)
|
||||
self.assertEqual(int(result['volume']), 17)
|
||||
|
||||
def test_status_method_contains_repeat_is_0(self):
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('repeat' in result)
|
||||
self.assertEqual(int(result['repeat']), 0)
|
||||
|
||||
def test_status_method_contains_repeat_is_1(self):
|
||||
self.b.playback.repeat = 1
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('repeat' in result)
|
||||
self.assertEqual(int(result['repeat']), 1)
|
||||
|
||||
def test_status_method_contains_random_is_0(self):
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('random' in result)
|
||||
self.assertEqual(int(result['random']), 0)
|
||||
|
||||
def test_status_method_contains_random_is_1(self):
|
||||
self.b.playback.random = 1
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('random' in result)
|
||||
self.assertEqual(int(result['random']), 1)
|
||||
|
||||
def test_status_method_contains_single(self):
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('single' in result)
|
||||
self.assert_(int(result['single']) in (0, 1))
|
||||
|
||||
def test_status_method_contains_consume_is_0(self):
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('consume' in result)
|
||||
self.assertEqual(int(result['consume']), 0)
|
||||
|
||||
def test_status_method_contains_consume_is_1(self):
|
||||
self.b.playback.consume = 1
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('consume' in result)
|
||||
self.assertEqual(int(result['consume']), 1)
|
||||
|
||||
def test_status_method_contains_playlist(self):
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('playlist' in result)
|
||||
self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1))
|
||||
|
||||
def test_status_method_contains_playlistlength(self):
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('playlistlength' in result)
|
||||
self.assert_(int(result['playlistlength']) >= 0)
|
||||
|
||||
def test_status_method_contains_xfade(self):
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('xfade' in result)
|
||||
self.assert_(int(result['xfade']) >= 0)
|
||||
|
||||
def test_status_method_contains_state_is_play(self):
|
||||
self.b.playback.state = self.b.playback.PLAYING
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('state' in result)
|
||||
self.assertEqual(result['state'], 'play')
|
||||
|
||||
def test_status_method_contains_state_is_stop(self):
|
||||
self.b.playback.state = self.b.playback.STOPPED
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('state' in result)
|
||||
self.assertEqual(result['state'], 'stop')
|
||||
|
||||
def test_status_method_contains_state_is_pause(self):
|
||||
self.b.playback.state = self.b.playback.PLAYING
|
||||
self.b.playback.state = self.b.playback.PAUSED
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('state' in result)
|
||||
self.assertEqual(result['state'], 'pause')
|
||||
|
||||
def test_status_method_when_playlist_loaded_contains_song(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.playback.play()
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('song' in result)
|
||||
self.assert_(int(result['song']) >= 0)
|
||||
|
||||
def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
|
||||
self.b.current_playlist.load([Track()])
|
||||
self.b.playback.play()
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('songid' in result)
|
||||
self.assertEqual(int(result['songid']), 1)
|
||||
|
||||
def test_status_method_when_playing_contains_time_with_no_length(self):
|
||||
self.b.current_playlist.load([Track(length=None)])
|
||||
self.b.playback.play()
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('time' in result)
|
||||
(position, total) = result['time'].split(':')
|
||||
position = int(position)
|
||||
@ -182,7 +182,7 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
def test_status_method_when_playing_contains_time_with_length(self):
|
||||
self.b.current_playlist.load([Track(length=10000)])
|
||||
self.b.playback.play()
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('time' in result)
|
||||
(position, total) = result['time'].split(':')
|
||||
position = int(position)
|
||||
@ -192,13 +192,13 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
def test_status_method_when_playing_contains_elapsed(self):
|
||||
self.b.playback.state = self.b.playback.PAUSED
|
||||
self.b.playback._play_time_accumulated = 59123
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('elapsed' in result)
|
||||
self.assertEqual(int(result['elapsed']), 59123)
|
||||
|
||||
def test_status_method_when_playing_contains_bitrate(self):
|
||||
self.b.current_playlist.load([Track(bitrate=320)])
|
||||
self.b.playback.play()
|
||||
result = dict(self.h._status_status())
|
||||
result = dict(frontend.status.status(self.h))
|
||||
self.assert_('bitrate' in result)
|
||||
self.assertEqual(int(result['bitrate']), 320)
|
||||
@ -1,8 +1,8 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import frontend
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.mpd import frontend
|
||||
|
||||
class StickersHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -2,9 +2,9 @@ import datetime as dt
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import frontend
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.models import Track, Playlist
|
||||
from mopidy.mpd import frontend
|
||||
|
||||
from tests import SkipTest
|
||||
|
||||
Loading…
Reference in New Issue
Block a user