MPD: Split protocol implementation into 11 modules

This commit is contained in:
Stein Magnus Jodal 2010-08-10 02:08:59 +02:00
parent ffe665572b
commit d0aac71cfb
18 changed files with 1819 additions and 1693 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,17 @@
"""
This is Mopidy's MPD protocol implementation.
This is partly based upon the `MPD protocol documentation
<http://www.musicpd.org/doc/protocol/>`_, which is a useful resource, but it is
rather incomplete with regards to data formats, both for requests and
responses. Thus, we have had to talk a great deal with the the original `MPD
server <http://mpd.wikia.com/>`_ using telnet to get the details we need to
implement our own MPD server which is compatible with the numerous existing
`MPD clients <http://mpd.wikia.com/wiki/Clients>`_.
"""
#: The MPD protocol uses UTF-8 for encoding all data.
ENCODING = u'utf-8'
#: The MPD protocol uses ``\n`` as line terminator.
LINE_TERMINATOR = u'\n'

View File

@ -0,0 +1,38 @@
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
@handle_pattern(r'^disableoutput "(?P<outputid>\d+)"$')
def disableoutput(frontend, outputid):
"""
*musicpd.org, audio output section:*
``disableoutput``
Turns an output off.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^enableoutput "(?P<outputid>\d+)"$')
def enableoutput(frontend, outputid):
"""
*musicpd.org, audio output section:*
``enableoutput``
Turns an output on.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^outputs$')
def outputs(frontend):
"""
*musicpd.org, audio output section:*
``outputs``
Shows information about all outputs.
"""
return [
('outputid', 0),
('outputname', frontend.backend.__class__.__name__),
('outputenabled', 1),
]

View File

@ -0,0 +1,47 @@
from mopidy.frontends.mpd import handle_pattern, MpdUnknownCommand
@handle_pattern(r'^command_list_begin$')
def command_list_begin(frontend):
"""
*musicpd.org, command list section:*
To facilitate faster adding of files etc. you can pass a list of
commands all at once using a command list. The command list begins
with ``command_list_begin`` or ``command_list_ok_begin`` and ends
with ``command_list_end``.
It does not execute any commands until the list has ended. The
return value is whatever the return for a list of commands is. On
success for all commands, ``OK`` is returned. If a command fails,
no more commands are executed and the appropriate ``ACK`` error is
returned. If ``command_list_ok_begin`` is used, ``list_OK`` is
returned for each successful command executed in the command list.
"""
frontend.command_list = []
frontend.command_list_ok = False
@handle_pattern(r'^command_list_end$')
def command_list_end(frontend):
"""See :meth:`command_list_begin()`."""
if frontend.command_list is False:
# Test for False exactly, and not e.g. empty list
raise MpdUnknownCommand(command='command_list_end')
(command_list, frontend.command_list) = (frontend.command_list, False)
(command_list_ok, frontend.command_list_ok) = (
frontend.command_list_ok, False)
result = []
for i, command in enumerate(command_list):
response = frontend.handle_request(command, command_list_index=i)
if response is not None:
result.append(response)
if response and response[-1].startswith(u'ACK'):
return result
if command_list_ok:
response.append(u'list_OK')
return result
@handle_pattern(r'^command_list_ok_begin$')
def command_list_ok_begin(frontend):
"""See :meth:`command_list_begin()`."""
frontend.command_list = []
frontend.command_list_ok = True

View File

