Question

I'm using the python-mpd2 module to control a media player on a Raspberry Pi in a GUI application. Thus, I'd like to gracefully handle connection errors and timeouts (the player in question drops MPD connections after 60 seconds) in the background. However, the MPD module has no single point of entry through which all commands are sent or information is retrieved that I could patch.

I'd like a class which allows access to all of the same methods as mpd.MPDClient, but let's me add my own error handling. In other words, if I do:

client.play()

And a connectione error is thrown, I'd like to catch it and resend the same command. Other than the small delay caused by having to reconnect to the server, the user shouldn't notice that anything is amiss.

So far, here is the solution I've come up with. It is working in my application, but doesn't really fulfill my objectives.

from functools import partial
from mpd import MPDClient, ConnectionError

class PersistentMPDClient(object):
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.client = MPDClient()
        self.client.connect(self.host, self.port)

    def command(self, cmd, *args, **kwargs):
        command_callable = partial(self.client.__getattribute__(cmd), *args, **kwargs)
        try:
            return command_callable()
        except ConnectionError:
            # Mopidy drops our connection after a while, so reconnect to send the command
            self.client._soc = None
            self.client.connect(self.host, self.port)
            return command_callable()

I could add a method to this class for every single MPD command, e.g.:

def play(self):
    return self.command("play")

But this seems far from the best way to accomplish it.

Was it helpful?

Solution

If you don't mind creating a list of all 91 strings forming command names, you can do something along the lines of this answer. I believe this approach has many advantages because it involves less magic.

OTOH, 91 is indeed a lot. So here's an automagical solution, making use of a custom __getattr__, which returns a wrapper:

from functools import partial
import types

class DummyClient(object):
    def connect(self, *a, **kw): print 'connecting %r %r' % (a, kw)
    def play(self): print 'playing'
    def stop(self): print 'stopping'

class PersistentMPDClient(object):
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.client = DummyClient()
        self.client.connect(self.host, self.port)

    def __getattr__(self, attr, *args):
        cmd = getattr(self.client, attr, *args)
        if isinstance(cmd, types.MethodType):
            # a method -- wrap
            return lambda *a, **kw: self.command(attr, *a, **kw)
        else:
            # anything else -- return unchanged
            return cmd

    def command(self, cmd, *args, **kwargs):
        command_callable = partial(self.client.__getattribute__(cmd), *args, **kwargs)
        try:
            return command_callable()
        except ConnectionError:
            # Mopidy drops our connection after a while, so reconnect to send the command
            self.client._soc = None
            self.client.connect(self.host, self.port)
            return command_callable()

c = PersistentMPDClient(hostname, port)
c.play()
c.stop()

As I was writing this up, I noticed @MatToufoutu had posted a similar solution (there are some differences, though). I don't know why s/he deleted it... If that answer gets undeleted, I'd gladly give it the credit it deserves.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top