This repository has been archived by the owner on Jan 10, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathgaudible.py
executable file
·234 lines (172 loc) · 7.64 KB
/
gaudible.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#!/usr/bin/env python3
import argparse
import logging
import os
import re
import subprocess
import sys
import threading
import time
from dbus import SessionBus
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib, Gio
DEFAULT_PLAYER = '/usr/bin/paplay'
DEFAULT_SOUND = '/usr/share/sounds/freedesktop/stereo/bell.oga'
DEFAULT_RATE_MS = 500
FILTERS = {
'calendar': ('org.gtk.Notifications', 'AddNotification', 'org.gnome.Evolution-alarm-notify'),
'calendar-legacy': ('org.freedesktop.Notifications', 'Notify', 'Evolution Reminders'),
'firefox': ('org.freedesktop.Notifications', 'Notify', 'Firefox'),
'librewolf': ('org.freedesktop.Notifications', 'Notify', 'LibreWolf'),
'notify-send': ('org.freedesktop.Notifications', 'Notify', 'notify-send'),
'chrome': ('org.freedesktop.Notifications', 'Notify', 'Google Chrome'),
}
GNOME_SETTINGS = Gio.Settings(schema='org.gnome.desktop.notifications')
PATTERN_BLOB = re.compile(r'\[(dbus.Byte\(\d+\)(, )?){5,}\]')
PATTERN_SOUNDSPEC = re.compile(r'^(?P<name>[\w\-]+):(?P<path>.*)$')
LOG = logging.getLogger('gaudible') # type: logging.Logger
def main():
ap = argparse.ArgumentParser()
ap.add_argument('-v', '--verbose', action='count', help='controls amount of log output (repeat for more verbosity)')
ap.add_argument('--sound', dest='sound_spec', action='append', help='registers a sound for a specific filter with format <filter-name>:<file-path> or use format <file-path> for everything')
ap.add_argument('--filter', dest='filters', action='append', choices=FILTERS.keys())
ap.add_argument('--player', default=DEFAULT_PLAYER)
ap.add_argument('--rate-ms', type=int, default=DEFAULT_RATE_MS)
ap.set_defaults(verbose=0)
params = ap.parse_args()
log_level = logging.WARNING
if params.verbose >= 2:
log_level = logging.DEBUG
elif params.verbose >= 1:
log_level = logging.INFO
logging.basicConfig(
datefmt='%H:%M:%S',
format='%(asctime)s %(levelname)5s - %(message)s',
level=log_level,
stream=sys.stdout,
)
LOG.debug('build sound registry: %s', params.sound_spec)
sounds = {'*': DEFAULT_SOUND}
if params.sound_spec:
for spec in params.sound_spec:
spec = spec.strip() # type: str
m = PATTERN_SOUNDSPEC.match(spec)
if m:
key = m.group('name').lower()
value = m.group('path')
if key not in FILTERS:
ap.error('unknown filter %r in sound spec %r' % (key, spec))
return
else:
key = '*'
value = spec
if not os.access(value, os.R_OK):
ap.error('audio file %r cannot be read in sound spec %r' % (value, spec))
return
sounds[key] = value
LOG.debug('sound registry: %s', sounds)
LOG.debug('check audio player')
if not os.access(params.player, os.R_OK | os.X_OK):
ap.error('player %r does not exist or is not executable' % (params.player,))
LOG.debug('initialize dbus')
DBusGMainLoop(set_as_default=True)
bus = SessionBus()
filter_keys = tuple(sorted(set(params.filters if params.filters else FILTERS.keys())))
subscribe_to_messages(bus, filter_keys)
audio_player = AudioPlayer(params.player, sounds, params.rate_ms)
attach_message_handler(bus, audio_player, filter_keys)
LOG.info('ONLINE')
loop = GLib.MainLoop()
try:
loop.run()
except KeyboardInterrupt:
loop.quit()
def attach_message_handler(bus, audio_player, filter_keys):
"""
:type bus: SessionBus
:type audio_player: AudioPlayer
:type filter_keys: tuple
References:
- https://developer.gnome.org/notification-spec/
- https://wiki.gnome.org/Projects/GLib/GNotification
"""
def on_message(_, msg):
"""
:type msg: dbus.lowlevel.SignalMessage
"""
try:
interface = msg.get_interface()
method = msg.get_member()
args = msg.get_args_list()
origin = str(args[0])
for filter_key in filter_keys:
filter_interface, filter_method, filter_origin = FILTERS[filter_key]
if filter_interface == interface and filter_method == method and filter_origin == origin:
# Suppress audio if we're in 'do not disturb' mode
if not GNOME_SETTINGS.get_boolean('show-banners'):
LOG.info('SUPPRESS: \033[1m%-15s\033[0m (from=%s:%s, args=%s)',
filter_key, interface, method, truncate_repr(args))
return
LOG.info('RECEIVE: \033[1m%-15s\033[0m (from=%s:%s, args=%s)',
filter_key, interface, method, truncate_repr(args))
audio_player.play(filter_key)
return
LOG.debug('DROP: \033[2m%s:%s\033[0m (args=%s)',
interface, method, args)
except Exception as e:
LOG.error('Something bad happened', exc_info=e)
bus.add_message_filter(on_message)
def subscribe_to_messages(bus, filter_keys):
"""
:type bus: SessionBus
:type filter_keys: [str]
References:
- https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules
"""
rules = set()
for k in filter_keys:
interface, method, origin = FILTERS[k]
rule = 'type=method_call, interface=%s, member=%s' % (interface, method)
# Prevent messages forwarded by org.freedesktop.Notifications to
# org.gtk.Notifications from showing up in the stream twice
if interface == 'org.freedesktop.Notifications':
rule = 'type=method_call, interface=%s, member=%s, sender=%s' % (interface, method, interface)
LOG.info('Subscribe: \033[1m%-15s\033[0m (rule=%r, origin=%r)', k, rule, origin)
rules.add(rule)
proxy = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') # type: dbus.proxies.ProxyObject
proxy.BecomeMonitor(rules, 0, dbus_interface='org.freedesktop.DBus.Monitoring')
def truncate_repr(o):
return PATTERN_BLOB.sub('<binary blob>', repr(o))
class AudioPlayer:
def __init__(self, player, files, rate_ms):
self._player = player
self._files = files # type: dict
self._rate_ms = max(0.01, rate_ms) / 1000
self._quiet_until = -1
def play(self, name):
if self._enforce_rate_limit():
return
t = threading.Thread(target=self._play, args=[name], name='%s:%s' % (self.__class__.__name__, time.time()))
t.start()
# HACK: Without this, sometimes the first execution gets deferred until
# the process is about to exit. Probably related to using GLib's
# event loop.
t.join(0.1)
return t
def _play(self, name):
sound_file = self._files.get(name, self._files.get('*'))
cmd = [self._player, sound_file]
LOG.debug('EXEC: %s (thread=%s)', cmd, threading.current_thread().name)
subprocess.check_call(cmd)
def _enforce_rate_limit(self):
now = time.time()
if now <= self._quiet_until:
LOG.debug('audioplayer: in quiet period for another %.2f seconds',
self._quiet_until - now)
return True
self._quiet_until = now + self._rate_ms
LOG.debug('audioplayer: setting quiet period: now=%.2f quiet_until=%.2f rate_ms=%.2f',
now, self._quiet_until, self._rate_ms)
return False
if __name__ == '__main__':
main()