Wikia

Music Player Daemon Community Wiki

Hack:mpd-notify.py

Talk0
514pages on
this wiki
  • Description: Simple python script which runs as a daemon that displays libnotify notifications with album-art when the mpd song or status changes. Requires python-mpd and libnotify. Use of notify-osd is recommended. If there are notifications from this script pending, their content is just updated, instead of flooding the screen with new ones. Album arts are taken from a specified location, e.g. ~/.covers and downloaded if not found there. Additionally album arts can be saved as jpg-file at a fixed location to be used e.g. with conky (requires pil).

Get the latest version from here: https://gist.github.com/1343124

  • Maintainer: Durand D'souza < durand1 [at] gmail [dot] com >
  • Contributors: Henning Hollermann < laclaro [at] mail [dot] com >, Patrick Hirt < patrick [dot] hirt [at] gmail [dot] com >
#!/usr/bin/env python

"""Notifies the user when MPD changes

This script polls mpd and sends out a notification using either pynotify or notify-send whenever either the status of
MPD changes or if a new song is being played. In general, it should work fine without any modifications.
Just run it with python mpd-notify.py. If that doesn't work, read on.

Specify the cover art folder, where downloaded cover images will be
stored, below. You can use pycoverart (http://pycoverart.googlecode.com/svn/trunk/cover_art.py) to download cover art
for your whole collection if necessary, otherwise, this script will download them when needed. If you want to copy the
art to a particular file location - to display it in conky, for example - you can specify a path to that file on line
101.

Note: The daemonizer apparently doesn't work for everyone. If this is the case for you, uncomment line 58
Dependencies:
    - python-mpd
    - python-notify (Optional but highly recommended)
    - python-imaging (Only for conky support)

Get the latest version of the script here: http://mpd.wikia.com/wiki/Hack:mpd-notify.py"""

from __future__ import print_function
import os
import time
import sys
import socket
import glob

try:
    import mpd
except ImportError:
    print("Can't import mpd. Please install the python bindings for MPD (python-mpd or mpd-python)")
    sys.exit(1)

# Try to import pynotify and fall back if we can't
try:
    import pynotify
    pynotify_module = True
except ImportError:
    print("Can't import pynotify so we'll use notify-send instead. We cannot update notifications using this method.")
    pynotify_module = False
try:
    from daemon import daemon as Daemon
    daemon_module = True
except ImportError:
    # We couldn't find the daemon module so we'll try to download it
    import urllib2
    print("This script requires a python module to daemonize notifications. Getting it now.")
    module = urllib2.urlopen("https://raw.github.com/gist/1343075/faac02ebde4f22756857f3dbc2babd001d593303/daemon.py")
    with open(sys.path[0] + "/daemon.py",'wb') as module_file:
        module_file.write(module.read())
    try:
        from daemon import daemon as Daemon
        daemon_module = True
        print("It worked!")
    except ImportError:
        print("It didn't work.")
        # Getting the module failed so fall back to a built in daemonizer
        daemon_module = False

# Uncomment the line below if the daemon module crashes for you.
#daemon_module = False

# Try to import pycoverart and fallback if we can't
try:
    import cover_art
    cover_art_module = True
except ImportError:
    # We couldn't find the coverart module so we'll try to download it
    import urllib2
    print("This script requires a python module to download cover art. Getting it now.")
    module = urllib2.urlopen("http://pycoverart.googlecode.com/svn-history/trunk/cover_art.py")
    with open(sys.path[0] + "/cover_art.py",'wb') as module_file:
        module_file.write(module.read())
    try:
        import cover_art
        cover_art_module = True
        print("It worked!")
    except ImportError:
        print("It didn't work.")
        # Getting the module failed
        cover_art_module = False
try:
    import Image
except ImportError:
    print("Warning: Can't import Image. Please install the Python Imaging Library (probably python-imaging) if you want to use conky to display your cover-art. The cover art will then additionally be copied to a fixed location in your filesystem.")

