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