Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpd: support MPD 0.16 protocol and more clients #3214

Merged
merged 14 commits into from
Jun 3, 2019
4 changes: 4 additions & 0 deletions beets/util/bluelet.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ def kill_thread(coro):
exc.args[0] == errno.EPIPE:
# Broken pipe. Remote host disconnected.
pass
elif isinstance(exc.args, tuple) and \
exc.args[0] == errno.ECONNRESET:
# Connection was reset by peer.
pass
else:
traceback.print_exc()
# Abort the coroutine.
Expand Down
85 changes: 56 additions & 29 deletions beetsplug/bpd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from mediafile import MediaFile
import six

PROTOCOL_VERSION = '0.14.0'
PROTOCOL_VERSION = '0.16.0'
BUFSIZE = 1024

HELLO = u'OK MPD %s' % PROTOCOL_VERSION
Expand Down Expand Up @@ -77,8 +77,8 @@
SUBSYSTEMS = [
u'update', u'player', u'mixer', u'options', u'playlist', u'database',
# Related to unsupported commands:
# u'stored_playlist', u'output', u'subscription', u'sticker', u'message',
# u'partition',
u'stored_playlist', u'output', u'subscription', u'sticker', u'message',
u'partition',
]

ITEM_KEYS_WRITABLE = set(MediaFile.fields()).intersection(Item._fields.keys())
Expand Down Expand Up @@ -412,6 +412,11 @@ def cmd_status(self, conn):
current_id = self._item_id(self.playlist[self.current_index])
yield u'song: ' + six.text_type(self.current_index)
yield u'songid: ' + six.text_type(current_id)
if len(self.playlist) > self.current_index + 1:
# If there's a next song, report its index too.
next_id = self._item_id(self.playlist[self.current_index + 1])
yield u'nextsong: ' + six.text_type(self.current_index + 1)
yield u'nextsongid: ' + six.text_type(next_id)

if self.error:
yield u'error: ' + self.error
Expand Down Expand Up @@ -454,7 +459,8 @@ def cmd_setvol(self, conn, vol):

def cmd_volume(self, conn, vol_delta):
"""Deprecated command to change the volume by a relative amount."""
raise BPDError(ERROR_SYSTEM, u'No mixer')
vol_delta = cast_arg(int, vol_delta)
return self.cmd_setvol(conn, self.volume + vol_delta)

def cmd_crossfade(self, conn, crossfade):
"""Set the number of seconds of crossfading."""
Expand Down Expand Up @@ -577,23 +583,27 @@ def cmd_urlhandlers(self, conn):
"""Indicates supported URL schemes. None by default."""
pass

def cmd_playlistinfo(self, conn, index=-1):
def cmd_playlistinfo(self, conn, index=None):
"""Gives metadata information about the entire playlist or a
single track, given by its index.
"""
index = cast_arg(int, index)
if index == -1:
if index is None:
for track in self.playlist:
yield self._item_info(track)
else:
indices = self._parse_range(index, accept_single_number=True)
try:
track = self.playlist[index]
tracks = [self.playlist[i] for i in indices]
except IndexError:
raise ArgumentIndexError()
yield self._item_info(track)
for track in tracks:
yield self._item_info(track)

def cmd_playlistid(self, conn, track_id=-1):
return self.cmd_playlistinfo(conn, self._id_to_index(track_id))
def cmd_playlistid(self, conn, track_id=None):
if track_id is not None:
track_id = cast_arg(int, track_id)
track_id = self._id_to_index(track_id)
return self.cmd_playlistinfo(conn, track_id)

