Internet DJ Console Homepage IDJC

website logo

With a little work this could be made to interface with your station's web page or with cloud services.

#! /usr/bin/env python

# idjcmon.py (C) 2012 Stephen Fairchild
# Released under the GNU Lesser General Public License version 2.0 (or
# at your option, any later version).

"""A monitoring class that keeps an eye on IDJC.

It can be extended to issue e-mail alerts if IDJC freezes or perform Twitter
updates when the music changes.

Requires IDJC 0.8.8 or higher.

As a standalone (essentially demo code):
for the default profile: ./idjcmon.py
alternatively:
./idjcmon.py [profilename]
./idjcmon.py session.[sessionname]
"""

import time
import sys

import dbus
from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)

import gobject
import glib
import psutil

__all__ = ["IDJCMonitor"]


class IDJCMonitor(gobject.GObject):
    """Monitor IDJC internals relating to a specific profile or session.
    
    Can obtain information about where streaming to or the music metadata.
    This info can then be published whereever without having to touch
    the IDJC source code and is therefore easy to maintain.
    """
    
    __gsignals__ = {
        'launch' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                                    (gobject.TYPE_STRING, gobject.TYPE_UINT)),
        'quit' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                                    (gobject.TYPE_STRING, gobject.TYPE_UINT)),
        'streamstate-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                (gobject.TYPE_INT, gobject.TYPE_BOOLEAN, gobject.TYPE_STRING)),
        
        'metadata-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                                                    (gobject.TYPE_STRING,) * 4),
        'frozen' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                (gobject.TYPE_STRING, gobject.TYPE_UINT, gobject.TYPE_BOOLEAN))
    }
    
    __gproperties__ = {
        'artist' : (gobject.TYPE_STRING, 'artist', 'artist from track metadata',
                                                    "", gobject.PARAM_READABLE),
        'title' : (gobject.TYPE_STRING, 'title', 'title from track metadata',
                                                    "", gobject.PARAM_READABLE),
        'album' : (gobject.TYPE_STRING, 'album', 'album from track metadata',
                                                    "", gobject.PARAM_READABLE),
        'songname' : (gobject.TYPE_STRING, 'songname',
                            'the song name from metadata tags when available'
                            ' and from the filenmame when not',
                            "", gobject.PARAM_READABLE),
        'streaminfo' : (gobject.TYPE_PYOBJECT, 'streaminfo',
                'information about the streams', gobject.PARAM_READABLE)
    }
    
    def __init__(self, profile):
        """Takes the profile parameter e.g. "default".
        
        Can also handle sessions with "session.sessionname"
        """
        
        gobject.GObject.__init__(self)
        self.__profile = profile
        self.__bus = dbus.SessionBus()
        self.__artist = self.__title = self.__album = self.__songname = ""
        self.__shutdown = False
        self._start_probing()
        
    def shutdown(self):
        """Block both signal emission and property reads."""
        
        self.__shutdown = True

    def _start_probing(self):
        self.__watchdog_id = None
        self.__probe_id = None
        self.__watchdog_notice = False
        self.__pid = 0
        self.__frozen = False
        self.__main = self.__output = None
        if not self.__shutdown:
            self.__probe_id = glib.timeout_add_seconds(
                                                2, self._idjc_started_probe)

    def _idjc_started_probe(self):
        # Check for a newly started IDJC instance of the correct profile.
        
        try:
            self.__main = self.__bus.get_object("net.sf.idjc." + self.__profile,
                                                        "/net/sf/idjc/main")
            self.__output = self.__bus.get_object("net.sf.idjc." +
                                        self.__profile, "/net/sf/idjc/output")
            main_iface = dbus.Interface(self.__main, "net.sf.idjc")
            main_iface.pid(reply_handler=self._pid_reply_handler,
                            error_handler=self._pid_error_handler)
        except dbus.exceptions.DBusException:
            # Keep searching periodically.
            return not self.__shutdown
        else:
            return False

    def _pid_reply_handler(self, value):
        self.__pid = value
        try:
            self.__main.connect_to_signal("track_metadata_changed",
                                                        self._metadata_handler)
            self.__main.connect_to_signal("quitting", self._quit_handler)
            self.__main.connect_to_signal("heartbeat", self._heartbeat_handler)
            self.__output.connect_to_signal("streamstate_changed",
                                                    self._streamstate_handler)

            # Start watchdog thread.
            self.__watchdog_id = glib.timeout_add_seconds(
                                                    3, self._watchdog)

            self.__streams = {n : (False, "unknown") for n in xrange(10)}
            output_iface = dbus.Interface(self.__output, "net.sf.idjc")
            
            self.emit("launch", self.__profile, self.__pid)
            
            # Tell IDJC to initialize as empty its cache of sent data.
            # This yields a dump of server related info.
            output_iface.new_plugin_started()
        except dbus.exceptions.DBusException:
            self._start_probing()

    def _pid_error_handler(self, error):
        self._start_probing()

    def _watchdog(self):
        if self.__watchdog_notice:
            if psutil.pid_exists(self.__pid):
                if not self.__frozen:
                    self.__frozen = True
                    self.emit("frozen", self.__profile, self.__pid, True)
                return True
            else:
                for id_, (conn, where) in self.__streams.iteritems():
                    if conn:
                        self._streamstate_handler(id_, 0, where)
                self._quit_handler()
                return False
        elif self.__frozen:
            self.__frozen = False
            self.emit("frozen", self.__profile, self.__pid, False)

        self.__watchdog_notice = True
        return not self.__shutdown

    def _heartbeat_handler(self):
        self.__watchdog_notice = False

    def _quit_handler(self):
        """Start scanning for a new bus object."""

        if self.__watchdog_id is not None:
            glib.source_remove(self.__watchdog_id)
            self.emit("quit", self.__profile, self.__pid)
        self._start_probing()
        
    def _streamstate_handler(self, numeric_id, connected, where):
        numeric_id = int(numeric_id)
        connected = bool(connected)
        where = where.encode("utf-8")
        self.__streams[numeric_id] = (connected, where)
        self.notify("streaminfo")
        self.emit("streamstate-changed", numeric_id, connected, where)

    def _metadata_handler(self, artist, title, album, songname):

        def update_property(name, value):
            oldvalue = getattr(self, "_IDJCMonitor__" + name)
            newvalue = value.encode("utf-8")
            if newvalue != oldvalue:
                setattr(self, "_IDJCMonitor__" + name, newvalue)
                self.notify(name)

        for name, value in zip("artist title album songname".split(),
                                        (artist, title, album, songname)):
            update_property(name, value)

        self.emit("metadata-changed", self.__artist, self.__title,
                                                self.__album, self.__songname)

    def do_get_property(self, prop):
        if self.__shutdown:
            raise AttributeError(
                        "Attempt to read property after shutdown was called.")
        
        name = prop.name
        
        if name in ("artist", "title", "album", "songname"):
            return getattr(self, "_IDJCMonitor__" + name)
        if name == "streaminfo":
            return tuple(self.__streams[n] for n in xrange(10))
        else:
            raise AttributeError("Unknown property %s in %s" % (
                                                            name, repr(self)))

    def notify(self, property_name):
        if not self.__shutdown:
            gobject.GObject.notify(self, property_name)
            
    def emit(self, *args, **kwargs):
        if not self.__shutdown:
            gobject.GObject.emit(self, *args, **kwargs)


if __name__ == "__main__":
    def launch_handler(monitor, profile, pid):
        print "Hello to IDJC '%s' with process ID %d." % (profile, pid)

    def quit_handler(monitor, profile, pid):
        print "Goodbye to IDJC '%s' with process ID %d." % (profile, pid)

    def streamstate_handler(monitor, which, state, where):
        print "Stream %d is %s on connection %s." % (
                                        which, ("down", "up")[state], where)

    def metadata_handler(monitor, artist, title, album, songname):
        print "Metadata is: artist: %s, title: %s, album: %s" % (
                                                    artist, title, album)

    def frozen_handler(monitor, profile, pid, frozen):
        print "IDJC '%s' with process ID %d is %s" % (
                        profile, pid, ("no longer frozen", "frozen")[frozen])

    def main():
        argv = sys.argv
        if len(argv) <= 1:
            profile = "default"
        else:
            profile = argv[1]

        monitor = IDJCMonitor(profile)
        monitor.connect("launch", launch_handler)
        monitor.connect("quit", quit_handler)
        monitor.connect("streamstate-changed", streamstate_handler)
        monitor.connect("metadata-changed", metadata_handler)
        monitor.connect("frozen", frozen_handler)
        
        glib.MainLoop().run()
        
    main()