Initial commit

master
Dustin 2014-08-05 13:36:06 -05:00
commit ec665403ea
4 changed files with 301 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
__pycache__/
*.egg-info/

19
setup.py Normal file
View File

@ -0,0 +1,19 @@
from setuptools import find_packages, setup
setup(
name='mpdnotify',
version='0.1',
description='Send libnotify notifications for MPD',
author='Dustin C. Hatch',
author_email='dustin@hatch.name',
url='http://dustin.hatch.name/',
license='',
packages=find_packages('src'),
package_dir={'': 'src'},
package_data={'': ['mpd-logo.png']},
entry_points={
'console_scripts': [
'mpdnotify=mpdnotify:main',
],
},
)

BIN
src/mpd-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

280
src/mpdnotify.py Normal file
View File

@ -0,0 +1,280 @@
from gi.repository import GdkPixbuf
from gi.repository import GLib
from gi.repository import Gtk
import argparse
import contextlib
import dbus.mainloop.glib
import hashlib
import json
import mpd
import os
import pkg_resources
import signal
import sys
import threading
import urllib.parse
import urllib.request
LASTFM_API_KEY = '929dc7a1779b10b09f9c3e375b43463c'
LASTFM_API_URL = 'http://ws.audioscrobbler.com/2.0'
XDG_CACHE_DIR = os.environ.get('XDG_CACHE_DIR', os.path.expanduser('~/.cache'))
MPD_HOST = os.environ.get('MPD_HOST', 'localhost')
MPD_PORT = int(os.environ.get('MPD_PORT', '6600'))
class Song(object):
CACHE_DIR = os.path.join(XDG_CACHE_DIR, 'album-art')
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
try:
key = ':'.join((self.artist, self.album)).encode('utf-8')
except AttributeError:
pass
else:
self.album_art = os.path.join(
self.CACHE_DIR,
hashlib.sha1(key).hexdigest(),
)
try:
duration = int(self.time)
except AttributeError:
pass
else:
self.duration = '{0}:{1}'.format(duration // 60, duration % 60)
def fetch_album_art(self, callback=None):
if not os.path.exists(os.path.dirname(self.album_art)):
os.makedirs(os.path.dirname(self.album_art))
try:
fd = os.open(self.album_art, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
except OSError:
return
params = {
'method': 'album.getinfo',
'api_key': LASTFM_API_KEY,
'artist': self.artist,
'album': self.album,
'format': 'json',
}
url = '{0}?{1}'.format(LASTFM_API_URL, urllib.parse.urlencode(params))
res = urllib.request.urlopen(url)
info = json.loads(res.read().decode('utf-8'))
for i in info['album']['image']:
if i['size'] != 'large' or not i['#text']:
continue
res = urllib.request.urlopen(i['#text'])
with os.fdopen(fd, 'wb') as f:
f.write(res.read())
break
else:
sys.stderr.write(
'Failed to fetch album art for {artist} - {album}\n'.format(
artist=self.artist,
album=self.album
)
)
os.unlink(self.album_art)
return
if callback:
callback()
class Notifier(object):
BUS_NAME = 'org.freedesktop.Notifications'
OBJECT_PATH = '/org/freedesktop/Notifications'
INTERFACE_NAME = 'org.freedesktop.Notifications'
APP_NAME = 'mpdnotify'
RECONNECT_INTERVAL = 1
def __init__(self, host, port, password=None):
self.icon = None
self.host = host
self.port = port
self.password = password
dbus_loop = dbus.mainloop.glib.DBusGMainLoop()
self.bus = dbus.SessionBus(mainloop=dbus_loop)
self.proxy = self.bus.get_object(self.BUS_NAME, self.OBJECT_PATH)
self.msg_id = 0
self.client = mpd.MPDClient()
def _changed(self, source, condition):
try:
self.client.fetch_idle()
self.notify()
self.client.send_idle('player')
except mpd.ConnectionError:
self._notify(
'Disconnected', 'Lost connection to MPD, reconnecting',
icon='audio-x-generic')
if self.icon:
self.icon.set_tooltip_text('Not connected')
self._reconnect()
return False
else:
return True
def _notify(self, summary, body='', icon='', actions=[], hints={},
expire_timeout=2000):
proxy = self.bus.get_object(self.BUS_NAME, self.OBJECT_PATH)
self.msg_id = proxy.get_dbus_method('Notify', self.INTERFACE_NAME)(
self.APP_NAME, self.msg_id, icon, summary, body, actions,
hints, expire_timeout
)
def _reconnect(self):
def reconnect(data):
try:
self.connect()
except:
return True
else:
self.notify()
self.client.send_idle('player')
return False
timer = GLib.Timeout(self.RECONNECT_INTERVAL * 1000)
timer.set_callback(reconnect)
timer.attach()
def connect(self):
self.client._sock = None
self.client.connect(self.host, self.port)
if self.password:
self.client.password(self.password)
GLib.io_add_watch(self.client._sock.fileno(), GLib.PRIORITY_DEFAULT,
GLib.IO_IN, self._changed)
def notify(self):
status = self.client.status()
song = Song(**self.client.currentsong())
try:
title = song.title
except AttributeError:
return
if status['state'] == 'pause':
title += ' (Paused)'
elif status['state'] == 'stop':
title += ' (Stopped)'
if self.icon:
self.icon.set_tooltip_text('{artist} - {title}'.format(
artist=song.artist,
title=title,
))
body = '<i>from</i> {album} <i>by</i> {artist} ({duration})'.format(
album=song.album,
artist=song.artist,
duration=song.duration,
)
hints = {'transient': True}
if os.path.exists(song.album_art):
icon = song.album_art
else:
def renotify():
self.client.noidle()
self.notify()
self.client.send_idle('player')
GLib.idle_add(song.fetch_album_art, renotify)
icon = 'audio-x-generic'
self._notify(title, body, icon=icon, hints=hints)
def start(self):
try:
self.connect()
except:
sys.stderr.write('Failed to connect to {0}, will retry in '
'{1} second(s).\n'
''.format(self.host, self.RECONNECT_INTERVAL))
self._reconnect()
else:
self.notify()
self.client.send_idle('player')
def show_status_icon(self):
loader = GdkPixbuf.PixbufLoader()
img = pkg_resources.resource_stream('mpdnotify', 'mpd-logo.png')
with contextlib.closing(loader):
for d in iter(lambda: img.read(1), b''):
loader.write(d)
self.icon = Gtk.StatusIcon.new_from_pixbuf(loader.get_pixbuf())
self.icon.set_tooltip_text('Not connected')
def stop(self):
try:
self.client.close()
except mpd.PendingCommandError:
self.client.noidle()
self.client.close()
except mpd.ConnectionError:
pass
else:
self.client.disconnect()
def _parse_args():
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--help', action='help',
help='show this help message and exit')
parser.add_argument('--no-fork', '-f', action='store_true', default=False,
help='Do not fork into the background')
parser.add_argument('--no-status-icon', action='store_true', default=False,
help=('Do not display a status icon in the '
'notification area (tray)'))
parser.add_argument('--host', '-h', default=MPD_HOST,
help=('The MPD server to connect to; if not given, '
'the value of the environment variable MPD_HOST '
'is checked before defaulting to localhost. To '
'use a password, provide a value of the form '
'"password@host".'))
parser.add_argument('--port', '-p', type=int, default=MPD_PORT,
help=('The MPD server port; if not given, the value '
'of the environment valriable MPD_PORT is '
'checked before defaulting to 6600'))
return parser.parse_args()
def daemonize():
if os.fork():
raise SystemExit
os.setsid()
os.chdir('/')
if os.fork():
raise SystemExit
def main():
args = _parse_args()
if not args.no_fork:
daemonize()
os.umask(0o0022)
if '@' in args.host:
password, host = args.host.split('@', 1)
else:
password = None
host = args.host
notifier = Notifier(host, args.port, password)
if not args.no_status_icon:
notifier.show_status_icon()
notifier.start()
def quit():
notifier.stop()
Gtk.main_quit()
raise SystemExit
for s in (signal.SIGINT, signal.SIGTERM):
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, s, quit)
Gtk.main()
if __name__ == '__main__':
main()