def cmd_plchanges(self, conn, version):
"""Sends playlist changes since the given version.
Expand Down Expand Up @@ -624,7 +634,6 @@ def cmd_next(self, conn):
"""Advance to the next song in the playlist."""
old_index = self.current_index
self.current_index = self._succ_idx()
self._send_event('playlist')
if self.consume:
# TODO how does consume interact with single+repeat?
self.playlist.pop(old_index)
Expand All @@ -645,7 +654,6 @@ def cmd_previous(self, conn):
"""Step back to the last song."""
old_index = self.current_index
self.current_index = self._prev_idx()
self._send_event('playlist')
if self.consume:
self.playlist.pop(old_index)
if self.current_index < 0:
Expand Down Expand Up @@ -844,6 +852,9 @@ def run(self):
yield self.send(err.response())
break
continue
if line == u'noidle':
# When not in idle, this command sends no response.
continue

if clist is not None:
# Command list already opened.
Expand Down Expand Up @@ -1094,6 +1105,8 @@ def __init__(self, library, host, port, password, ctrl_port, log):
self.cmd_update(None)
log.info(u'Server ready and listening on {}:{}'.format(
host, port))
log.debug(u'Listening for control signals on {}:{}'.format(
host, ctrl_port))

def run(self):
self.player.run()
Expand All @@ -1111,30 +1124,38 @@ def _item_info(self, item):
info_lines = [
u'file: ' + item.destination(fragment=True),
u'Time: ' + six.text_type(int(item.length)),
u'Title: ' + item.title,
u'Artist: ' + item.artist,
u'Album: ' + item.album,
u'Genre: ' + item.genre,
u'duration: ' + u'{:.3f}'.format(item.length),
u'Id: ' + six.text_type(item.id),
]

track = six.text_type(item.track)
if item.tracktotal:
track += u'/' + six.text_type(item.tracktotal)
info_lines.append(u'Track: ' + track)

info_lines.append(u'Date: ' + six.text_type(item.year))

try:
pos = self._id_to_index(item.id)
info_lines.append(u'Pos: ' + six.text_type(pos))
except ArgumentNotFoundError:
# Don't include position if not in playlist.
pass

info_lines.append(u'Id: ' + six.text_type(item.id))
for tagtype, field in self.tagtype_map.items():
info_lines.append(u'{}: {}'.format(
tagtype, six.text_type(getattr(item, field))))

return info_lines

def _parse_range(self, items, accept_single_number=False):
"""Convert a range of positions to a list of item info.
MPD specifies ranges as START:STOP (endpoint excluded) for some
commands. Sometimes a single number can be provided instead.
"""
try:
start, stop = str(items).split(':', 1)
except ValueError:
if accept_single_number:
return [cast_arg(int, items)]
raise BPDError(ERROR_ARG, u'bad range syntax')
start = cast_arg(int, start)
stop = cast_arg(int, stop)
return range(start, stop)

def _item_id(self, item):
return item.id

Expand Down Expand Up @@ -1335,18 +1356,24 @@ def cmd_decoders(self, conn):

tagtype_map = {
u'Artist': u'artist',
u'ArtistSort': u'artist_sort',
u'Album': u'album',
u'Title': u'title',
u'Track': u'track',
u'AlbumArtist': u'albumartist',
u'AlbumArtistSort': u'albumartist_sort',
# Name?
u'Label': u'label',
u'Genre': u'genre',
u'Date': u'year',
u'OriginalDate': u'original_year',
u'Composer': u'composer',
# Performer?
u'Disc': u'disc',
u'filename': u'path', # Suspect.
u'Comment': u'comments',
u'MUSICBRAINZ_TRACKID': u'mb_trackid',
u'MUSICBRAINZ_ALBUMID': u'mb_albumid',
u'MUSICBRAINZ_ARTISTID': u'mb_artistid',
u'MUSICBRAINZ_ALBUMARTISTID': u'mb_albumartistid',
u'MUSICBRAINZ_RELEASETRACKID': u'mb_releasetrackid',
}

def cmd_tagtypes(self, conn):
Expand Down Expand Up @@ -1543,7 +1570,7 @@ def cmd_stop(self, conn):
def cmd_seek(self, conn, index, pos):
"""Seeks to the specified position in the specified song."""
index = cast_arg(int, index)
pos = cast_arg(int, pos)
pos = cast_arg(float, pos)
super(Server, self).cmd_seek(conn, index, pos)
self.player.seek(pos)

Expand Down
10 changes: 10 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ New features:
(the MBID), and ``work_disambig`` (the disambiguation string).
Thanks to :user:`dosoe`.
:bug:`2580` :bug:`3272`
* :doc:`/plugins/bpd`: BPD now supports most of the features of version 0.16
of the MPD protocol. This is enough to get it talking to more complicated
clients like ncmpcpp, but there are still some incompatibilities, largely due
to MPD commands we don't support yet. Let us know if you find an MPD client
that doesn't get along with BPD!
:bug:`3214` :bug:`800`

Fixes:

Expand All @@ -20,6 +26,10 @@ Fixes:
objects could seem to reuse values from earlier objects when they were
missing a value for a given field. These values are now properly undefined.
:bug:`2406`
* :doc:`/plugins/bpd`: Seeking by fractions of a second now works as intended,
fixing crashes in MPD clients like mpDris2 on seek.
The ``playlistid`` command now works properly in its zero-argument form.
:bug:`3214`

For plugin developers:

Expand Down
5 changes: 2 additions & 3 deletions docs/plugins/bpd.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ but doesn't support many advanced playback features.
Differences from the real MPD
-----------------------------

BPD currently supports version 0.14 of `the MPD protocol`_, but several of the
BPD currently supports version 0.16 of `the MPD protocol`_, but several of the
commands and features are "pretend" implementations or have slightly different
behaviour to their MPD equivalents. BPD aims to look enough like MPD that it
can interact with the ecosystem of clients, but doesn't try to be
Expand All @@ -125,8 +125,7 @@ These are some of the known differences between BPD and MPD:
* Advanced playback features like cross-fade, ReplayGain and MixRamp are not
supported due to BPD's simple audio player backend.
* Advanced query syntax is not currently supported.
* Not all tags (fields) are currently exposed to BPD. Clients also can't use
the ``tagtypes`` mask to hide fields.
* Clients can't use the ``tagtypes`` mask to hide fields.
* BPD's ``random`` mode is not deterministic and doesn't support priorities.
* Mounts and streams are not supported. BPD can only play files from disk.
* Stickers are not supported (although this is basically a flexattr in beets
Expand Down
Loading