Commits

Luke Plant committed b107546

Initial import

Comments (0)

Files changed (13)

+syntax:glob
+*~
+*.pyc
+*\.\#*
+*\#
+settings*priv.py

playerdo/__init__.py

Empty file added.

playerdo/backends/__init__.py

+from playerdo.backends.amarok import *
+from playerdo.backends.clementine import *
+from playerdo.backends.exaile import *
+from playerdo.backends.moc import *
+from playerdo.backends.rhythmbox import *
+from playerdo.backends.shellfm import *

playerdo/backends/amarok.py

+from playerdo.backends.mpris import MprisPlayer
+
+
+class Amarok(MprisPlayer):
+
+    process_name = "amarok"
+    bus_name = "org.mpris.amarok"

playerdo/backends/base.py

+from playerdo.utils import program_running, catch_unimplemented
+
+# Base class useful for implementing players
+class Player(object):
+
+    process_name = None
+
+    def is_running(self):
+        """
+        Returns true if the player program is running.
+        Must be implemented (or process_name must be specified)
+        """
+        if self.process_name is None:
+            raise NotImplementedError
+        return program_running(self.process_name)
+
+    def is_stopped(self):
+        """
+        Returns True if the player is in a 'stopped' state (which does not
+        include 'paused')
+        """
+        raise NotImplementedError
+
+    def is_paused(self):
+        """
+        Returns True if the player is in a 'paused' state.
+        """
+        raise NotImplementedError
+
+    def do_command(self, command):
+        try:
+            m = getattr(self, command)
+        except AttributeError:
+            raise Exception("'%s' is not a valid command" % command)
+        return m()
+
+    # Commands
+    def play(self):
+        raise NotImplementedError
+
+    def pause(self):
+        raise NotImplementedError
+
+    def unpause(self):
+        raise NotImplementedError
+
+    def togglepause(self):
+        """
+        Play if paused, pause if playing
+        """
+        is_paused = catch_unimplemented(self.is_paused)
+
+        if is_paused == True:
+            self.unpause()
+        elif is_paused == False:
+            self.pause()
+        else:
+            # Just hope this does the right thing:
+            self.pause()
+
+    def playpause(self):
+        """
+        Play if stopped/paused, pause if playing
+        """
+        is_stopped = catch_unimplemented(self.is_stopped)
+
+        if is_stopped == True:
+            self.play()
+        elif is_stopped == False:
+            self.togglepause()
+        else:
+            # Just hope 'togglepause' does the right thing:
+            self.togglepause()
+
+    def stop(self):
+        raise NotImplementedError
+
+    def next(self):
+        raise NotImplementedError
+
+    def prev(self):
+        raise NotImplementedError
+
+    def osd(self):
+        raise NotImplementedError
+

playerdo/backends/clementine.py

+from playerdo.backends.mpris import MprisPlayer
+
+
+class Clementine(MprisPlayer):
+
+    process_name = "clementine"
+    bus_name = "org.mpris.clementine"

playerdo/backends/exaile.py

+from playerdo.backends.mpris import MprisPlayer
+from playerdo.utils import DBusObject
+
+
+class Exaile(MprisPlayer):
+
+    name = "Exaile"
+    bus_name = "org.mpris.exaile"
+
+    def is_stopped(self):
+        exaile = DBusObject(self.bus_name, "/org/exaile/Exaile")
+        return not bool(exaile.IsPlaying())

playerdo/backends/moc.py

+from playerdo.backends.base import Player
+from playerdo.utils import process_stdout, process_retval
+
+
+class Moc(Player):
+
+    process_name = "mocp"
+
+    def is_stopped(self):
+        info = process_stdout(["mocp", "-i"])
+        return "State: STOP" in info
+
+    def is_paused(self):
+        info = process_stdout(["mocp", "-i"])
+        return "State: PAUSE" in info
+
+    def play(self):
+        process_retval(["mocp", "--play"])
+
+    def pause(self):
+        process_retval(["mocp", "--pause"])
+
+    def unpause(self):
+        process_retval(["mocp", "--unpause"])
+
+    def stop(self):
+        process_retval(["mocp", "--stop"])
+
+    def next(self):
+        process_retval(["mocp", "--next"])
+
+    def prev(self):
+        process_retval(["mocp", "--previous"])

