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
|
||||
|
||||
|
||||
MPD protocol implementation
|
||||
===========================
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.frontend
|
||||
:synopsis: MPD protocol implementation
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
MPD server implementation
|
||||
=========================
|
||||
MPD server
|
||||
==========
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.server
|
||||
:synopsis: MPD server implementation
|
||||
: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 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:
|
||||
|
||||
- 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.
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import re
|
||||
|
||||
from mopidy import MopidyException
|
||||
|
||||
class MpdAckError(MopidyException):
|
||||
@ -55,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
|
||||
|
||||
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 asyncore
|
||||
import logging
|
||||
@ -11,16 +7,11 @@ 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.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):
|
||||
"""
|
||||
The MPD server. Creates a :class:`MpdSession` for each client connection.
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user