diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index 820e729d..ac5b8b9c 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -690,8 +690,7 @@ - - +

Pushalot

@@ -708,6 +707,10 @@
+ + + +

Synology NAS

@@ -776,7 +779,40 @@
- + +
+

OS X

+
+ +
+
+
+ Enter the path/application name to be registered with the Notification Center, default is /Applications/Headphones + + +
+
+ +
+
+
+ +
+

Boxcar2

+
+ +
+
+
+ +
+
+ +
+
+
+ + @@ -1457,8 +1493,48 @@ $("#twitteroptions").slideUp(); } }); - - if ($("#songkick").is(":checked")) + + if ($("#osx_notify").is(":checked")) + { + $("#osx_notify_options").show(); + } + else + { + $("#osx_notify_options").hide(); + } + + $("#osx_notify").click(function(){ + if ($("#osx_notify").is(":checked")) + { + $("#osx_notify_options").slideDown(); + } + else + { + $("#osx_notify_options").slideUp(); + } + }); + + if ($("#boxcar").is(":checked")) + { + $("#boxcar_options").show(); + } + else + { + $("#boxcar_options").hide(); + } + + $("#boxcar").click(function(){ + if ($("#boxcar").is(":checked")) + { + $("#boxcar_options").slideDown(); + } + else + { + $("#boxcar_options").slideUp(); + } + }); + + if ($("#songkick").is(":checked")) { $("#songkickoptions").show(); } @@ -1671,6 +1747,12 @@ function (data) { $('#ajaxMsg').html("
"+data+"
"); }); $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); }); + + $('#osxnotifyregister').click(function () { + var osx_notify_app = $("#osx_notify_reg").val(); + $.get("/osxnotifyregister", {'app': osx_notify_app}, function (data) { $('#ajaxMsg').html("
"+data+"
"); }); + $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut() + }) } $(document).ready(function() { diff --git a/headphones/__init__.py b/headphones/__init__.py index 47588ec4..3f7dd45e 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -270,6 +270,12 @@ TWITTER_ONSNATCH = False TWITTER_USERNAME = None TWITTER_PASSWORD = None TWITTER_PREFIX = None +OSX_NOTIFY_ENABLED = False +OSX_NOTIFY_ONSNATCH = False +OSX_NOTIFY_APP = None +BOXCAR_ENABLED = False +BOXCAR_ONSNATCH = False +BOXCAR_TOKEN = None MIRRORLIST = ["musicbrainz.org","headphones","custom"] MIRROR = None CUSTOMHOST = None @@ -349,7 +355,7 @@ def initialize(): INTERFACE, FOLDER_PERMISSIONS, FILE_PERMISSIONS, ENCODERFOLDER, ENCODER_PATH, ENCODER, XLDPROFILE, BITRATE, SAMPLINGFREQUENCY, \ MUSIC_ENCODER, ADVANCEDENCODER, ENCODEROUTPUTFORMAT, ENCODERQUALITY, ENCODERVBRCBR, ENCODERLOSSLESS, ENCODER_MULTICORE, ENCODER_MULTICORE_COUNT, DELETE_LOSSLESS_FILES, \ GROWL_ENABLED, GROWL_HOST, GROWL_PASSWORD, GROWL_ONSNATCH, PROWL_ENABLED, PROWL_PRIORITY, PROWL_KEYS, PROWL_ONSNATCH, PUSHOVER_ENABLED, PUSHOVER_PRIORITY, PUSHOVER_KEYS, PUSHOVER_ONSNATCH, PUSHOVER_APITOKEN, MIRRORLIST, \ - TWITTER_ENABLED, TWITTER_ONSNATCH, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, \ + TWITTER_ENABLED, TWITTER_ONSNATCH, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, OSX_NOTIFY_ENABLED, OSX_NOTIFY_ONSNATCH, OSX_NOTIFY_APP, BOXCAR_ENABLED, BOXCAR_ONSNATCH, BOXCAR_TOKEN, \ PUSHBULLET_ENABLED, PUSHBULLET_APIKEY, PUSHBULLET_DEVICEID, PUSHBULLET_ONSNATCH, \ MIRROR, CUSTOMHOST, CUSTOMPORT, CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, \ XBMC_NOTIFY, LMS_ENABLED, LMS_HOST, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \ @@ -386,6 +392,8 @@ def initialize(): CheckSection('Pushalot') CheckSection('Synoindex') CheckSection('Twitter') + CheckSection('OSX_Notify') + CheckSection('Boxcar') CheckSection('Songkick') CheckSection('Advanced') @@ -609,7 +617,15 @@ def initialize(): TWITTER_USERNAME = check_setting_str(CFG, 'Twitter', 'twitter_username', '') TWITTER_PASSWORD = check_setting_str(CFG, 'Twitter', 'twitter_password', '') TWITTER_PREFIX = check_setting_str(CFG, 'Twitter', 'twitter_prefix', 'Headphones') - + + OSX_NOTIFY_ENABLED = bool(check_setting_int(CFG, 'OSX_Notify', 'osx_notify_enabled', 0)) + OSX_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'OSX_Notify', 'osx_notify_onsnatch', 0)) + OSX_NOTIFY_APP = check_setting_str(CFG, 'OSX_Notify', 'osx_notify_app', '/Applications/Headphones') + + BOXCAR_ENABLED = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_enabled', 0)) + BOXCAR_ONSNATCH = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_onsnatch', 0)) + BOXCAR_TOKEN = check_setting_str(CFG, 'Boxcar', 'boxcar_token', '') + SONGKICK_ENABLED = bool(check_setting_int(CFG, 'Songkick', 'songkick_enabled', 1)) SONGKICK_APIKEY = check_setting_str(CFG, 'Songkick', 'songkick_apikey', 'nd1We7dFW2RqxPw8') SONGKICK_LOCATION = check_setting_str(CFG, 'Songkick', 'songkick_location', '') @@ -1025,6 +1041,16 @@ def config_write(): new_config['Twitter']['twitter_password'] = TWITTER_PASSWORD new_config['Twitter']['twitter_prefix'] = TWITTER_PREFIX + new_config['OSX_Notify'] = {} + new_config['OSX_Notify']['osx_notify_enabled'] = int(OSX_NOTIFY_ENABLED) + new_config['OSX_Notify']['osx_notify_onsnatch'] = int(OSX_NOTIFY_ONSNATCH) + new_config['OSX_Notify']['osx_notify_app'] = OSX_NOTIFY_APP + + new_config['Boxcar'] = {} + new_config['Boxcar']['boxcar_enabled'] = int(BOXCAR_ENABLED) + new_config['Boxcar']['boxcar_onsnatch'] = int(BOXCAR_ONSNATCH) + new_config['Boxcar']['boxcar_token'] = BOXCAR_TOKEN + new_config['Songkick'] = {} new_config['Songkick']['songkick_enabled'] = int(SONGKICK_ENABLED) new_config['Songkick']['songkick_apikey'] = SONGKICK_APIKEY diff --git a/headphones/notifiers.py b/headphones/notifiers.py index ff27077f..dec4df59 100644 --- a/headphones/notifiers.py +++ b/headphones/notifiers.py @@ -132,7 +132,7 @@ class PROWL: return http_handler = HTTPSConnection("api.prowlapp.com") - + data = {'apikey': headphones.PROWL_KEYS, 'application': 'Headphones', 'event': event, @@ -167,7 +167,7 @@ class PROWL: self.priority = priority self.notify('ZOMG Lazors Pewpewpew!', 'Test Message') - + class XBMC: def __init__(self): @@ -207,10 +207,10 @@ class XBMC: for host in hosts: logger.info('Sending library update command to XBMC @ '+host) request = self._sendjson(host, 'AudioLibrary.Scan') - + if not request: logger.warn('Error sending update request to XBMC') - + def notify(self, artist, album, albumartpath): hosts = [x.strip() for x in self.hosts.split(',')] @@ -238,14 +238,15 @@ class XBMC: except: logger.warn('Error sending notification request to XBMC') + class LMS: #Class for updating a Logitech Media Server def __init__(self): - + self.hosts = headphones.LMS_HOST - + def _sendjson(self, host): data = {'id': 1, 'method': 'slim.request', 'params': ["",["rescan"]]} #Had a lot of trouble with simplejson, but this works. data = simplejson.JSONEncoder().encode(data) @@ -262,7 +263,7 @@ class LMS: response = simplejson.JSONDecoder().decode(handle.read()) server_result = simplejson.dumps(response) - + try: return response[0]['result'] except: @@ -270,24 +271,24 @@ class LMS: return def update(self): - - #Send the ["rescan"] command to an LMS server. - #Note that the command must be prefixed with the 'player' that the command is aimed at, - #But with this being a request for the server to update its library, the player is blank, so "" + + #Send the ["rescan"] command to an LMS server. + #Note that the command must be prefixed with the 'player' that the command is aimed at, + #But with this being a request for the server to update its library, the player is blank, so "" hosts = [x.strip() for x in self.hosts.split(',')] for host in hosts: logger.info('Sending library rescan command to LMS @ '+host) request = self._sendjson(host) - + if not request: logger.warn('Error sending rescan request to LMS') class Plex: def __init__(self): - + self.server_hosts = headphones.PLEX_SERVER_HOST self.client_hosts = headphones.PLEX_CLIENT_HOST self.username = headphones.PLEX_USERNAME @@ -297,31 +298,31 @@ class Plex: username = self.username password = self.password - + url_command = urllib.urlencode(command) - + url = host + '/xbmcCmds/xbmcHttp/?' + url_command - + req = urllib2.Request(url) - + if password: base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '') req.add_header("Authorization", "Basic %s" % base64string) - + logger.info('Plex url: %s' % url) - + try: handle = urllib2.urlopen(req) except Exception, e: logger.warn('Error opening Plex url: %s' % e) return - + response = handle.read().decode(headphones.SYS_ENCODING) - + return response - + def update(self): - + # From what I read you can't update the music library on a per directory or per path basis # so need to update the whole thing @@ -349,7 +350,7 @@ class Plex: except Exception, e: logger.warn("Error updating library section for Plex Media Server: %s" % e) return False - + def notify(self, artist, album, albumartpath): hosts = [x.strip() for x in self.client_hosts.split(',')] @@ -361,12 +362,12 @@ class Plex: for host in hosts: logger.info('Sending notification command to Plex Media Server @ '+host) try: - notification = header + "," + message + "," + time + "," + albumartpath - notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification('+notification+')'} - request = self._sendhttp(host, notifycommand) + notification = header + "," + message + "," + time + "," + albumartpath + notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification('+notification+')'} + request = self._sendhttp(host, notifycommand) - if not request: - raise Exception + if not request: + raise Exception except: logger.warn('Error sending notification request to Plex Media Server') @@ -374,30 +375,30 @@ class Plex: class NMA: def __init__(self): - + self.apikey = headphones.NMA_APIKEY self.priority = headphones.NMA_PRIORITY - + def _send(self, data): return request.request_content('https://www.notifymyandroid.com/publicapi/notify', data=data) - + def notify(self, artist=None, album=None, snatched_nzb=None): - + apikey = self.apikey priority = self.priority - + if snatched_nzb: event = snatched_nzb + " snatched!" description = "Headphones has snatched: " + snatched_nzb + " and has sent it to SABnzbd+" else: event = artist + ' - ' + album + ' complete!' description = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']' - + data = { 'apikey': apikey, 'application':'Headphones', 'event': event, 'description': description, 'priority': priority} logger.info('Sending notification request to NotifyMyAndroid') request = self._send(data) - + if not request: logger.warn('Error sending notification request to NotifyMyAndroid') @@ -415,7 +416,7 @@ class PUSHBULLET: return http_handler = HTTPSConnection("api.pushbullet.com") - + data = {'device_iden': headphones.PUSHBULLET_DEVICEID, 'type': "note", 'title': "Headphones", @@ -454,21 +455,20 @@ class PUSHBULLET: self.notify('Main Screen Activate', 'Test Message') - class PUSHALOT: def notify(self, message, event): if not headphones.PUSHALOT_ENABLED: return - pushalot_authorizationtoken = headphones.PUSHALOT_APIKEY + pushalot_authorizationtoken = headphones.PUSHALOT_APIKEY - logger.debug(u"Pushalot event: " + event) - logger.debug(u"Pushalot message: " + message) - logger.debug(u"Pushalot api: " + pushalot_authorizationtoken) + logger.debug(u"Pushalot event: " + event) + logger.debug(u"Pushalot message: " + message) + logger.debug(u"Pushalot api: " + pushalot_authorizationtoken) http_handler = HTTPSConnection("pushalot.com") - + data = {'AuthorizationToken': pushalot_authorizationtoken, 'Title': event.encode('utf-8'), 'Body': message.encode("utf-8") } @@ -529,6 +529,7 @@ class Synoindex: if isinstance(path_list, list): for path in path_list: self.notify(path) + class PUSHOVER: application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz" @@ -551,7 +552,7 @@ class PUSHOVER: return http_handler = HTTPSConnection("api.pushover.net") - + data = {'token': self.application_token, 'user': headphones.PUSHOVER_KEYS, 'title': event, @@ -589,17 +590,17 @@ class PUSHOVER: self.priority = priority self.notify('Main Screen Activate', 'Test Message') - + class TwitterNotifier: consumer_key = "oYKnp2ddX5gbARjqX8ZAAg" consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk" - + REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' - + def notify_snatch(self, title): if headphones.TWITTER_ONSNATCH: self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH]+': '+title+' at '+helpers.now()) @@ -612,37 +613,37 @@ class TwitterNotifier: return self._notifyTwitter("This is a test notification from Headphones at "+helpers.now(), force=True) def _get_authorization(self): - + signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable 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') - + 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']) else: request_token = dict(parse_qsl(content)) - + headphones.TWITTER_USERNAME = request_token['oauth_token'] headphones.TWITTER_PASSWORD = request_token['oauth_token_secret'] - + return self.AUTHORIZATION_URL+"?oauth_token="+ request_token['oauth_token'] - + def _get_credentials(self, key): request_token = {} - + request_token['oauth_token'] = headphones.TWITTER_USERNAME request_token['oauth_token_secret'] = headphones.TWITTER_PASSWORD request_token['oauth_callback_confirmed'] = 'true' - + 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) - + signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) logger.info('oauth_consumer: '+str(oauth_consumer)) @@ -650,10 +651,10 @@ class TwitterNotifier: logger.info('oauth_client: '+str(oauth_client)) resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key) logger.info('resp, content: '+str(resp)+','+str(content)) - + access_token = dict(parse_qsl(content)) logger.info('access_token: '+str(access_token)) - + 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.ERROR) @@ -664,33 +665,115 @@ class TwitterNotifier: headphones.TWITTER_USERNAME = access_token['oauth_token'] headphones.TWITTER_PASSWORD = access_token['oauth_token_secret'] return True - - + + def _send_tweet(self, message=None): - + username=self.consumer_key password=self.consumer_secret access_token_key=headphones.TWITTER_USERNAME access_token_secret=headphones.TWITTER_PASSWORD - + logger.info(u"Sending tweet: "+message) - + api = twitter.Api(username, password, access_token_key, access_token_secret) - + try: api.PostUpdate(message) except Exception, e: logger.info(u"Error Sending Tweet: %s" % e) return False - + return True - + def _notifyTwitter(self, message='', force=False): prefix = headphones.TWITTER_PREFIX - + if not headphones.TWITTER_ENABLED and not force: return False - + return self._send_tweet(prefix+": "+message) notifier = TwitterNotifier + +class OSX_NOTIFY: + + objc = None + + def __init__(self): + try: + self.objc = __import__("objc") + except: + return False + + def swizzle(self, cls, SEL, func): + old_IMP = cls.instanceMethodForSelector_(SEL) + def wrapper(self, *args, **kwargs): + return func(self, old_IMP, *args, **kwargs) + new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector, + signature=old_IMP.signature) + self.objc.classAddMethod(cls, SEL, new_IMP) + + def notify(self, title, subtitle=None, text=None, sound=True): + + try: + self.swizzle(self.objc.lookUpClass('NSBundle'), + b'bundleIdentifier', + self.swizzled_bundleIdentifier) + + NSUserNotification = self.objc.lookUpClass('NSUserNotification') + NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter') + NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool') + + if not NSUserNotification or not NSUserNotificationCenter: + return False + + pool = NSAutoreleasePool.alloc().init() + + notification = NSUserNotification.alloc().init() + notification.setTitle_(title) + if subtitle: + notification.setSubtitle_(subtitle) + if text: + notification.setInformativeText_(text) + if sound: + notification.setSoundName_("NSUserNotificationDefaultSoundName") + notification.setHasActionButton_(False) + + notification_center = NSUserNotificationCenter.defaultUserNotificationCenter() + notification_center.deliverNotification_(notification) + + del pool + return True + + except Exception, e: + logger.warn('Error sending OS X Notification: %s' % e) + return False + + def swizzled_bundleIdentifier(self, original, swizzled): + return 'ade.headphones.osxnotify' + +class BOXCAR: + + def __init__(self): + + self.url = 'https://new.boxcar.io/api/notifications' + + def notify(self, title, message): + + try: + data = urllib.urlencode({ + 'user_credentials': headphones.BOXCAR_TOKEN, + 'notification[title]': title.encode('utf-8'), + 'notification[long_message]': message.encode('utf-8'), + 'notification[sound]': "done" + }) + + req = urllib2.Request(self.url) + handle = urllib2.urlopen(req, data) + handle.close() + return True + + except urllib2.URLError, e: + logger.warn('Error sending Boxcar2 Notification: %s' % e) + return False diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index 8e7dccb4..b163a01e 100644 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -473,7 +473,18 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, logger.info(u"Sending Twitter notification") twitter = notifiers.TwitterNotifier() twitter.notify_download(pushmessage) - + + if headphones.OSX_NOTIFY_ENABLED: + logger.info(u"Sending OS X notification") + osx_notify = notifiers.OSX_NOTIFY() + osx_notify.notify(release['ArtistName'], release['AlbumTitle'], "Download and Postprocessing completed") + + if headphones.BOXCAR_ENABLED: + pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] + logger.info(u"Sending Boxcar2 notification") + boxcar = notifiers.BOXCAR() + boxcar.notify('Headphones processed: ' + pushmessage, "Download and Postprocessing completed") + def embedAlbumArt(artwork, downloaded_track_list): logger.info('Embedding album art') diff --git a/headphones/sab.py b/headphones/sab.py index 2bcbe2d2..52d8e3e1 100644 --- a/headphones/sab.py +++ b/headphones/sab.py @@ -119,35 +119,6 @@ def sendNZB(nzb): if sabText == "ok": logger.info(u"NZB sent to SAB successfully") - if headphones.GROWL_ENABLED and headphones.GROWL_ONSNATCH: - logger.info(u"Sending Growl notification") - growl = notifiers.GROWL() - growl.notify(nzb.name,"Download started") - if headphones.PROWL_ENABLED and headphones.PROWL_ONSNATCH: - logger.info(u"Sending Prowl notification") - prowl = notifiers.PROWL() - prowl.notify(nzb.name,"Download started") - if headphones.PUSHOVER_ENABLED and headphones.PUSHOVER_ONSNATCH: - logger.info(u"Sending Pushover notification") - prowl = notifiers.PUSHOVER() - prowl.notify(nzb.name,"Download started") - if headphones.PUSHBULLET_ENABLED and headphones.PUSHBULLET_ONSNATCH: - logger.info(u"Sending PushBullet notification") - pushbullet = notifiers.PUSHBULLET() - pushbullet.notify(nzb.name + " has been snatched!", "Download started") - if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH: - logger.info(u"Sending Twitter notification") - twitter = notifiers.TwitterNotifier() - twitter.notify_snatch(nzb.name) - if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH: - logger.debug(u"Sending NMA notification") - nma = notifiers.NMA() - nma.notify(snatched_nzb=nzb.name) - if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH: - logger.info(u"Sending Pushalot notification") - pushalot = notifiers.PUSHALOT() - pushalot.notify(nzb.name,"Download started") - return True elif sabText == "Missing authentication": logger.info(u"Incorrect username/password sent to SAB, NZB not sent") diff --git a/headphones/searcher.py b/headphones/searcher.py index d9b74dc9..f1edfb66 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -31,7 +31,7 @@ import subprocess import headphones from headphones.common import USER_AGENT from headphones import logger, db, helpers, classes, sab, nzbget, request -from headphones import transmission +from headphones import transmission, notifiers import lib.bencode as bencode @@ -359,6 +359,9 @@ def searchNZB(album, new=False, losslessOnly=False): logger.info("Album type is audiobook/spokenword. Using audiobook category") for newznab_host in newznab_hosts: + + provider = newznab_host[0] + # Add a little mod for kere.ws if newznab_host[0] == "http://kere.ws": if categories == "3040": @@ -573,14 +576,16 @@ def send_to_downloader(data, bestqual, album): nzb = classes.NZBDataSearchResult() nzb.extraInfo.append(data) nzb.name = folder_name - nzbget.sendNZB(nzb) + if not nzbget.sendNZB(nzb): + return elif headphones.NZB_DOWNLOADER == 0: nzb = classes.NZBDataSearchResult() nzb.extraInfo.append(data) nzb.name = folder_name - sab.sendNZB(nzb) + if not sab.sendNZB(nzb): + return # If we sent the file to sab, we can check how it was renamed and insert that into the snatched table (replace_spaces, replace_dots) = sab.checkConfig() @@ -695,6 +700,52 @@ def send_to_downloader(data, bestqual, album): myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']]) myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name, kind]) + # notify + artist = album[1] + album = album[2] + title = artist + ' - ' + album + provider = bestqual[3] + if provider.startswith(("http://", "https://")): + provider = provider.split("//")[1] + + name = folder_name if folder_name else None + if headphones.GROWL_ENABLED and headphones.GROWL_ONSNATCH: + logger.info(u"Sending Growl notification") + growl = notifiers.GROWL() + growl.notify(name,"Download started") + if headphones.PROWL_ENABLED and headphones.PROWL_ONSNATCH: + logger.info(u"Sending Prowl notification") + prowl = notifiers.PROWL() + prowl.notify(name,"Download started") + if headphones.PUSHOVER_ENABLED and headphones.PUSHOVER_ONSNATCH: + logger.info(u"Sending Pushover notification") + prowl = notifiers.PUSHOVER() + prowl.notify(name,"Download started") + if headphones.PUSHBULLET_ENABLED and headphones.PUSHBULLET_ONSNATCH: + logger.info(u"Sending PushBullet notification") + pushbullet = notifiers.PUSHBULLET() + pushbullet.notify(name + " has been snatched!", "Download started") + if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH: + logger.info(u"Sending Twitter notification") + twitter = notifiers.TwitterNotifier() + twitter.notify_snatch(name) + if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH: + logger.info(u"Sending NMA notification") + nma = notifiers.NMA() + nma.notify(snatched_nzb=name) + if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH: + logger.info(u"Sending Pushalot notification") + pushalot = notifiers.PUSHALOT() + pushalot.notify(name,"Download started") + if headphones.OSX_NOTIFY_ENABLED and headphones.OSX_NOTIFY_ONSNATCH: + logger.info(u"Sending OS X notification") + osx_notify = notifiers.OSX_NOTIFY() + osx_notify.notify(artist, album, 'Snatched: ' + provider + '. ' + name) + if headphones.BOXCAR_ENABLED and headphones.BOXCAR_ONSNATCH: + logger.info(u"Sending Boxcar2 notification") + boxcar = notifiers.BOXCAR() + boxcar.notify('Headphones snatched: ' + title, 'From ' + provider + '. ' + name) + def verifyresult(title, artistterm, term, lossless): title = re.sub('[\.\-\/\_]', ' ', title) @@ -862,7 +913,7 @@ def searchTorrent(album, new=False, losslessOnly=False): minimumseeders = int(headphones.NUMBEROFSEEDERS) - 1 if headphones.KAT: - provider = "Kick Ass Torrent" + provider = "Kick Ass Torrents" providerurl = url_fix("http://kickass.to/usearch/" + term) if headphones.PREFERRED_QUALITY == 3 or losslessOnly: categories = "7" #music @@ -1324,7 +1375,7 @@ def preprocess(resultlist): #Get out of here if we're using Transmission or uTorrent if headphones.TORRENT_DOWNLOADER != 0: return True, result - # get outta here if rutracker or piratebay + # get outta here if rutracker if result[3] == 'rutracker.org': return True, result # Get out of here if it's a magnet link @@ -1334,7 +1385,7 @@ def preprocess(resultlist): # Download the torrent file headers = {} - if result[3] == 'Kick Ass Torrent': + if result[3] == 'Kick Ass Torrents': headers['Referer'] = 'http://kat.ph/' elif result[3] == 'What.cd': headers['User-Agent'] = 'Headphones' diff --git a/headphones/transmission.py b/headphones/transmission.py index 012153ef..e2e321b3 100644 --- a/headphones/transmission.py +++ b/headphones/transmission.py @@ -49,31 +49,6 @@ def addTorrent(link): retid = False logger.info(u"Torrent sent to Transmission successfully") - if headphones.GROWL_ENABLED and headphones.GROWL_ONSNATCH: - logger.info(u"Sending Growl notification") - growl = notifiers.GROWL() - growl.notify(name,"Download started") - if headphones.PROWL_ENABLED and headphones.PROWL_ONSNATCH: - logger.info(u"Sending Prowl notification") - prowl = notifiers.PROWL() - prowl.notify(name,"Download started") - if headphones.PUSHOVER_ENABLED and headphones.PUSHOVER_ONSNATCH: - logger.info(u"Sending Pushover notification") - pushover = notifiers.PUSHOVER() - pushover.notify(name,"Download started") - if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH: - logger.info(u"Sending Twitter notification") - twitter = notifiers.TwitterNotifier() - twitter.notify_snatch(nzb.name) - if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH: - logger.info(u"Sending NMA notification") - nma = notifiers.NMA() - nma.notify(snatched_nzb=name) - if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH: - logger.info(u"Sending Pushalot notification") - pushalot = notifiers.PUSHALOT() - pushalot.notify(name,"Download started") - return retid else: diff --git a/headphones/webserve.py b/headphones/webserve.py index 497ecd7b..a11ceac9 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1059,6 +1059,12 @@ class WebInterface(object): "pushbullet_deviceid": headphones.PUSHBULLET_DEVICEID, "twitter_enabled": checked(headphones.TWITTER_ENABLED), "twitter_onsnatch": checked(headphones.TWITTER_ONSNATCH), + "osx_notify_enabled": checked(headphones.OSX_NOTIFY_ENABLED), + "osx_notify_onsnatch": checked(headphones.OSX_NOTIFY_ONSNATCH), + "osx_notify_app": headphones.OSX_NOTIFY_APP, + "boxcar_enabled": checked(headphones.BOXCAR_ENABLED), + "boxcar_onsnatch": checked(headphones.BOXCAR_ONSNATCH), + "boxcar_token": headphones.BOXCAR_TOKEN, "mirror_list": headphones.MIRRORLIST, "mirror": headphones.MIRROR, "customhost": headphones.CUSTOMHOST, @@ -1105,10 +1111,11 @@ class WebInterface(object): bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0, delete_lossless_files=0, growl_enabled=0, growl_onsnatch=0, growl_host=None, growl_password=None, prowl_enabled=0, prowl_onsnatch=0, prowl_keys=None, prowl_priority=0, xbmc_enabled=0, xbmc_host=None, xbmc_username=None, xbmc_password=None, xbmc_update=0, xbmc_notify=0, nma_enabled=False, nma_apikey=None, nma_priority=0, nma_onsnatch=0, pushalot_enabled=False, pushalot_apikey=None, pushalot_onsnatch=0, synoindex_enabled=False, lms_enabled=0, lms_host=None, - pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, pushover_apitoken=None, pushbullet_enabled=0, pushbullet_onsnatch=0, pushbullet_apikey=None, pushbullet_deviceid=None, twitter_enabled=0, twitter_onsnatch=0, mirror=None, customhost=None, customport=None, - customsleep=None, hpuser=None, hppass=None, preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, cache_sizemb=None, - enable_https=0, https_cert=None, https_key=None, file_permissions=None, folder_permissions=None, plex_enabled=0, plex_server_host=None, plex_client_host=None, plex_username=None, - plex_password=None, plex_update=0, plex_notify=0, songkick_enabled=0, songkick_apikey=None, songkick_location=None, songkick_filter_enabled=0, encoder_multicore=False, encoder_multicore_count=0, **kwargs): + pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, pushover_apitoken=None, pushbullet_enabled=0, pushbullet_onsnatch=0, pushbullet_apikey=None, pushbullet_deviceid=None, twitter_enabled=0, twitter_onsnatch=0, + osx_notify_enabled=0, osx_notify_onsnatch=0, osx_notify_app=None, boxcar_enabled=0, boxcar_onsnatch=0, boxcar_token=None, mirror=None, customhost=None, customport=None, customsleep=None, hpuser=None, hppass=None, + preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, cache_sizemb=None, enable_https=0, https_cert=None, https_key=None, file_permissions=None, folder_permissions=None, + plex_enabled=0, plex_server_host=None, plex_client_host=None, plex_username=None, plex_password=None, plex_update=0, plex_notify=0, + songkick_enabled=0, songkick_apikey=None, songkick_location=None, songkick_filter_enabled=0, encoder_multicore=False, encoder_multicore_count=0, **kwargs): headphones.HTTP_HOST = http_host headphones.HTTP_PORT = http_port @@ -1268,6 +1275,15 @@ class WebInterface(object): headphones.SONGKICK_FILTER_ENABLED = songkick_filter_enabled headphones.TWITTER_ENABLED = twitter_enabled headphones.TWITTER_ONSNATCH = twitter_onsnatch + + headphones.OSX_NOTIFY_ENABLED = osx_notify_enabled + headphones.OSX_NOTIFY_ONSNATCH = osx_notify_onsnatch + headphones.OSX_NOTIFY_APP = osx_notify_app + + headphones.BOXCAR_ENABLED = boxcar_enabled + headphones.BOXCAR_ONSNATCH = boxcar_onsnatch + headphones.BOXCAR_TOKEN = boxcar_token + headphones.MIRROR = mirror headphones.CUSTOMHOST = customhost headphones.CUSTOMPORT = customport @@ -1435,6 +1451,19 @@ class WebInterface(object): return "Error sending tweet" testTwitter.exposed = True + def osxnotifyregister(self, app): + cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" + from lib.osxnotify import registerapp as osxnotify + result, msg = osxnotify.registerapp(app) + if result: + osx_notify = notifiers.OSX_NOTIFY() + osx_notify.notify('Registered', result, 'Success :-)') + logger.info('Registered %s, to re-register a different app, delete this app first' % result) + else: + logger.warn(msg) + return msg + osxnotifyregister.exposed = True + class Artwork(object): def index(self): return "Artwork" diff --git a/lib/osxnotify/__init__.py b/lib/osxnotify/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/lib/osxnotify/appIcon.icns b/lib/osxnotify/appIcon.icns new file mode 100755 index 00000000..caf5b293 Binary files /dev/null and b/lib/osxnotify/appIcon.icns differ diff --git a/lib/osxnotify/registerapp.py b/lib/osxnotify/registerapp.py new file mode 100644 index 00000000..25a1bb7f --- /dev/null +++ b/lib/osxnotify/registerapp.py @@ -0,0 +1,135 @@ +#!/usr/bin/python + +import shutil +import os +import stat +import platform +import subprocess + +def registerapp(app): + + # don't do any of this unless >= 10.8 + v, _, _ = platform.mac_ver() + v = float('.'.join(v.split('.')[:2])) + if v < 10.8: + return None, 'Registering requires OS X version >= 10.8' + + app_path = None + + # check app bundle doesn't already exist + app_path = subprocess.check_output(['/usr/bin/mdfind', 'kMDItemCFBundleIdentifier == "ade.headphones.osxnotify"']).strip() + if app_path: + return app_path, 'App previously registered' + + # check app doesn't already exist + app = app.strip() + if not app: + return None, 'Path/Application not entered' + if os.path.splitext(app)[1] == ".app": + app_path = app + else: + app_path = app + '.app' + if os.path.exists(app_path): + return None, 'App %s already exists, choose a different name' % app_path + + # generate app + try: + os.mkdir(app_path) + os.mkdir(app_path + "/Contents") + os.mkdir(app_path + "/Contents/MacOS") + os.mkdir(app_path + "/Contents/Resources") + shutil.copy(os.path.join(os.path.dirname(__file__), "appIcon.icns"), app_path + "/Contents/Resources/") + + version = "1.0.0" + bundleName = "OSXNotify" + bundleIdentifier = "ade.headphones.osxnotify" + + f = open(app_path + "/Contents/Info.plist", "w") + f.write(""" + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + main.py + CFBundleGetInfoString + %s + CFBundleIconFile + appIcon.icns + CFBundleIdentifier + %s + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + %s + CFBundlePackageType + APPL + CFBundleShortVersionString + %s + CFBundleSignature + ???? + CFBundleVersion + %s + NSAppleScriptEnabled + YES + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + +""" % (bundleName + " " + version, bundleIdentifier, bundleName, bundleName + " " + version, version)) + f.close() + + f = open(app_path + "/Contents/PkgInfo", "w") + f.write("APPL????") + f.close() + + f = open(app_path + "/Contents/MacOS/main.py", "w") + f.write("""#!/usr/bin/python + +objc = None + +def swizzle(cls, SEL, func): + old_IMP = cls.instanceMethodForSelector_(SEL) + def wrapper(self, *args, **kwargs): + return func(self, old_IMP, *args, **kwargs) + new_IMP = objc.selector(wrapper, selector=old_IMP.selector, + signature=old_IMP.signature) + objc.classAddMethod(cls, SEL, new_IMP) + +def notify(title, subtitle=None, text=None, sound=True): + global objc + objc = __import__("objc") + swizzle(objc.lookUpClass('NSBundle'), + b'bundleIdentifier', + swizzled_bundleIdentifier) + NSUserNotification = objc.lookUpClass('NSUserNotification') + NSUserNotificationCenter = objc.lookUpClass('NSUserNotificationCenter') + NSAutoreleasePool = objc.lookUpClass('NSAutoreleasePool') + pool = NSAutoreleasePool.alloc().init() + notification = NSUserNotification.alloc().init() + notification.setTitle_(title) + notification.setSubtitle_(subtitle) + notification.setInformativeText_(text) + notification.setSoundName_("NSUserNotificationDefaultSoundName") + notification_center = NSUserNotificationCenter.defaultUserNotificationCenter() + notification_center.deliverNotification_(notification) + del pool + +def swizzled_bundleIdentifier(self, original): + return 'ade.headphones.osxnotify' + +if __name__ == '__main__': + notify('Half Man Half Biscuit', 'Back in the DHSS', '99% Of Gargoyles Look Like Bob Todd') +""") + f.close() + + oldmode = os.stat(app_path + "/Contents/MacOS/main.py").st_mode + os.chmod(app_path + "/Contents/MacOS/main.py", oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + return app_path, 'App registered' + + except Exception, e: + return None, 'Error creating App %s. %s' % (app_path, e) \ No newline at end of file