@ -0,0 +1,48 @@
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
@handle_pattern(r'^close$')
def close(frontend):
"""
*musicpd.org, connection section:*
``close``
Closes the connection to MPD.
"""
# TODO Does not work after multiprocessing branch merge
#frontend.session.do_close()
@handle_pattern(r'^kill$')
def kill(frontend):
"""
*musicpd.org, connection section:*
``kill``
Kills MPD.
"""
# TODO Does not work after multiprocessing branch merge
#frontend.session.do_kill()
@handle_pattern(r'^password "(?P<password>[^"]+)"$')
def password_(frontend, password):
"""
*musicpd.org, connection section:*
``password {PASSWORD}``
This is used for authentication with the server. ``PASSWORD`` is
simply the plaintext password.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^ping$')
def ping(frontend):
"""
*musicpd.org, connection section:*
``ping``
Does nothing but return ``OK``.
"""
pass

View File

@ -0,0 +1,351 @@
from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError,
MpdNotImplemented)
@handle_pattern(r'^add "(?P<uri>[^"]*)"$')
def add(frontend, uri):
"""
*musicpd.org, current playlist section:*
``add {URI}``
Adds the file ``URI`` to the playlist (directories add recursively).
``URI`` can also be a single file.
"""
track = frontend.backend.library.lookup(uri)
if track is None:
raise MpdNoExistError(
u'directory or file not found', command=u'add')
frontend.backend.current_playlist.add(track)
@handle_pattern(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$')
def addid(frontend, uri, songpos=None):
"""
*musicpd.org, current playlist section:*
``addid {URI} [POSITION]``
Adds a song to the playlist (non-recursive) and returns the song id.
``URI`` is always a single file or URL. For example::
addid "foo.mp3"
Id: 999
OK
"""
if songpos is not None:
songpos = int(songpos)
track = frontend.backend.library.lookup(uri)
if track is None:
raise MpdNoExistError(u'No such song', command=u'addid')
if songpos and songpos > len(frontend.backend.current_playlist.tracks):
raise MpdArgError(u'Bad song index', command=u'addid')
cp_track = frontend.backend.current_playlist.add(track, at_position=songpos)
return ('Id', cp_track[0])
@handle_pattern(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
def delete_range(frontend, start, end=None):
"""
*musicpd.org, current playlist section:*
``delete [{POS} | {START:END}]``
Deletes a song from the playlist.
"""
start = int(start)
if end is not None:
end = int(end)
else:
end = len(frontend.backend.current_playlist.tracks)
cp_tracks = frontend.backend.current_playlist.cp_tracks[start:end]
if not cp_tracks:
raise MpdArgError(u'Bad song index', command=u'delete')
for (cpid, _) in cp_tracks:
frontend.backend.current_playlist.remove(cpid=cpid)
@handle_pattern(r'^delete "(?P<songpos>\d+)"$')
def delete_songpos(frontend, songpos):
"""See :meth:`delete_range`"""
try:
songpos = int(songpos)
(cpid, _) = frontend.backend.current_playlist.cp_tracks[songpos]
frontend.backend.current_playlist.remove(cpid=cpid)
except IndexError:
raise MpdArgError(u'Bad song index', command=u'delete')
@handle_pattern(r'^deleteid "(?P<cpid>\d+)"$')
def deleteid(frontend, cpid):
"""
*musicpd.org, current playlist section:*
``deleteid {SONGID}``
Deletes the song ``SONGID`` from the playlist
"""
try:
cpid = int(cpid)
return frontend.backend.current_playlist.remove(cpid=cpid)
except LookupError:
raise MpdNoExistError(u'No such song', command=u'deleteid')
@handle_pattern(r'^clear$')
def clear(frontend):
"""
*musicpd.org, current playlist section:*
``clear``
Clears the current playlist.
"""
frontend.backend.current_playlist.clear()
@handle_pattern(r'^move "(?P<start>\d+):(?P<end>\d+)*" "(?P<to>\d+)"$')
def move_range(frontend, start, to, end=None):
"""
*musicpd.org, current playlist section:*
``move [{FROM} | {START:END}] {TO}``
Moves the song at ``FROM`` or range of songs at ``START:END`` to
``TO`` in the playlist.
"""
if end is None:
end = len(frontend.backend.current_playlist.tracks)
start = int(start)
end = int(end)
to = int(to)
frontend.backend.current_playlist.move(start, end, to)
@handle_pattern(r'^move "(?P<songpos>\d+)" "(?P<to>\d+)"$')
def move_songpos(frontend, songpos, to):
"""See :meth:`move_range`."""
songpos = int(songpos)
to = int(to)
frontend.backend.current_playlist.move(songpos, songpos + 1, to)
@handle_pattern(r'^moveid "(?P<cpid>\d+)" "(?P<to>\d+)"$')
def moveid(frontend, cpid, to):
"""
*musicpd.org, current playlist section:*
``moveid {FROM} {TO}``
Moves the song with ``FROM`` (songid) to ``TO`` (playlist index) in
the playlist. If ``TO`` is negative, it is relative to the current
song in the playlist (if there is one).
"""
cpid = int(cpid)
to = int(to)
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
position = frontend.backend.current_playlist.cp_tracks.index(cp_track)
frontend.backend.current_playlist.move(position, position + 1, to)
@handle_pattern(r'^playlist$')
def playlist(frontend):
"""
*musicpd.org, current playlist section:*
``playlist``
Displays the current playlist.
.. note::
Do not use this, instead use ``playlistinfo``.
"""
return playlistinfo(frontend)
@handle_pattern(r'^playlistfind (?P<tag>[^"]+) "(?P<needle>[^"]+)"$')
@handle_pattern(r'^playlistfind "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
def playlistfind(frontend, tag, needle):
"""
*musicpd.org, current playlist section:*
``playlistfind {TAG} {NEEDLE}``
Finds songs in the current playlist with strict matching.
*GMPC:*
- does not add quotes around the tag.
"""
if tag == 'filename':
try:
cp_track = frontend.backend.current_playlist.get(uri=needle)
return cp_track[1].mpd_format()
except LookupError:
return None
raise MpdNotImplemented # TODO
@handle_pattern(r'^playlistid( "(?P<cpid>\d+)")*$')
def playlistid(frontend, cpid=None):
"""
*musicpd.org, current playlist section:*
``playlistid {SONGID}``
Displays a list of songs in the playlist. ``SONGID`` is optional
and specifies a single song to display info for.
"""
if cpid is not None:
try:
cpid = int(cpid)
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
position = frontend.backend.current_playlist.cp_tracks.index(
cp_track)
return cp_track[1].mpd_format(position=position, cpid=cpid)
except LookupError:
raise MpdNoExistError(u'No such song', command=u'playlistid')
else:
return frontend.backend.current_playlist.mpd_format()
@handle_pattern(r'^playlistinfo$')
@handle_pattern(r'^playlistinfo "(?P<songpos>-?\d+)"$')
@handle_pattern(r'^playlistinfo "(?P<start>\d+):(?P<end>\d+)*"$')
def playlistinfo(frontend, songpos=None,
start=None, end=None):
"""
*musicpd.org, current playlist section:*
``playlistinfo [[SONGPOS] | [START:END]]``
Displays a list of all songs in the playlist, or if the optional
argument is given, displays information only for the song
``SONGPOS`` or the range of songs ``START:END``.
*ncmpc and mpc:*
- uses negative indexes, like ``playlistinfo "-1"``, to request
the entire playlist
"""
if songpos == "-1":
songpos = None
if songpos is not None:
songpos = int(songpos)
start = songpos
end = songpos + 1
if start == -1:
end = None
return frontend.backend.current_playlist.mpd_format(start, end)
else:
if start is None:
start = 0
start = int(start)
if not (0 <= start <= len(frontend.backend.current_playlist.tracks)):
raise MpdArgError(u'Bad song index', command=u'playlistinfo')
if end is not None:
end = int(end)
if end > len(frontend.backend.current_playlist.tracks):
end = None
return frontend.backend.current_playlist.mpd_format(start, end)
@handle_pattern(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
@handle_pattern(r'^playlistsearch (?P<tag>\S+) "(?P<needle>[^"]+)"$')
def playlistsearch(frontend, tag, needle):
"""
*musicpd.org, current playlist section:*
``playlistsearch {TAG} {NEEDLE}``
Searches case-sensitively for partial matches in the current
playlist.
*GMPC:*
- does not add quotes around the tag
- uses ``filename`` and ``any`` as tags
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^plchanges "(?P<version>\d+)"$')
def plchanges(frontend, version):
"""
*musicpd.org, current playlist section:*
``plchanges {VERSION}``
Displays changed songs currently in the playlist since ``VERSION``.
To detect songs that were deleted at the end of the playlist, use
``playlistlength`` returned by status command.
"""
# XXX Naive implementation that returns all tracks as changed
if int(version) < frontend.backend.current_playlist.version:
return frontend.backend.current_playlist.mpd_format()
@handle_pattern(r'^plchangesposid "(?P<version>\d+)"$')
def plchangesposid(frontend, version):
"""
*musicpd.org, current playlist section:*
``plchangesposid {VERSION}``
Displays changed songs currently in the playlist since ``VERSION``.
This function only returns the position and the id of the changed
song, not the complete metadata. This is more bandwidth efficient.
To detect songs that were deleted at the end of the playlist, use
``playlistlength`` returned by status command.
"""
# XXX Naive implementation that returns all tracks as changed
if int(version) != frontend.backend.current_playlist.version:
result = []
for (position, (cpid, _)) in enumerate(
frontend.backend.current_playlist.cp_tracks):
result.append((u'cpos', position))
result.append((u'Id', cpid))
return result
@handle_pattern(r'^shuffle$')
@handle_pattern(r'^shuffle "(?P<start>\d+):(?P<end>\d+)*"$')
def shuffle(frontend, start=None, end=None):
"""
*musicpd.org, current playlist section:*
``shuffle [START:END]``
Shuffles the current playlist. ``START:END`` is optional and
specifies a range of songs.
"""
if start is not None:
start = int(start)
if end is not None:
end = int(end)
frontend.backend.current_playlist.shuffle(start, end)
@handle_pattern(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$')
def swap(frontend, songpos1, songpos2):
"""
*musicpd.org, current playlist section:*
``swap {SONG1} {SONG2}``
Swaps the positions of ``SONG1`` and ``SONG2``.
"""
songpos1 = int(songpos1)
songpos2 = int(songpos2)
tracks = frontend.backend.current_playlist.tracks
song1 = tracks[songpos1]
song2 = tracks[songpos2]
del tracks[songpos1]
tracks.insert(songpos1, song2)
del tracks[songpos2]
tracks.insert(songpos2, song1)
frontend.backend.current_playlist.load(tracks)
@handle_pattern(r'^swapid "(?P<cpid1>\d+)" "(?P<cpid2>\d+)"$')
def swapid(frontend, cpid1, cpid2):
"""
*musicpd.org, current playlist section:*
``swapid {SONG1} {SONG2}``
Swaps the positions of ``SONG1`` and ``SONG2`` (both song ids).
"""
cpid1 = int(cpid1)
cpid2 = int(cpid2)
cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1)
cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2)
position1 = frontend.backend.current_playlist.cp_tracks.index(cp_track1)
position2 = frontend.backend.current_playlist.cp_tracks.index(cp_track2)
swap(frontend, position1, position2)

