MPD: Split protocol implementation into 11 modules
This commit is contained in:
parent
ffe665572b
commit
d0aac71cfb
112
docs/api/mpd.rst
112
docs/api/mpd.rst
@ -6,21 +6,109 @@
|
|||||||
:synopsis: MPD frontend
|
:synopsis: MPD frontend
|
||||||
|
|
||||||
|
|
||||||
MPD protocol implementation
|
MPD server
|
||||||
===========================
|
==========
|
||||||
|
|
||||||
.. automodule:: mopidy.frontends.mpd.frontend
|
|
||||||
:synopsis: MPD protocol implementation
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
|
|
||||||
MPD server implementation
|
|
||||||
=========================
|
|
||||||
|
|
||||||
.. automodule:: mopidy.frontends.mpd.server
|
.. automodule:: mopidy.frontends.mpd.server
|
||||||
:synopsis: MPD server implementation
|
:synopsis: MPD server
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
.. inheritance-diagram:: mopidy.frontends.mpd.server
|
.. inheritance-diagram:: mopidy.frontends.mpd.server
|
||||||
|
|
||||||
|
|
||||||
|
MPD frontend
|
||||||
|
============
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.frontend
|
||||||
|
:synopsis: MPD request dispatcher
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
|
MPD protocol
|
||||||
|
============
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.protocol
|
||||||
|
:synopsis: MPD protocol
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
|
Audio output
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.protocol.audio_output
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
|
Command list
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.protocol.command_list
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
|
Connection
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.protocol.connection
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
|
Current playlist
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.protocol.current_playlist
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
Music database
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.protocol.music_db
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
|
Playback
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.protocol.playback
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
|
Reflection
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.protocol.reflection
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
|
Status
|
||||||
|
------
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.protocol.status
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
|
Stickers
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.protocol.stickers
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
|
Stored playlists
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. automodule:: mopidy.frontends.mpd.protocol.stored_playlists
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|||||||
@ -18,6 +18,7 @@ Another great release.
|
|||||||
- MPD frontend:
|
- MPD frontend:
|
||||||
|
|
||||||
- Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`.
|
- 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.
|
- Search improvements, including support for multi-word search.
|
||||||
- Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty.
|
- Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty.
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from mopidy import MopidyException
|
from mopidy import MopidyException
|
||||||
|
|
||||||
class MpdAckError(MopidyException):
|
class MpdAckError(MopidyException):
|
||||||
@ -55,3 +57,37 @@ class MpdNotImplemented(MpdAckError):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(MpdNotImplemented, self).__init__(*args, **kwargs)
|
super(MpdNotImplemented, self).__init__(*args, **kwargs)
|
||||||
self.message = u'Not implemented'
|
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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
17
mopidy/frontends/mpd/protocol/__init__.py
Normal file
17
mopidy/frontends/mpd/protocol/__init__.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"""
|
||||||
|
This is Mopidy's MPD protocol implementation.
|
||||||
|
|
||||||
|
This is partly based upon the `MPD protocol documentation
|
||||||
|
<http://www.musicpd.org/doc/protocol/>`_, which is a useful resource, but it is
|
||||||
|
rather incomplete with regards to data formats, both for requests and
|
||||||
|
responses. Thus, we have had to talk a great deal with the the original `MPD
|
||||||
|
server <http://mpd.wikia.com/>`_ using telnet to get the details we need to
|
||||||
|
implement our own MPD server which is compatible with the numerous existing
|
||||||
|
`MPD clients <http://mpd.wikia.com/wiki/Clients>`_.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: The MPD protocol uses UTF-8 for encoding all data.
|
||||||
|
ENCODING = u'utf-8'
|
||||||
|
|
||||||
|
#: The MPD protocol uses ``\n`` as line terminator.
|
||||||
|
LINE_TERMINATOR = u'\n'
|
||||||
38
mopidy/frontends/mpd/protocol/audio_output.py
Normal file
38
mopidy/frontends/mpd/protocol/audio_output.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
|
||||||
|
|
||||||
|
@handle_pattern(r'^disableoutput "(?P<outputid>\d+)"$')
|
||||||
|
def disableoutput(frontend, outputid):
|
||||||
|
"""
|
||||||
|
*musicpd.org, audio output section:*
|
||||||
|
|
||||||
|
``disableoutput``
|
||||||
|
|
||||||
|
Turns an output off.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^enableoutput "(?P<outputid>\d+)"$')
|
||||||
|
def enableoutput(frontend, outputid):
|
||||||
|
"""
|
||||||
|
*musicpd.org, audio output section:*
|
||||||
|
|
||||||
|
``enableoutput``
|
||||||
|
|
||||||
|
Turns an output on.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^outputs$')
|
||||||
|
def outputs(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, audio output section:*
|
||||||
|
|
||||||
|
``outputs``
|
||||||
|
|
||||||
|
Shows information about all outputs.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
('outputid', 0),
|
||||||
|
('outputname', frontend.backend.__class__.__name__),
|
||||||
|
('outputenabled', 1),
|
||||||
|
]
|
||||||
47
mopidy/frontends/mpd/protocol/command_list.py
Normal file
47
mopidy/frontends/mpd/protocol/command_list.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from mopidy.frontends.mpd import handle_pattern, MpdUnknownCommand
|
||||||
|
|
||||||
|
@handle_pattern(r'^command_list_begin$')
|
||||||
|
def command_list_begin(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, command list section:*
|
||||||
|
|
||||||
|
To facilitate faster adding of files etc. you can pass a list of
|
||||||
|
commands all at once using a command list. The command list begins
|
||||||
|
with ``command_list_begin`` or ``command_list_ok_begin`` and ends
|
||||||
|
with ``command_list_end``.
|
||||||
|
|
||||||
|
It does not execute any commands until the list has ended. The
|
||||||
|
return value is whatever the return for a list of commands is. On
|
||||||
|
success for all commands, ``OK`` is returned. If a command fails,
|
||||||
|
no more commands are executed and the appropriate ``ACK`` error is
|
||||||
|
returned. If ``command_list_ok_begin`` is used, ``list_OK`` is
|
||||||
|
returned for each successful command executed in the command list.
|
||||||
|
"""
|
||||||
|
frontend.command_list = []
|
||||||
|
frontend.command_list_ok = False
|
||||||
|
|
||||||
|
@handle_pattern(r'^command_list_end$')
|
||||||
|
def command_list_end(frontend):
|
||||||
|
"""See :meth:`command_list_begin()`."""
|
||||||
|
if frontend.command_list is False:
|
||||||
|
# Test for False exactly, and not e.g. empty list
|
||||||
|
raise MpdUnknownCommand(command='command_list_end')
|
||||||
|
(command_list, frontend.command_list) = (frontend.command_list, False)
|
||||||
|
(command_list_ok, frontend.command_list_ok) = (
|
||||||
|
frontend.command_list_ok, False)
|
||||||
|
result = []
|
||||||
|
for i, command in enumerate(command_list):
|
||||||
|
response = frontend.handle_request(command, command_list_index=i)
|
||||||
|
if response is not None:
|
||||||
|
result.append(response)
|
||||||
|
if response and response[-1].startswith(u'ACK'):
|
||||||
|
return result
|
||||||
|
if command_list_ok:
|
||||||
|
response.append(u'list_OK')
|
||||||
|
return result
|
||||||
|
|
||||||
|
@handle_pattern(r'^command_list_ok_begin$')
|
||||||
|
def command_list_ok_begin(frontend):
|
||||||
|
"""See :meth:`command_list_begin()`."""
|
||||||
|
frontend.command_list = []
|
||||||
|
frontend.command_list_ok = True
|
||||||
48
mopidy/frontends/mpd/protocol/connection.py
Normal file
48
mopidy/frontends/mpd/protocol/connection.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
|
||||||
|
|
||||||
|
@handle_pattern(r'^close$')
|
||||||
|
def close(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, connection section:*
|
||||||
|
|
||||||
|
``close``
|
||||||
|
|
||||||
|
Closes the connection to MPD.
|
||||||
|
"""
|
||||||
|
# TODO Does not work after multiprocessing branch merge
|
||||||
|
#frontend.session.do_close()
|
||||||
|
|
||||||
|
@handle_pattern(r'^kill$')
|
||||||
|
def kill(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, connection section:*
|
||||||
|
|
||||||
|
``kill``
|
||||||
|
|
||||||
|
Kills MPD.
|
||||||
|
"""
|
||||||
|
# TODO Does not work after multiprocessing branch merge
|
||||||
|
#frontend.session.do_kill()
|
||||||
|
|
||||||
|
@handle_pattern(r'^password "(?P<password>[^"]+)"$')
|
||||||
|
def password_(frontend, password):
|
||||||
|
"""
|
||||||
|
*musicpd.org, connection section:*
|
||||||
|
|
||||||
|
``password {PASSWORD}``
|
||||||
|
|
||||||
|
This is used for authentication with the server. ``PASSWORD`` is
|
||||||
|
simply the plaintext password.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^ping$')
|
||||||
|
def ping(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, connection section:*
|
||||||
|
|
||||||
|
``ping``
|
||||||
|
|
||||||
|
Does nothing but return ``OK``.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
351
mopidy/frontends/mpd/protocol/current_playlist.py
Normal file
351
mopidy/frontends/mpd/protocol/current_playlist.py
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError,
|
||||||
|
MpdNotImplemented)
|
||||||
|
|
||||||
|
@handle_pattern(r'^add "(?P<uri>[^"]*)"$')
|
||||||
|
def add(frontend, uri):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``add {URI}``
|
||||||
|
|
||||||
|
Adds the file ``URI`` to the playlist (directories add recursively).
|
||||||
|
``URI`` can also be a single file.
|
||||||
|
"""
|
||||||
|
track = frontend.backend.library.lookup(uri)
|
||||||
|
if track is None:
|
||||||
|
raise MpdNoExistError(
|
||||||
|
u'directory or file not found', command=u'add')
|
||||||
|
frontend.backend.current_playlist.add(track)
|
||||||
|
|
||||||
|
@handle_pattern(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$')
|
||||||
|
def addid(frontend, uri, songpos=None):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``addid {URI} [POSITION]``
|
||||||
|
|
||||||
|
Adds a song to the playlist (non-recursive) and returns the song id.
|
||||||
|
|
||||||
|
``URI`` is always a single file or URL. For example::
|
||||||
|
|
||||||
|
addid "foo.mp3"
|
||||||
|
Id: 999
|
||||||
|
OK
|
||||||
|
"""
|
||||||
|
if songpos is not None:
|
||||||
|
songpos = int(songpos)
|
||||||
|
track = frontend.backend.library.lookup(uri)
|
||||||
|
if track is None:
|
||||||
|
raise MpdNoExistError(u'No such song', command=u'addid')
|
||||||
|
if songpos and songpos > len(frontend.backend.current_playlist.tracks):
|
||||||
|
raise MpdArgError(u'Bad song index', command=u'addid')
|
||||||
|
cp_track = frontend.backend.current_playlist.add(track, at_position=songpos)
|
||||||
|
return ('Id', cp_track[0])
|
||||||
|
|
||||||
|
@handle_pattern(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||||
|
def delete_range(frontend, start, end=None):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``delete [{POS} | {START:END}]``
|
||||||
|
|
||||||
|
Deletes a song from the playlist.
|
||||||
|
"""
|
||||||
|
start = int(start)
|
||||||
|
if end is not None:
|
||||||
|
end = int(end)
|
||||||
|
else:
|
||||||
|
end = len(frontend.backend.current_playlist.tracks)
|
||||||
|
cp_tracks = frontend.backend.current_playlist.cp_tracks[start:end]
|
||||||
|
if not cp_tracks:
|
||||||
|
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||||
|
for (cpid, _) in cp_tracks:
|
||||||
|
frontend.backend.current_playlist.remove(cpid=cpid)
|
||||||
|
|
||||||
|
@handle_pattern(r'^delete "(?P<songpos>\d+)"$')
|
||||||
|
def delete_songpos(frontend, songpos):
|
||||||
|
"""See :meth:`delete_range`"""
|
||||||
|
try:
|
||||||
|
songpos = int(songpos)
|
||||||
|
(cpid, _) = frontend.backend.current_playlist.cp_tracks[songpos]
|
||||||
|
frontend.backend.current_playlist.remove(cpid=cpid)
|
||||||
|
except IndexError:
|
||||||
|
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||||
|
|
||||||
|
@handle_pattern(r'^deleteid "(?P<cpid>\d+)"$')
|
||||||
|
def deleteid(frontend, cpid):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``deleteid {SONGID}``
|
||||||
|
|
||||||
|
Deletes the song ``SONGID`` from the playlist
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cpid = int(cpid)
|
||||||
|
return frontend.backend.current_playlist.remove(cpid=cpid)
|
||||||
|
except LookupError:
|
||||||
|
raise MpdNoExistError(u'No such song', command=u'deleteid')
|
||||||
|
|
||||||
|
@handle_pattern(r'^clear$')
|
||||||
|
def clear(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``clear``
|
||||||
|
|
||||||
|
Clears the current playlist.
|
||||||
|
"""
|
||||||
|
frontend.backend.current_playlist.clear()
|
||||||
|
|
||||||
|
@handle_pattern(r'^move "(?P<start>\d+):(?P<end>\d+)*" "(?P<to>\d+)"$')
|
||||||
|
def move_range(frontend, start, to, end=None):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``move [{FROM} | {START:END}] {TO}``
|
||||||
|
|
||||||
|
Moves the song at ``FROM`` or range of songs at ``START:END`` to
|
||||||
|
``TO`` in the playlist.
|
||||||
|
"""
|
||||||
|
if end is None:
|
||||||
|
end = len(frontend.backend.current_playlist.tracks)
|
||||||
|
start = int(start)
|
||||||
|
end = int(end)
|
||||||
|
to = int(to)
|
||||||
|
frontend.backend.current_playlist.move(start, end, to)
|
||||||
|
|
||||||
|
@handle_pattern(r'^move "(?P<songpos>\d+)" "(?P<to>\d+)"$')
|
||||||
|
def move_songpos(frontend, songpos, to):
|
||||||
|
"""See :meth:`move_range`."""
|
||||||
|
songpos = int(songpos)
|
||||||
|
to = int(to)
|
||||||
|
frontend.backend.current_playlist.move(songpos, songpos + 1, to)
|
||||||
|
|
||||||
|
@handle_pattern(r'^moveid "(?P<cpid>\d+)" "(?P<to>\d+)"$')
|
||||||
|
def moveid(frontend, cpid, to):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``moveid {FROM} {TO}``
|
||||||
|
|
||||||
|
Moves the song with ``FROM`` (songid) to ``TO`` (playlist index) in
|
||||||
|
the playlist. If ``TO`` is negative, it is relative to the current
|
||||||
|
song in the playlist (if there is one).
|
||||||
|
"""
|
||||||
|
cpid = int(cpid)
|
||||||
|
to = int(to)
|
||||||
|
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
|
||||||
|
position = frontend.backend.current_playlist.cp_tracks.index(cp_track)
|
||||||
|
frontend.backend.current_playlist.move(position, position + 1, to)
|
||||||
|
|
||||||
|
@handle_pattern(r'^playlist$')
|
||||||
|
def playlist(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``playlist``
|
||||||
|
|
||||||
|
Displays the current playlist.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Do not use this, instead use ``playlistinfo``.
|
||||||
|
"""
|
||||||
|
return playlistinfo(frontend)
|
||||||
|
|
||||||
|
@handle_pattern(r'^playlistfind (?P<tag>[^"]+) "(?P<needle>[^"]+)"$')
|
||||||
|
@handle_pattern(r'^playlistfind "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
||||||
|
def playlistfind(frontend, tag, needle):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``playlistfind {TAG} {NEEDLE}``
|
||||||
|
|
||||||
|
Finds songs in the current playlist with strict matching.
|
||||||
|
|
||||||
|
*GMPC:*
|
||||||
|
|
||||||
|
- does not add quotes around the tag.
|
||||||
|
"""
|
||||||
|
if tag == 'filename':
|
||||||
|
try:
|
||||||
|
cp_track = frontend.backend.current_playlist.get(uri=needle)
|
||||||
|
return cp_track[1].mpd_format()
|
||||||
|
except LookupError:
|
||||||
|
return None
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^playlistid( "(?P<cpid>\d+)")*$')
|
||||||
|
def playlistid(frontend, cpid=None):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``playlistid {SONGID}``
|
||||||
|
|
||||||
|
Displays a list of songs in the playlist. ``SONGID`` is optional
|
||||||
|
and specifies a single song to display info for.
|
||||||
|
"""
|
||||||
|
if cpid is not None:
|
||||||
|
try:
|
||||||
|
cpid = int(cpid)
|
||||||
|
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
|
||||||
|
position = frontend.backend.current_playlist.cp_tracks.index(
|
||||||
|
cp_track)
|
||||||
|
return cp_track[1].mpd_format(position=position, cpid=cpid)
|
||||||
|
except LookupError:
|
||||||
|
raise MpdNoExistError(u'No such song', command=u'playlistid')
|
||||||
|
else:
|
||||||
|
return frontend.backend.current_playlist.mpd_format()
|
||||||
|
|
||||||
|
@handle_pattern(r'^playlistinfo$')
|
||||||
|
@handle_pattern(r'^playlistinfo "(?P<songpos>-?\d+)"$')
|
||||||
|
@handle_pattern(r'^playlistinfo "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||||
|
def playlistinfo(frontend, songpos=None,
|
||||||
|
start=None, end=None):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``playlistinfo [[SONGPOS] | [START:END]]``
|
||||||
|
|
||||||
|
Displays a list of all songs in the playlist, or if the optional
|
||||||
|
argument is given, displays information only for the song
|
||||||
|
``SONGPOS`` or the range of songs ``START:END``.
|
||||||
|
|
||||||
|
*ncmpc and mpc:*
|
||||||
|
|
||||||
|
- uses negative indexes, like ``playlistinfo "-1"``, to request
|
||||||
|
the entire playlist
|
||||||
|
"""
|
||||||
|
if songpos == "-1":
|
||||||
|
songpos = None
|
||||||
|
|
||||||
|
if songpos is not None:
|
||||||
|
songpos = int(songpos)
|
||||||
|
start = songpos
|
||||||
|
end = songpos + 1
|
||||||
|
if start == -1:
|
||||||
|
end = None
|
||||||
|
return frontend.backend.current_playlist.mpd_format(start, end)
|
||||||
|
else:
|
||||||
|
if start is None:
|
||||||
|
start = 0
|
||||||
|
start = int(start)
|
||||||
|
if not (0 <= start <= len(frontend.backend.current_playlist.tracks)):
|
||||||
|
raise MpdArgError(u'Bad song index', command=u'playlistinfo')
|
||||||
|
if end is not None:
|
||||||
|
end = int(end)
|
||||||
|
if end > len(frontend.backend.current_playlist.tracks):
|
||||||
|
end = None
|
||||||
|
return frontend.backend.current_playlist.mpd_format(start, end)
|
||||||
|
|
||||||
|
@handle_pattern(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
||||||
|
@handle_pattern(r'^playlistsearch (?P<tag>\S+) "(?P<needle>[^"]+)"$')
|
||||||
|
def playlistsearch(frontend, tag, needle):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``playlistsearch {TAG} {NEEDLE}``
|
||||||
|
|
||||||
|
Searches case-sensitively for partial matches in the current
|
||||||
|
playlist.
|
||||||
|
|
||||||
|
*GMPC:*
|
||||||
|
|
||||||
|
- does not add quotes around the tag
|
||||||
|
- uses ``filename`` and ``any`` as tags
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^plchanges "(?P<version>\d+)"$')
|
||||||
|
def plchanges(frontend, version):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``plchanges {VERSION}``
|
||||||
|
|
||||||
|
Displays changed songs currently in the playlist since ``VERSION``.
|
||||||
|
|
||||||
|
To detect songs that were deleted at the end of the playlist, use
|
||||||
|
``playlistlength`` returned by status command.
|
||||||
|
"""
|
||||||
|
# XXX Naive implementation that returns all tracks as changed
|
||||||
|
if int(version) < frontend.backend.current_playlist.version:
|
||||||
|
return frontend.backend.current_playlist.mpd_format()
|
||||||
|
|
||||||
|
@handle_pattern(r'^plchangesposid "(?P<version>\d+)"$')
|
||||||
|
def plchangesposid(frontend, version):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``plchangesposid {VERSION}``
|
||||||
|
|
||||||
|
Displays changed songs currently in the playlist since ``VERSION``.
|
||||||
|
This function only returns the position and the id of the changed
|
||||||
|
song, not the complete metadata. This is more bandwidth efficient.
|
||||||
|
|
||||||
|
To detect songs that were deleted at the end of the playlist, use
|
||||||
|
``playlistlength`` returned by status command.
|
||||||
|
"""
|
||||||
|
# XXX Naive implementation that returns all tracks as changed
|
||||||
|
if int(version) != frontend.backend.current_playlist.version:
|
||||||
|
result = []
|
||||||
|
for (position, (cpid, _)) in enumerate(
|
||||||
|
frontend.backend.current_playlist.cp_tracks):
|
||||||
|
result.append((u'cpos', position))
|
||||||
|
result.append((u'Id', cpid))
|
||||||
|
return result
|
||||||
|
|
||||||
|
@handle_pattern(r'^shuffle$')
|
||||||
|
@handle_pattern(r'^shuffle "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||||
|
def shuffle(frontend, start=None, end=None):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``shuffle [START:END]``
|
||||||
|
|
||||||
|
Shuffles the current playlist. ``START:END`` is optional and
|
||||||
|
specifies a range of songs.
|
||||||
|
"""
|
||||||
|
if start is not None:
|
||||||
|
start = int(start)
|
||||||
|
if end is not None:
|
||||||
|
end = int(end)
|
||||||
|
frontend.backend.current_playlist.shuffle(start, end)
|
||||||
|
|
||||||
|
@handle_pattern(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$')
|
||||||
|
def swap(frontend, songpos1, songpos2):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``swap {SONG1} {SONG2}``
|
||||||
|
|
||||||
|
Swaps the positions of ``SONG1`` and ``SONG2``.
|
||||||
|
"""
|
||||||
|
songpos1 = int(songpos1)
|
||||||
|
songpos2 = int(songpos2)
|
||||||
|
tracks = frontend.backend.current_playlist.tracks
|
||||||
|
song1 = tracks[songpos1]
|
||||||
|
song2 = tracks[songpos2]
|
||||||
|
del tracks[songpos1]
|
||||||
|
tracks.insert(songpos1, song2)
|
||||||
|
del tracks[songpos2]
|
||||||
|
tracks.insert(songpos2, song1)
|
||||||
|
frontend.backend.current_playlist.load(tracks)
|
||||||
|
|
||||||
|
@handle_pattern(r'^swapid "(?P<cpid1>\d+)" "(?P<cpid2>\d+)"$')
|
||||||
|
def swapid(frontend, cpid1, cpid2):
|
||||||
|
"""
|
||||||
|
*musicpd.org, current playlist section:*
|
||||||
|
|
||||||
|
``swapid {SONG1} {SONG2}``
|
||||||
|
|
||||||
|
Swaps the positions of ``SONG1`` and ``SONG2`` (both song ids).
|
||||||
|
"""
|
||||||
|
cpid1 = int(cpid1)
|
||||||
|
cpid2 = int(cpid2)
|
||||||
|
cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1)
|
||||||
|
cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2)
|
||||||
|
position1 = frontend.backend.current_playlist.cp_tracks.index(cp_track1)
|
||||||
|
position2 = frontend.backend.current_playlist.cp_tracks.index(cp_track2)
|
||||||
|
swap(frontend, position1, position2)
|
||||||
254
mopidy/frontends/mpd/protocol/music_db.py
Normal file
254
mopidy/frontends/mpd/protocol/music_db.py
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
|
||||||
|
from mopidy.frontends.mpd.protocol import stored_playlists
|
||||||
|
|
||||||
|
def build_query(mpd_query):
|
||||||
|
"""
|
||||||
|
Parses a MPD query string and converts it to the Mopidy query format.
|
||||||
|
"""
|
||||||
|
query_pattern = (
|
||||||
|
r'"?(?:[Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? "[^"]+"')
|
||||||
|
query_parts = re.findall(query_pattern, mpd_query)
|
||||||
|
query_part_pattern = (
|
||||||
|
r'"?(?P<field>([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny))"? '
|
||||||
|
r'"(?P<what>[^"]+)"')
|
||||||
|
query = {}
|
||||||
|
for query_part in query_parts:
|
||||||
|
m = re.match(query_part_pattern, query_part)
|
||||||
|
field = m.groupdict()['field'].lower()
|
||||||
|
if field == u'title':
|
||||||
|
field = u'track'
|
||||||
|
field = str(field) # Needed for kwargs keys on OS X and Windows
|
||||||
|
what = m.groupdict()['what'].lower()
|
||||||
|
if field in query:
|
||||||
|
query[field].append(what)
|
||||||
|
else:
|
||||||
|
query[field] = [what]
|
||||||
|
return query
|
||||||
|
|
||||||
|
@handle_pattern(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
|
||||||
|
def count(frontend, tag, needle):
|
||||||
|
"""
|
||||||
|
*musicpd.org, music database section:*
|
||||||
|
|
||||||
|
``count {TAG} {NEEDLE}``
|
||||||
|
|
||||||
|
Counts the number of songs and their total playtime in the db
|
||||||
|
matching ``TAG`` exactly.
|
||||||
|
"""
|
||||||
|
return [('songs', 0), ('playtime', 0)] # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^find '
|
||||||
|
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
|
||||||
|
' "[^"]+"\s?)+)$')
|
||||||
|
def find(frontend, mpd_query):
|
||||||
|
"""
|
||||||
|
*musicpd.org, music database section:*
|
||||||
|
|
||||||
|
``find {TYPE} {WHAT}``
|
||||||
|
|
||||||
|
Finds songs in the db that are exactly ``WHAT``. ``TYPE`` should be
|
||||||
|
``album``, ``artist``, or ``title``. ``WHAT`` is what to find.
|
||||||
|
|
||||||
|
*GMPC:*
|
||||||
|
|
||||||
|
- does not add quotes around the field argument.
|
||||||
|
- also uses ``find album "[ALBUM]" artist "[ARTIST]"`` to list album
|
||||||
|
tracks.
|
||||||
|
|
||||||
|
*ncmpc:*
|
||||||
|
|
||||||
|
- does not add quotes around the field argument.
|
||||||
|
- capitalizes the type argument.
|
||||||
|
"""
|
||||||
|
query = build_query(mpd_query)
|
||||||
|
return frontend.backend.library.find_exact(**query).mpd_format()
|
||||||
|
|
||||||
|
@handle_pattern(r'^findadd '
|
||||||
|
r'(?P<query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
|
||||||
|
'"[^"]+"\s?)+)$')
|
||||||
|
def findadd(frontend, query):
|
||||||
|
"""
|
||||||
|
*musicpd.org, music database section:*
|
||||||
|
|
||||||
|
``findadd {TYPE} {WHAT}``
|
||||||
|
|
||||||
|
Finds songs in the db that are exactly ``WHAT`` and adds them to
|
||||||
|
current playlist. ``TYPE`` can be any tag supported by MPD.
|
||||||
|
``WHAT`` is what to find.
|
||||||
|
"""
|
||||||
|
# TODO Add result to current playlist
|
||||||
|
#result = frontend.find(query)
|
||||||
|
|
||||||
|
@handle_pattern(r'^list (?P<field>[Aa]rtist)$')
|
||||||
|
@handle_pattern(r'^list "(?P<field>[Aa]rtist)"$')
|
||||||
|
@handle_pattern(r'^list (?P<field>album( artist)?)'
|
||||||
|
'( "(?P<artist>[^"]+)")*$')
|
||||||
|
@handle_pattern(r'^list "(?P<field>album(" "artist)?)"'
|
||||||
|
'( "(?P<artist>[^"]+)")*$')
|
||||||
|
def list_(frontend, field, artist=None):
|
||||||
|
"""
|
||||||
|
*musicpd.org, music database section:*
|
||||||
|
|
||||||
|
``list {TYPE} [ARTIST]``
|
||||||
|
|
||||||
|
Lists all tags of the specified type. ``TYPE`` should be ``album``,
|
||||||
|
``artist``, ``date``, or ``genre``.
|
||||||
|
|
||||||
|
``ARTIST`` is an optional parameter when type is ``album``,
|
||||||
|
``date``, or ``genre``.
|
||||||
|
|
||||||
|
This filters the result list by an artist.
|
||||||
|
|
||||||
|
*GMPC:*
|
||||||
|
|
||||||
|
- does not add quotes around the field argument.
|
||||||
|
- asks for "list artist" to get available artists and will not query
|
||||||
|
for artist/album information if this is not retrived
|
||||||
|
- asks for multiple fields, i.e.::
|
||||||
|
|
||||||
|
list album artist "an artist name"
|
||||||
|
|
||||||
|
returns the albums available for the asked artist::
|
||||||
|
|
||||||
|
list album artist "Tiesto"
|
||||||
|
Album: Radio Trance Vol 4-Promo-CD
|
||||||
|
Album: Ur A Tear in the Open CDR
|
||||||
|
Album: Simple Trance 2004 Step One
|
||||||
|
Album: In Concert 05-10-2003
|
||||||
|
|
||||||
|
*ncmpc:*
|
||||||
|
|
||||||
|
- does not add quotes around the field argument.
|
||||||
|
- capitalizes the field argument.
|
||||||
|
"""
|
||||||
|
field = field.lower()
|
||||||
|
if field == u'artist':
|
||||||
|
return _list_artist(frontend)
|
||||||
|
elif field == u'album artist':
|
||||||
|
return _list_album_artist(frontend, artist)
|
||||||
|
# TODO More to implement
|
||||||
|
|
||||||
|
def _list_artist(frontend):
|
||||||
|
"""
|
||||||
|
Since we don't know exactly all available artists, we respond with
|
||||||
|
the artists we know for sure, which is all artists in our stored playlists.
|
||||||
|
"""
|
||||||
|
artists = set()
|
||||||
|
for playlist in frontend.backend.stored_playlists.playlists:
|
||||||
|
for track in playlist.tracks:
|
||||||
|
for artist in track.artists:
|
||||||
|
artists.add((u'Artist', artist.name))
|
||||||
|
return artists
|
||||||
|
|
||||||
|
def _list_album_artist(frontend, artist):
|
||||||
|
playlist = frontend.backend.library.find_exact(artist=[artist])
|
||||||
|
albums = set()
|
||||||
|
for track in playlist.tracks:
|
||||||
|
albums.add((u'Album', track.album.name))
|
||||||
|
return albums
|
||||||
|
|
||||||
|
@handle_pattern(r'^listall "(?P<uri>[^"]+)"')
|
||||||
|
def listall(frontend, uri):
|
||||||
|
"""
|
||||||
|
*musicpd.org, music database section:*
|
||||||
|
|
||||||
|
``listall [URI]``
|
||||||
|
|
||||||
|
Lists all songs and directories in ``URI``.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^listallinfo "(?P<uri>[^"]+)"')
|
||||||
|
def listallinfo(frontend, uri):
|
||||||
|
"""
|
||||||
|
*musicpd.org, music database section:*
|
||||||
|
|
||||||
|
``listallinfo [URI]``
|
||||||
|
|
||||||
|
Same as ``listall``, except it also returns metadata info in the
|
||||||
|
same format as ``lsinfo``.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^lsinfo$')
|
||||||
|
@handle_pattern(r'^lsinfo "(?P<uri>[^"]*)"$')
|
||||||
|
def lsinfo(frontend, uri=None):
|
||||||
|
"""
|
||||||
|
*musicpd.org, music database section:*
|
||||||
|
|
||||||
|
``lsinfo [URI]``
|
||||||
|
|
||||||
|
Lists the contents of the directory ``URI``.
|
||||||
|
|
||||||
|
When listing the root directory, this currently returns the list of
|
||||||
|
stored playlists. This behavior is deprecated; use
|
||||||
|
``listplaylists`` instead.
|
||||||
|
|
||||||
|
MPD returns the same result, including both playlists and the files and
|
||||||
|
directories located at the root level, for both ``lsinfo``, ``lsinfo
|
||||||
|
""``, and ``lsinfo "/"``.
|
||||||
|
"""
|
||||||
|
if uri is None or uri == u'/' or uri == u'':
|
||||||
|
return stored_playlists.listplaylists(frontend)
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^rescan( "(?P<uri>[^"]+)")*$')
|
||||||
|
def rescan(frontend, uri=None):
|
||||||
|
"""
|
||||||
|
*musicpd.org, music database section:*
|
||||||
|
|
||||||
|
``rescan [URI]``
|
||||||
|
|
||||||
|
Same as ``update``, but also rescans unmodified files.
|
||||||
|
"""
|
||||||
|
return update(frontend, uri, rescan_unmodified_files=True)
|
||||||
|
|
||||||
|
@handle_pattern(r'^search '
|
||||||
|
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
|
||||||
|
' "[^"]+"\s?)+)$')
|
||||||
|
def search(frontend, mpd_query):
|
||||||
|
"""
|
||||||
|
*musicpd.org, music database section:*
|
||||||
|
|
||||||
|
``search {TYPE} {WHAT}``
|
||||||
|
|
||||||
|
Searches for any song that contains ``WHAT``. ``TYPE`` can be
|
||||||
|
``title``, ``artist``, ``album`` or ``filename``. Search is not
|
||||||
|
case sensitive.
|
||||||
|
|
||||||
|
*GMPC:*
|
||||||
|
|
||||||
|
- does not add quotes around the field argument.
|
||||||
|
- uses the undocumented field ``any``.
|
||||||
|
- searches for multiple words like this::
|
||||||
|
|
||||||
|
search any "foo" any "bar" any "baz"
|
||||||
|
|
||||||
|
*ncmpc:*
|
||||||
|
|
||||||
|
- does not add quotes around the field argument.
|
||||||
|
- capitalizes the field argument.
|
||||||
|
"""
|
||||||
|
query = build_query(mpd_query)
|
||||||
|
return frontend.backend.library.search(**query).mpd_format()
|
||||||
|
|
||||||
|
@handle_pattern(r'^update( "(?P<uri>[^"]+)")*$')
|
||||||
|
def update(frontend, uri=None, rescan_unmodified_files=False):
|
||||||
|
"""
|
||||||
|
*musicpd.org, music database section:*
|
||||||
|
|
||||||
|
``update [URI]``
|
||||||
|
|
||||||
|
Updates the music database: find new files, remove deleted files,
|
||||||
|
update modified files.
|
||||||
|
|
||||||
|
``URI`` is a particular directory or song/file to update. If you do
|
||||||
|
not specify it, everything is updated.
|
||||||
|
|
||||||
|
Prints ``updating_db: JOBID`` where ``JOBID`` is a positive number
|
||||||
|
identifying the update job. You can read the current job id in the
|
||||||
|
``status`` response.
|
||||||
|
"""
|
||||||
|
return {'updating_db': 0} # TODO
|
||||||
331
mopidy/frontends/mpd/protocol/playback.py
Normal file
331
mopidy/frontends/mpd/protocol/playback.py
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError,
|
||||||
|
MpdNotImplemented)
|
||||||
|
|
||||||
|
@handle_pattern(r'^consume "(?P<state>[01])"$')
|
||||||
|
def consume(frontend, state):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``consume {STATE}``
|
||||||
|
|
||||||
|
Sets consume state to ``STATE``, ``STATE`` should be 0 or
|
||||||
|
1. When consume is activated, each song played is removed from
|
||||||
|
playlist.
|
||||||
|
"""
|
||||||
|
if int(state):
|
||||||
|
frontend.backend.playback.consume = True
|
||||||
|
else:
|
||||||
|
frontend.backend.playback.consume = False
|
||||||
|
|
||||||
|
@handle_pattern(r'^crossfade "(?P<seconds>\d+)"$')
|
||||||
|
def crossfade(frontend, seconds):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``crossfade {SECONDS}``
|
||||||
|
|
||||||
|
Sets crossfading between songs.
|
||||||
|
"""
|
||||||
|
seconds = int(seconds)
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^next$')
|
||||||
|
def next_(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``next``
|
||||||
|
|
||||||
|
Plays next song in the playlist.
|
||||||
|
|
||||||
|
*MPD's behaviour when affected by repeat/random/single/consume:*
|
||||||
|
|
||||||
|
Given a playlist of three tracks numbered 1, 2, 3, and a currently
|
||||||
|
playing track ``c``. ``next_track`` is defined at the track that
|
||||||
|
will be played upon calls to ``next``.
|
||||||
|
|
||||||
|
Tests performed on MPD 0.15.4-1ubuntu3.
|
||||||
|
|
||||||
|
====== ====== ====== ======= ===== ===== ===== =====
|
||||||
|
Inputs next_track
|
||||||
|
------------------------------- ------------------- -----
|
||||||
|
repeat random single consume c = 1 c = 2 c = 3 Notes
|
||||||
|
====== ====== ====== ======= ===== ===== ===== =====
|
||||||
|
T T T T 2 3 EOPL
|
||||||
|
T T T . Rand Rand Rand [1]
|
||||||
|
T T . T Rand Rand Rand [4]
|
||||||
|
T T . . Rand Rand Rand [4]
|
||||||
|
T . T T 2 3 EOPL
|
||||||
|
T . T . 2 3 1
|
||||||
|
T . . T 3 3 EOPL
|
||||||
|
T . . . 2 3 1
|
||||||
|
. T T T Rand Rand Rand [3]
|
||||||
|
. T T . Rand Rand Rand [3]
|
||||||
|
. T . T Rand Rand Rand [2]
|
||||||
|
. T . . Rand Rand Rand [2]
|
||||||
|
. . T T 2 3 EOPL
|
||||||
|
. . T . 2 3 EOPL
|
||||||
|
. . . T 2 3 EOPL
|
||||||
|
. . . . 2 3 EOPL
|
||||||
|
====== ====== ====== ======= ===== ===== ===== =====
|
||||||
|
|
||||||
|
- When end of playlist (EOPL) is reached, the current track is
|
||||||
|
unset.
|
||||||
|
- [1] When *random* and *single* is combined, ``next`` selects
|
||||||
|
a track randomly at each invocation, and not just the next track
|
||||||
|
in an internal prerandomized playlist.
|
||||||
|
- [2] When *random* is active, ``next`` will skip through
|
||||||
|
all tracks in the playlist in random order, and finally EOPL is
|
||||||
|
reached.
|
||||||
|
- [3] *single* has no effect in combination with *random*
|
||||||
|
alone, or *random* and *consume*.
|
||||||
|
- [4] When *random* and *repeat* is active, EOPL is never
|
||||||
|
reached, but the playlist is played again, in the same random
|
||||||
|
order as the first time.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return frontend.backend.playback.next()
|
||||||
|
|
||||||
|
@handle_pattern(r'^pause "(?P<state>[01])"$')
|
||||||
|
def pause(frontend, state):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``pause {PAUSE}``
|
||||||
|
|
||||||
|
Toggles pause/resumes playing, ``PAUSE`` is 0 or 1.
|
||||||
|
"""
|
||||||
|
if int(state):
|
||||||
|
frontend.backend.playback.pause()
|
||||||
|
else:
|
||||||
|
frontend.backend.playback.resume()
|
||||||
|
|
||||||
|
@handle_pattern(r'^play$')
|
||||||
|
def play(frontend):
|
||||||
|
"""
|
||||||
|
The original MPD server resumes from the paused state on ``play``
|
||||||
|
without arguments.
|
||||||
|
"""
|
||||||
|
return frontend.backend.playback.play()
|
||||||
|
|
||||||
|
@handle_pattern(r'^playid "(?P<cpid>\d+)"$')
|
||||||
|
@handle_pattern(r'^playid "(?P<cpid>-1)"$')
|
||||||
|
def playid(frontend, cpid):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``playid [SONGID]``
|
||||||
|
|
||||||
|
Begins playing the playlist at song ``SONGID``.
|
||||||
|
|
||||||
|
*GMPC:*
|
||||||
|
|
||||||
|
- issues ``playid "-1"`` after playlist replacement to start playback
|
||||||
|
at the first track.
|
||||||
|
"""
|
||||||
|
cpid = int(cpid)
|
||||||
|
try:
|
||||||
|
if cpid == -1:
|
||||||
|
if not frontend.backend.current_playlist.cp_tracks:
|
||||||
|
return # Fail silently
|
||||||
|
cp_track = frontend.backend.current_playlist.cp_tracks[0]
|
||||||
|
else:
|
||||||
|
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
|
||||||
|
return frontend.backend.playback.play(cp_track)
|
||||||
|
except LookupError:
|
||||||
|
raise MpdNoExistError(u'No such song', command=u'playid')
|
||||||
|
|
||||||
|
@handle_pattern(r'^play "(?P<songpos>\d+)"$')
|
||||||
|
@handle_pattern(r'^play "(?P<songpos>-1)"$')
|
||||||
|
def playpos(frontend, songpos):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``play [SONGPOS]``
|
||||||
|
|
||||||
|
Begins playing the playlist at song number ``SONGPOS``.
|
||||||
|
|
||||||
|
*MPoD:*
|
||||||
|
|
||||||
|
- issues ``play "-1"`` after playlist replacement to start playback at
|
||||||
|
the first track.
|
||||||
|
"""
|
||||||
|
songpos = int(songpos)
|
||||||
|
try:
|
||||||
|
if songpos == -1:
|
||||||
|
if not frontend.backend.current_playlist.cp_tracks:
|
||||||
|
return # Fail silently
|
||||||
|
cp_track = frontend.backend.current_playlist.cp_tracks[0]
|
||||||
|
else:
|
||||||
|
cp_track = frontend.backend.current_playlist.cp_tracks[songpos]
|
||||||
|
return frontend.backend.playback.play(cp_track)
|
||||||
|
except IndexError:
|
||||||
|
raise MpdArgError(u'Bad song index', command=u'play')
|
||||||
|
|
||||||
|
@handle_pattern(r'^previous$')
|
||||||
|
def previous(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``previous``
|
||||||
|
|
||||||
|
Plays previous song in the playlist.
|
||||||
|
|
||||||
|
*MPD's behaviour when affected by repeat/random/single/consume:*
|
||||||
|
|
||||||
|
Given a playlist of three tracks numbered 1, 2, 3, and a currently
|
||||||
|
playing track ``c``. ``previous_track`` is defined at the track
|
||||||
|
that will be played upon ``previous`` calls.
|
||||||
|
|
||||||
|
Tests performed on MPD 0.15.4-1ubuntu3.
|
||||||
|
|
||||||
|
====== ====== ====== ======= ===== ===== =====
|
||||||
|
Inputs previous_track
|
||||||
|
------------------------------- -------------------
|
||||||
|
repeat random single consume c = 1 c = 2 c = 3
|
||||||
|
====== ====== ====== ======= ===== ===== =====
|
||||||
|
T T T T Rand? Rand? Rand?
|
||||||
|
T T T . 3 1 2
|
||||||
|
T T . T Rand? Rand? Rand?
|
||||||
|
T T . . 3 1 2
|
||||||
|
T . T T 3 1 2
|
||||||
|
T . T . 3 1 2
|
||||||
|
T . . T 3 1 2
|
||||||
|
T . . . 3 1 2
|
||||||
|
. T T T c c c
|
||||||
|
. T T . c c c
|
||||||
|
. T . T c c c
|
||||||
|
. T . . c c c
|
||||||
|
. . T T 1 1 2
|
||||||
|
. . T . 1 1 2
|
||||||
|
. . . T 1 1 2
|
||||||
|
. . . . 1 1 2
|
||||||
|
====== ====== ====== ======= ===== ===== =====
|
||||||
|
|
||||||
|
- If :attr:`time_position` of the current track is 15s or more,
|
||||||
|
``previous`` should do a seek to time position 0.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return frontend.backend.playback.previous()
|
||||||
|
|
||||||
|
@handle_pattern(r'^random "(?P<state>[01])"$')
|
||||||
|
def random(frontend, state):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``random {STATE}``
|
||||||
|
|
||||||
|
Sets random state to ``STATE``, ``STATE`` should be 0 or 1.
|
||||||
|
"""
|
||||||
|
if int(state):
|
||||||
|
frontend.backend.playback.random = True
|
||||||
|
else:
|
||||||
|
frontend.backend.playback.random = False
|
||||||
|
|
||||||
|
@handle_pattern(r'^repeat "(?P<state>[01])"$')
|
||||||
|
def repeat(frontend, state):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``repeat {STATE}``
|
||||||
|
|
||||||
|
Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1.
|
||||||
|
"""
|
||||||
|
if int(state):
|
||||||
|
frontend.backend.playback.repeat = True
|
||||||
|
else:
|
||||||
|
frontend.backend.playback.repeat = False
|
||||||
|
|
||||||
|
@handle_pattern(r'^replay_gain_mode "(?P<mode>(off|track|album))"$')
|
||||||
|
def replay_gain_mode(frontend, mode):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``replay_gain_mode {MODE}``
|
||||||
|
|
||||||
|
Sets the replay gain mode. One of ``off``, ``track``, ``album``.
|
||||||
|
|
||||||
|
Changing the mode during playback may take several seconds, because
|
||||||
|
the new settings does not affect the buffered data.
|
||||||
|
|
||||||
|
This command triggers the options idle event.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^replay_gain_status$')
|
||||||
|
def replay_gain_status(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``replay_gain_status``
|
||||||
|
|
||||||
|
Prints replay gain options. Currently, only the variable
|
||||||
|
``replay_gain_mode`` is returned.
|
||||||
|
"""
|
||||||
|
return u'off' # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^seek "(?P<songpos>\d+)" "(?P<seconds>\d+)"$')
|
||||||
|
def seek(frontend, songpos, seconds):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``seek {SONGPOS} {TIME}``
|
||||||
|
|
||||||
|
Seeks to the position ``TIME`` (in seconds) of entry ``SONGPOS`` in
|
||||||
|
the playlist.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^seekid "(?P<cpid>\d+)" "(?P<seconds>\d+)"$')
|
||||||
|
def seekid(frontend, cpid, seconds):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``seekid {SONGID} {TIME}``
|
||||||
|
|
||||||
|
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^setvol "(?P<volume>[-+]*\d+)"$')
|
||||||
|
def setvol(frontend, volume):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``setvol {VOL}``
|
||||||
|
|
||||||
|
Sets volume to ``VOL``, the range of volume is 0-100.
|
||||||
|
"""
|
||||||
|
volume = int(volume)
|
||||||
|
if volume < 0:
|
||||||
|
volume = 0
|
||||||
|
if volume > 100:
|
||||||
|
volume = 100
|
||||||
|
frontend.backend.mixer.volume = volume
|
||||||
|
|
||||||
|
@handle_pattern(r'^single "(?P<state>[01])"$')
|
||||||
|
def single(frontend, state):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``single {STATE}``
|
||||||
|
|
||||||
|
Sets single state to ``STATE``, ``STATE`` should be 0 or 1. When
|
||||||
|
single is activated, playback is stopped after current song, or
|
||||||
|
song is repeated if the ``repeat`` mode is enabled.
|
||||||
|
"""
|
||||||
|
if int(state):
|
||||||
|
frontend.backend.playback.single = True
|
||||||
|
else:
|
||||||
|
frontend.backend.playback.single = False
|
||||||
|
|
||||||
|
@handle_pattern(r'^stop$')
|
||||||
|
def stop(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, playback section:*
|
||||||
|
|
||||||
|
``stop``
|
||||||
|
|
||||||
|
Stops playing.
|
||||||
|
"""
|
||||||
|
frontend.backend.playback.stop()
|
||||||
79
mopidy/frontends/mpd/protocol/reflection.py
Normal file
79
mopidy/frontends/mpd/protocol/reflection.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
from mopidy.frontends.mpd import (handle_pattern, mpd_commands,
|
||||||
|
MpdNotImplemented)
|
||||||
|
|
||||||
|
@handle_pattern(r'^commands$')
|
||||||
|
def commands(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, reflection section:*
|
||||||
|
|
||||||
|
``commands``
|
||||||
|
|
||||||
|
Shows which commands the current user has access to.
|
||||||
|
|
||||||
|
As permissions is not implemented, any user has access to all commands.
|
||||||
|
"""
|
||||||
|
sorted_commands = sorted(list(mpd_commands))
|
||||||
|
|
||||||
|
# Not shown by MPD in its command list
|
||||||
|
sorted_commands.remove('command_list_begin')
|
||||||
|
sorted_commands.remove('command_list_ok_begin')
|
||||||
|
sorted_commands.remove('command_list_end')
|
||||||
|
sorted_commands.remove('idle')
|
||||||
|
sorted_commands.remove('noidle')
|
||||||
|
sorted_commands.remove('sticker')
|
||||||
|
|
||||||
|
return [('command', c) for c in sorted_commands]
|
||||||
|
|
||||||
|
@handle_pattern(r'^decoders$')
|
||||||
|
def decoders(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, reflection section:*
|
||||||
|
|
||||||
|
``decoders``
|
||||||
|
|
||||||
|
Print a list of decoder plugins, followed by their supported
|
||||||
|
suffixes and MIME types. Example response::
|
||||||
|
|
||||||
|
plugin: mad
|
||||||
|
suffix: mp3
|
||||||
|
suffix: mp2
|
||||||
|
mime_type: audio/mpeg
|
||||||
|
plugin: mpcdec
|
||||||
|
suffix: mpc
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^notcommands$')
|
||||||
|
def notcommands(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, reflection section:*
|
||||||
|
|
||||||
|
``notcommands``
|
||||||
|
|
||||||
|
Shows which commands the current user does not have access to.
|
||||||
|
|
||||||
|
As permissions is not implemented, any user has access to all commands.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@handle_pattern(r'^tagtypes$')
|
||||||
|
def tagtypes(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, reflection section:*
|
||||||
|
|
||||||
|
``tagtypes``
|
||||||
|
|
||||||
|
Shows a list of available song metadata.
|
||||||
|
"""
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^urlhandlers$')
|
||||||
|
def urlhandlers(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, reflection section:*
|
||||||
|
|
||||||
|
``urlhandlers``
|
||||||
|
|
||||||
|
Gets a list of available URL handlers.
|
||||||
|
"""
|
||||||
|
return [(u'handler', uri) for uri in frontend.backend.uri_handlers]
|
||||||
216
mopidy/frontends/mpd/protocol/status.py
Normal file
216
mopidy/frontends/mpd/protocol/status.py
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
|
||||||
|
|
||||||
|
@handle_pattern(r'^clearerror$')
|
||||||
|
def clearerror(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, status section:*
|
||||||
|
|
||||||
|
``clearerror``
|
||||||
|
|
||||||
|
Clears the current error message in status (this is also
|
||||||
|
accomplished by any command that starts playback).
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^currentsong$')
|
||||||
|
def currentsong(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, status section:*
|
||||||
|
|
||||||
|
``currentsong``
|
||||||
|
|
||||||
|
Displays the song info of the current song (same song that is
|
||||||
|
identified in status).
|
||||||
|
"""
|
||||||
|
if frontend.backend.playback.current_track is not None:
|
||||||
|
return frontend.backend.playback.current_track.mpd_format(
|
||||||
|
position=frontend.backend.playback.current_playlist_position,
|
||||||
|
cpid=frontend.backend.playback.current_cpid)
|
||||||
|
|
||||||
|
@handle_pattern(r'^idle$')
|
||||||
|
@handle_pattern(r'^idle (?P<subsystems>.+)$')
|
||||||
|
def idle(frontend, subsystems=None):
|
||||||
|
"""
|
||||||
|
*musicpd.org, status section:*
|
||||||
|
|
||||||
|
``idle [SUBSYSTEMS...]``
|
||||||
|
|
||||||
|
Waits until there is a noteworthy change in one or more of MPD's
|
||||||
|
subsystems. As soon as there is one, it lists all changed systems
|
||||||
|
in a line in the format ``changed: SUBSYSTEM``, where ``SUBSYSTEM``
|
||||||
|
is one of the following:
|
||||||
|
|
||||||
|
- ``database``: the song database has been modified after update.
|
||||||
|
- ``update``: a database update has started or finished. If the
|
||||||
|
database was modified during the update, the database event is
|
||||||
|
also emitted.
|
||||||
|
- ``stored_playlist``: a stored playlist has been modified,
|
||||||
|
renamed, created or deleted
|
||||||
|
- ``playlist``: the current playlist has been modified
|
||||||
|
- ``player``: the player has been started, stopped or seeked
|
||||||
|
- ``mixer``: the volume has been changed
|
||||||
|
- ``output``: an audio output has been enabled or disabled
|
||||||
|
- ``options``: options like repeat, random, crossfade, replay gain
|
||||||
|
|
||||||
|
While a client is waiting for idle results, the server disables
|
||||||
|
timeouts, allowing a client to wait for events as long as MPD runs.
|
||||||
|
The idle command can be canceled by sending the command ``noidle``
|
||||||
|
(no other commands are allowed). MPD will then leave idle mode and
|
||||||
|
print results immediately; might be empty at this time.
|
||||||
|
|
||||||
|
If the optional ``SUBSYSTEMS`` argument is used, MPD will only send
|
||||||
|
notifications when something changed in one of the specified
|
||||||
|
subsystems.
|
||||||
|
"""
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^noidle$')
|
||||||
|
def noidle(frontend):
|
||||||
|
"""See :meth:`_status_idle`."""
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^stats$')
|
||||||
|
def stats(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, status section:*
|
||||||
|
|
||||||
|
``stats``
|
||||||
|
|
||||||
|
Displays statistics.
|
||||||
|
|
||||||
|
- ``artists``: number of artists
|
||||||
|
- ``songs``: number of albums
|
||||||
|
- ``uptime``: daemon uptime in seconds
|
||||||
|
- ``db_playtime``: sum of all song times in the db
|
||||||
|
- ``db_update``: last db update in UNIX time
|
||||||
|
- ``playtime``: time length of music played
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'artists': 0, # TODO
|
||||||
|
'albums': 0, # TODO
|
||||||
|
'songs': 0, # TODO
|
||||||
|
# TODO Does not work after multiprocessing branch merge
|
||||||
|
'uptime': 0, # frontend.session.stats_uptime(),
|
||||||
|
'db_playtime': 0, # TODO
|
||||||
|
'db_update': 0, # TODO
|
||||||
|
'playtime': 0, # TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
@handle_pattern(r'^status$')
|
||||||
|
def status(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, status section:*
|
||||||
|
|
||||||
|
``status``
|
||||||
|
|
||||||
|
Reports the current status of the player and the volume level.
|
||||||
|
|
||||||
|
- ``volume``: 0-100
|
||||||
|
- ``repeat``: 0 or 1
|
||||||
|
- ``single``: 0 or 1
|
||||||
|
- ``consume``: 0 or 1
|
||||||
|
- ``playlist``: 31-bit unsigned integer, the playlist version
|
||||||
|
number
|
||||||
|
- ``playlistlength``: integer, the length of the playlist
|
||||||
|
- ``state``: play, stop, or pause
|
||||||
|
- ``song``: playlist song number of the current song stopped on or
|
||||||
|
playing
|
||||||
|
- ``songid``: playlist songid of the current song stopped on or
|
||||||
|
playing
|
||||||
|
- ``nextsong``: playlist song number of the next song to be played
|
||||||
|
- ``nextsongid``: playlist songid of the next song to be played
|
||||||
|
- ``time``: total time elapsed (of current playing/paused song)
|
||||||
|
- ``elapsed``: Total time elapsed within the current song, but with
|
||||||
|
higher resolution.
|
||||||
|
- ``bitrate``: instantaneous bitrate in kbps
|
||||||
|
- ``xfade``: crossfade in seconds
|
||||||
|
- ``audio``: sampleRate``:bits``:channels
|
||||||
|
- ``updatings_db``: job id
|
||||||
|
- ``error``: if there is an error, returns message here
|
||||||
|
"""
|
||||||
|
result = [
|
||||||
|
('volume', _status_volume(frontend)),
|
||||||
|
('repeat', _status_repeat(frontend)),
|
||||||
|
('random', _status_random(frontend)),
|
||||||
|
('single', _status_single(frontend)),
|
||||||
|
('consume', _status_consume(frontend)),
|
||||||
|
('playlist', _status_playlist_version(frontend)),
|
||||||
|
('playlistlength', _status_playlist_length(frontend)),
|
||||||
|
('xfade', _status_xfade(frontend)),
|
||||||
|
('state', _status_state(frontend)),
|
||||||
|
]
|
||||||
|
if frontend.backend.playback.current_track is not None:
|
||||||
|
result.append(('song', _status_songpos(frontend)))
|
||||||
|
result.append(('songid', _status_songid(frontend)))
|
||||||
|
if frontend.backend.playback.state in (frontend.backend.playback.PLAYING,
|
||||||
|
frontend.backend.playback.PAUSED):
|
||||||
|
result.append(('time', _status_time(frontend)))
|
||||||
|
result.append(('elapsed', _status_time_elapsed(frontend)))
|
||||||
|
result.append(('bitrate', _status_bitrate(frontend)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _status_bitrate(frontend):
|
||||||
|
if frontend.backend.playback.current_track is not None:
|
||||||
|
return frontend.backend.playback.current_track.bitrate
|
||||||
|
|
||||||
|
def _status_consume(frontend):
|
||||||
|
if frontend.backend.playback.consume:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _status_playlist_length(frontend):
|
||||||
|
return len(frontend.backend.current_playlist.tracks)
|
||||||
|
|
||||||
|
def _status_playlist_version(frontend):
|
||||||
|
return frontend.backend.current_playlist.version
|
||||||
|
|
||||||
|
def _status_random(frontend):
|
||||||
|
return int(frontend.backend.playback.random)
|
||||||
|
|
||||||
|
def _status_repeat(frontend):
|
||||||
|
return int(frontend.backend.playback.repeat)
|
||||||
|
|
||||||
|
def _status_single(frontend):
|
||||||
|
return int(frontend.backend.playback.single)
|
||||||
|
|
||||||
|
def _status_songid(frontend):
|
||||||
|
if frontend.backend.playback.current_cpid is not None:
|
||||||
|
return frontend.backend.playback.current_cpid
|
||||||
|
else:
|
||||||
|
return _status_songpos(frontend)
|
||||||
|
|
||||||
|
def _status_songpos(frontend):
|
||||||
|
return frontend.backend.playback.current_playlist_position
|
||||||
|
|
||||||
|
def _status_state(frontend):
|
||||||
|
if frontend.backend.playback.state == frontend.backend.playback.PLAYING:
|
||||||
|
return u'play'
|
||||||
|
elif frontend.backend.playback.state == frontend.backend.playback.STOPPED:
|
||||||
|
return u'stop'
|
||||||
|
elif frontend.backend.playback.state == frontend.backend.playback.PAUSED:
|
||||||
|
return u'pause'
|
||||||
|
|
||||||
|
def _status_time(frontend):
|
||||||
|
return u'%s:%s' % (_status_time_elapsed(frontend) // 1000,
|
||||||
|
_status_time_total(frontend) // 1000)
|
||||||
|
|
||||||
|
def _status_time_elapsed(frontend):
|
||||||
|
return frontend.backend.playback.time_position
|
||||||
|
|
||||||
|
def _status_time_total(frontend):
|
||||||
|
if frontend.backend.playback.current_track is None:
|
||||||
|
return 0
|
||||||
|
elif frontend.backend.playback.current_track.length is None:
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return frontend.backend.playback.current_track.length
|
||||||
|
|
||||||
|
def _status_volume(frontend):
|
||||||
|
if frontend.backend.mixer.volume is not None:
|
||||||
|
return frontend.backend.mixer.volume
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _status_xfade(frontend):
|
||||||
|
return 0 # TODO
|
||||||
64
mopidy/frontends/mpd/protocol/stickers.py
Normal file
64
mopidy/frontends/mpd/protocol/stickers.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
|
||||||
|
|
||||||
|
@handle_pattern(r'^sticker delete "(?P<field>[^"]+)" '
|
||||||
|
r'"(?P<uri>[^"]+)"( "(?P<name>[^"]+)")*$')
|
||||||
|
def sticker_delete(frontend, field, uri, name=None):
|
||||||
|
"""
|
||||||
|
*musicpd.org, sticker section:*
|
||||||
|
|
||||||
|
``sticker delete {TYPE} {URI} [NAME]``
|
||||||
|
|
||||||
|
Deletes a sticker value from the specified object. If you do not
|
||||||
|
specify a sticker name, all sticker values are deleted.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^sticker find "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||||
|
r'"(?P<name>[^"]+)"$')
|
||||||
|
def sticker_find(frontend, field, uri, name):
|
||||||
|
"""
|
||||||
|
*musicpd.org, sticker section:*
|
||||||
|
|
||||||
|
``sticker find {TYPE} {URI} {NAME}``
|
||||||
|
|
||||||
|
Searches the sticker database for stickers with the specified name,
|
||||||
|
below the specified directory (``URI``). For each matching song, it
|
||||||
|
prints the ``URI`` and that one sticker's value.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^sticker get "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||||
|
r'"(?P<name>[^"]+)"$')
|
||||||
|
def sticker_get(frontend, field, uri, name):
|
||||||
|
"""
|
||||||
|
*musicpd.org, sticker section:*
|
||||||
|
|
||||||
|
``sticker get {TYPE} {URI} {NAME}``
|
||||||
|
|
||||||
|
Reads a sticker value for the specified object.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^sticker list "(?P<field>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||||
|
def sticker_list(frontend, field, uri):
|
||||||
|
"""
|
||||||
|
*musicpd.org, sticker section:*
|
||||||
|
|
||||||
|
``sticker list {TYPE} {URI}``
|
||||||
|
|
||||||
|
Lists the stickers for the specified object.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
|
|
||||||
|
@handle_pattern(r'^sticker set "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||||
|
r'"(?P<name>[^"]+)" "(?P<value>[^"]+)"$')
|
||||||
|
def sticker_set(frontend, field, uri, name, value):
|
||||||
|
"""
|
||||||
|
*musicpd.org, sticker section:*
|
||||||
|
|
||||||
|
``sticker set {TYPE} {URI} {NAME} {VALUE}``
|
||||||
|
|
||||||
|
Adds a sticker value to the specified object. If a sticker item
|
||||||
|
with that name already exists, it is replaced.
|
||||||
|
"""
|
||||||
|
raise MpdNotImplemented # TODO
|
||||||
180
mopidy/frontends/mpd/protocol/stored_playlists.py
Normal file
180
mopidy/frontends/mpd/protocol/stored_playlists.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from mopidy.frontends.mpd import (handle_pattern, MpdNoExistError,
|
||||||
|
MpdNotImplemented)
|
||||||
|
|
||||||
|
@handle_pattern(r'^listplaylist "(?P<name>[^"]+)"$')
|
||||||
|
def listplaylist(frontend, name):
|
||||||
|
"""
|
||||||
|
*musicpd.org, stored playlists section:*
|
||||||
|
|
||||||
|
``listplaylist {NAME}``
|
||||||
|
|
||||||
|
Lists the files in the playlist ``NAME.m3u``.
|
||||||
|
|
||||||
|
Output format::
|
||||||
|
|
||||||
|
file: relative/path/to/file1.flac
|
||||||
|
file: relative/path/to/file2.ogg
|
||||||
|
file: relative/path/to/file3.mp3
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return ['file: %s' % t.uri
|
||||||
|
for t in frontend.backend.stored_playlists.get(name=name).tracks]
|
||||||
|
except LookupError:
|
||||||
|
raise MpdNoExistError(u'No such playlist', command=u'listplaylist')
|
||||||
|
|
||||||
|
@handle_pattern(r'^listplaylistinfo "(?P<name>[^"]+)"$')
|
||||||
|
def listplaylistinfo(frontend, name):
|
||||||
|
"""
|
||||||
|
*musicpd.org, stored playlists section:*
|
||||||
|
|
||||||
|
``listplaylistinfo {NAME}``
|
||||||
|
|
||||||
|
Lists songs in the playlist ``NAME.m3u``.
|
||||||
|
|
||||||
|
Output format:
|
||||||
|
|
||||||
|
Standard track listing, with fields: file, Time, Title, Date,
|
||||||
|
Album, Artist, Track
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return frontend.backend.stored_playlists.get(name=name).mpd_format()
|
||||||
|
except LookupError:
|
||||||
|
raise MpdNoExistError(
|
||||||
|
u'No such playlist', command=u'listplaylistinfo')
|
||||||
|
|
||||||
|
@handle_pattern(r'^listplaylists$')
|
||||||
|
def listplaylists(frontend):
|
||||||
|
"""
|
||||||
|
*musicpd.org, stored playlists section:*
|
||||||
|
|
||||||
|
``listplaylists``
|
||||||
|
|
||||||
|
Prints a list of the playlist directory.
|
||||||
|
|
||||||
|
After each playlist name the server sends its last modification
|
||||||
|
time as attribute ``Last-Modified`` in ISO 8601 format. To avoid
|
||||||
|
problems due to clock differences between clients and the server,
|
||||||
|
clients should not compare this value with their local clock.
|
||||||
|
|
||||||
|
Output format::
|
||||||
|
|
||||||
|
playlist: a
|
||||||
|
Last-Modified: 2010-02-06T02:10:25Z
|
||||||
|
playlist: b
|
||||||
|
Last-Modified: 2010-02-06T02:11:08Z
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for playlist in frontend.backend.stored_playlists.playlists:
|
||||||
|
result.append((u'playlist', playlist.name))
|
||||||
|
last_modified = (playlist.last_modified or
|
||||||
|
dt.datetime.now()).isoformat()
|
||||||
|
# Remove microseconds
|
||||||
|
last_modified = last_modified.split('.')[0]
|
||||||
|
# Add time zone information
|
||||||
|
# TODO Convert to UTC before adding Z
|
||||||
|
last_modified = last_modified + 'Z'
|
||||||
|
result.append((u'Last-Modified', last_modified))
|
||||||
|
return result
|
||||||
|
|
||||||
|
@handle_pattern(r'^load "(?P<name>[^"]+)"$')
|
||||||
|
def load(frontend, name):
|
||||||
|
"""
|
||||||
|
*musicpd.org, stored playlists section:*
|
||||||
|
|
||||||
|
``load {NAME}``
|
||||||
|
|
||||||
|
Loads the playlist ``NAME.m3u`` from the playlist directory.
|
||||||
|
"""
|
||||||
|
matches = frontend.backend.stored_playlists.search(name)
|
||||||
|
if matches:
|
||||||
|
frontend.backend.current_playlist.load(matches[0].tracks)
|
||||||
|
|
||||||
|
@handle_pattern(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||||
|
def _stored_playlist_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 _stored_playlist_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 _stored_playlist_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 _stored_playlist_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
|
||||||
@ -1,7 +1,3 @@
|
|||||||
"""
|
|
||||||
This is our MPD server implementation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asynchat
|
import asynchat
|
||||||
import asyncore
|
import asyncore
|
||||||
import logging
|
import logging
|
||||||
@ -11,16 +7,11 @@ import socket
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from mopidy import get_mpd_protocol_version, settings
|
from mopidy import get_mpd_protocol_version, settings
|
||||||
|
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR
|
||||||
from mopidy.utils import indent, pickle_connection
|
from mopidy.utils import indent, pickle_connection
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.frontends.mpd.server')
|
logger = logging.getLogger('mopidy.frontends.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'
|
|
||||||
|
|
||||||
class MpdServer(asyncore.dispatcher):
|
class MpdServer(asyncore.dispatcher):
|
||||||
"""
|
"""
|
||||||
The MPD server. Creates a :class:`MpdSession` for each client connection.
|
The MPD server. Creates a :class:`MpdSession` for each client connection.
|
||||||
@ -31,6 +22,7 @@ class MpdServer(asyncore.dispatcher):
|
|||||||
self.core_queue = core_queue
|
self.core_queue = core_queue
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
"""Start MPD server."""
|
||||||
try:
|
try:
|
||||||
if socket.has_ipv6:
|
if socket.has_ipv6:
|
||||||
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
|
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)
|
sys.exit('MPD server startup failed: %s' % e)
|
||||||
|
|
||||||
def handle_accept(self):
|
def handle_accept(self):
|
||||||
|
"""Handle new client connection."""
|
||||||
(client_socket, client_socket_address) = self.accept()
|
(client_socket, client_socket_address) = self.accept()
|
||||||
logger.info(u'MPD client connection from [%s]:%s',
|
logger.info(u'MPD client connection from [%s]:%s',
|
||||||
client_socket_address[0], client_socket_address[1])
|
client_socket_address[0], client_socket_address[1])
|
||||||
@ -56,6 +49,7 @@ class MpdServer(asyncore.dispatcher):
|
|||||||
self.core_queue).start()
|
self.core_queue).start()
|
||||||
|
|
||||||
def handle_close(self):
|
def handle_close(self):
|
||||||
|
"""Handle end of client connection."""
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def _format_hostname(self, hostname):
|
def _format_hostname(self, hostname):
|
||||||
@ -67,7 +61,8 @@ class MpdServer(asyncore.dispatcher):
|
|||||||
|
|
||||||
class MpdSession(asynchat.async_chat):
|
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,
|
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))
|
self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
"""Start a new client session."""
|
||||||
self.send_response(u'OK MPD %s' % get_mpd_protocol_version())
|
self.send_response(u'OK MPD %s' % get_mpd_protocol_version())
|
||||||
|
|
||||||
def collect_incoming_data(self, data):
|
def collect_incoming_data(self, data):
|
||||||
|
"""Collect incoming data into buffer until a terminator is found."""
|
||||||
self.input_buffer.append(data)
|
self.input_buffer.append(data)
|
||||||
|
|
||||||
def found_terminator(self):
|
def found_terminator(self):
|
||||||
|
"""Handle request when a terminator is found."""
|
||||||
data = ''.join(self.input_buffer).strip()
|
data = ''.join(self.input_buffer).strip()
|
||||||
self.input_buffer = []
|
self.input_buffer = []
|
||||||
try:
|
try:
|
||||||
@ -98,6 +96,7 @@ class MpdSession(asynchat.async_chat):
|
|||||||
logger.warning(u'Received invalid data: %s', e)
|
logger.warning(u'Received invalid data: %s', e)
|
||||||
|
|
||||||
def handle_request(self, request):
|
def handle_request(self, request):
|
||||||
|
"""Handle request by sending it to the MPD frontend."""
|
||||||
my_end, other_end = multiprocessing.Pipe()
|
my_end, other_end = multiprocessing.Pipe()
|
||||||
self.core_queue.put({
|
self.core_queue.put({
|
||||||
'command': 'mpd_request',
|
'command': 'mpd_request',
|
||||||
@ -110,9 +109,11 @@ class MpdSession(asynchat.async_chat):
|
|||||||
self.handle_response(response)
|
self.handle_response(response)
|
||||||
|
|
||||||
def handle_response(self, response):
|
def handle_response(self, response):
|
||||||
|
"""Handle response from the MPD frontend."""
|
||||||
self.send_response(LINE_TERMINATOR.join(response))
|
self.send_response(LINE_TERMINATOR.join(response))
|
||||||
|
|
||||||
def send_response(self, output):
|
def send_response(self, output):
|
||||||
|
"""Send a response to the client."""
|
||||||
logger.debug(u'Output to [%s]:%s: %s', self.client_address,
|
logger.debug(u'Output to [%s]:%s: %s', self.client_address,
|
||||||
self.client_port, indent(output))
|
self.client_port, indent(output))
|
||||||
output = u'%s%s' % (output, LINE_TERMINATOR)
|
output = u'%s%s' % (output, LINE_TERMINATOR)
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class RequestHandlerTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_finding_handler_for_known_command_returns_handler_and_kwargs(self):
|
def test_finding_handler_for_known_command_returns_handler_and_kwargs(self):
|
||||||
expected_handler = lambda x: None
|
expected_handler = lambda x: None
|
||||||
frontend._request_handlers['known_command (?P<arg1>.+)'] = \
|
frontend.request_handlers['known_command (?P<arg1>.+)'] = \
|
||||||
expected_handler
|
expected_handler
|
||||||
(handler, kwargs) = self.h.find_handler('known_command an_arg')
|
(handler, kwargs) = self.h.find_handler('known_command an_arg')
|
||||||
self.assertEqual(handler, expected_handler)
|
self.assertEqual(handler, expected_handler)
|
||||||
@ -42,7 +42,7 @@ class RequestHandlerTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_handling_known_request(self):
|
def test_handling_known_request(self):
|
||||||
expected = 'magic'
|
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')
|
result = self.h.handle_request('known request')
|
||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
self.assert_(expected in result)
|
self.assert_(expected in result)
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class StatusHandlerTest(unittest.TestCase):
|
|||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_stats_method(self):
|
def test_stats_method(self):
|
||||||
result = self.h._status_stats()
|
result = frontend.status.stats(self.h)
|
||||||
self.assert_('artists' in result)
|
self.assert_('artists' in result)
|
||||||
self.assert_(int(result['artists']) >= 0)
|
self.assert_(int(result['artists']) >= 0)
|
||||||
self.assert_('albums' in result)
|
self.assert_('albums' in result)
|
||||||
@ -73,106 +73,106 @@ class StatusHandlerTest(unittest.TestCase):
|
|||||||
self.assert_(u'OK' in result)
|
self.assert_(u'OK' in result)
|
||||||
|
|
||||||
def test_status_method_contains_volume_which_defaults_to_0(self):
|
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.assert_('volume' in result)
|
||||||
self.assertEqual(int(result['volume']), 0)
|
self.assertEqual(int(result['volume']), 0)
|
||||||
|
|
||||||
def test_status_method_contains_volume(self):
|
def test_status_method_contains_volume(self):
|
||||||
self.b.mixer.volume = 17
|
self.b.mixer.volume = 17
|
||||||
result = dict(self.h._status_status())
|
result = dict(frontend.status.status(self.h))
|
||||||
self.assert_('volume' in result)
|
self.assert_('volume' in result)
|
||||||
self.assertEqual(int(result['volume']), 17)
|
self.assertEqual(int(result['volume']), 17)
|
||||||
|
|
||||||
def test_status_method_contains_repeat_is_0(self):
|
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.assert_('repeat' in result)
|
||||||
self.assertEqual(int(result['repeat']), 0)
|
self.assertEqual(int(result['repeat']), 0)
|
||||||
|
|
||||||
def test_status_method_contains_repeat_is_1(self):
|
def test_status_method_contains_repeat_is_1(self):
|
||||||
self.b.playback.repeat = 1
|
self.b.playback.repeat = 1
|
||||||
result = dict(self.h._status_status())
|
result = dict(frontend.status.status(self.h))
|
||||||
self.assert_('repeat' in result)
|
self.assert_('repeat' in result)
|
||||||
self.assertEqual(int(result['repeat']), 1)
|
self.assertEqual(int(result['repeat']), 1)
|
||||||
|
|
||||||
def test_status_method_contains_random_is_0(self):
|
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.assert_('random' in result)
|
||||||
self.assertEqual(int(result['random']), 0)
|
self.assertEqual(int(result['random']), 0)
|
||||||
|
|
||||||
def test_status_method_contains_random_is_1(self):
|
def test_status_method_contains_random_is_1(self):
|
||||||
self.b.playback.random = 1
|
self.b.playback.random = 1
|
||||||
result = dict(self.h._status_status())
|
result = dict(frontend.status.status(self.h))
|
||||||
self.assert_('random' in result)
|
self.assert_('random' in result)
|
||||||
self.assertEqual(int(result['random']), 1)
|
self.assertEqual(int(result['random']), 1)
|
||||||
|
|
||||||
def test_status_method_contains_single(self):
|
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_('single' in result)
|
||||||
self.assert_(int(result['single']) in (0, 1))
|
self.assert_(int(result['single']) in (0, 1))
|
||||||
|
|
||||||
def test_status_method_contains_consume_is_0(self):
|
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.assert_('consume' in result)
|
||||||
self.assertEqual(int(result['consume']), 0)
|
self.assertEqual(int(result['consume']), 0)
|
||||||
|
|
||||||
def test_status_method_contains_consume_is_1(self):
|
def test_status_method_contains_consume_is_1(self):
|
||||||
self.b.playback.consume = 1
|
self.b.playback.consume = 1
|
||||||
result = dict(self.h._status_status())
|
result = dict(frontend.status.status(self.h))
|
||||||
self.assert_('consume' in result)
|
self.assert_('consume' in result)
|
||||||
self.assertEqual(int(result['consume']), 1)
|
self.assertEqual(int(result['consume']), 1)
|
||||||
|
|
||||||
def test_status_method_contains_playlist(self):
|
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_('playlist' in result)
|
||||||
self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1))
|
self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1))
|
||||||
|
|
||||||
def test_status_method_contains_playlistlength(self):
|
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_('playlistlength' in result)
|
||||||
self.assert_(int(result['playlistlength']) >= 0)
|
self.assert_(int(result['playlistlength']) >= 0)
|
||||||
|
|
||||||
def test_status_method_contains_xfade(self):
|
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_('xfade' in result)
|
||||||
self.assert_(int(result['xfade']) >= 0)
|
self.assert_(int(result['xfade']) >= 0)
|
||||||
|
|
||||||
def test_status_method_contains_state_is_play(self):
|
def test_status_method_contains_state_is_play(self):
|
||||||
self.b.playback.state = self.b.playback.PLAYING
|
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.assert_('state' in result)
|
||||||
self.assertEqual(result['state'], 'play')
|
self.assertEqual(result['state'], 'play')
|
||||||
|
|
||||||
def test_status_method_contains_state_is_stop(self):
|
def test_status_method_contains_state_is_stop(self):
|
||||||
self.b.playback.state = self.b.playback.STOPPED
|
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.assert_('state' in result)
|
||||||
self.assertEqual(result['state'], 'stop')
|
self.assertEqual(result['state'], 'stop')
|
||||||
|
|
||||||
def test_status_method_contains_state_is_pause(self):
|
def test_status_method_contains_state_is_pause(self):
|
||||||
self.b.playback.state = self.b.playback.PLAYING
|
self.b.playback.state = self.b.playback.PLAYING
|
||||||
self.b.playback.state = self.b.playback.PAUSED
|
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.assert_('state' in result)
|
||||||
self.assertEqual(result['state'], 'pause')
|
self.assertEqual(result['state'], 'pause')
|
||||||
|
|
||||||
def test_status_method_when_playlist_loaded_contains_song(self):
|
def test_status_method_when_playlist_loaded_contains_song(self):
|
||||||
self.b.current_playlist.load([Track()])
|
self.b.current_playlist.load([Track()])
|
||||||
self.b.playback.play()
|
self.b.playback.play()
|
||||||
result = dict(self.h._status_status())
|
result = dict(frontend.status.status(self.h))
|
||||||
self.assert_('song' in result)
|
self.assert_('song' in result)
|
||||||
self.assert_(int(result['song']) >= 0)
|
self.assert_(int(result['song']) >= 0)
|
||||||
|
|
||||||
def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
|
def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
|
||||||
self.b.current_playlist.load([Track()])
|
self.b.current_playlist.load([Track()])
|
||||||
self.b.playback.play()
|
self.b.playback.play()
|
||||||
result = dict(self.h._status_status())
|
result = dict(frontend.status.status(self.h))
|
||||||
self.assert_('songid' in result)
|
self.assert_('songid' in result)
|
||||||
self.assertEqual(int(result['songid']), 1)
|
self.assertEqual(int(result['songid']), 1)
|
||||||
|
|
||||||
def test_status_method_when_playing_contains_time_with_no_length(self):
|
def test_status_method_when_playing_contains_time_with_no_length(self):
|
||||||
self.b.current_playlist.load([Track(length=None)])
|
self.b.current_playlist.load([Track(length=None)])
|
||||||
self.b.playback.play()
|
self.b.playback.play()
|
||||||
result = dict(self.h._status_status())
|
result = dict(frontend.status.status(self.h))
|
||||||
self.assert_('time' in result)
|
self.assert_('time' in result)
|
||||||
(position, total) = result['time'].split(':')
|
(position, total) = result['time'].split(':')
|
||||||
position = int(position)
|
position = int(position)
|
||||||
@ -182,7 +182,7 @@ class StatusHandlerTest(unittest.TestCase):
|
|||||||
def test_status_method_when_playing_contains_time_with_length(self):
|
def test_status_method_when_playing_contains_time_with_length(self):
|
||||||
self.b.current_playlist.load([Track(length=10000)])
|
self.b.current_playlist.load([Track(length=10000)])
|
||||||
self.b.playback.play()
|
self.b.playback.play()
|
||||||
result = dict(self.h._status_status())
|
result = dict(frontend.status.status(self.h))
|
||||||
self.assert_('time' in result)
|
self.assert_('time' in result)
|
||||||
(position, total) = result['time'].split(':')
|
(position, total) = result['time'].split(':')
|
||||||
position = int(position)
|
position = int(position)
|
||||||
@ -192,13 +192,13 @@ class StatusHandlerTest(unittest.TestCase):
|
|||||||
def test_status_method_when_playing_contains_elapsed(self):
|
def test_status_method_when_playing_contains_elapsed(self):
|
||||||
self.b.playback.state = self.b.playback.PAUSED
|
self.b.playback.state = self.b.playback.PAUSED
|
||||||
self.b.playback._play_time_accumulated = 59123
|
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.assert_('elapsed' in result)
|
||||||
self.assertEqual(int(result['elapsed']), 59123)
|
self.assertEqual(int(result['elapsed']), 59123)
|
||||||
|
|
||||||
def test_status_method_when_playing_contains_bitrate(self):
|
def test_status_method_when_playing_contains_bitrate(self):
|
||||||
self.b.current_playlist.load([Track(bitrate=320)])
|
self.b.current_playlist.load([Track(bitrate=320)])
|
||||||
self.b.playback.play()
|
self.b.playback.play()
|
||||||
result = dict(self.h._status_status())
|
result = dict(frontend.status.status(self.h))
|
||||||
self.assert_('bitrate' in result)
|
self.assert_('bitrate' in result)
|
||||||
self.assertEqual(int(result['bitrate']), 320)
|
self.assertEqual(int(result['bitrate']), 320)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user