From e23af0e4623bfde1a6c411118d3c368c4504fd09 Mon Sep 17 00:00:00 2001 From: Supremicus Date: Tue, 17 Feb 2015 16:39:35 +1000 Subject: [PATCH] Update Pushbullet notifier (port from midgetspy/sickbeard) --- CHANGES.md | 1 + .../default/config_notifications.tmpl | 23 +-- gui/slick/js/configNotifications.js | 83 ++++---- sickbeard/__init__.py | 14 +- sickbeard/config.py | 9 +- sickbeard/notifiers/pushbullet.py | 181 ++++++++++-------- sickbeard/webserve.py | 16 +- 7 files changed, 176 insertions(+), 151 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e7825ddc..197da80e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -60,6 +60,7 @@ * Add anime release groups to add new show options page * Add setting "Update shows during hour" to General Config/Misc * Add max-width to prevent ui glitch on Pull request and Branch Version selectors on config/General/Advanced and change tags to html5 +* Update Pushbullet notifier (port from midgetspy/sickbeard) [develop changelog] * Change uT params from unicode to str.format as magnet URLs worked but sending files in POST bodies failed diff --git a/gui/slick/interfaces/default/config_notifications.tmpl b/gui/slick/interfaces/default/config_notifications.tmpl index 7b8fff43..9efce083 100644 --- a/gui/slick/interfaces/default/config_notifications.tmpl +++ b/gui/slick/interfaces/default/config_notifications.tmpl @@ -1118,7 +1118,7 @@

Pushbullet

-

Pushbullet is a platform for receiving custom push notifications to connected devices running Android and desktop Chrome browsers.

+

Pushbullet allows you to send push notifications to Android, iOS and supported browsers.