View File

@ -0,0 +1,254 @@
import re
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
from mopidy.frontends.mpd.protocol import stored_playlists
def build_query(mpd_query):
"""
Parses a MPD query string and converts it to the Mopidy query format.
"""
query_pattern = (
r'"?(?:[Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? "[^"]+"')
query_parts = re.findall(query_pattern, mpd_query)
query_part_pattern = (
r'"?(?P<field>([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny))"? '
r'"(?P<what>[^"]+)"')
query = {}
for query_part in query_parts:
m = re.match(query_part_pattern, query_part)
field = m.groupdict()['field'].lower()
if field == u'title':
field = u'track'
field = str(field) # Needed for kwargs keys on OS X and Windows
what = m.groupdict()['what'].lower()
if field in query:
query[field].append(what)
else:
query[field] = [what]
return query
@handle_pattern(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
def count(frontend, tag, needle):
"""
*musicpd.org, music database section:*
``count {TAG} {NEEDLE}``
Counts the number of songs and their total playtime in the db
matching ``TAG`` exactly.
"""
return [('songs', 0), ('playtime', 0)] # TODO
@handle_pattern(r'^find '
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
' "[^"]+"\s?)+)$')
def find(frontend, mpd_query):
"""
*musicpd.org, music database section:*
``find {TYPE} {WHAT}``
Finds songs in the db that are exactly ``WHAT``. ``TYPE`` should be
``album``, ``artist``, or ``title``. ``WHAT`` is what to find.
*GMPC:*
- does not add quotes around the field argument.
- also uses ``find album "[ALBUM]" artist "[ARTIST]"`` to list album
tracks.
*ncmpc:*
- does not add quotes around the field argument.
- capitalizes the type argument.
"""
query = build_query(mpd_query)
return frontend.backend.library.find_exact(**query).mpd_format()
@handle_pattern(r'^findadd '
r'(?P<query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
'"[^"]+"\s?)+)$')
def findadd(frontend, query):
"""
*musicpd.org, music database section:*
``findadd {TYPE} {WHAT}``
Finds songs in the db that are exactly ``WHAT`` and adds them to
current playlist. ``TYPE`` can be any tag supported by MPD.
``WHAT`` is what to find.
"""
# TODO Add result to current playlist
#result = frontend.find(query)
@handle_pattern(r'^list (?P<field>[Aa]rtist)$')
@handle_pattern(r'^list "(?P<field>[Aa]rtist)"$')
@handle_pattern(r'^list (?P<field>album( artist)?)'
'( "(?P<artist>[^"]+)")*$')
@handle_pattern(r'^list "(?P<field>album(" "artist)?)"'
'( "(?P<artist>[^"]+)")*$')
def list_(frontend, field, artist=None):
"""
*musicpd.org, music database section:*
``list {TYPE} [ARTIST]``
Lists all tags of the specified type. ``TYPE`` should be ``album``,
``artist``, ``date``, or ``genre``.
``ARTIST`` is an optional parameter when type is ``album``,
``date``, or ``genre``.
This filters the result list by an artist.
*GMPC:*
- does not add quotes around the field argument.
- asks for "list artist" to get available artists and will not query
for artist/album information if this is not retrived
- asks for multiple fields, i.e.::
list album artist "an artist name"
returns the albums available for the asked artist::
list album artist "Tiesto"
Album: Radio Trance Vol 4-Promo-CD
Album: Ur A Tear in the Open CDR
Album: Simple Trance 2004 Step One
Album: In Concert 05-10-2003
*ncmpc:*
- does not add quotes around the field argument.
- capitalizes the field argument.
"""
field = field.lower()
if field == u'artist':
return _list_artist(frontend)
elif field == u'album artist':
return _list_album_artist(frontend, artist)
# TODO More to implement
def _list_artist(frontend):
"""
Since we don't know exactly all available artists, we respond with
the artists we know for sure, which is all artists in our stored playlists.
"""
artists = set()
for playlist in frontend.backend.stored_playlists.playlists:
for track in playlist.tracks:
for artist in track.artists:
artists.add((u'Artist', artist.name))
return artists
def _list_album_artist(frontend, artist):
playlist = frontend.backend.library.find_exact(artist=[artist])
albums = set()
for track in playlist.tracks:
albums.add((u'Album', track.album.name))
return albums
@handle_pattern(r'^listall "(?P<uri>[^"]+)"')
def listall(frontend, uri):
"""
*musicpd.org, music database section:*
``listall [URI]``
Lists all songs and directories in ``URI``.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^listallinfo "(?P<uri>[^"]+)"')
def listallinfo(frontend, uri):
"""
*musicpd.org, music database section:*
``listallinfo [URI]``
Same as ``listall``, except it also returns metadata info in the
same format as ``lsinfo``.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^lsinfo$')
@handle_pattern(r'^lsinfo "(?P<uri>[^"]*)"$')
def lsinfo(frontend, uri=None):
"""
*musicpd.org, music database section:*
``lsinfo [URI]``
Lists the contents of the directory ``URI``.
When listing the root directory, this currently returns the list of
stored playlists. This behavior is deprecated; use
``listplaylists`` instead.
MPD returns the same result, including both playlists and the files and
directories located at the root level, for both ``lsinfo``, ``lsinfo
""``, and ``lsinfo "/"``.
"""
if uri is None or uri == u'/' or uri == u'':
return stored_playlists.listplaylists(frontend)
raise MpdNotImplemented # TODO
@handle_pattern(r'^rescan( "(?P<uri>[^"]+)")*$')
def rescan(frontend, uri=None):
"""
*musicpd.org, music database section:*
``rescan [URI]``
Same as ``update``, but also rescans unmodified files.
"""
return update(frontend, uri, rescan_unmodified_files=True)
@handle_pattern(r'^search '
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
' "[^"]+"\s?)+)$')
def search(frontend, mpd_query):
"""
*musicpd.org, music database section:*
``search {TYPE} {WHAT}``
Searches for any song that contains ``WHAT``. ``TYPE`` can be
``title``, ``artist``, ``album`` or ``filename``. Search is not
case sensitive.
*GMPC:*
- does not add quotes around the field argument.
- uses the undocumented field ``any``.
- searches for multiple words like this::
search any "foo" any "bar" any "baz"
*ncmpc:*
- does not add quotes around the field argument.
- capitalizes the field argument.
"""
query = build_query(mpd_query)
return frontend.backend.library.search(**query).mpd_format()
@handle_pattern(r'^update( "(?P<uri>[^"]+)")*$')
def update(frontend, uri=None, rescan_unmodified_files=False):
"""
*musicpd.org, music database section:*
``update [URI]``
Updates the music database: find new files, remove deleted files,
update modified files.
``URI`` is a particular directory or song/file to update. If you do
not specify it, everything is updated.
Prints ``updating_db: JOBID`` where ``JOBID`` is a positive number
identifying the update job. You can read the current job id in the
``status`` response.
"""
return {'updating_db': 0} # TODO

View File

@ -0,0 +1,331 @@
from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError,
MpdNotImplemented)
@handle_pattern(r'^consume "(?P<state>[01])"$')
def consume(frontend, state):
"""
*musicpd.org, playback section:*
``consume {STATE}``
Sets consume state to ``STATE``, ``STATE`` should be 0 or
1. When consume is activated, each song played is removed from
playlist.
"""
if int(state):
frontend.backend.playback.consume = True
else:
frontend.backend.playback.consume = False
@handle_pattern(r'^crossfade "(?P<seconds>\d+)"$')
def crossfade(frontend, seconds):
"""
*musicpd.org, playback section:*
``crossfade {SECONDS}``
Sets crossfading between songs.
"""
seconds = int(seconds)
raise MpdNotImplemented # TODO
@handle_pattern(r'^next$')
def next_(frontend):
"""
*musicpd.org, playback section:*
``next``
Plays next song in the playlist.
*MPD's behaviour when affected by repeat/random/single/consume:*
Given a playlist of three tracks numbered 1, 2, 3, and a currently
playing track ``c``. ``next_track`` is defined at the track that
will be played upon calls to ``next``.
Tests performed on MPD 0.15.4-1ubuntu3.
====== ====== ====== ======= ===== ===== ===== =====
Inputs next_track
------------------------------- ------------------- -----
repeat random single consume c = 1 c = 2 c = 3 Notes
====== ====== ====== ======= ===== ===== ===== =====
T T T T 2 3 EOPL
T T T . Rand Rand Rand [1]
T T . T Rand Rand Rand [4]
T T . . Rand Rand Rand [4]
T . T T 2 3 EOPL
T . T . 2 3 1
T . . T 3 3 EOPL
T . . . 2 3 1
. T T T Rand Rand Rand [3]
. T T . Rand Rand Rand [3]
. T . T Rand Rand Rand [2]
. T . . Rand Rand Rand [2]
. . T T 2 3 EOPL
. . T . 2 3 EOPL
. . . T 2 3 EOPL
. . . . 2 3 EOPL
====== ====== ====== ======= ===== ===== ===== =====
- When end of playlist (EOPL) is reached, the current track is
unset.
- [1] When *random* and *single* is combined, ``next`` selects
a track randomly at each invocation, and not just the next track
in an internal prerandomized playlist.
- [2] When *random* is active, ``next`` will skip through
all tracks in the playlist in random order, and finally EOPL is
reached.
- [3] *single* has no effect in combination with *random*
alone, or *random* and *consume*.
- [4] When *random* and *repeat* is active, EOPL is never
reached, but the playlist is played again, in the same random
order as the first time.
"""
return frontend.backend.playback.next()
@handle_pattern(r'^pause "(?P<state>[01])"$')
def pause(frontend, state):
"""
*musicpd.org, playback section:*
``pause {PAUSE}``
Toggles pause/resumes playing, ``PAUSE`` is 0 or 1.
"""
if int(state):
frontend.backend.playback.pause()
else:
frontend.backend.playback.resume()
@handle_pattern(r'^play$')
def play(frontend):
"""
The original MPD server resumes from the paused state on ``play``
without arguments.
"""
return frontend.backend.playback.play()
@handle_pattern(r'^playid "(?P<cpid>\d+)"$')
@handle_pattern(r'^playid "(?P<cpid>-1)"$')
def playid(frontend, cpid):
"""
*musicpd.org, playback section:*
``playid [SONGID]``
Begins playing the playlist at song ``SONGID``.
*GMPC:*
- issues ``playid "-1"`` after playlist replacement to start playback
at the first track.
"""
cpid = int(cpid)
try:
if cpid == -1:
if not frontend.backend.current_playlist.cp_tracks:
return # Fail silently
cp_track = frontend.backend.current_playlist.cp_tracks[0]
else:
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
return frontend.backend.playback.play(cp_track)
except LookupError:
raise MpdNoExistError(u'No such song', command=u'playid')
@handle_pattern(r'^play "(?P<songpos>\d+)"$')
@handle_pattern(r'^play "(?P<songpos>-1)"$')
def playpos(frontend, songpos):
"""
*musicpd.org, playback section:*
``play [SONGPOS]``
Begins playing the playlist at song number ``SONGPOS``.
*MPoD:*
- issues ``play "-1"`` after playlist replacement to start playback at
the first track.
"""
songpos = int(songpos)
try:
if songpos == -1:
if not frontend.backend.current_playlist.cp_tracks:
return # Fail silently
cp_track = frontend.backend.current_playlist.cp_tracks[0]
else:
cp_track = frontend.backend.current_playlist.cp_tracks[songpos]
return frontend.backend.playback.play(cp_track)
except IndexError:
raise MpdArgError(u'Bad song index', command=u'play')
@handle_pattern(r'^previous$')
def previous(frontend):
"""
*musicpd.org, playback section:*
``previous``
Plays previous song in the playlist.
*MPD's behaviour when affected by repeat/random/single/consume:*
Given a playlist of three tracks numbered 1, 2, 3, and a currently
playing track ``c``. ``previous_track`` is defined at the track
that will be played upon ``previous`` calls.
Tests performed on MPD 0.15.4-1ubuntu3.
====== ====== ====== ======= ===== ===== =====
Inputs previous_track
------------------------------- -------------------
repeat random single consume c = 1 c = 2 c = 3
====== ====== ====== ======= ===== ===== =====
T T T T Rand? Rand? Rand?
T T T . 3 1 2
T T . T Rand? Rand? Rand?
T T . . 3 1 2
T . T T 3 1 2
T . T . 3 1 2
T . . T 3 1 2
T . . . 3 1 2
. T T T c c c
. T T . c c c
. T . T c c c
. T . . c c c
. . T T 1 1 2
. . T . 1 1 2
. . . T 1 1 2
. . . . 1 1 2
====== ====== ====== ======= ===== ===== =====
- If :attr:`time_position` of the current track is 15s or more,
``previous`` should do a seek to time position 0.
"""
return frontend.backend.playback.previous()
@handle_pattern(r'^random "(?P<state>[01])"$')
def random(frontend, state):
"""
*musicpd.org, playback section:*
``random {STATE}``
Sets random state to ``STATE``, ``STATE`` should be 0 or 1.
"""
if int(state):
frontend.backend.playback.random = True
else:
frontend.backend.playback.random = False
@handle_pattern(r'^repeat "(?P<state>[01])"$')
def repeat(frontend, state):
"""
*musicpd.org, playback section:*
``repeat {STATE}``
Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1.
"""
if int(state):
frontend.backend.playback.repeat = True
else:
frontend.backend.playback.repeat = False
@handle_pattern(r'^replay_gain_mode "(?P<mode>(off|track|album))"$')
def replay_gain_mode(frontend, mode):
"""
*musicpd.org, playback section:*
``replay_gain_mode {MODE}``
Sets the replay gain mode. One of ``off``, ``track``, ``album``.
Changing the mode during playback may take several seconds, because
the new settings does not affect the buffered data.
This command triggers the options idle event.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^replay_gain_status$')
def replay_gain_status(frontend):
"""
*musicpd.org, playback section:*
``replay_gain_status``
Prints replay gain options. Currently, only the variable
``replay_gain_mode`` is returned.
"""
return u'off' # TODO
@handle_pattern(r'^seek "(?P<songpos>\d+)" "(?P<seconds>\d+)"$')
def seek(frontend, songpos, seconds):
"""
*musicpd.org, playback section:*
``seek {SONGPOS} {TIME}``
Seeks to the position ``TIME`` (in seconds) of entry ``SONGPOS`` in
the playlist.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^seekid "(?P<cpid>\d+)" "(?P<seconds>\d+)"$')
def seekid(frontend, cpid, seconds):
"""
*musicpd.org, playback section:*
``seekid {SONGID} {TIME}``
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^setvol "(?P<volume>[-+]*\d+)"$')
def setvol(frontend, volume):
"""
*musicpd.org, playback section:*
``setvol {VOL}``
Sets volume to ``VOL``, the range of volume is 0-100.
"""
volume = int(volume)
if volume < 0:
volume = 0
if volume > 100:
volume = 100
frontend.backend.mixer.volume = volume
@handle_pattern(r'^single "(?P<state>[01])"$')
def single(frontend, state):
"""
*musicpd.org, playback section:*
``single {STATE}``
Sets single state to ``STATE``, ``STATE`` should be 0 or 1. When
single is activated, playback is stopped after current song, or
song is repeated if the ``repeat`` mode is enabled.
"""
if int(state):
frontend.backend.playback.single = True
else:
frontend.backend.playback.single = False
@handle_pattern(r'^stop$')
def stop(frontend):
"""
*musicpd.org, playback section:*
``stop``
Stops playing.
"""
frontend.backend.playback.stop()

View File

@ -0,0 +1,79 @@
from mopidy.frontends.mpd import (handle_pattern, mpd_commands,
MpdNotImplemented)
@handle_pattern(r'^commands$')
def commands(frontend):
"""
*musicpd.org, reflection section:*
``commands``
Shows which commands the current user has access to.
As permissions is not implemented, any user has access to all commands.
"""
sorted_commands = sorted(list(mpd_commands))
# Not shown by MPD in its command list
sorted_commands.remove('command_list_begin')
sorted_commands.remove('command_list_ok_begin')
sorted_commands.remove('command_list_end')
sorted_commands.remove('idle')
sorted_commands.remove('noidle')
sorted_commands.remove('sticker')
return [('command', c) for c in sorted_commands]
@handle_pattern(r'^decoders$')
def decoders(frontend):
"""
*musicpd.org, reflection section:*
``decoders``
Print a list of decoder plugins, followed by their supported
suffixes and MIME types. Example response::
plugin: mad
suffix: mp3
suffix: mp2
mime_type: audio/mpeg
plugin: mpcdec
suffix: mpc
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^notcommands$')
def notcommands(frontend):
"""
*musicpd.org, reflection section:*
``notcommands``
Shows which commands the current user does not have access to.
As permissions is not implemented, any user has access to all commands.
"""
pass
@handle_pattern(r'^tagtypes$')
def tagtypes(frontend):
"""
*musicpd.org, reflection section:*
``tagtypes``
Shows a list of available song metadata.
"""
pass # TODO
@handle_pattern(r'^urlhandlers$')
def urlhandlers(frontend):
"""
*musicpd.org, reflection section:*
``urlhandlers``
Gets a list of available URL handlers.
"""
return [(u'handler', uri) for uri in frontend.backend.uri_handlers]