playerdo/backends/mpris.py

+from playerdo.backends.base import Player
+from playerdo.utils import DBusObject
+import dbus
+
+
+class MprisPlayer(Player):
+
+    bus_name = None
+    player_object_name = "/Player"
+    tracklist_object_name = "/TrackList"
+
+    @property
+    def player(self):
+        if self.bus_name is None or self.player_object_name is None:
+            raise NotImplementedError
+
+        try:
+            return self._player
+        except AttributeError:
+            obj = DBusObject(self.bus_name, self.player_object_name)
+            self._player = obj
+            return obj
+
+    @property
+    def tracklist(self):
+        if self.bus_name is None or self.tracklist_object_name is None:
+            raise NotImplementedError
+
+        try:
+            return self._tracklist
+        except AttributeError:
+            obj = DBusObject(self.bus_name, self.tracklist_object_name)
+            self._tracklist = obj
+            return obj
+
+    def is_running(self):
+        # pidof doesn't work for some apps (e.g. exaile), but this should work
+        # for all Mpris apps.
+        try:
+            # Force evaluation:
+            bus = self.player._bus
+            return True
+        except dbus.DBusException:
+            return False
+
+    def is_stopped(self):
+        # This seems to work for exaile and clementine
+        return self.tracklist.GetCurrentTrack() == -1
+
+    def play(self):
+        self.player.Play()
+
+    def pause(self):
+        self.player.Pause()
+
+    def unpause(self):
+        self.play()
+
+    def playpause(self):
+        try:
+            # Some define this e.g. Exaile
+            self.player.PlayPause()
+        except dbus.DBusException:
+            super(MprisPlayer, self).playpause()
+
+    def next(self):
+        self.player.Next()
+
+    def prev(self):
+        self.player.Prev()
+
+    def stop(self):
+        self.player.Stop()
+
+    def osd(self):
+        self.player.ShowOSD()
+
+

playerdo/backends/rhythmbox.py

+from playerdo.backends.base import Player
+from playerdo.utils import process_retval
+
+
+class RhythmBox(Player):
+
+    process_name = "rhythmbox"
+
+    def is_stopped(self):
+        # rhythmbox doesn't seem to have this state
+        return False
+
+    # is_paused - no way to know
+
+    def play(self):
+        process_retval(["rhythmbox-client", "--play"])
+
+    def pause(self):
+        process_retval(["rhythmbox-client", "--pause"])
+
+    def unpause(self):
+        self.play()
+
+    def togglepause(self):
+        process_retval(["rhythmbox-client", "--play-pause"])
+
+    def stop(self):
+        # This isn't quite right, but better than nothing
+        self.pause()
+
+    def next(self):
+        process_retval(["rhythmbox-client", "--next"])
+
+    def prev(self):
+        process_retval(["rhythmbox-client", "--previous"])

playerdo/backends/shellfm.py