@@ -1160,25 +1160,26 @@
-
-
Click below to test.
diff --git a/gui/slick/js/configNotifications.js b/gui/slick/js/configNotifications.js index 63830ac9..03c33428 100644 --- a/gui/slick/js/configNotifications.js +++ b/gui/slick/js/configNotifications.js @@ -377,68 +377,73 @@ $(document).ready(function(){ }); $('#testPushbullet').click(function () { - var pushbullet_api = $.trim($('#pushbullet_api').val()); - if (!pushbullet_api) { + var pushbullet_access_token = $.trim($('#pushbullet_access_token').val()); + var pushbullet_device_iden = $('#pushbullet_device_iden').val(); + if (!pushbullet_access_token) { $('#testPushbullet-result').html('Please fill out the necessary fields above.'); - $('#pushbullet_api').addClass('warning'); + $('#pushbullet_access_token').addClass('warning'); return; } - $('#pushbullet_api').removeClass('warning'); + $('#pushbullet_access_token').removeClass('warning'); $(this).prop('disabled', true); $('#testPushbullet-result').html(loading); - $.get(sbRoot + '/home/testPushbullet', {'api': pushbullet_api}) + $.get(sbRoot + '/home/testPushbullet', {'accessToken': pushbullet_access_token, 'device_iden': pushbullet_device_iden}) .done(function (data) { $('#testPushbullet-result').html(data); $('#testPushbullet').prop('disabled', false); }); }); - function get_pushbullet_devices(msg){ - - if(msg){ + function get_pushbullet_devices (msg) { + var pushbullet_access_token = $.trim($('#pushbullet_access_token').val()); + if (!pushbullet_access_token) { + $('#testPushbullet-result').html('Please fill out the necessary fields above.'); + $('#pushbullet_access_token').addClass('warning'); + return; + } + $(this).prop("disabled", true); + if (msg) { $('#testPushbullet-result').html(loading); } - - var pushbullet_api = $("#pushbullet_api").val(); - - if(!pushbullet_api) { - $('#testPushbullet-result').html("You didn't supply a Pushbullet api key"); - $("#pushbullet_api").focus(); - return false; - } - - var current_pushbullet_device = $("#pushbullet_device").val(); - $.get(sbRoot + "/home/getPushbulletDevices", {'api': pushbullet_api}, - function (data) { - var devices = jQuery.parseJSON(data).devices; - $("#pushbullet_device_list").html(''); - for (var i = 0; i < devices.length; i++) { - if(devices[i].active == true) { - if(current_pushbullet_device == devices[i].iden) { - $("#pushbullet_device_list").append('') - } else { - $("#pushbullet_device_list").append('') + var current_pushbullet_device = $('#pushbullet_device_iden').val(); + $.get(sbRoot + '/home/getPushbulletDevices', {'accessToken': pushbullet_access_token}) + .done(function (data) { + var devices = jQuery.parseJSON(data || '{}').devices; + $('#pushbullet_device_list').html(''); + // add default option to send to all devices + $('#pushbullet_device_list').append(''); + if (devices) { + for (var i = 0; i < devices.length; i++) { + // only list active device targets + if (devices[i].active == true) { + // if a device in the list matches our current iden, select it + if (current_pushbullet_device == devices[i].iden) { + $('#pushbullet_device_list').append(''); + } else { + $('#pushbullet_device_list').append(''); + } } } } - if(msg) { + $('#getPushbulletDevices').prop('disabled', false); + if (msg) { $('#testPushbullet-result').html(msg); } - } - ); + }); - $("#pushbullet_device_list").change(function(){ - $("#pushbullet_device").val($("#pushbullet_device_list").val()); - $('#testPushbullet-result').html("Don't forget to save your new pushbullet settings."); + $('#pushbullet_device_list').change(function () { + $('#pushbullet_device_iden').val($('#pushbullet_device_list').val()); + $('#testPushbullet-result').html('Don\'t forget to save your new Pushbullet settings.'); }); - }; + } - $('#getPushbulletDevices').click(function(){ - get_pushbullet_devices("Device list updated. Please choose a device to push to."); + $('#getPushbulletDevices').click(function () { + get_pushbullet_devices('Device list updated. Select specific device to use.'); }); - // we have to call this function on dom ready to create the devices select - get_pushbullet_devices(); + if ($('#use_pushbullet').prop('checked')) { + get_pushbullet_devices(); + } $('#email_show').change(function () { var key = parseInt($('#email_show').val(), 10); diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index ff02b2da..105c5c6a 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -394,8 +394,8 @@ USE_PUSHBULLET = False PUSHBULLET_NOTIFY_ONSNATCH = False PUSHBULLET_NOTIFY_ONDOWNLOAD = False PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = False -PUSHBULLET_API = None -PUSHBULLET_DEVICE = None +PUSHBULLET_ACCESS_TOKEN = None +PUSHBULLET_DEVICE_IDEN = None USE_EMAIL = False EMAIL_NOTIFY_ONSNATCH = False @@ -477,7 +477,7 @@ def initialize(consoleLogging=True): USE_PYTIVO, PYTIVO_NOTIFY_ONSNATCH, PYTIVO_NOTIFY_ONDOWNLOAD, PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD, PYTIVO_UPDATE_LIBRARY, PYTIVO_HOST, PYTIVO_SHARE_NAME, PYTIVO_TIVO_NAME, \ USE_NMA, NMA_NOTIFY_ONSNATCH, NMA_NOTIFY_ONDOWNLOAD, NMA_NOTIFY_ONSUBTITLEDOWNLOAD, NMA_API, NMA_PRIORITY, \ USE_PUSHALOT, PUSHALOT_NOTIFY_ONSNATCH, PUSHALOT_NOTIFY_ONDOWNLOAD, PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHALOT_AUTHORIZATIONTOKEN, \ - USE_PUSHBULLET, PUSHBULLET_NOTIFY_ONSNATCH, PUSHBULLET_NOTIFY_ONDOWNLOAD, PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHBULLET_API, PUSHBULLET_DEVICE, \ + USE_PUSHBULLET, PUSHBULLET_NOTIFY_ONSNATCH, PUSHBULLET_NOTIFY_ONDOWNLOAD, PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHBULLET_ACCESS_TOKEN, PUSHBULLET_DEVICE_IDEN, \ versionCheckScheduler, VERSION_NOTIFY, AUTO_UPDATE, NOTIFY_ON_UPDATE, PROCESS_AUTOMATICALLY, UNPACK, CPU_PRESET, \ KEEP_PROCESSED_DIR, PROCESS_METHOD, TV_DOWNLOAD_DIR, MIN_RECENTSEARCH_FREQUENCY, DEFAULT_UPDATE_FREQUENCY, MIN_UPDATE_FREQUENCY, UPDATE_FREQUENCY, \ showQueueScheduler, searchQueueScheduler, ROOT_DIRS, CACHE_DIR, ACTUAL_CACHE_DIR, TIMEZONE_DISPLAY, \ @@ -870,8 +870,8 @@ def initialize(consoleLogging=True): PUSHBULLET_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Pushbullet', 'pushbullet_notify_ondownload', 0)) PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = bool( check_setting_int(CFG, 'Pushbullet', 'pushbullet_notify_onsubtitledownload', 0)) - PUSHBULLET_API = check_setting_str(CFG, 'Pushbullet', 'pushbullet_api', '') - PUSHBULLET_DEVICE = check_setting_str(CFG, 'Pushbullet', 'pushbullet_device', '') + PUSHBULLET_ACCESS_TOKEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_access_token', '') + PUSHBULLET_DEVICE_IDEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_device_iden', '') USE_EMAIL = bool(check_setting_int(CFG, 'Email', 'use_email', 0)) EMAIL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Email', 'email_notify_onsnatch', 0)) @@ -1719,8 +1719,8 @@ def save_config(): new_config['Pushbullet']['pushbullet_notify_onsnatch'] = int(PUSHBULLET_NOTIFY_ONSNATCH) new_config['Pushbullet']['pushbullet_notify_ondownload'] = int(PUSHBULLET_NOTIFY_ONDOWNLOAD) new_config['Pushbullet']['pushbullet_notify_onsubtitledownload'] = int(PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Pushbullet']['pushbullet_api'] = PUSHBULLET_API - new_config['Pushbullet']['pushbullet_device'] = PUSHBULLET_DEVICE + new_config['Pushbullet']['pushbullet_access_token'] = PUSHBULLET_ACCESS_TOKEN + new_config['Pushbullet']['pushbullet_device_iden'] = PUSHBULLET_DEVICE_IDEN new_config['Email'] = {} new_config['Email']['use_email'] = int(USE_EMAIL) diff --git a/sickbeard/config.py b/sickbeard/config.py index a50dc5b0..576bcb9c 100644 --- a/sickbeard/config.py +++ b/sickbeard/config.py @@ -449,8 +449,9 @@ class ConfigMigrator(): 4: 'Add newznab catIDs', 5: 'Metadata update', 6: 'Rename daily search to recent search', - 7: 'Rename coming episodes to episode view' - } + 7: 'Rename coming episodes to episode view', + 8: 'Disable searches on start', + 9: 'Rename pushbullet variables',} def migrate_config(self): """ @@ -745,3 +746,7 @@ class ConfigMigrator(): # removing settings from gui and making it a hidden debug option sickbeard.RECENTSEARCH_STARTUP = False sickbeard.BACKLOG_STARTUP = False + + def _migrate_v9(self): + sickbeard.PUSHBULLET_ACCESS_TOKEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_api', '') + sickbeard.PUSHBULLET_DEVICE_IDEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_device', '') \ No newline at end of file diff --git a/sickbeard/notifiers/pushbullet.py b/sickbeard/notifiers/pushbullet.py index 76fc81c0..8aa3e97f 100644 --- a/sickbeard/notifiers/pushbullet.py +++ b/sickbeard/notifiers/pushbullet.py @@ -1,5 +1,4 @@ -# Author: Pedro Correia (http://github.com/pedrocorreia/) -# Based on pushalot.py by Nic Wolfe +# Author: Nic Wolfe # URL: http://code.google.com/p/sickbeard/ # # This file is part of SickGear. @@ -17,109 +16,123 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . +import urllib +import urllib2 import socket import base64 -from httplib import HTTPSConnection, HTTPException -import json -from ssl import SSLError + import sickbeard -from sickbeard import logger, common + +from sickbeard import logger +from sickbeard.common import notifyStrings, NOTIFY_SNATCH, NOTIFY_DOWNLOAD, NOTIFY_SUBTITLE_DOWNLOAD, NOTIFY_GIT_UPDATE, NOTIFY_GIT_UPDATE_TEXT +from sickbeard.exceptions import ex + +PUSHAPI_ENDPOINT = 'https://api.pushbullet.com/v2/pushes' +DEVICEAPI_ENDPOINT = 'https://api.pushbullet.com/v2/devices' class PushbulletNotifier: - def test_notify(self, pushbullet_api): - return self._sendPushbullet(pushbullet_api, event="Test", message="Testing Pushbullet settings from SickGear", - method="POST", notificationType="note", force=True) - def get_devices(self, pushbullet_api): - return self._sendPushbullet(pushbullet_api, method="GET", force=True) + def get_devices(self, accessToken=None): + # fill in omitted parameters + if not accessToken: + accessToken = sickbeard.PUSHBULLET_ACCESS_TOKEN - def notify_snatch(self, ep_name): - if sickbeard.PUSHBULLET_NOTIFY_ONSNATCH: - self._sendPushbullet(pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_SNATCH] + " : " + ep_name, message=ep_name, - notificationType="note", method="POST") + # get devices from pushbullet + try: + req = urllib2.Request(DEVICEAPI_ENDPOINT) + base64string = base64.encodestring('%s:%s' % (accessToken, ''))[:-1] + req.add_header('Authorization', 'Basic %s' % base64string) + handle = urllib2.urlopen(req) + if handle: + result = handle.read() + handle.close() + return result + except urllib2.URLError: + return None + except socket.timeout: + return None - def notify_download(self, ep_name): - if sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD: - self._sendPushbullet(pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_DOWNLOAD] + " : " + ep_name, - message=ep_name, notificationType="note", method="POST") + def _sendPushbullet(self, title, body, accessToken, device_iden): - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD: - self._sendPushbullet(pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD] + " : " + ep_name + " : " + lang, - message=ep_name + ": " + lang, notificationType="note", method="POST") - - def notify_git_update(self, new_version = "??"): - if sickbeard.USE_PUSHBULLET: - update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._sendPushbullet(pushbullet_api=None, event=title, message=update_text + new_version, method="POST") + # build up the URL and parameters + body = body.strip().encode('utf-8') - def _sendPushbullet(self, pushbullet_api=None, pushbullet_device=None, event=None, message=None, - notificationType=None, method=None, force=False): + data = urllib.urlencode({ + 'type': 'note', + 'title': title, + 'body': body, + 'device_iden': device_iden + }) + # send the request to pushbullet + try: + req = urllib2.Request(PUSHAPI_ENDPOINT) + base64string = base64.encodestring('%s:%s' % (accessToken, ''))[:-1] + req.add_header('Authorization', 'Basic %s' % base64string) + handle = urllib2.urlopen(req, data) + handle.close() + except socket.timeout: + return False + except urllib2.URLError, e: + + if e.code == 404: + logger.log(u'PUSHBULLET: Access token is wrong/not associated to a device.', logger.ERROR) + elif e.code == 401: + logger.log(u'PUSHBULLET: Unauthorized, not a valid access token.', logger.ERROR) + elif e.code == 400: + logger.log(u'PUSHBULLET: Bad request, missing required parameter.', logger.ERROR) + elif e.code == 503: + logger.log(u'PUSHBULLET: Pushbullet server to busy to handle the request at this time.', logger.WARNING) + return False + + logger.log(u'PUSHBULLET: Notification successful.', logger.MESSAGE) + return True + + def _notifyPushbullet(self, title, body, accessToken=None, device_iden=None, force=False): + """ + Sends a pushbullet notification based on the provided info or SG config + + title: The title of the notification to send + body: The body string to send + accessToken: The access token to grant access + device_iden: The iden of a specific target, if none provided send to all devices + force: If True then the notification will be sent even if Pushbullet is disabled in the config + """ + + # suppress notifications if the notifier is disabled but the notify options are checked if not sickbeard.USE_PUSHBULLET and not force: return False - if pushbullet_api == None: - pushbullet_api = sickbeard.PUSHBULLET_API - if pushbullet_device == None: - pushbullet_device = sickbeard.PUSHBULLET_DEVICE + # fill in omitted parameters + if not accessToken: + accessToken = sickbeard.PUSHBULLET_ACCESS_TOKEN + if not device_iden: + device_iden = sickbeard.PUSHBULLET_DEVICE_IDEN - if method == 'POST': - uri = '/v2/pushes' - else: - uri = '/v2/devices' + logger.log(u'PUSHBULLET: Sending notice with details: \"%s - %s\", device_iden: %s' % (title, body, device_iden), logger.DEBUG) - logger.log(u"Pushbullet event: " + str(event), logger.DEBUG) - logger.log(u"Pushbullet message: " + str(message), logger.DEBUG) - logger.log(u"Pushbullet api: " + str(pushbullet_api), logger.DEBUG) - logger.log(u"Pushbullet devices: " + str(pushbullet_device), logger.DEBUG) - logger.log(u"Pushbullet notification type: " + str(notificationType), logger.DEBUG) + return self._sendPushbullet(title, body, accessToken, device_iden) - http_handler = HTTPSConnection("api.pushbullet.com") + def notify_snatch(self, ep_name): + if sickbeard.PUSHBULLET_NOTIFY_ONSNATCH: + self._notifyPushbullet(notifyStrings[NOTIFY_SNATCH], ep_name) - if notificationType == None: - testMessage = True - try: - logger.log(u"Testing Pushbullet authentication and retrieving the device list.", logger.DEBUG) - http_handler.request(method, uri, None, headers={'Authorization': 'Bearer %s' % pushbullet_api}) - except (SSLError, HTTPException, socket.error): - logger.log(u"Pushbullet notification failed.", logger.ERROR) - return False - else: - testMessage = False - try: - data = { - 'title': event.encode('utf-8'), - 'body': message.encode('utf-8'), - 'device_iden': pushbullet_device, - 'type': notificationType} - data = json.dumps(data) - http_handler.request(method, uri, body=data, - headers={'Content-Type': 'application/json', 'Authorization': 'Bearer %s' % pushbullet_api}) - pass - except (SSLError, HTTPException, socket.error): - return False + def notify_download(self, ep_name): + if sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD: + self._notifyPushbullet(notifyStrings[NOTIFY_DOWNLOAD], ep_name) - response = http_handler.getresponse() - request_body = response.read() - request_status = response.status - logger.log(u"Pushbullet response: %s" % request_body, logger.DEBUG) + def notify_subtitle_download(self, ep_name, lang): + if sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD: + self._notifyPushbullet(notifyStrings[NOTIFY_SUBTITLE_DOWNLOAD], ep_name + ': ' + lang) - if request_status == 200: - if testMessage: - return request_body - else: - logger.log(u"Pushbullet notifications sent.", logger.DEBUG) - return True - elif request_status == 410: - logger.log(u"Pushbullet authentication failed: %s" % response.reason, logger.ERROR) - return False - else: - logger.log(u"Pushbullet notification failed.", logger.ERROR) - return False + def notify_git_update(self, new_version = '??'): + if sickbeard.USE_PUSHBULLET: + update_text=notifyStrings[NOTIFY_GIT_UPDATE_TEXT] + title=notifyStrings[NOTIFY_GIT_UPDATE] + self._notifyPushbullet(title, update_text + new_version) + def test_notify(self, accessToken, device_iden): + return self._notifyPushbullet('Test', 'This is a test notification from SickGear', accessToken, device_iden, force=True) notifier = PushbulletNotifier - diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index ed7ef2b8..4892daaf 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -2354,8 +2354,8 @@ class ConfigNotifications(MainHandler): use_pushalot=None, pushalot_notify_onsnatch=None, pushalot_notify_ondownload=None, pushalot_notify_onsubtitledownload=None, pushalot_authorizationtoken=None, use_pushbullet=None, pushbullet_notify_onsnatch=None, pushbullet_notify_ondownload=None, - pushbullet_notify_onsubtitledownload=None, pushbullet_api=None, pushbullet_device=None, - pushbullet_device_list=None, + pushbullet_notify_onsubtitledownload=None, pushbullet_access_token=None, + pushbullet_device_iden=None, pushbullet_device_list=None, use_email=None, email_notify_onsnatch=None, email_notify_ondownload=None, email_notify_onsubtitledownload=None, email_host=None, email_port=25, email_from=None, email_tls=None, email_user=None, email_password=None, email_list=None, email_show_list=None, @@ -2497,8 +2497,8 @@ class ConfigNotifications(MainHandler): sickbeard.PUSHBULLET_NOTIFY_ONSNATCH = config.checkbox_to_value(pushbullet_notify_onsnatch) sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pushbullet_notify_ondownload) sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pushbullet_notify_onsubtitledownload) - sickbeard.PUSHBULLET_API = pushbullet_api - sickbeard.PUSHBULLET_DEVICE = pushbullet_device_list + sickbeard.PUSHBULLET_ACCESS_TOKEN = pushbullet_access_token + sickbeard.PUSHBULLET_DEVICE_IDEN = pushbullet_device_iden sickbeard.save_config() @@ -3554,20 +3554,20 @@ class Home(MainHandler): return "Error sending Pushalot notification" - def testPushbullet(self, api=None): + def testPushbullet(self, accessToken=None, device_iden=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - result = notifiers.pushbullet_notifier.test_notify(api) + result = notifiers.pushbullet_notifier.test_notify(accessToken, device_iden) if result: return "Pushbullet notification succeeded. Check your device to make sure it worked" else: return "Error sending Pushbullet notification" - def getPushbulletDevices(self, api=None): + def getPushbulletDevices(self, accessToken=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - result = notifiers.pushbullet_notifier.get_devices(api) + result = notifiers.pushbullet_notifier.get_devices(accessToken) if result: return result else: