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: extend coverage of MPD protocol #3200

Merged
merged 23 commits into from Apr 2, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
de6718a
bpd: separate tests by command category
arcresu Apr 1, 2019
d94a539
bpd: fix crossfade command
arcresu Mar 30, 2019
0f53ae9
bpd: error instead of crashing on extra argument
arcresu Mar 30, 2019
1511e31
bpd: add mixramp commands
arcresu Mar 30, 2019
67a0b38
bpd: add dummy command for volume
arcresu Mar 30, 2019
e585186
bpd: add replay_gain_* commands
arcresu Mar 30, 2019
859e16d
bpd: support consume command
arcresu Mar 30, 2019
71e7621
bpd: no-op support for persistent playlists
arcresu Apr 1, 2019
bae9c40
bpd: support the single command
arcresu Apr 1, 2019
b245c0e
bpd: test fields returned by status command
arcresu Apr 1, 2019
0c3a63e
bpd: fix repeat mode behaviour
arcresu Apr 1, 2019
a4fe687
bpd: fix bug in bounds check of current song index
arcresu Apr 1, 2019
12e49b3
bpd: skipping backwards through zero keeps playing
arcresu Apr 1, 2019
146c5f5
bpd: fix repeat, consume and single in reverse
arcresu Apr 1, 2019
e839e4e
bpd: improve exception handling
arcresu Apr 1, 2019
9622e74
bpd: return real audio data
arcresu Apr 1, 2019
36c85a8
Fix beets.util.inspect for Python 3
arcresu Apr 1, 2019
4be2e1b
Remove beets.util.inspect wrapper
arcresu Apr 1, 2019
28db7d3
bpd: provide precision time in status
arcresu Apr 2, 2019
d074dac
bpd: add comments to the error handling code
arcresu Apr 2, 2019
20e2f8b
bpd: output an info-level message when ready
arcresu Apr 2, 2019
140d25d
Changelog for #3200
arcresu Apr 2, 2019
95dd513
bpd: add flake8 exception for test command
arcresu Apr 2, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
149 changes: 136 additions & 13 deletions beetsplug/bpd/__init__.py
Expand Up @@ -25,6 +25,7 @@
import traceback
import random
import time
import math

import beets
from beets.plugins import BeetsPlugin
Expand Down Expand Up @@ -172,8 +173,13 @@ def __init__(self, host, port, password, log):
# Default server values.
self.random = False
self.repeat = False
self.consume = False
self.single = False
self.volume = VOLUME_MAX
self.crossfade = 0
self.mixrampdb = 0.0
self.mixrampdelay = float('nan')
self.replay_gain_mode = 'off'
self.playlist = []
self.playlist_version = 0
self.current_index = -1
Expand Down Expand Up @@ -227,10 +233,10 @@ def _random_idx(self):

def _succ_idx(self):
"""Returns the index for the next song to play.
It also considers random and repeat flags.
It also considers random, single and repeat flags.
No boundaries are checked.
"""
if self.repeat:
if self.repeat and self.single:
return self.current_index
if self.random:
return self._random_idx()
Expand All @@ -241,7 +247,7 @@ def _prev_idx(self):
It also considers random and repeat flags.
No boundaries are checked.
"""
if self.repeat:
if self.repeat and self.single:
return self.current_index
if self.random:
return self._random_idx()
Expand Down Expand Up @@ -305,11 +311,18 @@ def cmd_status(self, conn):
u'volume: ' + six.text_type(self.volume),
u'repeat: ' + six.text_type(int(self.repeat)),
u'random: ' + six.text_type(int(self.random)),
u'consume: ' + six.text_type(int(self.consume)),
u'single: ' + six.text_type(int(self.single)),
u'playlist: ' + six.text_type(self.playlist_version),
u'playlistlength: ' + six.text_type(len(self.playlist)),
u'xfade: ' + six.text_type(self.crossfade),
u'mixrampdb: ' + six.text_type(self.mixrampdb),
)

if not math.isnan(self.mixrampdelay):
yield u'mixrampdelay: ' + six.text_type(self.mixrampdelay)
if self.crossfade > 0:
yield u'xfade: ' + six.text_type(self.crossfade)

if self.current_index == -1:
state = u'stop'
elif self.paused:
Expand Down Expand Up @@ -341,18 +354,60 @@ def cmd_repeat(self, conn, state):
"""Set or unset repeat mode."""
self.repeat = cast_arg('intbool', state)

def cmd_consume(self, conn, state):
"""Set or unset consume mode."""
self.consume = cast_arg('intbool', state)

def cmd_single(self, conn, state):
"""Set or unset single mode."""
# TODO support oneshot in addition to 0 and 1 [MPD 0.20]
self.single = cast_arg('intbool', state)

def cmd_setvol(self, conn, vol):
"""Set the player's volume level (0-100)."""
vol = cast_arg(int, vol)
if vol < VOLUME_MIN or vol > VOLUME_MAX:
raise BPDError(ERROR_ARG, u'volume out of range')
self.volume = 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')

def cmd_crossfade(self, conn, crossfade):
"""Set the number of seconds of crossfading."""
crossfade = cast_arg(int, crossfade)
if crossfade < 0:
raise BPDError(ERROR_ARG, u'crossfade time must be nonnegative')
self._log.warning(u'crossfade is not implemented in bpd')
self.crossfade = crossfade

def cmd_mixrampdb(self, conn, db):
"""Set the mixramp normalised max volume in dB."""
db = cast_arg(float, db)
if db > 0:
raise BPDError(ERROR_ARG, u'mixrampdb time must be negative')
self._log.warning('mixramp is not implemented in bpd')
self.mixrampdb = db

