Merge branch 'master' of git://github.com/jodal/mopidy into gstreamer

This commit is contained in:
Johannes Knutsen 2010-08-10 14:59:55 +02:00
commit f098919aa0
54 changed files with 2921 additions and 2704 deletions

View File

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

View File

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

View File

@ -23,6 +23,6 @@ Data model API
==============
.. automodule:: mopidy.models
:synopsis: Immutable data models.
:synopsis: Data model API
:members:
:undoc-members:

View File

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

View File

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

View File

@ -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'])``.

View File

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

View File

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

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

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

View 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

View 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

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

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

View 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

View 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

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

View 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

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

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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View 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 CommandListsTest(unittest.TestCase):
def setUp(self):

View 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 ConnectionHandlerTest(unittest.TestCase):
def setUp(self):

View File

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

View File

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

View 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 MusicDatabaseHandlerTest(unittest.TestCase):
def setUp(self):

View File

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

View 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 ReflectionHandlerTest(unittest.TestCase):
def setUp(self):

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import unittest
from mopidy.mpd import server
from mopidy.frontends.mpd import server
class MpdServerTest(unittest.TestCase):
def setUp(self):

View File

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

View 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 StickersHandlerTest(unittest.TestCase):
def setUp(self):

View File

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