Initial commit
commit
ec665403ea
|
@ -0,0 +1,2 @@
|
||||||
|
__pycache__/
|
||||||
|
*.egg-info/
|
|
@ -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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
Binary file not shown.
After Width: | Height: | Size: 9.7 KiB |
|
@ -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()
|
Loading…
Reference in New Issue