mirror of
https://github.com/beetbox/beets.git
synced 2026-01-17 05:34:23 +01:00
added BPD with main protocol implemented
--HG-- extra : convert_revision : svn%3A41726ec3-264d-0410-9c23-a9f1637257cc/trunk%4092
This commit is contained in:
parent
1560259c84
commit
45a777fc46
1 changed files with 243 additions and 0 deletions
243
beets/bpd.py
Executable file
243
beets/bpd.py
Executable file
|
|
@ -0,0 +1,243 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""A clone of the Music Player Daemon (MPD) that plays music from a
|
||||
Beets library. Attempts to implement a compatible protocol to allow
|
||||
use of the wide range of MPD clients.
|
||||
"""
|
||||
|
||||
import eventlet.api
|
||||
import re
|
||||
from string import Template
|
||||
|
||||
|
||||
|
||||
DEFAULT_PORT = 6600
|
||||
PROTOCOL_VERSION = '0.12.2'
|
||||
|
||||
HELLO = 'OK MPD %s' % PROTOCOL_VERSION
|
||||
CLIST_BEGIN = 'command_list_begin'
|
||||
CLIST_VERBOSE_BEGIN = 'command_list_ok_begin'
|
||||
CLIST_END = 'command_list_end'
|
||||
RESP_OK = 'OK'
|
||||
RESP_CLIST_VERBOSE = 'list_OK'
|
||||
RESP_ERR = 'ACK'
|
||||
|
||||
NEWLINE = "\n"
|
||||
|
||||
ERROR_NOT_LIST = 1
|
||||
ERROR_ARG = 2
|
||||
ERROR_PASSWORD = 3
|
||||
ERROR_PERMISSION = 4
|
||||
ERROR_UNKNOWN = 5
|
||||
ERROR_NO_EXIST = 50
|
||||
ERROR_PLAYLIST_MAX = 51
|
||||
ERROR_SYSTEM = 52
|
||||
ERROR_PLAYLIST_LOAD = 53
|
||||
ERROR_UPDATE_ALREADY = 54
|
||||
ERROR_PLAYER_SYNC = 55
|
||||
ERROR_EXIST = 56
|
||||
|
||||
|
||||
|
||||
class Server(object):
|
||||
"""A MPD-compatible music player server.
|
||||
"""
|
||||
|
||||
def __init__(self, host, port=DEFAULT_PORT):
|
||||
"""Create a new server bound to address `host` and listening
|
||||
on port `port`.
|
||||
"""
|
||||
self.host, self.port = host, port
|
||||
|
||||
def run(self):
|
||||
"""Block and start listening for connections from clients. An
|
||||
interrupt (^C) closes the server.
|
||||
"""
|
||||
self.listener = eventlet.api.tcp_listener((self.host, self.port))
|
||||
while True:
|
||||
try:
|
||||
sock, address = self.listener.accept()
|
||||
except KeyboardInterrupt:
|
||||
break # ^C ends the server.
|
||||
eventlet.api.spawn(Connection.handle, sock)
|
||||
|
||||
class Connection(object):
|
||||
"""A connection between a client and the server. Handles input and
|
||||
output from and to the client.
|
||||
"""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Create a new connection for the accepted socket `client`.
|
||||
"""
|
||||
self.fp = client.makefile()
|
||||
|
||||
def send(self, data):
|
||||
"""Send data, which is either a string or an iterable
|
||||
consisting of strings, to the client. A newline is added after
|
||||
every string.
|
||||
"""
|
||||
if isinstance(data, basestring): # Passed a single string.
|
||||
lines = (data,)
|
||||
else: # Passed an iterable of strings (for instance, a Response).
|
||||
lines = data
|
||||
|
||||
for line in lines:
|
||||
self.fp.write(line + NEWLINE)
|
||||
|
||||
def run(self):
|
||||
"""Send a greeting to the client and begin processing commands
|
||||
as they arrive. Blocks until the client disconnects.
|
||||
"""
|
||||
self.send(HELLO)
|
||||
|
||||
clist = None # Initially, no command list is being constructed.
|
||||
for line in self.fp:
|
||||
line = line.rstrip()
|
||||
|
||||
if clist is not None:
|
||||
# Command list already opened.
|
||||
if line == CLIST_END:
|
||||
self.send(clist.run())
|
||||
clist = None
|
||||
else:
|
||||
clist.append(Command(line))
|
||||
|
||||
elif line == CLIST_BEGIN or line == CLIST_VERBOSE_BEGIN:
|
||||
# Begin a command list.
|
||||
clist = CommandList(line == CLIST_VERBOSE_BEGIN)
|
||||
|
||||
else:
|
||||
# Ordinary command.
|
||||
self.send(Command(line).run())
|
||||
|
||||
@classmethod
|
||||
def handle(cls, client):
|
||||
"""Creates a new `Connection` for `client` and runs it.
|
||||
"""
|
||||
cls(client).run()
|
||||
|
||||
|
||||
|
||||
class Command(object):
|
||||
"""A command issued by the client for processing by the server.
|
||||
|
||||
The functions with the `cmd_` prefix are invoked in response to
|
||||
client commands. For instance, if the client says `status`,
|
||||
`cmd_status` will be invoked. The arguments to the client's commands
|
||||
are used as function arguments (*args). The functions should return
|
||||
a `Response` object.
|
||||
"""
|
||||
|
||||
command_re = re.compile(r'^([^ \t]+)[ \t]*')
|
||||
arg_re = re.compile(r'"((?:\\"|[^"])+)"|([^ \t"]+)')
|
||||
|
||||
def __init__(self, s):
|
||||
"""Creates a new `Command` from the given string, `s`, parsing
|
||||
the string for command name and arguments.
|
||||
"""
|
||||
command_match = self.command_re.match(s)
|
||||
self.name = command_match.group(1)
|
||||
arg_matches = self.arg_re.findall(s[command_match.end():])
|
||||
self.args = [m[0] or m[1] for m in arg_matches]
|
||||
|
||||
def run(self):
|
||||
"""Executes the command, returning a `Response` object.
|
||||
"""
|
||||
func_name = 'cmd_' + self.name
|
||||
if hasattr(self, func_name):
|
||||
return getattr(self, func_name)(*self.args)
|
||||
else:
|
||||
return ErrorResponse(ERROR_UNKNOWN, self.name, 'unknown command')
|
||||
|
||||
class CommandList(list):
|
||||
"""A list of commands issued by the client for processing by the
|
||||
server. May be verbose, in which case the response is delimited, or
|
||||
not. Should be a list of `Command` objects.
|
||||
"""
|
||||
def __init__(self, sequence=None, verbose=False):
|
||||
"""Create a new `CommandList` from the given sequence of
|
||||
`Command`s. If `verbose`, this is a verbose command list.
|
||||
"""
|
||||
if sequence:
|
||||
for item in sequence:
|
||||
self.append(item)
|
||||
self.verbose = verbose
|
||||
|
||||
def run(self):
|
||||
"""Execute all the commands in this list, returning a list of
|
||||
strings to be sent as a response.
|
||||
"""
|
||||
out = []
|
||||
|
||||
for i, command in enumerate(self):
|
||||
resp = command.run()
|
||||
out.extend(resp.items)
|
||||
|
||||
# If the command failed, stop executing and send the completion
|
||||
# code for this command.
|
||||
if isinstance(self, ErrorResponse):
|
||||
resp.index = i # Give the error the correct index.
|
||||
break
|
||||
|
||||
# Otherwise, possibly send the output delimeter if we're in a
|
||||
# verbose ("OK") command list.
|
||||
if self.verbose:
|
||||
out.append(RESP_CLIST_VERBOSE)
|
||||
|
||||
# Give a completion code matching that of the last command (correct
|
||||
# for both success and failure).
|
||||
out.append(resp.completion())
|
||||
|
||||
return out
|
||||
|
||||
|
||||
|
||||
class Response(object):
|
||||
"""A result of executing a single `Command`. A `Response` is
|
||||
iterable and consists of zero or more lines of response data
|
||||
(`items`) and a completion code. It is an abstract class.
|
||||
"""
|
||||
def __init__(self, items=None):
|
||||
self.items = (items if items else [])
|
||||
def __iter__(self):
|
||||
"""Iterate through the `Response`'s items and then its
|
||||
completion code."""
|
||||
return iter(self.items + [self.completion()])
|
||||
def completion(self):
|
||||
"""Returns the completion code of the response."""
|
||||
raise NotImplementedError
|
||||
|
||||
class ErrorResponse(Response):
|
||||
"""A result of a command that fails.
|
||||
"""
|
||||
template = Template('$resp [$code@$index] {$cmd_name} $message')
|
||||
|
||||
def __init__(self, code, cmd_name, message, index=0, items=None):
|
||||
"""Create a new `ErrorResponse` for error code `code`
|
||||
resulting from command with name `cmd_name`. `message` is an
|
||||
explanatory error message, `index` is the index of a command
|
||||
in a command list, and `items` is the additional data to be
|
||||
send to the client.
|
||||
"""
|
||||
super(ErrorResponse, self).__init__(items)
|
||||
self.code, self.index, self.cmd_name, self.message = \
|
||||
code, index, cmd_name, message
|
||||
|
||||
def completion(self):
|
||||
return self.template.substitute({'resp': RESP_ERR,
|
||||
'code': self.code,
|
||||
'index': self.index,
|
||||
'cmd_name': self.cmd_name,
|
||||
'message': self.message
|
||||
})
|
||||
|
||||
class SuccessResponse(Response):
|
||||
"""A result of a command that succeeds.
|
||||
"""
|
||||
def completion(self):
|
||||
return RESP_OK
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Server('0.0.0.0').run()
|
||||
Loading…
Reference in a new issue