__author__ = "Durand D'souza"
__email__ = "durand1 [at] gmail [dot] com"
__credits__ = ["Durand D'souza",
               ("Patrick Hirt", "patrick [dot] hirt [at] gmail [dot] com"),
               ("Henning Hollermann", "laclaro [at] mail [dot] com")]
__version__ = "28/07/12"
__license__ = "GPL v3"

# MPD settings
MPD_HOST = "localhost"
MPD_PORT = 6600

# Folder containing album covers. New album arts will be saved to this location as files like "artist-album.ext".
COVERS_FOLDER = "~/.covers"

# Fixed .jpg-file that the cover art should additionally be written to on song change. You can use this to, e.g, display
# the album art with conky. Leave this empty "" to disable.
COVER_ART_CONKY = "/tmp/conky_mpd_cover_art.jpg"

# Time to wait in seconds between checking mpd status changes
POLLING_INTERVAL = 0.5
# Time to wait in seconds between checking for mpd existing
SLEEP_INTERVAL = 5

def sanitize(name):
    """Replaces disallowed characters with an underscore"""
    ## Disallowed characters in filenames
    DISALLOWED_CHARS = "\\/:<>?*|"
    if name == None:
        name = "Unknown"
    for character in DISALLOWED_CHARS:
        name = name.replace(character,'_')
    # Replace " with '
    name = name.replace('"', "'")

    return name

def cover_exists(artist, album, location):
    """Check if a cover exists for a particular album"""
    return glob.glob(u"{0}/{1}-{2}.*".format(location, sanitize(artist), sanitize(album)).replace("[", "\\[").replace("]", "\\]"))

def get_album_cover(artist, album):
    """This function returns a cover image path or an icon if a cover doesn't exist"""
    # If we're allowed to use the cover art module
    if cover_art_module:
        cover_path = cover_art.get_cover(artist, album, os.path.expanduser(COVERS_FOLDER))
        # If it is valid, return it
        if cover_path:
            return cover_path
    # Otherwise, use a simplified approach and don't download new art
    else:
        # Just get what's available
        cover_path = cover_exists(artist, album, os.path.expanduser(COVERS_FOLDER))[0]
        if cover_path:
            return cover_path
    # If an image doesn't exist, return an icon
    return "sonata"


def send_notification(summary, message="", icon=None, update=True):
    """This function takes information and sends a notification to the notification daemon using pynotify or
    notify-send. If update is set to False, a new notification will be created rather than updating the previous one"""
    if icon == None:
        icon = "sonata"

    if pynotify_module:
        global notification
        if update:
            notification.update(summary=summary, message=message, icon=icon)
        else:
            notification = pynotify.Notification(summary=summary, message=message, icon=icon)
        notification.show()
    else:
        # Use notify-send instead
        os.popen('notify-send -i "{0}" "{1}" "{2}"'.format(icon.replace('"', '\\"'), summary.replace('"', '\\"'), message.replace('"', '\\"')))

def observe_mpd(client, log_file):
    """This is the main function in the script. It observes mpd and notifies the user of any changes."""

    if pynotify_module:
        # Initialise notifications
        pynotify.init("icon-summary-body")
        global notification
        notification = pynotify.Notification("MPD notify initialising")
        #notification.show()

    # Loop and detect mpd changes
    last_status = "Initial"
    last_song = "Initial"

    while True:
        # Get status
        current_status = client.status()['state']
        # There might be errors when getting song details if there is no song in the playlist
        try:
            current_song = client.currentsong()['file']
            # Get song details
            artist = client.currentsong()['artist']
            album = client.currentsong()['album']
            title = client.currentsong()['title']
        except KeyError:
            current_song, artist, album, title = ("", "", "", "")

        # If the song or status has changed, notify the user
        if current_status != last_status and last_status != "Initial":
            print("{0}: Status change: {1}".format(time.strftime("%a, %d %b %Y %H:%M:%S"), current_status),
                  file=log_file)
            if current_status == "play": # Not sure how to make this one show in an efficient way so we won't bother
                pass # send_notification(summary="MPD", message="Playing", icon="media-playback-start")
            elif current_status == "pause":
                send_notification(summary="MPD", message="Paused", icon="media-playback-pause")
            else: # Otherwise we assume that mpd has stopped playing completely
                send_notification(summary="MPD", message="Stopped", icon="media-playback-stop")

        if (current_song != last_song and current_song != "") or (current_status != last_status and current_status ==
                                                                  "play") and last_song != "Initial":
            print("{0}: Song change: {1} by {2} in {3}".format(time.strftime("%a, %d %b %Y %H:%M:%S"), title, artist,
                                                               album), file=log_file)
            cover_art = get_album_cover(artist, album)
            send_notification(summary=artist, message=title, icon=cover_art, update=True)
            if COVER_ART_CONKY != "":
               # If we got a cover_art-image copy it to /tmp/ to be able to display it in conky
               if cover_art != "sonata":
                  im = Image.open(cover_art)
                  im.save(COVER_ART_CONKY, "JPEG")
               # else just remove the temporary file
               else:
                   os.remove(COVER_ART_CONKY)

        # Save current status to compare with later
        last_status = current_status
        last_song = current_song
        # Sleep for some time before checking status again
        time.sleep(POLLING_INTERVAL)

