Music Player Daemon Community Wiki

522pages on
this wiki
Add New Page
Talk0 Share
  • 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:

  • 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 If that doesn't work, read on.

Specify the cover art folder, where downloaded cover images will be
stored, below. You can use pycoverart ( 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

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

Get the latest version of the script here:"""

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

    import mpd
 except ImportError:
     print("Can't import mpd. Please install the python bindings for MPD (python-mpd or mpd-python)")
 # Try to import pynotify and fall back if we can't
    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
    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("")
    with open(sys.path[0] + "/",'wb') as module_file:
         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
    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("")
    with open(sys.path[0] + "/",'wb') as module_file:
         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
    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
# Time to wait in seconds between checking for mpd existing

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
         # 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)
             notification = pynotify.Notification(summary=summary, message=message, icon=icon)
         # 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
        global notification
        notification = pynotify.Notification("MPD notify initialising")

    # 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
             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),
            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 =
        , "JPEG")
               # else just remove the temporary file

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

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:
             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:
        except (socket.error, mpd.ConnectionError):
            print("{0}: Cannot connect to MPD".format(time.strftime("%a, %d %b %Y %H:%M:%S")), file=log_file)

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
    Contributed by Patrick Hirt"""
         pid = os.fork()
        if pid > 0:
    except OSError, e:
            sys.stderr.write('Fork #1 failed: (%d) %s\n' % (e.errno, e.strerror))

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

    for f in sys.stdout, sys.stderr:
    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/")
        USAGE = "Usage: {0} start|stop|restart".format(sys.argv[0])
    if len(sys.argv) == 2:
        if daemon_module:
            if sys.argv[1] == "start":
            elif sys.argv[1] == "stop":
            elif sys.argv[1] == "restart":
                print("Unknown command")
             if sys.argv[1] == "daemonize":
                # If we don't use the daemon module, just use daemonize()
                print("""Usage: {0} [daemonize]""".format(sys.argv[0]))

         if daemon_module:
             # Just run the notifier withouth daemonizing
            print("""Couldn't import the daemon module. Get the daemon module from and save it next\
 to this script or type -h for usage""")

Ad blocker interference detected!

Wikia is a free-to-use site that makes money from advertising. We have a modified experience for viewers using ad blockers

Wikia is not accessible if you’ve made further modifications. Remove the custom ad blocker rule(s) and the page will load as expected.