def cmd_mixrampdelay(self, conn, delay):
"""Set the mixramp delay in seconds."""
delay = cast_arg(float, delay)
if delay < 0:
raise BPDError(ERROR_ARG, u'mixrampdelay time must be nonnegative')
self._log.warning('mixramp is not implemented in bpd')
self.mixrampdelay = delay

def cmd_replay_gain_mode(self, conn, mode):
"""Set the replay gain mode."""
if mode not in ['off', 'track', 'album', 'auto']:
raise BPDError(ERROR_ARG, u'Unrecognised replay gain mode')
self._log.warning('replay gain is not implemented in bpd')
self.replay_gain_mode = mode

def cmd_replay_gain_status(self, conn):
"""Get the replaygain mode."""
yield u'replay_gain_mode: ' + six.text_type(self.replay_gain_mode)

def cmd_clear(self, conn):
"""Clear the playlist."""
Expand Down Expand Up @@ -477,20 +532,36 @@ def cmd_currentsong(self, conn):

def cmd_next(self, conn):
"""Advance to the next song in the playlist."""
old_index = self.current_index
self.current_index = self._succ_idx()
if self.consume:
# TODO how does consume interact with single+repeat?
self.playlist.pop(old_index)
if self.current_index > old_index:
self.current_index -= 1
if self.current_index >= len(self.playlist):
# Fallen off the end. Just move to stopped state.
# Fallen off the end. Move to stopped state or loop.
if self.repeat:
self.current_index = -1
return self.cmd_play(conn)
return self.cmd_stop(conn)
elif self.single and not self.repeat:
return self.cmd_stop(conn)
else:
return self.cmd_play(conn)

def cmd_previous(self, conn):
"""Step back to the last song."""
old_index = self.current_index
self.current_index = self._prev_idx()
if self.consume:
self.playlist.pop(old_index)
if self.current_index < 0:
return self.cmd_stop(conn)
else:
return self.cmd_play(conn)
if self.repeat:
self.current_index = len(self.playlist) - 1
else:
self.current_index = 0
return self.cmd_play(conn)

def cmd_pause(self, conn, state=None):
"""Set the pause state playback."""
Expand All @@ -503,7 +574,7 @@ def cmd_play(self, conn, index=-1):
"""Begin playback, possibly at a specified playlist index."""
index = cast_arg(int, index)

if index < -1 or index > len(self.playlist):
if index < -1 or index >= len(self.playlist):
raise ArgumentIndexError()

if index == -1: # No index specified: start where we are.
Expand Down Expand Up @@ -670,7 +741,8 @@ def run(self, conn):
# Attempt to get correct command function.
func_name = 'cmd_' + self.name
if not hasattr(conn.server, func_name):
raise BPDError(ERROR_UNKNOWN, u'unknown command', self.name)
raise BPDError(ERROR_UNKNOWN,
u'unknown command "{}"'.format(self.name))
func = getattr(conn.server, func_name)

# Ensure we have permission for this command.
Expand All @@ -686,6 +758,13 @@ def run(self, conn):
for data in results:
yield conn.send(data)

except TypeError:
# The client provided too many arguments.
raise BPDError(ERROR_ARG,
u'wrong number of arguments for "{}"'
.format(self.name),
self.name)
arcresu marked this conversation as resolved.
Show resolved Hide resolved

except BPDError as e:
# An exposed error. Set the command name and then let
# the Connection handle it.
Expand Down Expand Up @@ -939,11 +1018,19 @@ def cmd_status(self, conn):
if self.current_index > -1:
item = self.playlist[self.current_index]

yield u'bitrate: ' + six.text_type(item.bitrate / 1000)
# Missing 'audio'.
yield (
u'bitrate: ' + six.text_type(item.bitrate / 1000),
# TODO provide a real value samplerate:bits:channels 44100:24:2
u'audio: 0:0:0',
arcresu marked this conversation as resolved.
Show resolved Hide resolved
)

(pos, total) = self.player.time()
yield u'time: ' + six.text_type(pos) + u':' + six.text_type(total)
yield (
u'time: ' + six.text_type(pos) + u':' + six.text_type(total),
# TODO provide elapsed and duration with higher precision
u'elapsed: ' + six.text_type(float(pos)),
u'duration: ' + six.text_type(float(total)),
)

# Also missing 'updating_db'.

Expand Down Expand Up @@ -1075,6 +1162,42 @@ def cmd_count(self, conn, tag, value):
yield u'songs: ' + six.text_type(songs)
yield u'playtime: ' + six.text_type(int(playtime))

# Persistent playlist manipulation. In MPD this is an optional feature so
# these dummy implementations match MPD's behaviour with the feature off.

def cmd_listplaylist(self, conn, playlist):
raise BPDError(ERROR_NO_EXIST, u'No such playlist')

def cmd_listplaylistinfo(self, conn, playlist):
raise BPDError(ERROR_NO_EXIST, u'No such playlist')

def cmd_listplaylists(self, conn):
raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled')

def cmd_load(self, conn, playlist):
raise BPDError(ERROR_NO_EXIST, u'Stored playlists are disabled')

def cmd_playlistadd(self, conn, playlist, uri):
raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled')

def cmd_playlistclear(self, conn, playlist):
raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled')

def cmd_playlistdelete(self, conn, playlist, index):
raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled')

def cmd_playlistmove(self, conn, playlist, from_index, to_index):
raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled')

def cmd_rename(self, conn, playlist, new_name):
raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled')

def cmd_rm(self, conn, playlist):
raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled')

def cmd_save(self, conn, playlist):
raise BPDError(ERROR_UNKNOWN, u'Stored playlists are disabled')

# "Outputs." Just a dummy implementation because we don't control
# any outputs.

Expand Down