def run_notifier(self=None):
    """Runs the notifier"""
    # Opens the log file to send errors to
    log_file = open("/tmp/mpd_notify.log", "w", 0)
    # Initialise mpd client and wait till we have a connection
    while True:
        try:
            client = mpd.MPDClient()
            client.connect(MPD_HOST, int(MPD_PORT))
            print("{0}: Connected to MPD".format(time.strftime("%a, %d %b %Y %H:%M:%S")), file=log_file)
            # Run the observer but watch for mpd crashes
            observe_mpd(client, log_file)
        except KeyboardInterrupt:
            print("\nLater!")
            sys.exit()
        except (socket.error, mpd.ConnectionError):
            print("{0}: Cannot connect to MPD".format(time.strftime("%a, %d %b %Y %H:%M:%S")), file=log_file)
            time.sleep(SLEEP_INTERVAL)

if daemon_module:
    class MPDNotify(Daemon):
        """Daemon class to run notifier"""
        run = run_notifier

def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
    """Fork the notification daemon using an alternate method to daemon.py
    Contributed by Patrick Hirt"""
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError, e:
            sys.stderr.write('Fork #1 failed: (%d) %s\n' % (e.errno, e.strerror))
            sys.exit(1)

    os.chdir('/')
    os.umask(0)
    os.setsid()

    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError, e:
        sys.stderr.write('Fork #2 failed: (%d) %s\n' % (e.errno, e.strerror))
        sys.exit(1)

    for f in sys.stdout, sys.stderr:
        f.flush()
    si = file(stdin, 'r')
    so = file(stdout, 'a+')
    se = file(stderr, 'a+', 0)
    os.dup2(si.fileno(), sys.stdin.fileno())
    os.dup2(so.fileno(), sys.stdout.fileno())
    os.dup2(se.fileno(), sys.stderr.fileno())

if __name__ == "__main__":
    if daemon_module:
        # If we are using the daemon module
        daemon = MPDNotify("/tmp/mpd_notify.pid")
        USAGE = "Usage: {0} start|stop|restart".format(sys.argv[0])
    if len(sys.argv) == 2:
        if daemon_module:
            if sys.argv[1] == "start":
                daemon.start()
            elif sys.argv[1] == "stop":
                daemon.stop()
            elif sys.argv[1] == "restart":
                daemon.restart()
            else:
                print("Unknown command")
                print(USAGE)
                sys.exit(2)
            sys.exit(0)
        else:
            if sys.argv[1] == "daemonize":
                # If we don't use the daemon module, just use daemonize()
                daemonize()
                run_notifier()
            else:
                print("""Usage: {0} [daemonize]""".format(sys.argv[0]))

    else:
        if daemon_module:
            print(USAGE)
            sys.exit(2)
        else:
            # Just run the notifier withouth daemonizing
            print("""Couldn't import the daemon module. Get the daemon module from http://is.gd/IXTtnT and save it next\
 to this script or type -h for usage""")
            run_notifier()
Advertisement | Your ad here

Around Wikia's network

Random Wiki