init
commit
a53302eb62
|
@ -0,0 +1,10 @@
|
|||
__pycache__
|
||||
*.txt
|
||||
|
||||
._*
|
||||
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import gi, sys, os
|
||||
# import dbus
|
||||
# import dbus.service, dbus.mainloop.glib
|
||||
from pprint import pprint as print
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf
|
||||
|
||||
from window import SonistWindow
|
||||
# from mpd.asyncio import MPDClient
|
||||
from mpd.base import MPDClient
|
||||
|
||||
app_id = 'fun.wkit.sonist'
|
||||
|
||||
|
||||
class Application(Gtk.Application):
|
||||
def __init__(self):
|
||||
Gtk.Application.__init__(self, application_id = app_id)
|
||||
|
||||
self.window = SonistWindow()
|
||||
|
||||
self.mpc = MPDClient()
|
||||
self.mpc.timeout = 10
|
||||
self.mpc.connect("localhost", 6600)
|
||||
|
||||
|
||||
def do_activate(self):
|
||||
print('hello mpc')
|
||||
# self.window.show_all()
|
||||
|
||||
|
||||
|
||||
""" class ApplicationService(dbus.service.Object):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
bus_name = dbus.service.BusName(app_id, bus = dbus.SessionBus())
|
||||
dbus.service.Object.__init__(self, bus_name, '/')
|
||||
|
||||
|
||||
@dbus.service.method(app_id)
|
||||
def call_app(self):
|
||||
self.app.present()
|
||||
"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
# dbus.mainloop.glib.DBusGMainLoop(set_as_default = True)
|
||||
# bus = dbus.SessionBus()
|
||||
|
||||
# try:
|
||||
# obj = bus.get_object(app_id, '/')
|
||||
# obj.call_app()
|
||||
# sys.exit(0)
|
||||
# except dbus.DBusException:
|
||||
# pass
|
||||
|
||||
|
||||
app = Application()
|
||||
app.run(sys.argv)
|
||||
|
||||
# ApplicationService(app)
|
||||
|
||||
Gtk.main()
|
|
@ -0,0 +1,38 @@
|
|||
# python-mpd2: Python MPD client library
|
||||
#
|
||||
# Copyright (C) 2008-2010 J. Alexander Treuman <jat@spatialrift.net>
|
||||
# Copyright (C) 2012 J. Thalheim <jthalheim@gmail.com>
|
||||
# Copyright (C) 2016 Robert Niederreiter <rnix@squarewave.at>
|
||||
#
|
||||
# python-mpd2 is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# python-mpd2 is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with python-mpd2. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mpd.base import CommandError
|
||||
from mpd.base import CommandListError
|
||||
from mpd.base import ConnectionError
|
||||
from mpd.base import FailureResponseCode
|
||||
from mpd.base import IteratingError
|
||||
from mpd.base import MPDClient
|
||||
from mpd.base import MPDError
|
||||
from mpd.base import PendingCommandError
|
||||
from mpd.base import ProtocolError
|
||||
from mpd.base import VERSION
|
||||
|
||||
|
||||
try:
|
||||
from mpd.twisted import MPDProtocol
|
||||
except ImportError:
|
||||
|
||||
class MPDProtocol:
|
||||
def __init__():
|
||||
raise "No twisted module found"
|
|
@ -0,0 +1,593 @@
|
|||
"""Asynchronous access to MPD using the asyncio methods of Python 3.
|
||||
|
||||
Interaction happens over the mpd.asyncio.MPDClient class, whose connect and
|
||||
command methods are coroutines.
|
||||
|
||||
Some commands (eg. listall) additionally support the asynchronous iteration
|
||||
(aiter, `async for`) interface; using it allows the library user to obtain
|
||||
items of result as soon as they arrive.
|
||||
|
||||
The .idle() method works differently here: It is an asynchronous iterator that
|
||||
produces a list of changed subsystems whenever a new one is available. The
|
||||
MPDClient object automatically switches in and out of idle mode depending on
|
||||
which subsystems there is currently interest in.
|
||||
|
||||
Command lists are currently not supported.
|
||||
|
||||
|
||||
This module requires Python 3.5.2 or later to run.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from typing import Optional, List, Tuple, Iterable, Callable, Union
|
||||
|
||||
from mpd.base import HELLO_PREFIX, ERROR_PREFIX, SUCCESS
|
||||
from mpd.base import MPDClientBase
|
||||
from mpd.base import MPDClient as SyncMPDClient
|
||||
from mpd.base import ProtocolError, ConnectionError, CommandError, CommandListError
|
||||
from mpd.base import mpd_command_provider
|
||||
|
||||
|
||||
class BaseCommandResult(asyncio.Future):
|
||||
"""A future that carries its command/args/callback with it for the
|
||||
convenience of passing it around to the command queue."""
|
||||
|
||||
def __init__(self, command, args, callback):
|
||||
super().__init__()
|
||||
self._command = command
|
||||
self._args = args
|
||||
self._callback = callback
|
||||
|
||||
async def _feed_from(self, mpdclient):
|
||||
while True:
|
||||
line = await mpdclient._read_line()
|
||||
self._feed_line(line)
|
||||
if line is None:
|
||||
return
|
||||
|
||||
|
||||
class CommandResult(BaseCommandResult):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__spooled_lines = []
|
||||
|
||||
def _feed_line(self, line): # FIXME just inline?
|
||||
"""Put the given line into the callback machinery, and set the result on a None line."""
|
||||
if line is None:
|
||||
if self.cancelled():
|
||||
# Data was still pulled out of the connection, but the original
|
||||
# requester has cancelled the request -- no need to filter the
|
||||
# data through the preprocessing callback
|
||||
pass
|
||||
else:
|
||||
self.set_result(self._callback(self.__spooled_lines))
|
||||
else:
|
||||
self.__spooled_lines.append(line)
|
||||
|
||||
def _feed_error(self, error):
|
||||
if not self.done():
|
||||
self.set_exception(error)
|
||||
else:
|
||||
# These do occur (especially during the test suite run) when a
|
||||
# disconnect was already initialized, but the run task being
|
||||
# cancelled has not ever yielded at all and thus still needs to run
|
||||
# through to its first await point (which is then in a situation
|
||||
# where properties it'd like to access are already cleaned up,
|
||||
# resulting in an AttributeError)
|
||||
#
|
||||
# Rather than quenching them here, they are made visible (so that
|
||||
# other kinds of double errors raise visibly, even though none are
|
||||
# known right now); instead, the run loop yields initially with a
|
||||
# sleep(0) that ensures it can be cancelled properly at any time.
|
||||
raise error
|
||||
|
||||
class BinaryCommandResult(asyncio.Future):
|
||||
# Unlike the regular commands that defer to any callback that may be
|
||||
# defined for them, this uses the predefined _read_binary mechanism of the
|
||||
# mpdclient
|
||||
async def _feed_from(self, mpdclient):
|
||||
# Data must be pulled out no matter whether will later be ignored or not
|
||||
binary = await mpdclient._read_binary()
|
||||
if self.cancelled():
|
||||
pass
|
||||
else:
|
||||
self.set_result(binary)
|
||||
|
||||
_feed_error = CommandResult._feed_error
|
||||
|
||||
class CommandResultIterable(BaseCommandResult):
|
||||
"""Variant of CommandResult where the underlying callback is an
|
||||
asynchronous` generator, and can thus interpret lines as they come along.
|
||||
|
||||
The result can be used with the aiter interface (`async for`). If it is
|
||||
still used as a future instead, it eventually results in a list.
|
||||
|
||||
Commands used with this CommandResult must use their passed lines not like
|
||||
an iterable (as in the synchronous implementation), but as a asyncio.Queue.
|
||||
Furthermore, they must check whether the queue elements are exceptions, and
|
||||
raise them.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__spooled_lines = asyncio.Queue()
|
||||
|
||||
def _feed_line(self, line):
|
||||
self.__spooled_lines.put_nowait(line)
|
||||
|
||||
_feed_error = _feed_line
|
||||
|
||||
def __await__(self):
|
||||
asyncio.Task(self.__feed_future())
|
||||
return super().__await__()
|
||||
|
||||
__iter__ = __await__ # for 'yield from' style invocation
|
||||
|
||||
async def __feed_future(self):
|
||||
result = []
|
||||
try:
|
||||
async for r in self:
|
||||
result.append(r)
|
||||
except Exception as e:
|
||||
self.set_exception(e)
|
||||
else:
|
||||
if not self.cancelled():
|
||||
self.set_result(result)
|
||||
|
||||
def __aiter__(self):
|
||||
if self.done():
|
||||
raise RuntimeError("Command result is already being consumed")
|
||||
return self._callback(self.__spooled_lines).__aiter__()
|
||||
|
||||
|
||||
@mpd_command_provider
|
||||
class MPDClient(MPDClientBase):
|
||||
__run_task = None # doubles as indicator for being connected
|
||||
|
||||
#: Indicator of whether there is a pending idle command that was not terminated yet.
|
||||
# When in doubt; this is True, thus erring at the side of caution (because
|
||||
# a "noidle" being sent while racing against an incoming idle notification
|
||||
# does no harm)
|
||||
__in_idle = False
|
||||
|
||||
#: Indicator that the last attempted idle failed.
|
||||
#
|
||||
# When set, IMMEDIATE_COMMAND_TIMEOUT is ignored in favor of waiting until
|
||||
# *something* else happens, and only then retried.
|
||||
#
|
||||
# Note that the only known condition in which this happens is when between
|
||||
# start of the connection and the presentation of credentials, more than
|
||||
# IMMEDIATE_COMMAND_TIMEOUT passes.
|
||||
__idle_failed = False
|
||||
|
||||
#: Seconds after a command's completion to send idle. Setting this too high
|
||||
# causes "blind spots" in the client's view of the server, setting it too
|
||||
# low sends needless idle/noidle after commands in quick succession.
|
||||
IMMEDIATE_COMMAND_TIMEOUT = 0.1
|
||||
|
||||
#: FIFO list of processors that may consume the read stream one after the
|
||||
# other
|
||||
#
|
||||
# As we don't have any other form of backpressure in the sending side
|
||||
# (which is not expected to be limited), its limit of COMMAND_QUEUE_LENGTH
|
||||
# serves as a limit against commands queuing up indefinitely. (It's not
|
||||
# *directly* throttling output, but as the convention is to put the
|
||||
# processor on the queue and then send the command, and commands are of
|
||||
# limited size, this is practically creating backpressure.)
|
||||
__command_queue = None
|
||||
|
||||
#: Construction size of __command_queue. The default limit is high enough
|
||||
# that a client can easily send off all existing commands simultaneously
|
||||
# without needlessly blocking the TCP flow, but small enough that
|
||||
# freespinning tasks create warnings.
|
||||
COMMAND_QUEUE_LENGTH = 128
|
||||
|
||||
#: Callbacks registered by any current callers of `idle()`.
|
||||
#
|
||||
# The first argument lists the changes that the caller is interested in
|
||||
# (and all current listeners' union is used to populate the `idle`
|
||||
# command's arguments), the latter is an actual callback that will be
|
||||
# passed either a set of changes or an exception.
|
||||
__idle_consumers: Optional[List[Tuple[
|
||||
Iterable[str],
|
||||
Callable[[Union[List[str], Exception]], None]
|
||||
]]] = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__rfile = self.__wfile = None
|
||||
|
||||
async def connect(self, host, port=6600, loop=None):
|
||||
if loop is not None:
|
||||
warnings.warn("loop passed into MPDClient.connect is ignored, this will become an error", DeprecationWarning)
|
||||
if host.startswith("@"):
|
||||
host = "\0" + host[1:]
|
||||
if host.startswith("\0") or "/" in host:
|
||||
r, w = await asyncio.open_unix_connection(host)
|
||||
else:
|
||||
r, w = await asyncio.open_connection(host, port)
|
||||
self.__rfile, self.__wfile = r, w
|
||||
|
||||
self.__command_queue = asyncio.Queue(maxsize=self.COMMAND_QUEUE_LENGTH)
|
||||
self.__idle_consumers = [] #: list of (subsystem-list, callbacks) tuples
|
||||
|
||||
try:
|
||||
helloline = await asyncio.wait_for(self.__readline(), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
self.disconnect()
|
||||
raise ConnectionError("No response from server while reading MPD hello")
|
||||
# FIXME should be reusable w/o reaching in
|
||||
SyncMPDClient._hello(self, helloline)
|
||||
|
||||
self.__run_task = asyncio.Task(self.__run())
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return self.__run_task is not None
|
||||
|
||||
def disconnect(self):
|
||||
if (
|
||||
self.__run_task is not None
|
||||
): # is None eg. when connection fails in .connect()
|
||||
self.__run_task.cancel()
|
||||
if self.__wfile is not None:
|
||||
self.__wfile.close()
|
||||
self.__rfile = self.__wfile = None
|
||||
self.__run_task = None
|
||||
self.__command_queue = None
|
||||
if self.__idle_consumers is not None:
|
||||
# copying the list as each raising callback will remove itself from __idle_consumers
|
||||
for subsystems, callback in list(self.__idle_consumers):
|
||||
callback(ConnectionError())
|
||||
self.__idle_consumers = None
|
||||
|
||||
def _get_idle_interests(self):
|
||||
"""Accumulate a set of interests from the current __idle_consumers.
|
||||
Returns the union of their subscribed subjects, [] if at least one of
|
||||
them is the empty catch-all set, or None if there are no interests at
|
||||
all."""
|
||||
|
||||
if not self.__idle_consumers:
|
||||
return None
|
||||
if any(len(s) == 0 for (s, c) in self.__idle_consumers):
|
||||
return []
|
||||
return set.union(*(set(s) for (s, c) in self.__idle_consumers))
|
||||
|
||||
def _end_idle(self):
|
||||
"""If the main task is currently idling, make it leave idle and process
|
||||
the next command (if one is present) or just restart idle"""
|
||||
|
||||
if self.__in_idle:
|
||||
self.__write("noidle\n")
|
||||
self.__in_idle = False
|
||||
|
||||
async def __run(self):
|
||||
# See CommandResult._feed_error documentation
|
||||
await asyncio.sleep(0)
|
||||
result = None
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
self.__command_queue.get(),
|
||||
timeout=self.IMMEDIATE_COMMAND_TIMEOUT,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
# The cancellation of the __command_queue.get() that happens
|
||||
# in this case is intended, and is just what asyncio.Queue
|
||||
# suggests for "get with timeout".
|
||||
|
||||
if not self.__command_queue.empty():
|
||||
# A __command_queue.put() has happened after the
|
||||
# asyncio.wait_for() timeout but before execution of
|
||||
# this coroutine resumed. Looping around again will
|
||||
# fetch the new entry from the queue.
|
||||
continue
|
||||
|
||||
if self.__idle_failed:
|
||||
# We could try for a more elaborate path where we now
|
||||
# await the command queue indefinitely, but as we're
|
||||
# already in an error case, this whole situation only
|
||||
# persists until the error is processed somewhere else,
|
||||
# so ticking once per idle timeout is OK to keep things
|
||||
# simple.
|
||||
continue
|
||||
|
||||
subsystems = self._get_idle_interests()
|
||||
if subsystems is None:
|
||||
# The presumably most quiet subsystem -- in this case,
|
||||
# idle is only used to keep the connection alive.
|
||||
subsystems = ["database"]
|
||||
|
||||
# Careful: There can't be any await points between the
|
||||
# except and here, or the sequence between the idle and the
|
||||
# command processor might be wrong.
|
||||
result = CommandResult("idle", subsystems, self._parse_list)
|
||||
result.add_done_callback(self.__idle_result)
|
||||
self.__in_idle = True
|
||||
self._write_command(result._command, result._args)
|
||||
|
||||
# A new command was issued, so there's a chance that whatever
|
||||
# made idle fail is now fixed.
|
||||
self.__idle_failed = False
|
||||
|
||||
try:
|
||||
await result._feed_from(self)
|
||||
except CommandError as e:
|
||||
result._feed_error(e)
|
||||
# This kind of error we can tolerate without breaking up
|
||||
# the connection; any other would fly out, be reported
|
||||
# through the result and terminate the connection
|
||||
|
||||
except Exception as e:
|
||||
# Prevent the destruction of the pending task in the shutdown
|
||||
# function -- it's just shutting down by itself.
|
||||
self.__run_task = None
|
||||
self.disconnect()
|
||||
|
||||
if result is not None:
|
||||
# The last command has failed: Forward that result.
|
||||
#
|
||||
# (In idle, that's fine too -- everyone watching see a
|
||||
# nonspecific event).
|
||||
result._feed_error(e)
|
||||
return
|
||||
else:
|
||||
raise
|
||||
# Typically this is a bug in mpd.asyncio.
|
||||
|
||||
def __idle_result(self, result):
|
||||
try:
|
||||
idle_changes = result.result()
|
||||
except CommandError as e:
|
||||
# Don't retry until something changed
|
||||
self.__idle_failed = True
|
||||
|
||||
# Not raising this any further: The callbacks are notified that
|
||||
# "something is up" (which is all their API gives), and whichever
|
||||
# command is issued to act on it will hopefully run into the same
|
||||
# condition.
|
||||
#
|
||||
# This does swallow the exact error cause.
|
||||
|
||||
idle_changes = set()
|
||||
for subsystems, _ in self.__idle_consumers:
|
||||
idle_changes = idle_changes.union(subsystems)
|
||||
|
||||
# make generator accessible multiple times
|
||||
idle_changes = list(idle_changes)
|
||||
|
||||
for subsystems, callback in self.__idle_consumers:
|
||||
if not subsystems or any(s in subsystems for s in idle_changes):
|
||||
callback(idle_changes)
|
||||
|
||||
# helper methods
|
||||
|
||||
async def __readline(self):
|
||||
"""Wrapper around .__rfile.readline that handles encoding"""
|
||||
data = await self.__rfile.readline()
|
||||
try:
|
||||
return data.decode("utf8")
|
||||
except UnicodeDecodeError:
|
||||
self.disconnect()
|
||||
raise ProtocolError("Invalid UTF8 received")
|
||||
|
||||
async def _read_chunk(self, length):
|
||||
try:
|
||||
return await self.__rfile.readexactly(length)
|
||||
except asyncio.IncompleteReadError:
|
||||
raise ConnectionError("Connection lost while reading binary")
|
||||
|
||||
def __write(self, text):
|
||||
"""Wrapper around .__wfile.write that handles encoding."""
|
||||
self.__wfile.write(text.encode("utf8"))
|
||||
|
||||
# copied and subtly modifiedstuff from base
|
||||
|
||||
# This is just a wrapper for the below.
|
||||
def _write_line(self, text):
|
||||
self.__write(text + "\n")
|
||||
|
||||
# FIXME This code should be shareable.
|
||||
_write_command = SyncMPDClient._write_command
|
||||
|
||||
async def _read_line(self):
|
||||
line = await self.__readline()
|
||||
if not line.endswith("\n"):
|
||||
raise ConnectionError("Connection lost while reading line")
|
||||
line = line.rstrip("\n")
|
||||
if line.startswith(ERROR_PREFIX):
|
||||
error = line[len(ERROR_PREFIX) :].strip()
|
||||
raise CommandError(error)
|
||||
if line == SUCCESS:
|
||||
return None
|
||||
return line
|
||||
|
||||
async def _parse_objects_direct(self, lines, delimiters=[], lookup_delimiter=False):
|
||||
obj = {}
|
||||
while True:
|
||||
line = await lines.get()
|
||||
if isinstance(line, BaseException):
|
||||
raise line
|
||||
if line is None:
|
||||
break
|
||||
key, value = self._parse_pair(line, separator=": ")
|
||||
key = key.lower()
|
||||
if lookup_delimiter and not delimiters:
|
||||
delimiters = [key]
|
||||
if obj:
|
||||
if key in delimiters:
|
||||
yield obj
|
||||
obj = {}
|
||||
elif key in obj:
|
||||
if not isinstance(obj[key], list):
|
||||
obj[key] = [obj[key], value]
|
||||
else:
|
||||
obj[key].append(value)
|
||||
continue
|
||||
obj[key] = value
|
||||
if obj:
|
||||
yield obj
|
||||
|
||||
async def _execute_binary(self, command, args):
|
||||
# Fun fact: By fetching data in lockstep, this is a bit less efficient
|
||||
# than it could be (which would be "after having received the first
|
||||
# chunk, guess that the other chunks are of equal size and request at
|
||||
# several multiples concurrently, ensuring the TCP connection can stay
|
||||
# full), but at the other hand it leaves the command queue empty so
|
||||
# that more time critical commands can be executed right away
|
||||
|
||||
data = None
|
||||
args = list(args)
|
||||
assert len(args) == 1
|
||||
args.append(0)
|
||||
final_metadata = None
|
||||
while True:
|
||||
partial_result = BinaryCommandResult()
|
||||
await self.__command_queue.put(partial_result)
|
||||
self._end_idle()
|
||||
self._write_command(command, args)
|
||||
metadata = await partial_result
|
||||
chunk = metadata.pop('binary', None)
|
||||
|
||||
if final_metadata is None:
|
||||
data = chunk
|
||||
final_metadata = metadata
|
||||
if not data:
|
||||
break
|
||||
try:
|
||||
size = int(final_metadata['size'])
|
||||
except KeyError:
|
||||
size = len(chunk)
|
||||
except ValueError:
|
||||
raise CommandError("Size data unsuitable for binary transfer")
|
||||
else:
|
||||
if metadata != final_metadata:
|
||||
raise CommandError("Metadata of binary data changed during transfer")
|
||||
if chunk is None:
|
||||
raise CommandError("Binary field vanished changed during transfer")
|
||||
data += chunk
|
||||
args[-1] = len(data)
|
||||
if len(data) > size:
|
||||
raise CommandListError("Binary data announced size exceeded")
|
||||
elif len(data) == size:
|
||||
break
|
||||
|
||||
if data is not None:
|
||||
final_metadata['binary'] = data
|
||||
|
||||
final_metadata.pop('size', None)
|
||||
|
||||
return final_metadata
|
||||
|
||||
# omits _read_chunk checking because the async version already
|
||||
# raises; otherwise it's just awaits sprinkled in
|
||||
async def _read_binary(self):
|
||||
obj = {}
|
||||
|
||||
while True:
|
||||
line = await self._read_line()
|
||||
if line is None:
|
||||
break
|
||||
|
||||
key, value = self._parse_pair(line, ": ")
|
||||
|
||||
if key == "binary":
|
||||
chunk_size = int(value)
|
||||
value = await self._read_chunk(chunk_size)
|
||||
|
||||
if await self.__rfile.readexactly(1) != b"\n":
|
||||
# newline after binary content
|
||||
self.disconnect()
|
||||
raise ConnectionError("Connection lost while reading line")
|
||||
|
||||
obj[key] = value
|
||||
return obj
|
||||
|
||||
# command provider interface
|
||||
@classmethod
|
||||
def add_command(cls, name, callback):
|
||||
if callback.mpd_commands_binary:
|
||||
async def f(self, *args):
|
||||
result = await self._execute_binary(name, args)
|
||||
|
||||
# With binary, the callback is applied to the final result
|
||||
# rather than to the iterator over the lines (cf.
|
||||
# MPDClient._execute_binary)
|
||||
return callback(self, result)
|
||||
else:
|
||||
command_class = (
|
||||
CommandResultIterable if callback.mpd_commands_direct else CommandResult
|
||||
)
|
||||
if hasattr(cls, name):
|
||||
# Idle and noidle are explicitly implemented, skipping them.
|
||||
return
|
||||
|
||||
def f(self, *args):
|
||||
result = command_class(name, args, partial(callback, self))
|
||||
if self.__run_task is None:
|
||||
raise ConnectionError("Can not send command to disconnected client")
|
||||
|
||||
try:
|
||||
self.__command_queue.put_nowait(result)
|
||||
except asyncio.QueueFull as e:
|
||||
e.args = ("Command queue overflowing; this indicates the"
|
||||
" application sending commands in an uncontrolled"
|
||||
" fashion without awaiting them, and typically"
|
||||
" indicates a memory leak.",)
|
||||
# While we *could* indicate to the queued result that it has
|
||||
# yet to send its request, that'd practically create a queue of
|
||||
# awaited items in the user application that's growing
|
||||
# unlimitedly, eliminating any chance of timely responses.
|
||||
# Furthermore, the author sees no practical use case that's not
|
||||
# violating MPD's guidance of "Do not manage a client-side copy
|
||||
# of MPD's database". If a use case *does* come up, any change
|
||||
# would need to maintain the property of providing backpressure
|
||||
# information. That would require an API change.
|
||||
raise
|
||||
|
||||
self._end_idle()
|
||||
# Careful: There can't be any await points between the queue
|
||||
# appending and the write
|
||||
try:
|
||||
self._write_command(result._command, result._args)
|
||||
except BaseException as e:
|
||||
self.disconnect()
|
||||
result.set_exception(e)
|
||||
return result
|
||||
|
||||
escaped_name = name.replace(" ", "_")
|
||||
f.__name__ = escaped_name
|
||||
setattr(cls, escaped_name, f)
|
||||
|
||||
# commands that just work differently
|
||||
async def idle(self, subsystems=()):
|
||||
if self.__idle_consumers is None:
|
||||
raise ConnectionError("Can not start idle on a disconnected client")
|
||||
|
||||
interests_before = self._get_idle_interests()
|
||||
# A queue accepting either a list of things that changed in a single
|
||||
# idle cycle, or an exception to be raised
|
||||
changes = asyncio.Queue()
|
||||
try:
|
||||
entry = (subsystems, changes.put_nowait)
|
||||
self.__idle_consumers.append(entry)
|
||||
if self._get_idle_interests != interests_before:
|
||||
# Technically this does not enter idle *immediately* but rather
|
||||
# only after any commands after IMMEDIATE_COMMAND_TIMEOUT;
|
||||
# practically that should be a good thing.
|
||||
self._end_idle()
|
||||
while True:
|
||||
item = await changes.get()
|
||||
if isinstance(item, Exception):
|
||||
raise item
|
||||
yield item
|
||||
finally:
|
||||
if self.__idle_consumers is not None:
|
||||
self.__idle_consumers.remove(entry)
|
||||
|
||||
def noidle(self):
|
||||
raise AttributeError("noidle is not supported / required in mpd.asyncio")
|
|
@ -0,0 +1,866 @@
|
|||
# python-mpd2: Python MPD client library
|
||||
#
|
||||
# Copyright (C) 2008-2010 J. Alexander Treuman <jat@spatialrift.net>
|
||||
# Copyright (C) 2012 J. Thalheim <jthalheim@gmail.com>
|
||||
# Copyright (C) 2016 Robert Niederreiter <rnix@squarewave.at>
|
||||
#
|
||||
# python-mpd2 is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# python-mpd2 is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with python-mpd2. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
VERSION = (3, 1, 0)
|
||||
HELLO_PREFIX = "OK MPD "
|
||||
ERROR_PREFIX = "ACK "
|
||||
ERROR_PATTERN = re.compile(r"\[(?P<errno>\d+)@(?P<offset>\d+)\]\s+{(?P<command>\w+)}\s+(?P<msg>.*)")
|
||||
SUCCESS = "OK"
|
||||
NEXT = "list_OK"
|
||||
|
||||
|
||||
def escape(text):
|
||||
return text.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
|
||||
try:
|
||||
from logging import NullHandler
|
||||
except ImportError: # NullHandler was introduced in python2.7
|
||||
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.addHandler(NullHandler())
|
||||
|
||||
|
||||
# MPD Protocol errors as found in CommandError exceptions
|
||||
# https://github.com/MusicPlayerDaemon/MPD/blob/master/src/protocol/Ack.hxx
|
||||
class FailureResponseCode(Enum):
|
||||
NOT_LIST = 1
|
||||
ARG = 2
|
||||
PASSWORD = 3
|
||||
PERMISSION = 4
|
||||
UNKNOWN = 5
|
||||
|
||||
NO_EXIST = 50
|
||||
PLAYLIST_MAX = 51
|
||||
SYSTEM = 52
|
||||
PLAYLIST_LOAD = 53
|
||||
UPDATE_ALREADY = 54
|
||||
PLAYER_SYNC = 55
|
||||
EXIST = 56
|
||||
|
||||
|
||||
class MPDError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionError(MPDError):
|
||||
pass
|
||||
|
||||
|
||||
class ProtocolError(MPDError):
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(MPDError):
|
||||
def __init__(self, error):
|
||||
self.errno = None
|
||||
self.offset = None
|
||||
self.command = None
|
||||
self.msg = None
|
||||
|
||||
match = ERROR_PATTERN.match(error)
|
||||
if match:
|
||||
self.errno = FailureResponseCode(int(match.group("errno")))
|
||||
self.offset = int(match.group("offset"))
|
||||
self.command = match.group("command")
|
||||
self.msg = match.group("msg")
|
||||
|
||||
|
||||
class CommandListError(MPDError):
|
||||
pass
|
||||
|
||||
|
||||
class PendingCommandError(MPDError):
|
||||
pass
|
||||
|
||||
|
||||
class IteratingError(MPDError):
|
||||
pass
|
||||
|
||||
|
||||
class mpd_commands(object):
|
||||
"""Decorator for registering MPD commands with it's corresponding result
|
||||
callback.
|
||||
"""
|
||||
|
||||
def __init__(self, *commands, **kwargs):
|
||||
self.commands = commands
|
||||
self.is_direct = kwargs.pop("is_direct", False)
|
||||
self.is_binary = kwargs.pop("is_binary", False)
|
||||
if kwargs:
|
||||
raise AttributeError(
|
||||
"mpd_commands() got unexpected keyword"
|
||||
" arguments %s" % ",".join(kwargs)
|
||||
)
|
||||
|
||||
def __call__(self, ob):
|
||||
ob.mpd_commands = self.commands
|
||||
ob.mpd_commands_direct = self.is_direct
|
||||
ob.mpd_commands_binary = self.is_binary
|
||||
return ob
|
||||
|
||||
|
||||
def mpd_command_provider(cls):
|
||||
"""Decorator hooking up registered MPD commands to concrete client
|
||||
implementation.
|
||||
|
||||
A class using this decorator must inherit from ``MPDClientBase`` and
|
||||
implement it's ``add_command`` function.
|
||||
"""
|
||||
|
||||
def collect(cls, callbacks=dict()):
|
||||
"""Collect MPD command callbacks from given class.
|
||||
|
||||
Searches class __dict__ on given class and all it's bases for functions
|
||||
which have been decorated with @mpd_commands and returns a dict
|
||||
containing callback name as keys and
|
||||
(callback, callback implementing class) tuples as values.
|
||||
"""
|
||||
for name, ob in cls.__dict__.items():
|
||||
if hasattr(ob, "mpd_commands") and name not in callbacks:
|
||||
callbacks[name] = (ob, cls)
|
||||
for base in cls.__bases__:
|
||||
callbacks = collect(base, callbacks)
|
||||
return callbacks
|
||||
|
||||
for name, value in collect(cls).items():
|
||||
callback, from_ = value
|
||||
for command in callback.mpd_commands:
|
||||
cls.add_command(command, callback)
|
||||
return cls
|
||||
|
||||
|
||||
class Noop(object):
|
||||
"""An instance of this class represents a MPD command callback which
|
||||
does nothing.
|
||||
"""
|
||||
|
||||
mpd_commands = None
|
||||
|
||||
|
||||
class MPDClientBase(object):
|
||||
"""Abstract MPD client.
|
||||
|
||||
This class defines a general client contract, provides MPD protocol parsers
|
||||
and defines all available MPD commands and it's corresponding result
|
||||
parsing callbacks. There might be the need of overriding some callbacks on
|
||||
subclasses.
|
||||
"""
|
||||
|
||||
def __init__(self, use_unicode=None):
|
||||
self.iterate = False
|
||||
if use_unicode is not None:
|
||||
warnings.warn(
|
||||
"use_unicode parameter to ``MPDClientBase`` constructor is "
|
||||
"deprecated",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self._reset()
|
||||
|
||||
@property
|
||||
def use_unicode(self):
|
||||
warnings.warn(
|
||||
"``use_unicode`` is deprecated: python-mpd 2.x always uses "
|
||||
"Unicode",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def add_command(cls, name, callback):
|
||||
raise NotImplementedError(
|
||||
"Abstract ``MPDClientBase`` does not implement ``add_command``"
|
||||
)
|
||||
|
||||
def noidle(self):
|
||||
raise NotImplementedError(
|
||||
"Abstract ``MPDClientBase`` does not implement ``noidle``"
|
||||
)
|
||||
|
||||
def command_list_ok_begin(self):
|
||||
raise NotImplementedError(
|
||||
"Abstract ``MPDClientBase`` does not implement " "``command_list_ok_begin``"
|
||||
)
|
||||
|
||||
def command_list_end(self):
|
||||
raise NotImplementedError(
|
||||
"Abstract ``MPDClientBase`` does not implement " "``command_list_end``"
|
||||
)
|
||||
|
||||
def _reset(self):
|
||||
self.mpd_version = None
|
||||
self._command_list = None
|
||||
|
||||
def _parse_pair(self, line, separator):
|
||||
if line is None:
|
||||
return
|
||||
pair = line.split(separator, 1)
|
||||
if len(pair) < 2:
|
||||
raise ProtocolError("Could not parse pair: '{}'".format(line))
|
||||
return pair
|
||||
|
||||
def _parse_pairs(self, lines, separator=": "):
|
||||
for line in lines:
|
||||
yield self._parse_pair(line, separator)
|
||||
|
||||
def _parse_objects(self, lines, delimiters=[], lookup_delimiter=False):
|
||||
obj = {}
|
||||
for key, value in self._parse_pairs(lines):
|
||||
key = key.lower()
|
||||
if lookup_delimiter and key not in delimiters:
|
||||
delimiters = delimiters + [key]
|
||||
if obj:
|
||||
if key in delimiters:
|
||||
if lookup_delimiter:
|
||||
if key in obj:
|
||||
yield obj
|
||||
obj = obj.copy()
|
||||
while delimiters[-1] != key:
|
||||
obj.pop(delimiters[-1], None)
|
||||
delimiters.pop()
|
||||
else:
|
||||
yield obj
|
||||
obj = {}
|
||||
elif key in obj:
|
||||
if not isinstance(obj[key], list):
|
||||
obj[key] = [obj[key], value]
|
||||
else:
|
||||
obj[key].append(value)
|
||||
continue
|
||||
obj[key] = value
|
||||
if obj:
|
||||
yield obj
|
||||
|
||||
# Use this instead of _parse_objects whenever the result is returned
|
||||
# immediately in a command implementation
|
||||
_parse_objects_direct = _parse_objects
|
||||
|
||||
def _parse_raw_stickers(self, lines):
|
||||
for key, sticker in self._parse_pairs(lines):
|
||||
value = sticker.split("=", 1)
|
||||
if len(value) < 2:
|
||||
raise ProtocolError("Could not parse sticker: {}".format(repr(sticker)))
|
||||
yield tuple(value)
|
||||
|
||||
NOOP = mpd_commands("close", "kill")(Noop())
|
||||
|
||||
@mpd_commands("plchangesposid", is_direct=True)
|
||||
def _parse_changes(self, lines):
|
||||
return self._parse_objects_direct(lines, ["cpos"])
|
||||
|
||||
@mpd_commands("listall", "listallinfo", "listfiles", "lsinfo", is_direct=True)
|
||||
def _parse_database(self, lines):
|
||||
return self._parse_objects_direct(lines, ["file", "directory", "playlist"])
|
||||
|
||||
@mpd_commands("idle")
|
||||
def _parse_idle(self, lines):
|
||||
return self._parse_list(lines)
|
||||
|
||||
@mpd_commands("addid", "config", "replay_gain_status", "rescan", "update")
|
||||
def _parse_item(self, lines):
|
||||
pairs = list(self._parse_pairs(lines))
|
||||
if len(pairs) != 1:
|
||||
return
|
||||
return pairs[0][1]
|
||||
|
||||
@mpd_commands(
|
||||
"channels", "commands", "listplaylist", "notcommands", "tagtypes", "urlhandlers"
|
||||
)
|
||||
def _parse_list(self, lines):
|
||||
seen = None
|
||||
for key, value in self._parse_pairs(lines):
|
||||
if key != seen:
|
||||
if seen is not None:
|
||||
raise ProtocolError("Expected key '{}', got '{}'".format(seen, key))
|
||||
seen = key
|
||||
yield value
|
||||
|
||||
@mpd_commands("list", is_direct=True)
|
||||
def _parse_list_groups(self, lines):
|
||||
return self._parse_objects_direct(lines, lookup_delimiter=True)
|
||||
|
||||
@mpd_commands("readmessages", is_direct=True)
|
||||
def _parse_messages(self, lines):
|
||||
return self._parse_objects_direct(lines, ["channel"])
|
||||
|
||||
@mpd_commands("listmounts", is_direct=True)
|
||||
def _parse_mounts(self, lines):
|
||||
return self._parse_objects_direct(lines, ["mount"])
|
||||
|
||||
@mpd_commands("listneighbors", is_direct=True)
|
||||
def _parse_neighbors(self, lines):
|
||||
return self._parse_objects_direct(lines, ["neighbor"])
|
||||
|
||||
@mpd_commands("listpartitions", is_direct=True)
|
||||
def _parse_partitions(self, lines):
|
||||
return self._parse_objects_direct(lines, ["partition"])
|
||||
|
||||
@mpd_commands(
|
||||
"add",
|
||||
"addtagid",
|
||||
"binarylimit",
|
||||
"clear",
|
||||
"clearerror",
|
||||
"cleartagid",
|
||||
"consume",
|
||||
"crossfade",
|
||||
"delete",
|
||||
"deleteid",
|
||||
"delpartition",
|
||||
"disableoutput",
|
||||
"enableoutput",
|
||||
"findadd",
|
||||
"load",
|
||||
"mixrampdb",
|
||||
"mixrampdelay",
|
||||
"mount",
|
||||
"move",
|
||||
"moveid",
|
||||
"moveoutput",
|
||||
"newpartition",
|
||||
"next",
|
||||
"outputvolume",
|
||||
"partition",
|
||||
"password",
|
||||
"pause",
|
||||
"ping",
|
||||
"play",
|
||||
"playid",
|
||||
"playlistadd",
|
||||
"playlistclear",
|
||||
"playlistdelete",
|
||||
"playlistmove",
|
||||
"previous",
|
||||
"prio",
|
||||
"prioid",
|
||||
"random",
|
||||
"rangeid",
|
||||
"rename",
|
||||
"repeat",
|
||||
"replay_gain_mode",
|
||||
"rm",
|
||||
"save",
|
||||
"searchadd",
|
||||
"searchaddpl",
|
||||
"seek",
|
||||
"seekcur",
|
||||
"seekid",
|
||||
"sendmessage",
|
||||
"setvol",
|
||||
"shuffle",
|
||||
"single",
|
||||
"sticker delete",
|
||||
"sticker set",
|
||||
"stop",
|
||||
"subscribe",
|
||||
"swap",
|
||||
"swapid",
|
||||
"toggleoutput",
|
||||
"unmount",
|
||||
"unsubscribe",
|
||||
"volume",
|
||||
)
|
||||
def _parse_nothing(self, lines):
|
||||
for line in lines:
|
||||
raise ProtocolError(
|
||||
"Got unexpected return value: '{}'".format(", ".join(lines))
|
||||
)
|
||||
|
||||
@mpd_commands("count", "currentsong", "readcomments", "stats", "status")
|
||||
def _parse_object(self, lines):
|
||||
objs = list(self._parse_objects(lines))
|
||||
if not objs:
|
||||
return {}
|
||||
return objs[0]
|
||||
|
||||
@mpd_commands("outputs", is_direct=True)
|
||||
def _parse_outputs(self, lines):
|
||||
return self._parse_objects_direct(lines, ["outputid"])
|
||||
|
||||
@mpd_commands("playlist")
|
||||
def _parse_playlist(self, lines):
|
||||
for key, value in self._parse_pairs(lines, ":"):
|
||||
yield value
|
||||
|
||||
@mpd_commands("listplaylists", is_direct=True)
|
||||
def _parse_playlists(self, lines):
|
||||
return self._parse_objects_direct(lines, ["playlist"])
|
||||
|
||||
@mpd_commands("decoders", is_direct=True)
|
||||
def _parse_plugins(self, lines):
|
||||
return self._parse_objects_direct(lines, ["plugin"])
|
||||
|
||||
@mpd_commands(
|
||||
"find",
|
||||
"listplaylistinfo",
|
||||
"playlistfind",
|
||||
"playlistid",
|
||||
"playlistinfo",
|
||||
"playlistsearch",
|
||||
"plchanges",
|
||||
"search",
|
||||
"sticker find",
|
||||
is_direct=True,
|
||||
)
|
||||
def _parse_songs(self, lines):
|
||||
return self._parse_objects_direct(lines, ["file"])
|
||||
|
||||
@mpd_commands("sticker get")
|
||||
def _parse_sticker(self, lines):
|
||||
key, value = list(self._parse_raw_stickers(lines))[0]
|
||||
return value
|
||||
|
||||
@mpd_commands("sticker list")
|
||||
def _parse_stickers(self, lines):
|
||||
return dict(self._parse_raw_stickers(lines))
|
||||
|
||||
@mpd_commands("albumart", "readpicture", is_binary=True)
|
||||
def _parse_plain_binary(self, structure):
|
||||
return structure
|
||||
|
||||
|
||||
def _create_callback(self, function, wrap_result):
|
||||
"""Create MPD command related response callback.
|
||||
"""
|
||||
if not callable(function):
|
||||
return None
|
||||
|
||||
def command_callback():
|
||||
# command result callback expects response from MPD as iterable lines,
|
||||
# thus read available lines from socket
|
||||
res = function(self, self._read_lines())
|
||||
# wrap result in iterator helper if desired
|
||||
if wrap_result:
|
||||
res = self._wrap_iterator(res)
|
||||
return res
|
||||
|
||||
return command_callback
|
||||
|
||||
|
||||
def _create_command(wrapper, name, return_value, wrap_result):
|
||||
"""Create MPD command related function.
|
||||
"""
|
||||
|
||||
def mpd_command(self, *args):
|
||||
callback = _create_callback(self, return_value, wrap_result)
|
||||
return wrapper(self, name, args, callback)
|
||||
|
||||
return mpd_command
|
||||
|
||||
|
||||
class _NotConnected(object):
|
||||
def __getattr__(self, attr):
|
||||
return self._dummy
|
||||
|
||||
def _dummy(*args):
|
||||
raise ConnectionError("Not connected")
|
||||
|
||||
|
||||
@mpd_command_provider
|
||||
class MPDClient(MPDClientBase):
|
||||
idletimeout = None
|
||||
_timeout = None
|
||||
_wrap_iterator_parsers = [
|
||||
MPDClientBase._parse_list,
|
||||
MPDClientBase._parse_list_groups,
|
||||
MPDClientBase._parse_playlist,
|
||||
MPDClientBase._parse_changes,
|
||||
MPDClientBase._parse_songs,
|
||||
MPDClientBase._parse_mounts,
|
||||
MPDClientBase._parse_neighbors,
|
||||
MPDClientBase._parse_partitions,
|
||||
MPDClientBase._parse_playlists,
|
||||
MPDClientBase._parse_database,
|
||||
MPDClientBase._parse_messages,
|
||||
MPDClientBase._parse_outputs,
|
||||
MPDClientBase._parse_plugins,
|
||||
]
|
||||
|
||||
def __init__(self, use_unicode=None):
|
||||
if use_unicode is not None:
|
||||
warnings.warn(
|
||||
"use_unicode parameter to ``MPDClient`` constructor is "
|
||||
"deprecated",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super(MPDClient, self).__init__()
|
||||
|
||||
def _reset(self):
|
||||
super(MPDClient, self)._reset()
|
||||
self._iterating = False
|
||||
self._sock = None
|
||||
self._rbfile = _NotConnected()
|
||||
self._wfile = _NotConnected()
|
||||
|
||||
def _execute(self, command, args, retval):
|
||||
if self._iterating:
|
||||
raise IteratingError("Cannot execute '{}' while iterating".format(command))
|
||||
if self._command_list is not None:
|
||||
if not callable(retval):
|
||||
raise CommandListError(
|
||||
"'{}' not allowed in command list".format(command)
|
||||
)
|
||||
self._write_command(command, args)
|
||||
self._command_list.append(retval)
|
||||
else:
|
||||
self._write_command(command, args)
|
||||
if callable(retval):
|
||||
return retval()
|
||||
return retval
|
||||
|
||||
def _write_line(self, line):
|
||||
try:
|
||||
self._wfile.write("{}\n".format(line))
|
||||
self._wfile.flush()
|
||||
except socket.error as e:
|
||||
error_message = "Connection to server was reset"
|
||||
logger.info(error_message)
|
||||
self._reset()
|
||||
e = ConnectionError(error_message)
|
||||
raise e.with_traceback(sys.exc_info()[2])
|
||||
|
||||
def _write_command(self, command, args=[]):
|
||||
parts = [command]
|
||||
for arg in args:
|
||||
if type(arg) is tuple:
|
||||
if len(arg) == 0:
|
||||
parts.append('":"')
|
||||
elif len(arg) == 1:
|
||||
parts.append('"{}:"'.format(int(arg[0])))
|
||||
else:
|
||||
parts.append('"{}:{}"'.format(int(arg[0]), int(arg[1])))
|
||||
else:
|
||||
parts.append('"{}"'.format(escape(str(arg))))
|
||||
# Minimize logging cost if the logging is not activated.
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
if command == "password":
|
||||
logger.debug("Calling MPD password(******)")
|
||||
else:
|
||||
logger.debug("Calling MPD %s%r", command, args)
|
||||
cmd = " ".join(parts)
|
||||
self._write_line(cmd)
|
||||
|
||||
def _read_line(self):
|
||||
line = self._rbfile.readline().decode("utf-8")
|
||||
if not line.endswith("\n"):
|
||||
self.disconnect()
|
||||
raise ConnectionError("Connection lost while reading line")
|
||||
line = line.rstrip("\n")
|
||||
if line.startswith(ERROR_PREFIX):
|
||||
error = line[len(ERROR_PREFIX) :].strip()
|
||||
raise CommandError(error)
|
||||
if self._command_list is not None:
|
||||
if line == NEXT:
|
||||
return
|
||||
if line == SUCCESS:
|
||||
raise ProtocolError("Got unexpected '{}'".format(SUCCESS))
|
||||
elif line == SUCCESS:
|
||||
return
|
||||
return line
|
||||
|
||||
def _read_lines(self):
|
||||
line = self._read_line()
|
||||
while line is not None:
|
||||
yield line
|
||||
line = self._read_line()
|
||||
|
||||
def _read_chunk(self, amount):
|
||||
chunk = bytearray()
|
||||
while amount > 0:
|
||||
result = self._rbfile.read(amount)
|
||||
if len(result) == 0:
|
||||
break
|
||||
chunk.extend(result)
|
||||
amount -= len(result)
|
||||
return bytes(chunk)
|
||||
|
||||
def _read_binary(self):
|
||||
"""From the data stream, read Unicode lines until one says "binary:
|
||||
<number>\\n"; at that point, read binary data of the given length.
|
||||
|
||||
This behaves like _parse_objects (with empty set of delimiters; even
|
||||
returning only a single result), but rather than feeding from a lines
|
||||
iterable (which would be preprocessed too far), it reads directly off
|
||||
the stream."""
|
||||
|
||||
obj = {}
|
||||
|
||||
while True:
|
||||
line = self._read_line()
|
||||
if line is None:
|
||||
break
|
||||
|
||||
key, value = self._parse_pair(line, ": ")
|
||||
|
||||
if key == "binary":
|
||||
chunk_size = int(value)
|
||||
value = self._read_chunk(chunk_size)
|
||||
|
||||
if len(value) != chunk_size:
|
||||
self.disconnect()
|
||||
raise ConnectionError(
|
||||
"Connection lost while reading binary data: "
|
||||
"expected %d bytes, got %d" % (chunk_size, len(value))
|
||||
)
|
||||
|
||||
if self._rbfile.read(1) != b"\n":
|
||||
# newline after binary content
|
||||
self.disconnect()
|
||||
raise ConnectionError("Connection lost while reading line")
|
||||
|
||||
obj[key] = value
|
||||
return obj
|
||||
|
||||
def _execute_binary(self, command, args):
|
||||
"""Execute a command repeatedly with an additional offset argument,
|
||||
keeping all the identical returned dictionary items and concatenating
|
||||
the binary chunks following the binary item into one of exactly size.
|
||||
|
||||
This differs from _execute in that rather than passing the lines to the
|
||||
callback which'd then call on something like _parse_objects, it builds
|
||||
a parsed object on its own (as a prerequisite to the chunk driving
|
||||
process) and then joins together the chunks into a single big response."""
|
||||
if self._iterating or self._command_list is not None:
|
||||
raise IteratingError("Cannot execute '{}' with command lists".format(command))
|
||||
data = None
|
||||
args = list(args)
|
||||
assert len(args) == 1
|
||||
args.append(0)
|
||||
final_metadata = None
|
||||
while True:
|
||||
self._write_command(command, args)
|
||||
metadata = self._read_binary()
|
||||
chunk = metadata.pop('binary', None)
|
||||
|
||||
if final_metadata is None:
|
||||
data = chunk
|
||||
final_metadata = metadata
|
||||
if not data:
|
||||
break
|
||||
try:
|
||||
size = int(final_metadata['size'])
|
||||
except KeyError:
|
||||
size = len(chunk)
|
||||
except ValueError:
|
||||
raise CommandError("Size data unsuitable for binary transfer")
|
||||
else:
|
||||
if metadata != final_metadata:
|
||||
raise CommandError("Metadata of binary data changed during transfer")
|
||||
if chunk is None:
|
||||
raise CommandError("Binary field vanished changed during transfer")
|
||||
data += chunk
|
||||
args[-1] = len(data)
|
||||
if len(data) > size:
|
||||
raise CommandListError("Binary data announced size exceeded")
|
||||
elif len(data) == size:
|
||||
break
|
||||
|
||||
if data is not None:
|
||||
final_metadata['binary'] = data
|
||||
|
||||
final_metadata.pop('size', None)
|
||||
|
||||
return final_metadata
|
||||
|
||||
def _read_command_list(self):
|
||||
try:
|
||||
for retval in self._command_list:
|
||||
yield retval()
|
||||
finally:
|
||||
self._command_list = None
|
||||
self._parse_nothing(self._read_lines())
|
||||
|
||||
def _iterator_wrapper(self, iterator):
|
||||
try:
|
||||
for item in iterator:
|
||||
yield item
|
||||
finally:
|
||||
self._iterating = False
|
||||
|
||||
def _wrap_iterator(self, iterator):
|
||||
if not self.iterate:
|
||||
return list(iterator)
|
||||
self._iterating = True
|
||||
return self._iterator_wrapper(iterator)
|
||||
|
||||
def _hello(self, line):
|
||||
if not line.endswith("\n"):
|
||||
self.disconnect()
|
||||
raise ConnectionError("Connection lost while reading MPD hello")
|
||||
line = line.rstrip("\n")
|
||||
if not line.startswith(HELLO_PREFIX):
|
||||
raise ProtocolError("Got invalid MPD hello: '{}'".format(line))
|
||||
self.mpd_version = line[len(HELLO_PREFIX) :].strip()
|
||||
|
||||
def _connect_unix(self, path):
|
||||
if not hasattr(socket, "AF_UNIX"):
|
||||
raise ConnectionError("Unix domain sockets not supported on this platform")
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect(path)
|
||||
return sock
|
||||
|
||||
def _connect_tcp(self, host, port):
|
||||
try:
|
||||
flags = socket.AI_ADDRCONFIG
|
||||
except AttributeError:
|
||||
flags = 0
|
||||
err = None
|
||||
for res in socket.getaddrinfo(
|
||||
host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, flags
|
||||
):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.socket(af, socktype, proto)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect(sa)
|
||||
return sock
|
||||
except socket.error as e:
|
||||
err = e
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
if err is not None:
|
||||
raise err
|
||||
else:
|
||||
raise ConnectionError("getaddrinfo returns an empty list")
|
||||
|
||||
@mpd_commands("idle")
|
||||
def _parse_idle(self, lines):
|
||||
self._sock.settimeout(self.idletimeout)
|
||||
ret = self._wrap_iterator(self._parse_list(lines))
|
||||
self._sock.settimeout(self._timeout)
|
||||
return ret
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
return self._timeout
|
||||
|
||||
@timeout.setter
|
||||
def timeout(self, timeout):
|
||||
self._timeout = timeout
|
||||
if self._sock is not None:
|
||||
self._sock.settimeout(timeout)
|
||||
|
||||
def connect(self, host, port=None, timeout=None):
|
||||
logger.info("Calling MPD connect(%r, %r, timeout=%r)", host, port, timeout)
|
||||
if self._sock is not None:
|
||||
raise ConnectionError("Already connected")
|
||||
if timeout is not None:
|
||||
warnings.warn(
|
||||
"The timeout parameter in connect() is deprecated! "
|
||||
"Use MPDClient.timeout = yourtimeout instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
self.timeout = timeout
|
||||
if host.startswith("@"):
|
||||
host = "\0" + host[1:]
|
||||
if host.startswith(("/", "\0")):
|
||||
self._sock = self._connect_unix(host)
|
||||
else:
|
||||
if port is None:
|
||||
raise ValueError(
|
||||
"port argument must be specified when connecting via tcp"
|
||||
)
|
||||
self._sock = self._connect_tcp(host, port)
|
||||
|
||||
# - Force UTF-8 encoding, since this is dependant from the LC_CTYPE
|
||||
# locale.
|
||||
# - by setting newline explicit, we force to send '\n' also on
|
||||
# windows
|
||||
self._rbfile = self._sock.makefile("rb", newline="\n")
|
||||
self._wfile = self._sock.makefile("w", encoding="utf-8", newline="\n")
|
||||
|
||||
try:
|
||||
helloline = self._rbfile.readline().decode("utf-8")
|
||||
self._hello(helloline)
|
||||
except Exception:
|
||||
self.disconnect()
|
||||
raise
|
||||
|
||||
def disconnect(self):
|
||||
logger.info("Calling MPD disconnect()")
|
||||
if self._rbfile is not None and not isinstance(self._rbfile, _NotConnected):
|
||||
self._rbfile.close()
|
||||
if self._wfile is not None and not isinstance(self._wfile, _NotConnected):
|
||||
self._wfile.close()
|
||||
if self._sock is not None:
|
||||
self._sock.close()
|
||||
self._reset()
|
||||
|
||||
def fileno(self):
|
||||
if self._sock is None:
|
||||
raise ConnectionError("Not connected")
|
||||
return self._sock.fileno()
|
||||
|
||||
def command_list_ok_begin(self):
|
||||
if self._command_list is not None:
|
||||
raise CommandListError("Already in command list")
|
||||
if self._iterating:
|
||||
raise IteratingError("Cannot begin command list while iterating")
|
||||
self._write_command("command_list_ok_begin")
|
||||
self._command_list = []
|
||||
|
||||
def command_list_end(self):
|
||||
if self._command_list is None:
|
||||
raise CommandListError("Not in command list")
|
||||
if self._iterating:
|
||||
raise IteratingError("Already iterating over a command list")
|
||||
self._write_command("command_list_end")
|
||||
return self._wrap_iterator(self._read_command_list())
|
||||
|
||||
@classmethod
|
||||
def add_command(cls, name, callback):
|
||||
wrap_result = callback in cls._wrap_iterator_parsers
|
||||
if callback.mpd_commands_binary:
|
||||
method = lambda self, *args: callback(self, cls._execute_binary(self, name, args))
|
||||
else:
|
||||
method = _create_command(cls._execute, name, callback, wrap_result)
|
||||
# create new mpd commands as function:
|
||||
escaped_name = name.replace(" ", "_")
|
||||
setattr(cls, escaped_name, method)
|
||||
|
||||
@classmethod
|
||||
def remove_command(cls, name):
|
||||
if not hasattr(cls, name):
|
||||
raise ValueError("Can't remove not existent '{}' command".format(name))
|
||||
name = name.replace(" ", "_")
|
||||
delattr(cls, str(name))
|
||||
|
||||
|
||||
# vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79:
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import gi, sys, os
|
||||
from pprint import pprint as print
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf
|
||||
|
||||
class SonistWindow(Gtk.Window):
|
||||
def __init__(self):
|
||||
Gtk.Window.__init__(self)
|
||||
|
||||
self.set_default_size(480, 320)
|
||||
|
||||
button = Gtk.Button()
|
||||
|
||||
self.add(button)
|
||||
|
||||
self.show_all()
|
Loading…
Reference in New Issue