From 50de38ce824d6ea79c3cb69326862f34e82c251c Mon Sep 17 00:00:00 2001 From: Travis Golliher Date: Wed, 31 May 2017 21:04:30 -0700 Subject: [PATCH] Join notification support Initial Join by Joaoapps API Notification Support. See issue #2712. --- data/interfaces/default/config.html | 38 +++++ headphones/config.py | 4 + headphones/notifiers.py | 213 ++++++++++++++++++++-------- headphones/postprocessor.py | 5 + headphones/searcher.py | 4 + headphones/webserve.py | 15 +- 6 files changed, 219 insertions(+), 60 deletions(-) diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index 02f40718..3859d4a7 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -1372,6 +1372,23 @@ +
+
+ +
+
+
+ +
+
+ Comma separated list. Leave blank to send to all devices +
+
+ +
+
+
+ @@ -2168,6 +2185,27 @@ } }); + if ($("#join").is(":checked")) + { + $("#joinoptions").show(); + } + else + { + $("#joinoptions").hide(); + } + + + $("#join").click(function(){ + if ($("#join").is(":checked")) + { + $("#joinoptions").slideDown(); + } + else + { + $("#joinoptions").slideUp(); + } + }); + if ($("#twitter").is(":checked")) { $("#twitteroptions").show(); diff --git a/headphones/config.py b/headphones/config.py index 98b23785..83c73415 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -145,6 +145,10 @@ _CONFIG_DEFINITIONS = { 'IGNORED_FILES': (list, 'Advanced', []), # path 'INCLUDE_EXTRAS': (int, 'General', 0), 'INTERFACE': (str, 'General', 'default'), + 'JOIN_APIKEY': (str, 'Join', ''), + 'JOIN_DEVICEID': (str, 'Join', ''), + 'JOIN_ENABLED': (int, 'Join', 0), + 'JOIN_ONSNATCH': (int, 'Join', 0), 'JOURNAL_MODE': (str, 'Advanced', 'wal'), 'KAT': (int, 'Kat', 0), 'KAT_PROXY_URL': (str, 'Kat', ''), diff --git a/headphones/notifiers.py b/headphones/notifiers.py index f8f1b3f8..5664c2e9 100644 --- a/headphones/notifiers.py +++ b/headphones/notifiers.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from urllib import urlencode +from urllib import urlencode, quote_plus import urllib import subprocess import json @@ -148,7 +148,8 @@ class PROWL(object): http_handler.request("POST", "/publicapi/add", - headers={'Content-type': "application/x-www-form-urlencoded"}, + headers={ + 'Content-type': "application/x-www-form-urlencoded"}, body=urlencode(data)) response = http_handler.getresponse() request_status = response.status @@ -203,20 +204,25 @@ class XBMC(object): url = host + '/xbmcCmds/xbmcHttp/?' + url_command if self.password: - return request.request_content(url, auth=(self.username, self.password)) + return request.request_content(url, + auth=(self.username, self.password)) else: return request.request_content(url) def _sendjson(self, host, method, params={}): - data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] + data = [ + {'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] headers = {'Content-Type': 'application/json'} url = host + '/jsonrpc' if self.password: - response = request.request_json(url, method="post", data=json.dumps(data), - headers=headers, auth=(self.username, self.password)) + response = request.request_json(url, method="post", + data=json.dumps(data), + headers=headers, auth=( + self.username, self.password)) else: - response = request.request_json(url, method="post", data=json.dumps(data), + response = request.request_json(url, method="post", + data=json.dumps(data), headers=headers) if response: @@ -247,7 +253,8 @@ class XBMC(object): logger.info('Sending notification command to XMBC @ ' + host) try: version = self._sendjson(host, 'Application.GetProperties', - {'properties': ['version']})['version']['major'] + {'properties': ['version']})[ + 'version']['major'] if version < 12: # Eden notification = header + "," + message + "," + time + "," + albumartpath @@ -256,9 +263,11 @@ class XBMC(object): request = self._sendhttp(host, notifycommand) else: # Frodo - params = {'title': header, 'message': message, 'displaytime': int(time), + params = {'title': header, 'message': message, + 'displaytime': int(time), 'image': albumartpath} - request = self._sendjson(host, 'GUI.ShowNotification', params) + request = self._sendjson(host, 'GUI.ShowNotification', + params) if not request: raise Exception @@ -323,22 +332,27 @@ class Plex(object): url = host + '/xbmcCmds/xbmcHttp/?' + command if self.password: - response = request.request_response(url, auth=(self.username, self.password)) + response = request.request_response(url, auth=( + self.username, self.password)) else: response = request.request_response(url) return response def _sendjson(self, host, method, params={}): - data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] + data = [ + {'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}] headers = {'Content-Type': 'application/json'} url = host + '/jsonrpc' if self.password: - response = request.request_json(url, method="post", data=json.dumps(data), - headers=headers, auth=(self.username, self.password)) + response = request.request_json(url, method="post", + data=json.dumps(data), + headers=headers, auth=( + self.username, self.password)) else: - response = request.request_json(url, method="post", data=json.dumps(data), + response = request.request_json(url, method="post", + data=json.dumps(data), headers=headers) if response: @@ -352,7 +366,8 @@ class Plex(object): hosts = [x.strip() for x in self.server_hosts.split(',')] for host in hosts: - logger.info('Sending library update command to Plex Media Server@ ' + host) + logger.info( + 'Sending library update command to Plex Media Server@ ' + host) url = "%s/library/sections" % host if self.token: params = {'X-Plex-Token': self.token} @@ -369,7 +384,8 @@ class Plex(object): for s in sections: if s.getAttribute('type') == "artist": - url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key')) + url = "%s/library/sections/%s/refresh" % ( + host, s.getAttribute('key')) request.request_response(url, params=params) def notify(self, artist, album, albumartpath): @@ -381,10 +397,12 @@ class Plex(object): time = "3000" # in ms for host in hosts: - logger.info('Sending notification command to Plex client @ ' + host) + logger.info( + 'Sending notification command to Plex client @ ' + host) try: version = self._sendjson(host, 'Application.GetProperties', - {'properties': ['version']})['version']['major'] + {'properties': ['version']})[ + 'version']['major'] if version < 12: # Eden notification = header + "," + message + "," + time + "," + albumartpath @@ -393,15 +411,18 @@ class Plex(object): request = self._sendhttp(host, notifycommand) else: # Frodo - params = {'title': header, 'message': message, 'displaytime': int(time), + params = {'title': header, 'message': message, + 'displaytime': int(time), 'image': albumartpath} - request = self._sendjson(host, 'GUI.ShowNotification', params) + request = self._sendjson(host, 'GUI.ShowNotification', + params) if not request: raise Exception except Exception: - logger.error('Error sending notification request to Plex client @ ' + host) + logger.error( + 'Error sending notification request to Plex client @ ' + host) class NMA(object): @@ -433,7 +454,8 @@ class NMA(object): if len(keys) > 1: batch = True - response = p.push(title, event, message, priority=nma_priority, batch_mode=batch) + response = p.push(title, event, message, priority=nma_priority, + batch_mode=batch) if not response[api][u'code'] == u'200': logger.error(u'Could not send notification to NotifyMyAndroid') @@ -463,7 +485,8 @@ class PUSHBULLET(object): headers = {'Content-type': "application/json", 'Authorization': 'Bearer ' + headphones.CONFIG.PUSHBULLET_APIKEY} - response = request.request_json(url, method="post", headers=headers, data=json.dumps(data)) + response = request.request_json(url, method="post", headers=headers, + data=json.dumps(data)) if response: logger.info(u"PushBullet notifications sent.") @@ -492,7 +515,8 @@ class PUSHALOT(object): http_handler.request("POST", "/api/sendmessage", - headers={'Content-type': "application/x-www-form-urlencoded"}, + headers={ + 'Content-type': "application/x-www-form-urlencoded"}, body=urlencode(data)) response = http_handler.getresponse() request_status = response.status @@ -512,6 +536,49 @@ class PUSHALOT(object): return False +class JOIN(object): + def __init__(self): + + self.enabled = headphones.CONFIG.JOIN_ENABLED + self.apikey = headphones.CONFIG.JOIN_APIKEY + self.deviceid = headphones.CONFIG.JOIN_DEVICEID + self.url = 'https://joinjoaomgcd.appspot.com/_ah/' \ + 'api/messaging/v1/sendPush?apikey={apikey}' \ + '&title={title}&text={text}' \ + '&icon={icon}' + + def notify(self, message, event): + if not headphones.CONFIG.JOIN_ENABLED or \ + not headphones.CONFIG.JOIN_APIKEY: + return + + icon = "https://cdn.rawgit.com/Headphones/" \ + "headphones/develop/data/images/headphoneslogo.png" + + if not self.deviceid: + self.deviceid = "group.all" + l = [x.strip() for x in self.deviceid.split(',')] + if len(l) > 1: + self.url += '&deviceIds={deviceid}' + else: + self.url += '&deviceId={deviceid}' + + response = urllib2.urlopen(self.url.format(apikey=self.apikey, + title=quote_plus(event), + text=quote_plus( + message.encode( + "utf-8")), + icon=icon, + deviceid=self.deviceid)) + + if response: + logger.info(u"Join notifications sent.") + return True + else: + logger.error(u"Join notification failed.") + return False + + class Synoindex(object): def __init__(self, util_loc='/usr/syno/bin/synoindex'): self.util_loc = util_loc @@ -539,7 +606,8 @@ class Synoindex(object): cmd = [self.util_loc, cmd_arg, path] logger.info("Calling synoindex command: %s" % str(cmd)) try: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, cwd=headphones.PROG_DIR) out, error = p.communicate() # synoindex never returns any codes other than '0', highly irritating @@ -580,7 +648,8 @@ class PUSHOVER(object): headers = {'Content-type': "application/x-www-form-urlencoded"} - response = request.request_response(url, method="POST", headers=headers, data=data) + response = request.request_response(url, method="POST", + headers=headers, data=data) if response: logger.info(u"Pushover notifications sent.") @@ -614,7 +683,8 @@ class TwitterNotifier(object): def notify_snatch(self, title): if headphones.CONFIG.TWITTER_ONSNATCH: self._notifyTwitter( - common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now()) + common.notifyStrings[ + common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now()) def notify_download(self, title): if headphones.CONFIG.TWITTER_ENABLED: @@ -623,11 +693,13 @@ class TwitterNotifier(object): def test_notify(self): return self._notifyTwitter( - "This is a test notification from Headphones at " + helpers.now(), force=True) + "This is a test notification from Headphones at " + helpers.now(), + force=True) def _get_authorization(self): - oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) + oauth_consumer = oauth.Consumer(key=self.consumer_key, + secret=self.consumer_secret) oauth_client = oauth.Client(oauth_consumer) logger.info('Requesting temp token from Twitter') @@ -635,32 +707,41 @@ class TwitterNotifier(object): resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET') if resp['status'] != '200': - logger.info('Invalid respond from Twitter requesting temp token: %s' % resp['status']) + logger.info( + 'Invalid respond from Twitter requesting temp token: %s' % + resp['status']) else: request_token = dict(parse_qsl(content)) headphones.CONFIG.TWITTER_USERNAME = request_token['oauth_token'] - headphones.CONFIG.TWITTER_PASSWORD = request_token['oauth_token_secret'] + headphones.CONFIG.TWITTER_PASSWORD = request_token[ + 'oauth_token_secret'] - return self.AUTHORIZATION_URL + "?oauth_token=" + request_token['oauth_token'] + return self.AUTHORIZATION_URL + "?oauth_token=" + request_token[ + 'oauth_token'] def _get_credentials(self, key): request_token = {} request_token['oauth_token'] = headphones.CONFIG.TWITTER_USERNAME - request_token['oauth_token_secret'] = headphones.CONFIG.TWITTER_PASSWORD + request_token[ + 'oauth_token_secret'] = headphones.CONFIG.TWITTER_PASSWORD request_token['oauth_callback_confirmed'] = 'true' - token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) + token = oauth.Token(request_token['oauth_token'], + request_token['oauth_token_secret']) token.set_verifier(key) - logger.info('Generating and signing request for an access token using key ' + key) + logger.info( + 'Generating and signing request for an access token using key ' + key) - oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) + oauth_consumer = oauth.Consumer(key=self.consumer_key, + secret=self.consumer_secret) logger.info('oauth_consumer: ' + str(oauth_consumer)) oauth_client = oauth.Client(oauth_consumer, token) logger.info('oauth_client: ' + str(oauth_client)) - resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', + resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, + method='POST', body='oauth_verifier=%s' % key) logger.info('resp, content: ' + str(resp) + ',' + str(content)) @@ -669,14 +750,18 @@ class TwitterNotifier(object): logger.info('resp[status] = ' + str(resp['status'])) if resp['status'] != '200': - logger.info('The request for a token with did not succeed: ' + str(resp['status']), + logger.info('The request for a token with did not succeed: ' + str( + resp['status']), logger.ERROR) return False else: - logger.info('Your Twitter Access Token key: %s' % access_token['oauth_token']) - logger.info('Access Token secret: %s' % access_token['oauth_token_secret']) + logger.info('Your Twitter Access Token key: %s' % access_token[ + 'oauth_token']) + logger.info( + 'Access Token secret: %s' % access_token['oauth_token_secret']) headphones.CONFIG.TWITTER_USERNAME = access_token['oauth_token'] - headphones.CONFIG.TWITTER_PASSWORD = access_token['oauth_token_secret'] + headphones.CONFIG.TWITTER_PASSWORD = access_token[ + 'oauth_token_secret'] return True def _send_tweet(self, message=None): @@ -688,7 +773,8 @@ class TwitterNotifier(object): logger.info(u"Sending tweet: " + message) - api = twitter.Api(username, password, access_token_key, access_token_secret) + api = twitter.Api(username, password, access_token_key, + access_token_secret) try: api.PostUpdate(message) @@ -741,7 +827,8 @@ class OSX_NOTIFY(object): ) NSUserNotification = self.objc.lookUpClass('NSUserNotification') - NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter') + NSUserNotificationCenter = self.objc.lookUpClass( + 'NSUserNotificationCenter') NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool') if not NSUserNotification or not NSUserNotificationCenter: @@ -756,9 +843,11 @@ class OSX_NOTIFY(object): if text: notification.setInformativeText_(text) if sound: - notification.setSoundName_("NSUserNotificationDefaultSoundName") + notification.setSoundName_( + "NSUserNotificationDefaultSoundName") if image: - source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image) + source_img = self.AppKit.NSImage.alloc().initByReferencingFile_( + image) notification.setContentImage_(source_img) # notification.set_identityImage_(source_img) notification.setHasActionButton_(False) @@ -818,8 +907,9 @@ class SubSonicNotifier(object): self.host = self.host + "/" # Invoke request - request.request_response(self.host + "musicFolderSettings.view?scanNow", - auth=(self.username, self.password)) + request.request_response( + self.host + "musicFolderSettings.view?scanNow", + auth=(self.username, self.password)) class Email(object): @@ -827,13 +917,15 @@ class Email(object): message = MIMEText(message, 'plain', "utf-8") message['Subject'] = subject - message['From'] = email.utils.formataddr(('Headphones', headphones.CONFIG.EMAIL_FROM)) + message['From'] = email.utils.formataddr( + ('Headphones', headphones.CONFIG.EMAIL_FROM)) message['To'] = headphones.CONFIG.EMAIL_TO try: if headphones.CONFIG.EMAIL_SSL: - mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER, - headphones.CONFIG.EMAIL_SMTP_PORT) + mailserver = smtplib.SMTP_SSL( + headphones.CONFIG.EMAIL_SMTP_SERVER, + headphones.CONFIG.EMAIL_SMTP_PORT) else: mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER, headphones.CONFIG.EMAIL_SMTP_PORT) @@ -847,7 +939,8 @@ class Email(object): mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER, headphones.CONFIG.EMAIL_SMTP_PASSWORD) - mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, headphones.CONFIG.EMAIL_TO, + mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, + headphones.CONFIG.EMAIL_TO, message.as_string()) mailserver.quit() return True @@ -858,7 +951,6 @@ class Email(object): class TELEGRAM(object): - def notify(self, message, status): if not headphones.CONFIG.TELEGRAM_ENABLED: return @@ -876,14 +968,17 @@ class TELEGRAM(object): # Send message to user using Telegram's Bot API try: - response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload) + response = requests.post(TELEGRAM_API % (token, "sendMessage"), + data=payload) except Exception, e: logger.info(u'Telegram notify failed: ' + str(e)) # Error logging sent_successfuly = True if not response.status_code == 200: - logger.info(u'Could not send notification to TelegramBot (token=%s). Response: [%s]', (token, response.text)) + logger.info( + u'Could not send notification to TelegramBot (token=%s). Response: [%s]', + (token, response.text)) sent_successfuly = False logger.info(u"Telegram notifications sent.") @@ -891,7 +986,6 @@ class TELEGRAM(object): class SLACK(object): - def notify(self, message, status): if not headphones.CONFIG.SLACK_ENABLED: return @@ -902,7 +996,8 @@ class SLACK(object): channel = headphones.CONFIG.SLACK_CHANNEL emoji = headphones.CONFIG.SLACK_EMOJI - payload = {'channel': channel, 'text': status + ': ' + message, 'icon_emoji': emoji} + payload = {'channel': channel, 'text': status + ': ' + message, + 'icon_emoji': emoji} try: response = requests.post(SLACK_URL, json=payload) @@ -911,7 +1006,9 @@ class SLACK(object): sent_successfuly = True if not response.status_code == 200: - logger.info(u'Could not send notification to Slack. Response: [%s]', (response.text)) + logger.info( + u'Could not send notification to Slack. Response: [%s]', + (response.text)) sent_successfuly = False logger.info(u"Slack notifications sent.") diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index 38e4bba5..ed3c136b 100755 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -574,6 +574,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, pushbullet = notifiers.PUSHBULLET() pushbullet.notify(pushmessage, statusmessage) + if headphones.CONFIG.JOIN_ENABLED: + logger.info(u"Join request") + join = notifiers.JOIN() + join.notify(pushmessage, statusmessage) + if headphones.CONFIG.TELEGRAM_ENABLED: logger.info(u"Telegram request") telegram = notifiers.TELEGRAM() diff --git a/headphones/searcher.py b/headphones/searcher.py index b0652f7f..17619977 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -1076,6 +1076,10 @@ def send_to_downloader(data, bestqual, album): logger.info(u"Sending PushBullet notification") pushbullet = notifiers.PUSHBULLET() pushbullet.notify(name, "Download started") + if headphones.CONFIG.JOIN_ENABLED and headphones.CONFIG.JOIN_ONSNATCH: + logger.info(u"Sending Join notification") + join = notifiers.JOIN() + join.notify(name, "Download started") if headphones.CONFIG.SLACK_ENABLED and headphones.CONFIG.SLACK_ONSNATCH: logger.info(u"Sending Slack notification") slack = notifiers.SLACK() diff --git a/headphones/webserve.py b/headphones/webserve.py index ee353fe2..959af6ce 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1412,7 +1412,11 @@ class WebInterface(object): "slack_url": headphones.CONFIG.SLACK_URL, "slack_channel": headphones.CONFIG.SLACK_CHANNEL, "slack_emoji": headphones.CONFIG.SLACK_EMOJI, - "slack_onsnatch": checked(headphones.CONFIG.SLACK_ONSNATCH) + "slack_onsnatch": checked(headphones.CONFIG.SLACK_ONSNATCH), + "join_enabled": checked(headphones.CONFIG.JOIN_ENABLED), + "join_onsnatch": checked(headphones.CONFIG.JOIN_ONSNATCH), + "join_apikey": headphones.CONFIG.JOIN_APIKEY, + "join_deviceid": headphones.CONFIG.JOIN_DEVICEID } for k, v in config.iteritems(): @@ -1480,7 +1484,8 @@ class WebInterface(object): "osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch", "songkick_enabled", "songkick_filter_enabled", "mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch", - "customauth", "idtag", "deluge_paused" + "customauth", "idtag", "deluge_paused", + "join_enabled", "join_onsnatch" ] for checked_config in checked_configs: if checked_config not in kwargs: @@ -1749,6 +1754,12 @@ class WebInterface(object): telegram = notifiers.TELEGRAM() telegram.notify("it works!", "lazers pew pew") + @cherrypy.expose + def testJoin(self): + logger.info("Testing Join notifications") + join = notifiers.JOIN() + join.notify("it works!", "Test message") + class Artwork(object): @cherrypy.expose