Source

KUSC Player / kusc.py

#!/usr/bin/env python

__appname__ = 'KUSC.org Standalone Player'
__author__ = 'Derek M. Payton <derek.payton@gmail.com>'
__copyright__ = 'Copyright (c) 2010 Derek M. Payton'
__license__ = 'MIT License'
__version__ = '0.1-dev'

import os
import platform
import re
import sys
import urllib2
from BeautifulSoup import BeautifulSoup
from PyQt4 import QtCore, QtGui, QtWebKit

KUSC_HOMEPAGE = 'http://www.kusc.org/classical/'
KUSC_PLAYER = 'http://www.kusc.org/classical/stream/listen.html'
PYTHON_VERSION = platform.python_version()
SYSTEM = platform.system()

class DataStore(object):
    def __init__(self, **kwargs):
        for key, value in kwargs.iteritems():
            setattr(self, key, value)

def load_icon(icon_name):
    path = os.path.join(os.getcwd(), 'icons', icon_name)
    return QtGui.QIcon(path)

## http://effbot.org/zone/re-sub.htm#unescape-html
def unescape(text):
    def fixup(m):
        text = m.group(0)
        if text[:2] == "&#":
            # character reference
            try:
                if text[:3] == "&#x":
                    return unichr(int(text[3:-1], 16))
                else:
                    return unichr(int(text[2:-1]))
            except ValueError:
                pass
        else:
            # named entity
            try:
                text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
            except KeyError:
                pass
        return text # leave as is
    return re.sub("&#?\w+;", fixup, text)

class SysTrayIcon(QtGui.QSystemTrayIcon):
    def __init__(self, parent, app):
        ## Initial setup for our sys tray icon
        super(QtGui.QSystemTrayIcon, self).__init__(parent)
        self.app = app
        self.setIcon(load_icon('kusc.ico'))
        self.setToolTip('KUSC Player')

        ## Setup the menu
        self.menu = QtGui.QMenu(parent)
        self.actions = DataStore(
            play=self.menu.addAction(load_icon('control-stop-square.ico'), 'Stop Music'),
            about=self.menu.addAction(load_icon('information.ico'), 'About'),
            exit=self.menu.addAction(load_icon('slash.ico'), 'Exit')
            )
        self.connect(self.actions.play, QtCore.SIGNAL('triggered()'), self.toggle_playing)
        self.connect(self.actions.about, QtCore.SIGNAL('triggered()'), self.about)
        self.connect(self.actions.exit, QtCore.SIGNAL('triggered()'), self.exit)
        self.setContextMenu(self.menu)

        self.current_song = None

        def init():
            ''' This is a hack '''
            self.start_playing()
            self.now_playing()
        QtCore.QTimer.singleShot(1000, init)

    def exit(self):
        ''' Exit the program '''
        self.stop_playing()
        self.app.exit()

    def toggle_playing(self):
        if hasattr(self, 'web'):
            self.stop_playing()
        else:
            self.start_playing()

    def start_playing(self):
        if not hasattr(self, 'web'):
            self.showMessage('Loading...', 'Please wait while the player loads.')
            self.actions.play.setIcon(load_icon('control-stop-square.ico'))
            self.actions.play.setText('Stop Music')
            self.web = QtWebKit.QWebView()
            self.web.settings().setAttribute(QtWebKit.QWebSettings.PluginsEnabled, True)
            self.web.load(QtCore.QUrl(KUSC_PLAYER))

    def stop_playing(self):
        if hasattr(self, 'web'):
            self.actions.play.setIcon(load_icon('control.ico'))
            self.actions.play.setText('Play Music')
            del self.web

    def now_playing(self):
        print 'fetching song...'

        ## Grab the HTML of the homepage...
        request = urllib2.Request(
            url=KUSC_HOMEPAGE,
            headers={'User-agent': 'KUSC Standalone Player v%s / python %s (%s)' % (
                __version__,
                PYTHON_VERSION,
                SYSTEM
                )}
            )
        response = urllib2.urlopen(request).read()

        ## Parse out the currently playing song...
        soup = BeautifulSoup(response)
        onnowtext = soup.findAll('p', {'id': 'onnowtext'})[0]
        now_playing = []

        ## Try to get the program name...
        try:
            now_playing.append(unescape(onnowtext.findAll('span')[1].contents[0]))
        except Exception:
            pass

        ## Try to get the song name...
        try:
            now_playing.append(unescape(onnowtext.findAll('span')[2].contents[0]))
        except Exception:
            pass

        ## Couldn't get either? Bummer... Try again in a few minutes
        if not now_playing:
            self.showMessage('Error', 'Could not get currently playing song from KUSC.org', self.Warning)
            QtCore.QTimer.singleShot(180000, self.now_playing)
            return

        now_playing = '\n'.join(now_playing)

        ## If it's a new song, set it
        if now_playing == self.current_song:
            ## Check again in 1 minute
            print '\told song, try again in 1 minute'
            QtCore.QTimer.singleShot(60000, self.now_playing)
        else:
            ## Set the new song, and dont check again for 5 minutes
            print '\tnew song, check again in 5 minutes'
            self.current_song = now_playing
            self.setToolTip(now_playing)
            self.showMessage('Now Playing on KUSC:', now_playing)
            QtCore.QTimer.singleShot(300000, self.now_playing)

    def about(self):
        content = '''
            <p>
                <b>%(appname)s</b> v%(version)s<br />
                <small>Python %(py_ver)s - Qt %(qt_ver)s - PyQt %(pyqt_ver)s - %(system)s</small>
            </p>
            <p><b>KUSC</b> is a listener-supported classical music radio station,
                owned and operated by the University of Southern California.  It
                is the largest non-profit classical music station in the country
                and the only classical radio station in Southern California.
            </p>
            <p>This program was written by Derek Payton, a software developer living
                just outside KUSC's broadcast area in Southern California. Visit
                him online at <a href="http://www.dmpayton.com">dmpayton.com</a>.
            </p>
            <p>
                <b>Support KUSC</b> &mdash;
                <a href="http://www.kusc.org/classical/SupportKUSC/">Click here to learn more!</a>
            </p>
            <p><small>
                This software is not affiliate with or endorsed by KUSC.<br />
                Released under an open source license.
                <a href="http://bitbucket.org/dmpayton/kusc-player/">Get the source.</a>
            </small></p>
            ''' % {
                'appname': __appname__,
                'version': __version__,
                'copyright': __copyright__.replace('(c)', '&copy;'),
                'py_ver': PYTHON_VERSION,
                'qt_ver': QtCore.QT_VERSION_STR,
                'pyqt_ver': QtCore.PYQT_VERSION_STR,
                'system': SYSTEM,
            }
        QtGui.QMessageBox.about(self.parent(), __appname__, content)

def main():
    ## Create the app and widget
    app = QtGui.QApplication(sys.argv)
    widget = QtGui.QWidget()
    widget.setWindowIcon(load_icon('kusc-logo.ico'))

    tray_icon = SysTrayIcon(widget, app)
    tray_icon.show()
    app.setQuitOnLastWindowClosed(False)

    sys.exit(app.exec_())

if __name__ == '__main__':
    main()