+from playerdo.backends.base import Player
+from playerdo.utils import process_retval
+import os
+
+
+class ShellFm(Player):
+    # Requirements:
+    #  shc (compiled version of shc.hs from shell-fm's sources)
+
+    process_name = "shell-fm"
+
+    def is_stopped(self):
+        return not os.path.isfile(os.path.join(os.environ['HOME'],
+                                               ".shell-fm",
+                                               "nowplaying"))
+
+    # Can't implement 'play', because once you are stopped, shell-fm/shc needs
+    # you to specify a station if you want it to play.
+
+    def pause(self):
+        process_retval(["shc", "pause"])
+
+    def unpause(self):
+        self.pause()
+
+    def stop(self):
+        process_retval(["shc", "stop"])
+
+    def next(self):
+        process_retval(["shc", "next"])
+#!/usr/bin/env python
+
+#
+# Wrapper for controlling various music players, so that you can define keyboard
+# shortcuts that work for whatever player you are using.  Ideally it will still
+# work even if multiple players are running if all but one are 'stopped', but
+# some music players do not have such a state or it cannot be determined.
+#
+
+import sys
+
+# Execute commands:
+
+def main(command, players):
+    if command == "test":
+        do_test(players)
+    elif command == "help":
+        print_help(players)
+    else:
+        do_command(command, players)
+
+
+def do_test(players):
+    # Check dependencies of all players
+    pass
+
+
+def print_help(players):
+    # Print help and list of supported players
+    pass
+
+def do_command(command, players):
+    # Get running players
+    running_ps = []
+    for P in players:
+        p = P()
+        try:
+            running = p.is_running()
+            if running:
+                running_ps.append(p)
+        except NotImplementedError:
+            pass
+
+    if len(running_ps) == 0:
+        sys.stderr.write("No players running!\n")
+        sys.exit(1)
+
+
+    # Try to find out which one is playing
+    state = []
+    for p in running_ps:
+        try:
+            is_stopped = p.is_stopped()
+        except NotImplementedError:
+            is_stopped = None
+
+        if is_stopped == True:
+            state.append(2)
+        elif is_stopped == False:
+            state.append(0)
+        else:
+            # In-between value for unknowns, because a player that is definitely
+            # stopped is less preferred than one that *might* be playing.
+            state.append(1)
+
+    l = zip(state, running_ps)
+    l.sort()
+
+    # Use the first one
+    player = l[0][1]
+    try:
+        player.do_command(command)
+    except NotImplementedError:
+        sys.stderr.write("Operation '%s' not support for player '%s'." % (command, player.__name__))
+        sys.exit(1)
+
+from playerdo.backends import *
+from playerdo.backends.base import Player
+
+def find_players():
+    return [v for v in globals().values() if type(v) is type and issubclass(v, Player)]
+
+if __name__ == '__main__':
+    import sys
+    main(sys.argv[1], find_players())
+

playerdo/utils.py

+import subprocess
+import dbus
+
+
+# Helpers for process output
+def process_stdout(args, input=None):
+    """
+    Executes the process with the commandline specified in 'args' and returns
+    the standard output
+    """
+    p = subprocess.Popen(args, stdout=subprocess.PIPE)
+    retval = p.communicate(input=input)[0]
+    try:
+        p.terminate()
+    except OSError:
+        pass
+    return retval
+
+def process_retval(args, input=None):
+    """
+    Executes the process with the commandline specified in 'args' and returns
+    the return value
+    """
+    p = subprocess.Popen(args, stdout=subprocess.PIPE)
+    p.communicate(input=input)
+    return p.returncode
+
+def process_true(args, input=None):
+    """
+    Executes a process and rturns true if the process has a zero return code
+    """
+    return process_retval(args, input=input) == 0
+
+def program_running(progam):
+    return process_true(["pidof", progam])
+
+
+# DBus helpers
+class DBusObject(object):
+    """
+    Wrapper for a dbus object, so that the bus name and object name only needs
+    to be specified once.
+    """
+    def __init__(self, bus_name, object_name):
+        bus = dbus.SessionBus()
+        self._bus = bus
+        self._obj = bus.get_object(bus_name, object_name)
+
+    def __getattr__(self, name):
+        def f(*args, **kwargs):
+            return getattr(self._obj, name)(*args, **kwargs)
+        return f
+
+
+# Misc helpers
+def catch_unimplemented(c, replacement=None):
+    """
+    Execute a callable c, returning replacement if it throws NotImplementedError
+    """
+    try:
+        return c()
+    except NotImplementedError:
+        return replacement
+