mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 16:42:42 +01:00
bpd: implement the idle command
Getting this command puts the connection into a special mode where it awaits MPD events (like the player changing state or the playlist changing due to other clients interacting with the server. The MPD specification states that events should queue while a client is connected, and when it issues the `idle` command any matching events should be sent immediately if there are any, or as soon as they happen otherwise.
This commit is contained in:
parent
ee0c31ba6a
commit
7105c800aa
1 changed files with 78 additions and 0 deletions
|
|
@ -27,6 +27,7 @@ import random
|
|||
import time
|
||||
import math
|
||||
import inspect
|
||||
import socket
|
||||
|
||||
import beets
|
||||
from beets.plugins import BeetsPlugin
|
||||
|
|
@ -72,6 +73,10 @@ SAFE_COMMANDS = (
|
|||
u'close', u'commands', u'notcommands', u'password', u'ping',
|
||||
)
|
||||
|
||||
# List of subsystems/events used by the `idle` command.
|
||||
SUBSYSTEMS = [
|
||||
]
|
||||
|
||||
ITEM_KEYS_WRITABLE = set(MediaFile.fields()).intersection(Item._fields.keys())
|
||||
|
||||
|
||||
|
|
@ -147,6 +152,16 @@ class BPDClose(Exception):
|
|||
should be closed.
|
||||
"""
|
||||
|
||||
|
||||
class BPDIdle(Exception):
|
||||
"""Raised by a command to indicate the client wants to enter the idle state
|
||||
and should be notified when a relevant event happens.
|
||||
"""
|
||||
def __init__(self, subsystems):
|
||||
super(BPDIdle, self).__init__()
|
||||
self.subsystems = set(subsystems)
|
||||
|
||||
|
||||
# Generic server infrastructure, implementing the basic protocol.
|
||||
|
||||
|
||||
|
|
@ -211,6 +226,11 @@ class BaseServer(object):
|
|||
bluelet.run(bluelet.server(self.host, self.port,
|
||||
Connection.handler(self)))
|
||||
|
||||
def _send_event(self, event):
|
||||
"""Notify subscribed connections of an event."""
|
||||
for conn in self.connections:
|
||||
conn.notify(event)
|
||||
|
||||
def _item_info(self, item):
|
||||
"""An abstract method that should response lines containing a
|
||||
single song's metadata.
|
||||
|
|
@ -271,6 +291,14 @@ class BaseServer(object):
|
|||
"""Succeeds."""
|
||||
pass
|
||||
|
||||
def cmd_idle(self, conn, *subsystems):
|
||||
subsystems = subsystems or SUBSYSTEMS
|
||||
for system in subsystems:
|
||||
if system not in SUBSYSTEMS:
|
||||
raise BPDError(ERROR_ARG,
|
||||
u'Unrecognised idle event: {}'.format(system))
|
||||
raise BPDIdle(subsystems) # put the connection to sleep
|
||||
|
||||
def cmd_kill(self, conn):
|
||||
"""Exits the server process."""
|
||||
exit(0)
|
||||
|
|
@ -657,6 +685,7 @@ class Connection(object):
|
|||
self.sock = sock
|
||||
self.authenticated = False
|
||||
self.address = u'{}:{}'.format(*sock.sock.getpeername())
|
||||
self.notifications = set()
|
||||
|
||||
def send(self, lines):
|
||||
"""Send lines, which which is either a single string or an
|
||||
|
|
@ -692,6 +721,50 @@ class Connection(object):
|
|||
self.server.disconnect(self)
|
||||
self.server._log.debug(u'*[{}]: disconnected', self.address)
|
||||
|
||||
def poll_notifications(self, subsystems):
|
||||
"""Sleep until we have some notifications from the subsystems given.
|
||||
In order to promptly detect if the client has disconnected while
|
||||
idling, try reading a single byte from the socket. According to the MPD
|
||||
protocol the client can send the special command `noidle` to cancel
|
||||
idle mode, otherwise we're expecting either a timeout or a zero-byte
|
||||
reply. When we have notifications, send them to the client.
|
||||
"""
|
||||
while True:
|
||||
mpd_events = self.notifications.intersection(subsystems)
|
||||
if mpd_events:
|
||||
break
|
||||
current_timeout = self.sock.sock.gettimeout()
|
||||
try:
|
||||
self.sock.sock.settimeout(0.01)
|
||||
data = self.sock.sock.recv(1)
|
||||
if data: # Client sent data when it was meant to by idle.
|
||||
line = yield self.sock.readline()
|
||||
command = (data + line).rstrip()
|
||||
if command == b'noidle':
|
||||
self.server._log.debug(
|
||||
u'<[{}]: noidle'.format(self.address))
|
||||
break
|
||||
err = BPDError(
|
||||
ERROR_UNKNOWN,
|
||||
u'Got command while idle: {}'.format(
|
||||
command.decode('utf-8')))
|
||||
yield self.send(err.response())
|
||||
return
|
||||
else: # The socket has been closed.
|
||||
return
|
||||
except socket.timeout: # The socket is still alive.
|
||||
pass
|
||||
finally:
|
||||
self.sock.sock.settimeout(current_timeout)
|
||||
yield bluelet.sleep(0.02)
|
||||
self.notifications = self.notifications.difference(subsystems)
|
||||
for event in mpd_events:
|
||||
yield self.send(u'changed: {}'.format(event))
|
||||
yield self.send(RESP_OK)
|
||||
|
||||
def notify(self, event):
|
||||
self.notifications.add(event)
|
||||
|
||||
def run(self):
|
||||
"""Send a greeting to the client and begin processing commands
|
||||
as they arrive.
|
||||
|
|
@ -738,6 +811,8 @@ class Connection(object):
|
|||
self.sock.close()
|
||||
self.disconnect() # Client explicitly closed.
|
||||
return
|
||||
except BPDIdle as e:
|
||||
yield bluelet.call(self.poll_notifications(e.subsystems))
|
||||
|
||||
@classmethod
|
||||
def handler(cls, server):
|
||||
|
|
@ -832,6 +907,9 @@ class Command(object):
|
|||
# it on the Connection.
|
||||
raise
|
||||
|
||||
except BPDIdle:
|
||||
raise
|
||||
|
||||
except Exception:
|
||||
# An "unintentional" error. Hide it from the client.
|
||||
conn.server._log.error('{}', traceback.format_exc())
|
||||
|
|
|
|||
Loading…
Reference in a new issue