1365 lines
44 KiB
Python
1365 lines
44 KiB
Python
"""
|
|
Our 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>`_.
|
|
"""
|
|
|
|
import logging
|
|
import re
|
|
import sys
|
|
|
|
from mopidy.exceptions import MpdAckError, MpdNotImplemented
|
|
|
|
logger = logging.getLogger('mpd.handler')
|
|
|
|
_request_handlers = {}
|
|
|
|
def handle_pattern(pattern):
|
|
def decorator(func):
|
|
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
|
|
|
|
def flatten(the_list):
|
|
result = []
|
|
for element in the_list:
|
|
if isinstance(element, list):
|
|
result.extend(flatten(element))
|
|
else:
|
|
result.append(element)
|
|
return result
|
|
|
|
class MpdHandler(object):
|
|
def __init__(self, session=None, backend=None):
|
|
self.session = session
|
|
self.backend = backend
|
|
self.command_list = False
|
|
|
|
def handle_request(self, request, add_ok=True):
|
|
if self.command_list is not False and request != u'command_list_end':
|
|
self.command_list.append(request)
|
|
return None
|
|
for pattern in _request_handlers:
|
|
matches = re.match(pattern, request)
|
|
if matches is not None:
|
|
groups = matches.groupdict()
|
|
try:
|
|
result = _request_handlers[pattern](self, **groups)
|
|
except MpdAckError, e:
|
|
return self.handle_response(u'ACK %s' % e, add_ok=False)
|
|
if self.command_list is not False:
|
|
return None
|
|
else:
|
|
return self.handle_response(result, add_ok)
|
|
raise MpdAckError(u'Unknown command: %s' % request)
|
|
|
|
def handle_response(self, result, add_ok=True):
|
|
response = []
|
|
if result is None:
|
|
result = []
|
|
elif not isinstance(result, list):
|
|
result = [result]
|
|
for line in flatten(result):
|
|
if isinstance(line, dict):
|
|
for (key, value) in line.items():
|
|
response.append(u'%s: %s' % (key, value))
|
|
elif isinstance(line, tuple):
|
|
(key, value) = line
|
|
response.append(u'%s: %s' % (key, value))
|
|
else:
|
|
response.append(line)
|
|
if add_ok and (not response or not response[-1].startswith(u'ACK')):
|
|
response.append(u'OK')
|
|
return response
|
|
|
|
@handle_pattern(r'^ack$')
|
|
def _ack(self):
|
|
"""
|
|
Always returns an 'ACK'. Not a part of the MPD protocol.
|
|
"""
|
|
raise MpdNotImplemented
|
|
|
|
@handle_pattern(r'^disableoutput "(?P<outputid>\d+)"$')
|
|
def _audio_output_disableoutput(self, outputid):
|
|
"""
|
|
*musicpd.org, audio output section:*
|
|
|
|
``disableoutput``
|
|
|
|
Turns an output off.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^enableoutput "(?P<outputid>\d+)"$')
|
|
def _audio_output_enableoutput(self, outputid):
|
|
"""
|
|
*musicpd.org, audio output section:*
|
|
|
|
``enableoutput``
|
|
|
|
Turns an output on.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^outputs$')
|
|
def _audio_output_outputs(self):
|
|
"""
|
|
*musicpd.org, audio output section:*
|
|
|
|
``outputs``
|
|
|
|
Shows information about all outputs.
|
|
"""
|
|
return [
|
|
('outputid', 0),
|
|
('outputname', self.backend.__class__.__name__),
|
|
('outputenabled', 1),
|
|
]
|
|
|
|
@handle_pattern(r'^command_list_begin$')
|
|
def _command_list_begin(self):
|
|
"""
|
|
*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.
|
|
"""
|
|
self.command_list = []
|
|
self.command_list_ok = False
|
|
|
|
@handle_pattern(r'^command_list_end$')
|
|
def _command_list_end(self):
|
|
"""See :meth:`_command_list_begin`."""
|
|
(command_list, self.command_list) = (self.command_list, False)
|
|
(command_list_ok, self.command_list_ok) = (self.command_list_ok, False)
|
|
result = []
|
|
for command in command_list:
|
|
response = self.handle_request(command, add_ok=False)
|
|
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(self):
|
|
"""See :meth:`_command_list_begin`."""
|
|
self.command_list = []
|
|
self.command_list_ok = True
|
|
|
|
@handle_pattern(r'^close$')
|
|
def _connection_close(self):
|
|
"""
|
|
*musicpd.org, connection section:*
|
|
|
|
``close``
|
|
|
|
Closes the connection to MPD.
|
|
"""
|
|
self.session.do_close()
|
|
|
|
@handle_pattern(r'^kill$')
|
|
def _connection_kill(self):
|
|
"""
|
|
*musicpd.org, connection section:*
|
|
|
|
``kill``
|
|
|
|
Kills MPD.
|
|
"""
|
|
self.session.do_kill()
|
|
|
|
@handle_pattern(r'^password "(?P<password>[^"]+)"$')
|
|
def _connection_password(self, 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 _connection_ping(self):
|
|
"""
|
|
*musicpd.org, connection section:*
|
|
|
|
``ping``
|
|
|
|
Does nothing but return ``OK``.
|
|
"""
|
|
pass
|
|
|
|
@handle_pattern(r'^add "(?P<uri>[^"]*)"$')
|
|
def _current_playlist_add(self, 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.
|
|
"""
|
|
self._current_playlist_addid(uri)
|
|
|
|
@handle_pattern(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$')
|
|
def _current_playlist_addid(self, 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 = self.backend.library.lookup(uri)
|
|
if track is not None:
|
|
self.backend.current_playlist.add(track, at_position=songpos)
|
|
return ('Id', track.id)
|
|
|
|
@handle_pattern(r'^delete "(?P<songpos>\d+)"$')
|
|
@handle_pattern(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
|
|
def _current_playlist_delete(self, songpos=None, start=None, end=None):
|
|
"""
|
|
*musicpd.org, current playlist section:*
|
|
|
|
``delete [{POS} | {START:END}]``
|
|
|
|
Deletes a song from the playlist.
|
|
"""
|
|
try:
|
|
tracks = []
|
|
if songpos is not None:
|
|
songpos = int(songpos)
|
|
tracks = [
|
|
self.backend.current_playlist.playlist.tracks[songpos]]
|
|
elif start is not None:
|
|
start = int(start)
|
|
if end is not None:
|
|
end = int(end)
|
|
else:
|
|
end = self.backend.current_playlist.playlist.length
|
|
tracks = self.backend.current_playlist.playlist.tracks[
|
|
start:end]
|
|
for track in tracks:
|
|
self.backend.current_playlist.remove(track)
|
|
except IndexError, e:
|
|
raise MpdAckError(u'Position out of bounds')
|
|
|
|
@handle_pattern(r'^deleteid "(?P<songid>\d+)"$')
|
|
def _current_playlist_deleteid(self, songid):
|
|
"""
|
|
*musicpd.org, current playlist section:*
|
|
|
|
``deleteid {SONGID}``
|
|
|
|
Deletes the song ``SONGID`` from the playlist
|
|
"""
|
|
songid = int(songid)
|
|
try:
|
|
track = self.backend.current_playlist.get_by_id(songid)
|
|
return self.backend.current_playlist.remove(track)
|
|
except KeyError, e:
|
|
raise MpdAckError(unicode(e))
|
|
|
|
@handle_pattern(r'^clear$')
|
|
def _current_playlist_clear(self):
|
|
"""
|
|
*musicpd.org, current playlist section:*
|
|
|
|
``clear``
|
|
|
|
Clears the current playlist.
|
|
"""
|
|
self.backend.current_playlist.clear()
|
|
|
|
@handle_pattern(r'^move "(?P<songpos>\d+)" "(?P<to>\d+)"$')
|
|
@handle_pattern(r'^move "(?P<start>\d+):(?P<end>\d+)*" "(?P<to>\d+)"$')
|
|
def _current_playlist_move(self, songpos=None,
|
|
start=None, end=None, to=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.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^moveid "(?P<songid>\d+)" "(?P<to>\d+)"$')
|
|
def _current_playlist_moveid(self, songid, 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).
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^playlist$')
|
|
def _current_playlist_playlist(self):
|
|
"""
|
|
*musicpd.org, current playlist section:*
|
|
|
|
``playlist``
|
|
|
|
Displays the current playlist.
|
|
|
|
.. note::
|
|
|
|
Do not use this, instead use ``playlistinfo``.
|
|
"""
|
|
return self._current_playlist_playlistinfo()
|
|
|
|
@handle_pattern(r'^playlistfind "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
|
def _current_playlist_playlistfind(self, tag, needle):
|
|
"""
|
|
*musicpd.org, current playlist section:*
|
|
|
|
``playlistfind {TAG} {NEEDLE}``
|
|
|
|
Finds songs in the current playlist with strict matching.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^playlistid( "(?P<songid>\S+)")*$')
|
|
def _current_playlist_playlistid(self, songid=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.
|
|
"""
|
|
# TODO Limit selection to songid
|
|
return self.backend.current_playlist.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 _current_playlist_playlistinfo(self, 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``.
|
|
"""
|
|
if songpos is not None:
|
|
songpos = int(songpos)
|
|
return self.backend.current_playlist.playlist.mpd_format(
|
|
songpos, songpos + 1)
|
|
else:
|
|
if start is None:
|
|
start = 0
|
|
start = int(start)
|
|
if end is not None:
|
|
end = int(end)
|
|
return self.backend.current_playlist.playlist.mpd_format(start, end)
|
|
|
|
@handle_pattern(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
|
def _current_playlist_playlistsearch(self, tag, needle):
|
|
"""
|
|
*musicpd.org, current playlist section:*
|
|
|
|
``playlistsearch {TAG} {NEEDLE}``
|
|
|
|
Searches case-sensitively for partial matches in the current
|
|
playlist.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^plchanges "(?P<version>\d+)"$')
|
|
def _current_playlist_plchanges(self, 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.
|
|
"""
|
|
if int(version) < self.backend.current_playlist.version:
|
|
return self.backend.current_playlist.playlist.mpd_format()
|
|
|
|
@handle_pattern(r'^plchangesposid "(?P<version>\d+)"$')
|
|
def _current_playlist_plchangesposid(self, 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.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^shuffle$')
|
|
@handle_pattern(r'^shuffle "(?P<start>\d+):(?P<end>\d+)*"$')
|
|
def _current_playlist_shuffle(self, 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.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$')
|
|
def _current_playlist_swap(self, songpos1, songpos2):
|
|
"""
|
|
*musicpd.org, current playlist section:*
|
|
|
|
``swap {SONG1} {SONG2}``
|
|
|
|
Swaps the positions of ``SONG1`` and ``SONG2``.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^swapid "(?P<songid1>\d+)" "(?P<songid2>\d+)"$')
|
|
def _current_playlist_swapid(self, songid1, songid2):
|
|
"""
|
|
*musicpd.org, current playlist section:*
|
|
|
|
``swapid {SONG1} {SONG2}``
|
|
|
|
Swaps the positions of ``SONG1`` and ``SONG2`` (both song ids).
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^$')
|
|
def _empty(self):
|
|
"""The original MPD server returns ``OK`` on an empty request.``"""
|
|
pass
|
|
|
|
@handle_pattern(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
|
def _music_db_count(self, 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.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^find "(?P<type>(album|artist|title))" '
|
|
r'"(?P<what>[^"]+)"$')
|
|
def _music_db_find(self, type, what):
|
|
"""
|
|
*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.
|
|
"""
|
|
if type == u'title':
|
|
type = u'track'
|
|
return self.backend.library.find_exact(type, what).mpd_format(
|
|
search_result=True)
|
|
|
|
@handle_pattern(r'^findadd "(?P<type>(album|artist|title))" '
|
|
r'"(?P<what>[^"]+)"$')
|
|
def _music_db_findadd(self, type, what):
|
|
"""
|
|
*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.
|
|
"""
|
|
result = self._music_db_find(type, what)
|
|
# TODO Add result to current playlist
|
|
#return result
|
|
|
|
@handle_pattern(r'^list "(?P<type>artist)"$')
|
|
@handle_pattern(r'^list "(?P<type>album)"( "(?P<artist>[^"]+)")*$')
|
|
def _music_db_list(self, type, artist=None):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``list {TYPE} [ARTIST]``
|
|
|
|
Lists all tags of the specified type. ``TYPE`` should be ``album``
|
|
or artist.
|
|
|
|
``ARTIST`` is an optional parameter when type is ``album``, this
|
|
specifies to list albums by an artist.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^listall "(?P<uri>[^"]+)"')
|
|
def _music_db_listall(self, 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 _music_db_listallinfo(self, 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 _music_db_lsinfo(self, 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.
|
|
"""
|
|
if uri == u'/' or uri is None:
|
|
return self._stored_playlists_listplaylists()
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^rescan( "(?P<uri>[^"]+)")*$')
|
|
def _music_db_rescan(self, uri=None):
|
|
"""
|
|
*musicpd.org, music database section:*
|
|
|
|
``rescan [URI]``
|
|
|
|
Same as ``update``, but also rescans unmodified files.
|
|
"""
|
|
return self._music_db_update(uri, rescan_unmodified_files=True)
|
|
|
|
@handle_pattern(r'^search "(?P<type>(album|artist|filename|title))" '
|
|
r'"(?P<what>[^"]+)"$')
|
|
def _music_db_search(self, type, what):
|
|
"""
|
|
*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.
|
|
"""
|
|
if type == u'title':
|
|
type = u'track'
|
|
return self.backend.library.search(type, what).mpd_format(
|
|
search_result=True)
|
|
|
|
@handle_pattern(r'^update( "(?P<uri>[^"]+)")*$')
|
|
def _music_db_update(self, 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
|
|
|
|
@handle_pattern(r'^consume "(?P<state>[01])"$')
|
|
def _playback_consume(self, 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.
|
|
"""
|
|
state = int(state)
|
|
if state:
|
|
raise MpdNotImplemented # TODO
|
|
else:
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^crossfade "(?P<seconds>\d+)"$')
|
|
def _playback_crossfade(self, seconds):
|
|
"""
|
|
*musicpd.org, playback section:*
|
|
|
|
``crossfade {SECONDS}``
|
|
|
|
Sets crossfading between songs.
|
|
"""
|
|
seconds = int(seconds)
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^next$')
|
|
def _playback_next(self):
|
|
"""
|
|
*musicpd.org, playback section:*
|
|
|
|
``next``
|
|
|
|
Plays next song in the playlist.
|
|
"""
|
|
return self.backend.playback.next()
|
|
|
|
@handle_pattern(r'^pause "(?P<state>[01])"$')
|
|
def _playback_pause(self, state):
|
|
"""
|
|
*musicpd.org, playback section:*
|
|
|
|
``pause {PAUSE}``
|
|
|
|
Toggles pause/resumes playing, ``PAUSE`` is 0 or 1.
|
|
"""
|
|
if int(state):
|
|
self.backend.playback.pause()
|
|
else:
|
|
self.backend.playback.resume()
|
|
|
|
@handle_pattern(r'^play$')
|
|
def _playback_play(self):
|
|
"""
|
|
The original MPD server resumes from the paused state on ``play``
|
|
without arguments.
|
|
"""
|
|
return self.backend.playback.play()
|
|
@handle_pattern(r'^playid "(?P<songid>\d+)"$')
|
|
def _playback_playid(self, songid):
|
|
"""
|
|
*musicpd.org, playback section:*
|
|
|
|
``playid [SONGID]``
|
|
|
|
Begins playing the playlist at song ``SONGID``.
|
|
"""
|
|
songid = int(songid)
|
|
try:
|
|
track = self.backend.current_playlist.get_by_id(songid)
|
|
return self.backend.playback.play(track)
|
|
except KeyError, e:
|
|
raise MpdAckError(unicode(e))
|
|
|
|
@handle_pattern(r'^play "(?P<songpos>\d+)"$')
|
|
def _playback_playpos(self, songpos):
|
|
"""
|
|
*musicpd.org, playback section:*
|
|
|
|
``play [SONGPOS]``
|
|
|
|
Begins playing the playlist at song number ``SONGPOS``.
|
|
"""
|
|
songpos = int(songpos)
|
|
try:
|
|
track = self.backend.current_playlist.playlist.tracks[songpos]
|
|
return self.backend.playback.play(track)
|
|
except IndexError:
|
|
raise MpdAckError(u'Position out of bounds')
|
|
|
|
@handle_pattern(r'^previous$')
|
|
def _playback_previous(self):
|
|
"""
|
|
*musicpd.org, playback section:*
|
|
|
|
``previous``
|
|
|
|
Plays previous song in the playlist.
|
|
"""
|
|
return self.backend.playback.previous()
|
|
|
|
@handle_pattern(r'^random "(?P<state>[01])"$')
|
|
def _playback_random(self, state):
|
|
"""
|
|
*musicpd.org, playback section:*
|
|
|
|
``random {STATE}``
|
|
|
|
Sets random state to ``STATE``, ``STATE`` should be 0 or 1.
|
|
"""
|
|
state = int(state)
|
|
if state:
|
|
raise MpdNotImplemented # TODO
|
|
else:
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^repeat "(?P<state>[01])"$')
|
|
def _playback_repeat(self, state):
|
|
"""
|
|
*musicpd.org, playback section:*
|
|
|
|
``repeat {STATE}``
|
|
|
|
Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1.
|
|
"""
|
|
state = int(state)
|
|
if state:
|
|
raise MpdNotImplemented # TODO
|
|
else:
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^replay_gain_mode "(?P<mode>(off|track|album))"$')
|
|
def _playback_replay_gain_mode(self, 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 _playback_replay_gain_status(self):
|
|
"""
|
|
*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 _playback_seek(self, 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<songid>\d+)" "(?P<seconds>\d+)"$')
|
|
def _playback_seekid(self, songid, 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 _playback_setvol(self, 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
|
|
self.backend.playback.volume = volume
|
|
|
|
@handle_pattern(r'^single "(?P<state>[01])"$')
|
|
def _playback_single(self, 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.
|
|
"""
|
|
state = int(state)
|
|
if state:
|
|
raise MpdNotImplemented # TODO
|
|
else:
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^stop$')
|
|
def _playback_stop(self):
|
|
"""
|
|
*musicpd.org, playback section:*
|
|
|
|
``stop``
|
|
|
|
Stops playing.
|
|
"""
|
|
self.backend.playback.stop()
|
|
|
|
@handle_pattern(r'^commands$')
|
|
def _reflection_commands(self):
|
|
"""
|
|
*musicpd.org, reflection section:*
|
|
|
|
``commands``
|
|
|
|
Shows which commands the current user has access to.
|
|
"""
|
|
pass # TODO
|
|
|
|
@handle_pattern(r'^decoders$')
|
|
def _reflection_decoders(self):
|
|
"""
|
|
*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 _reflection_notcommands(self):
|
|
"""
|
|
*musicpd.org, reflection section:*
|
|
|
|
``notcommands``
|
|
|
|
Shows which commands the current user does not have access to.
|
|
"""
|
|
pass # TODO
|
|
|
|
@handle_pattern(r'^tagtypes$')
|
|
def _reflection_tagtypes(self):
|
|
"""
|
|
*musicpd.org, reflection section:*
|
|
|
|
``tagtypes``
|
|
|
|
Shows a list of available song metadata.
|
|
"""
|
|
pass # TODO
|
|
|
|
@handle_pattern(r'^urlhandlers$')
|
|
def _reflection_urlhandlers(self):
|
|
"""
|
|
*musicpd.org, reflection section:*
|
|
|
|
``urlhandlers``
|
|
|
|
Gets a list of available URL handlers.
|
|
"""
|
|
return self.backend.uri_handlers
|
|
|
|
@handle_pattern(r'^clearerror$')
|
|
def _status_clearerror(self):
|
|
"""
|
|
*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 _status_currentsong(self):
|
|
"""
|
|
*musicpd.org, status section:*
|
|
|
|
``currentsong``
|
|
|
|
Displays the song info of the current song (same song that is
|
|
identified in status).
|
|
"""
|
|
if self.backend.playback.current_track is not None:
|
|
return self.backend.playback.current_track.mpd_format(
|
|
position=self.backend.playback.playlist_position)
|
|
|
|
@handle_pattern(r'^idle$')
|
|
@handle_pattern(r'^idle (?P<subsystems>.+)$')
|
|
def _status_idle(self, 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.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^noidle$')
|
|
def _status_noidle(self):
|
|
"""See :meth:`_idle`."""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^stats$')
|
|
def _status_stats(self):
|
|
"""
|
|
*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
|
|
'uptime': self.session.stats_uptime(),
|
|
'db_playtime': 0, # TODO
|
|
'db_update': 0, # TODO
|
|
'playtime': 0, # TODO
|
|
}
|
|
|
|
@handle_pattern(r'^status$')
|
|
def _status_status(self):
|
|
"""
|
|
*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', self.__status_status_volume()),
|
|
('repeat', self.__status_status_repeat()),
|
|
('random', self.__status_status_random()),
|
|
('single', self.__status_status_single()),
|
|
('consume', self.__status_status_consume()),
|
|
('playlist', self.__status_status_playlist_version()),
|
|
('playlistlength', self.__status_status_playlist_length()),
|
|
('xfade', self.__status_status_xfade()),
|
|
('state', self.__status_status_state()),
|
|
]
|
|
if self.backend.playback.current_track is not None:
|
|
result.append(('song', self.__status_status_songpos()))
|
|
result.append(('songid', self.__status_status_songid()))
|
|
if self.backend.playback.state in (
|
|
self.backend.playback.PLAYING, self.backend.playback.PAUSED):
|
|
result.append(('time', self.__status_status_time()))
|
|
result.append(('elapsed', self.__status_status_time_elapsed()))
|
|
result.append(('bitrate', self.__status_status_bitrate()))
|
|
return result
|
|
|
|
def __status_status_bitrate(self):
|
|
if self.backend.playback.current_track is not None:
|
|
return self.backend.playback.current_track.bitrate
|
|
|
|
def __status_status_consume(self):
|
|
if self.backend.playback.consume:
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
def __status_status_playlist_length(self):
|
|
return self.backend.current_playlist.playlist.length
|
|
|
|
def __status_status_playlist_version(self):
|
|
return self.backend.current_playlist.version
|
|
|
|
def __status_status_random(self):
|
|
if self.backend.playback.random:
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
def __status_status_repeat(self):
|
|
if self.backend.playback.repeat:
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
def __status_status_single(self):
|
|
return 0 # TODO
|
|
|
|
def __status_status_songid(self):
|
|
if self.backend.playback.current_track.id is not None:
|
|
return self.backend.playback.current_track.id
|
|
else:
|
|
return self.__status_status_songpos()
|
|
|
|
def __status_status_songpos(self):
|
|
return self.backend.playback.playlist_position
|
|
|
|
def __status_status_state(self):
|
|
if self.backend.playback.state == self.backend.playback.PLAYING:
|
|
return u'play'
|
|
elif self.backend.playback.state == self.backend.playback.STOPPED:
|
|
return u'stop'
|
|
elif self.backend.playback.state == self.backend.playback.PAUSED:
|
|
return u'pause'
|
|
|
|
def __status_status_time(self):
|
|
return u'%s:%s' % (self.__status_status_time_elapsed() // 1000,
|
|
self.__status_status_time_total() // 1000)
|
|
|
|
def __status_status_time_elapsed(self):
|
|
return self.backend.playback.time_position
|
|
|
|
def __status_status_time_total(self):
|
|
if self.backend.playback.current_track is None:
|
|
return 0
|
|
elif self.backend.playback.current_track.length is None:
|
|
return 0
|
|
else:
|
|
return self.backend.playback.current_track.length
|
|
|
|
def __status_status_volume(self):
|
|
if self.backend.playback.volume is not None:
|
|
return self.backend.playback.volume
|
|
else:
|
|
return 0
|
|
|
|
def __status_status_xfade(self):
|
|
return 0 # TODO
|
|
|
|
@handle_pattern(r'^sticker delete "(?P<type>[^"]+)" '
|
|
r'"(?P<uri>[^"]+)"( "(?P<name>[^"]+)")*$')
|
|
def _sticker_delete(self, type, 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<type>[^"]+)" "(?P<uri>[^"]+)" '
|
|
r'"(?P<name>[^"]+)"$')
|
|
def _sticker_find(self, type, 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<type>[^"]+)" "(?P<uri>[^"]+)" '
|
|
r'"(?P<name>[^"]+)"$')
|
|
def _sticker_get(self, type, 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<type>[^"]+)" "(?P<uri>[^"]+)"$')
|
|
def _sticker_list(self, type, 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<type>[^"]+)" "(?P<uri>[^"]+)" '
|
|
r'"(?P<name>[^"]+)" "(?P<value>[^"]+)"$')
|
|
def _sticker_set(self, type, 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
|
|
|
|
@handle_pattern(r'^listplaylist "(?P<name>[^"]+)"$')
|
|
def _stored_playlists_listplaylist(self, 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 self.backend.stored_playlists.get_by_name(name).tracks]
|
|
except KeyError, e:
|
|
raise MpdAckError(e)
|
|
|
|
@handle_pattern(r'^listplaylistinfo "(?P<name>[^"]+)"$')
|
|
def _stored_playlists_listplaylistinfo(self, 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 self.backend.stored_playlists.get_by_name(name).mpd_format(
|
|
search_result=True)
|
|
except KeyError, e:
|
|
raise MpdAckError(e)
|
|
|
|
@handle_pattern(r'^listplaylists$')
|
|
def _stored_playlists_listplaylists(self):
|
|
"""
|
|
*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
|
|
"""
|
|
# TODO Add Last-Modified attribute to output
|
|
return [u'playlist: %s' % p.name
|
|
for p in self.backend.stored_playlists.playlists]
|
|
|
|
@handle_pattern(r'^load "(?P<name>[^"]+)"$')
|
|
def _stored_playlists_load(self, name):
|
|
"""
|
|
*musicpd.org, stored playlists section:*
|
|
|
|
``load {NAME}``
|
|
|
|
Loads the playlist ``NAME.m3u`` from the playlist directory.
|
|
"""
|
|
matches = self.backend.stored_playlists.search(name)
|
|
if matches:
|
|
self.backend.current_playlist.load(matches[0])
|
|
self.backend.playback.new_playlist_loaded_callback()
|
|
|
|
@handle_pattern(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
|
|
def _stored_playlist_playlistadd(self, 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(self, 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(self, 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<songid>\d+)" "(?P<songpos>\d+)"$')
|
|
def _stored_playlist_playlistmove(self, name, songid, songpos):
|
|
"""
|
|
*musicpd.org, stored playlists section:*
|
|
|
|
``playlistmove {NAME} {SONGID} {SONGPOS}``
|
|
|
|
Moves ``SONGID`` in the playlist ``NAME.m3u`` to the position
|
|
``SONGPOS``.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|
|
|
|
@handle_pattern(r'^rename "(?P<old_name>[^"]+)" "(?P<new_name>[^"]+)"$')
|
|
def _stored_playlists_rename(self, 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 _stored_playlists_rm(self, 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 _stored_playlists_save(self, name):
|
|
"""
|
|
*musicpd.org, stored playlists section:*
|
|
|
|
``save {NAME}``
|
|
|
|
Saves the current playlist to ``NAME.m3u`` in the playlist
|
|
directory.
|
|
"""
|
|
raise MpdNotImplemented # TODO
|