View File

@ -0,0 +1,216 @@
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
@handle_pattern(r'^clearerror$')
def clearerror(frontend):
"""
*musicpd.org, status section:*
``clearerror``
Clears the current error message in status (this is also
accomplished by any command that starts playback).
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^currentsong$')
def currentsong(frontend):
"""
*musicpd.org, status section:*
``currentsong``
Displays the song info of the current song (same song that is
identified in status).
"""
if frontend.backend.playback.current_track is not None:
return frontend.backend.playback.current_track.mpd_format(
position=frontend.backend.playback.current_playlist_position,
cpid=frontend.backend.playback.current_cpid)
@handle_pattern(r'^idle$')
@handle_pattern(r'^idle (?P<subsystems>.+)$')
def idle(frontend, subsystems=None):
"""
*musicpd.org, status section:*
``idle [SUBSYSTEMS...]``
Waits until there is a noteworthy change in one or more of MPD's
subsystems. As soon as there is one, it lists all changed systems
in a line in the format ``changed: SUBSYSTEM``, where ``SUBSYSTEM``
is one of the following:
- ``database``: the song database has been modified after update.
- ``update``: a database update has started or finished. If the
database was modified during the update, the database event is
also emitted.
- ``stored_playlist``: a stored playlist has been modified,
renamed, created or deleted
- ``playlist``: the current playlist has been modified
- ``player``: the player has been started, stopped or seeked
- ``mixer``: the volume has been changed
- ``output``: an audio output has been enabled or disabled
- ``options``: options like repeat, random, crossfade, replay gain
While a client is waiting for idle results, the server disables
timeouts, allowing a client to wait for events as long as MPD runs.
The idle command can be canceled by sending the command ``noidle``
(no other commands are allowed). MPD will then leave idle mode and
print results immediately; might be empty at this time.
If the optional ``SUBSYSTEMS`` argument is used, MPD will only send
notifications when something changed in one of the specified
subsystems.
"""
pass # TODO
@handle_pattern(r'^noidle$')
def noidle(frontend):
"""See :meth:`_status_idle`."""
pass # TODO
@handle_pattern(r'^stats$')
def stats(frontend):
"""
*musicpd.org, status section:*
``stats``
Displays statistics.
- ``artists``: number of artists
- ``songs``: number of albums
- ``uptime``: daemon uptime in seconds
- ``db_playtime``: sum of all song times in the db
- ``db_update``: last db update in UNIX time
- ``playtime``: time length of music played
"""
return {
'artists': 0, # TODO
'albums': 0, # TODO
'songs': 0, # TODO
# TODO Does not work after multiprocessing branch merge
'uptime': 0, # frontend.session.stats_uptime(),
'db_playtime': 0, # TODO
'db_update': 0, # TODO
'playtime': 0, # TODO
}
@handle_pattern(r'^status$')
def status(frontend):
"""
*musicpd.org, status section:*
``status``
Reports the current status of the player and the volume level.
- ``volume``: 0-100
- ``repeat``: 0 or 1
- ``single``: 0 or 1
- ``consume``: 0 or 1
- ``playlist``: 31-bit unsigned integer, the playlist version
number
- ``playlistlength``: integer, the length of the playlist
- ``state``: play, stop, or pause
- ``song``: playlist song number of the current song stopped on or
playing
- ``songid``: playlist songid of the current song stopped on or
playing
- ``nextsong``: playlist song number of the next song to be played
- ``nextsongid``: playlist songid of the next song to be played
- ``time``: total time elapsed (of current playing/paused song)
- ``elapsed``: Total time elapsed within the current song, but with
higher resolution.
- ``bitrate``: instantaneous bitrate in kbps
- ``xfade``: crossfade in seconds
- ``audio``: sampleRate``:bits``:channels
- ``updatings_db``: job id
- ``error``: if there is an error, returns message here
"""
result = [
('volume', _status_volume(frontend)),
('repeat', _status_repeat(frontend)),
('random', _status_random(frontend)),
('single', _status_single(frontend)),
('consume', _status_consume(frontend)),
('playlist', _status_playlist_version(frontend)),
('playlistlength', _status_playlist_length(frontend)),
('xfade', _status_xfade(frontend)),
('state', _status_state(frontend)),
]
if frontend.backend.playback.current_track is not None:
result.append(('song', _status_songpos(frontend)))
result.append(('songid', _status_songid(frontend)))
if frontend.backend.playback.state in (frontend.backend.playback.PLAYING,
frontend.backend.playback.PAUSED):
result.append(('time', _status_time(frontend)))
result.append(('elapsed', _status_time_elapsed(frontend)))
result.append(('bitrate', _status_bitrate(frontend)))
return result
def _status_bitrate(frontend):
if frontend.backend.playback.current_track is not None:
return frontend.backend.playback.current_track.bitrate
def _status_consume(frontend):
if frontend.backend.playback.consume:
return 1
else:
return 0
def _status_playlist_length(frontend):
return len(frontend.backend.current_playlist.tracks)
def _status_playlist_version(frontend):
return frontend.backend.current_playlist.version
def _status_random(frontend):
return int(frontend.backend.playback.random)
def _status_repeat(frontend):
return int(frontend.backend.playback.repeat)
def _status_single(frontend):
return int(frontend.backend.playback.single)
def _status_songid(frontend):
if frontend.backend.playback.current_cpid is not None:
return frontend.backend.playback.current_cpid
else:
return _status_songpos(frontend)
def _status_songpos(frontend):
return frontend.backend.playback.current_playlist_position
def _status_state(frontend):
if frontend.backend.playback.state == frontend.backend.playback.PLAYING:
return u'play'
elif frontend.backend.playback.state == frontend.backend.playback.STOPPED:
return u'stop'
elif frontend.backend.playback.state == frontend.backend.playback.PAUSED:
return u'pause'
def _status_time(frontend):
return u'%s:%s' % (_status_time_elapsed(frontend) // 1000,
_status_time_total(frontend) // 1000)
def _status_time_elapsed(frontend):
return frontend.backend.playback.time_position
def _status_time_total(frontend):
if frontend.backend.playback.current_track is None:
return 0
elif frontend.backend.playback.current_track.length is None:
return 0
else:
return frontend.backend.playback.current_track.length
def _status_volume(frontend):
if frontend.backend.mixer.volume is not None:
return frontend.backend.mixer.volume
else:
return 0
def _status_xfade(frontend):
return 0 # TODO

View File

@ -0,0 +1,64 @@
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
@handle_pattern(r'^sticker delete "(?P<field>[^"]+)" '
r'"(?P<uri>[^"]+)"( "(?P<name>[^"]+)")*$')
def sticker_delete(frontend, field, uri, name=None):
"""
*musicpd.org, sticker section:*
``sticker delete {TYPE} {URI} [NAME]``
Deletes a sticker value from the specified object. If you do not
specify a sticker name, all sticker values are deleted.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^sticker find "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
r'"(?P<name>[^"]+)"$')
def sticker_find(frontend, field, uri, name):
"""
*musicpd.org, sticker section:*
``sticker find {TYPE} {URI} {NAME}``
Searches the sticker database for stickers with the specified name,
below the specified directory (``URI``). For each matching song, it
prints the ``URI`` and that one sticker's value.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^sticker get "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
r'"(?P<name>[^"]+)"$')
def sticker_get(frontend, field, uri, name):
"""
*musicpd.org, sticker section:*
``sticker get {TYPE} {URI} {NAME}``
Reads a sticker value for the specified object.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^sticker list "(?P<field>[^"]+)" "(?P<uri>[^"]+)"$')
def sticker_list(frontend, field, uri):
"""
*musicpd.org, sticker section:*
``sticker list {TYPE} {URI}``
Lists the stickers for the specified object.
"""
raise MpdNotImplemented # TODO
@handle_pattern(r'^sticker set "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
r'"(?P<name>[^"]+)" "(?P<value>[^"]+)"$')
def sticker_set(frontend, field, uri, name, value):
"""
*musicpd.org, sticker section:*
``sticker set {TYPE} {URI} {NAME} {VALUE}``
Adds a sticker value to the specified object. If a sticker item
with that name already exists, it is replaced.
"""
raise MpdNotImplemented # TODO

View File

@ -0,0 +1,180 @@
import datetime as dt
from mopidy.frontends.mpd import (handle_pattern, MpdNoExistError,
MpdNotImplemented)
@handle_pattern(r'^listplaylist "(?P<name>[^"]+)"$')
def listplaylist(frontend, name):
"""
*musicpd.org, stored playlists section:*
``listplaylist {NAME}``
Lists the files in the playlist ``NAME.m3u``.
Output format::
file: relative/path/to/file1.flac
file: relative/path/to/file2.ogg
file: relative/path/to/file3.mp3
"""
try:
return ['file: %s' % t.uri
for t in frontend.backend.stored_playlists.get(name=name).tracks]
except LookupError:
raise MpdNoExistError(u'No such playlist', command=u'listplaylist')
@handle_pattern(r'^listplaylistinfo "(?P<name>[^"]+)"$')
def listplaylistinfo(frontend, name):
"""
*musicpd.org, stored playlists section:*
``listplaylistinfo {NAME}``
Lists songs in the playlist ``NAME.m3u``.
Output format:
Standard track listing, with fields: file, Time, Title, Date,
Album, Artist, Track
"""
try:
return frontend.backend.stored_playlists.get(name=name).mpd_format()
except LookupError:
raise MpdNoExistError(
u'No such playlist', command=u'listplaylistinfo')
@handle_pattern(r'^listplaylists$')
def listplaylists(frontend):
"""
*musicpd.org, stored playlists section:*
``listplaylists``
Prints a list of the playlist directory.
After each playlist name the server sends its last modification
time as attribute ``Last-Modified`` in ISO 8601 format. To avoid
problems due to clock differences between clients and the server,
clients should not compare this value with their local clock.
Output format::
playlist: a
Last-Modified: 2010-02-06T02:10:25Z
playlist: b
Last-Modified: 2010-02-06T02:11:08Z
"""
result = []
for playlist in frontend.backend.stored_playlists.playlists:
result.append((u'playlist', playlist.name))
last_modified = (playlist.last_modified or
dt.datetime.now()).isoformat()
# Remove microseconds
last_modified = last_modified.split('.')[0]
# Add time zone information
# TODO Convert to UTC before adding Z
last_modified = last_modified + 'Z'
result.append((u'Last-Modified', last_modified))
return result
@handle_pattern(r'^load "(?P<name>[^"]+)"$')
def load(frontend, name):
"""
*musicpd.org, stored playlists section:*
``load {NAME}``
Loads the playlist ``NAME.m3u`` from the playlist directory.
"""
matches = frontend.backend.stored_playlists.search(name)
if matches:
frontend.backend.current_playlist.load(matches[0].tracks)
@handle_pattern(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
def _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

View File

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

View File

@ -29,7 +29,7 @@ class RequestHandlerTest(unittest.TestCase):
def test_finding_handler_for_known_command_returns_handler_and_kwargs(self):
expected_handler = lambda x: None
frontend._request_handlers['known_command (?P<arg1>.+)'] = \
frontend.request_handlers['known_command (?P<arg1>.+)'] = \
expected_handler
(handler, kwargs) = self.h.find_handler('known_command an_arg')
self.assertEqual(handler, expected_handler)
@ -42,7 +42,7 @@ class RequestHandlerTest(unittest.TestCase):
def test_handling_known_request(self):
expected = 'magic'
frontend._request_handlers['known request'] = lambda x: expected
frontend.request_handlers['known request'] = lambda x: expected
result = self.h.handle_request('known request')
self.assert_(u'OK' in result)
self.assert_(expected in result)

View File

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