def process(opcode, data): from plexpy import activity_handler if opcode not in opcode_data: return False try: info = json.loads(data) except Exception as ex: logger.warn(u'PlexPy WebSocket :: Error decoding message from websocket: %s' % ex) logger.debug(data) return False type = info.get('type') if not type: return False if type == 'playing': # logger.debug('%s.playing %s' % (name, info)) try: time_line = info.get('_children') except: logger.debug(u"PlexPy WebSocket :: Session found but unable to get timeline data.") return False activity = activity_handler.ActivityHandler(timeline=time_line[0]) activity.process() return True
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 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) url = "%s/library/sections" % host try: xml_sections = minidom.parse(urllib.urlopen(url)) except IOError, e: logger.warn("Error while trying to contact Plex Media Server: %s" % e) return False sections = xml_sections.getElementsByTagName('Directory') if not sections: logger.info(u"Plex Media Server not running on: " + host) return False for s in sections: if s.getAttribute('type') == "artist": url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key')) try: urllib.urlopen(url) except Exception as e: logger.warn("Error updating library section for Plex Media Server: %s" % e) return False
def get_current_activity(self): session_data = self.get_sessions(output_format="xml") try: xml_head = session_data.getElementsByTagName("MediaContainer") except: logger.warn("Unable to parse XML for get_sessions.") return [] session_list = [] for a in xml_head: if a.getAttribute("size"): if a.getAttribute("size") == "0": session_list = {"stream_count": "0", "sessions": []} return session_list if a.getElementsByTagName("Track"): session_data = a.getElementsByTagName("Track") session_type = "track" for session in session_data: session_output = self.get_session_each(session_type, session) session_list.append(session_output) if a.getElementsByTagName("Video"): session_data = a.getElementsByTagName("Video") session_type = "video" for session in session_data: session_output = self.get_session_each(session_type, session) session_list.append(session_output) output = {"stream_count": helpers.get_xml_attr(xml_head[0], "size"), "sessions": session_list} return output
def refresh_users(): logger.info(u"PlexPy PlexTV :: Requesting users list refresh...") result = PlexTV().get_full_users_list() monitor_db = database.MonitorDatabase() if len(result) > 0: for item in result: control_value_dict = {"user_id": item['user_id']} new_value_dict = {"username": item['username'], "thumb": item['thumb'], "email": item['email'], "is_home_user": item['is_home_user'], "is_allow_sync": item['is_allow_sync'], "is_restricted": item['is_restricted'] } # Check if we've set a custom avatar if so don't overwrite it. if item['user_id']: avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url ' 'FROM users WHERE user_id = ?', [item['user_id']]) if avatar_urls: if not avatar_urls[0]['custom_avatar_url'] or \ avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']: new_value_dict['custom_avatar_url'] = item['thumb'] else: new_value_dict['custom_avatar_url'] = item['thumb'] monitor_db.upsert('users', new_value_dict, control_value_dict) logger.info(u"PlexPy PlexTV :: Users list refreshed.") else: logger.warn(u"PlexPy PlexTV :: Unable to refresh users list.")
def get_user_list(self, kwargs=None): data_tables = datatables.DataTables() columns = ['session_history.id', 'users.thumb as thumb', '(case when users.friendly_name is null then session_history.user else \ users.friendly_name end) as friendly_name', 'session_history.started', 'session_history.ip_address', 'COUNT(session_history.id) as plays', 'session_history.user', 'session_history.user_id', '(case when typeof(session_history.user_id) = "integer" \ then session_history.user_id else -1 end) as grp_id', ] try: query = data_tables.ssp_query(table_name='session_history', columns=columns, custom_where=[], group_by=['grp_id'], join_types=['LEFT OUTER JOIN'], join_tables=['users'], join_evals=[['grp_id', 'users.user_id']], kwargs=kwargs) except: logger.warn("Unable to execute database query.") return {'recordsFiltered': 0, 'recordsTotal': 0, 'draw': 0, 'data': 'null', 'error': 'Unable to execute database query.'} users = query['result'] rows = [] for item in users: if not item['thumb'] or item['thumb'] == '': user_thumb = common.DEFAULT_USER_THUMB else: user_thumb = item['thumb'] row = {"id": item['id'], "plays": item['plays'], "started": item['started'], "friendly_name": item["friendly_name"], "ip_address": item["ip_address"], "thumb": user_thumb, "user": item["user"], "user_id": item['user_id'] } rows.append(row) dict = {'recordsFiltered': query['filteredCount'], 'recordsTotal': query['totalCount'], 'data': rows, 'draw': query['draw'] } return dict
def shutdown(restart=False, update=False): cherrypy.engine.exit() SCHED.shutdown(wait=False) CONFIG.write() if not restart and not update: logger.info('PlexPy is shutting down...') if update: logger.info('PlexPy is updating...') try: versioncheck.update() except Exception as e: logger.warn('PlexPy failed to update: %s. Restarting.', e) if CREATEPID: logger.info('Removing pidfile %s', PIDFILE) os.remove(PIDFILE) if restart: logger.info('PlexPy is restarting...') popen_list = [sys.executable, FULL_PATH] popen_list += ARGS if '--nolaunch' not in popen_list: popen_list += ['--nolaunch'] logger.info('Restarting PlexPy with %s', popen_list) subprocess.Popen(popen_list, cwd=os.getcwd()) os._exit(0)
def notify(self, subject, message): if not subject or not message: return message = MIMEText(message, 'plain', "utf-8") message['Subject'] = subject message['From'] = email.utils.formataddr(('PlexPy', plexpy.CONFIG.EMAIL_FROM)) message['To'] = plexpy.CONFIG.EMAIL_TO try: mailserver = smtplib.SMTP(plexpy.CONFIG.EMAIL_SMTP_SERVER, plexpy.CONFIG.EMAIL_SMTP_PORT) if (plexpy.CONFIG.EMAIL_TLS): mailserver.starttls() mailserver.ehlo() if plexpy.CONFIG.EMAIL_SMTP_USER: mailserver.login(plexpy.CONFIG.EMAIL_SMTP_USER, plexpy.CONFIG.EMAIL_SMTP_PASSWORD) mailserver.sendmail(plexpy.CONFIG.EMAIL_FROM, plexpy.CONFIG.EMAIL_TO, message.as_string()) mailserver.quit() return True except Exception, e: logger.warn('Error sending Email: %s' % e) return False
def discover(self): """ Query plex for all servers online. Returns the ones you own in a selectize format """ result = self.get_plextv_resources(include_https=True, output_format='raw') servers = xmltodict.parse(result, process_namespaces=True, attr_prefix='') clean_servers = [] try: if servers: # Fix if its only one "device" if int(servers['MediaContainer']['size']) == 1: servers['MediaContainer']['Device'] = [servers['MediaContainer']['Device']] for server in servers['MediaContainer']['Device']: # Only grab servers online and own if server.get('presence', None) == '1' and server.get('owned', None) == '1' and server.get('provides', None) == 'server': # If someone only has one connection.. if isinstance(server['Connection'], dict): server['Connection'] = [server['Connection']] for s in server['Connection']: # to avoid circular ref d = {} d.update(s) d.update(server) d['label'] = d['name'] d['value'] = d['address'] del d['Connection'] clean_servers.append(d) except Exception as e: logger.warn('Failed to get servers from plex %s' % e) return clean_servers return json.dumps(clean_servers, indent=4)
def get_user_stats(self, section_id=None): monitor_db = database.MonitorDatabase() user_stats = [] try: if str(section_id).isdigit(): query = 'SELECT (CASE WHEN users.friendly_name IS NULL THEN users.username ' \ 'ELSE users.friendly_name END) AS user, users.user_id, users.thumb, COUNT(user) AS user_count ' \ 'FROM session_history ' \ 'JOIN session_history_metadata ON session_history_metadata.id = session_history.id ' \ 'JOIN users ON users.user_id = session_history.user_id ' \ 'WHERE section_id = ? ' \ 'GROUP BY users.user_id ' \ 'ORDER BY user_count DESC' result = monitor_db.select(query, args=[section_id]) else: result = [] except Exception as e: logger.warn(u"PlexPy Libraries :: Unable to execute database query for get_user_stats: %s." % e) result = [] for item in result: row = {'user': item['user'], 'user_id': item['user_id'], 'thumb': item['thumb'], 'total_plays': item['user_count'] } user_stats.append(row) return user_stats
def delete_all_history(self, user_id=None): monitor_db = database.MonitorDatabase() try: if str(user_id).isdigit(): logger.info(u"PlexPy DataFactory :: Deleting all history for user id %s from database." % user_id) session_history_media_info_del = \ monitor_db.action('DELETE FROM ' 'session_history_media_info ' 'WHERE session_history_media_info.id IN (SELECT session_history_media_info.id ' 'FROM session_history_media_info ' 'JOIN session_history ON session_history_media_info.id = session_history.id ' 'WHERE session_history.user_id = ?)', [user_id]) session_history_metadata_del = \ monitor_db.action('DELETE FROM ' 'session_history_metadata ' 'WHERE session_history_metadata.id IN (SELECT session_history_metadata.id ' 'FROM session_history_metadata ' 'JOIN session_history ON session_history_metadata.id = session_history.id ' 'WHERE session_history.user_id = ?)', [user_id]) session_history_del = \ monitor_db.action('DELETE FROM ' 'session_history ' 'WHERE session_history.user_id = ?', [user_id]) return 'Deleted all items for user_id %s.' % user_id else: return 'Unable to delete items. Input user_id not valid.' except Exception as e: logger.warn(u"PlexPy Users :: Unable to execute database query for delete_all_history: %s." % e)
def check_server_response(): with monitor_lock: pms_connect = pmsconnect.PmsConnect() server_response = pms_connect.get_server_response() global ext_ping_count # Check for remote access if server_response: mapping_state = server_response['mapping_state'] mapping_error = server_response['mapping_error'] # Check if the port is mapped if not mapping_state == 'mapped': ext_ping_count += 1 logger.warn(u"PlexPy Monitor :: Plex remote access port not mapped, ping attempt %s." \ % str(ext_ping_count)) # Check if the port is open elif mapping_error == 'unreachable': ext_ping_count += 1 logger.warn(u"PlexPy Monitor :: Plex remote access port mapped, but mapping failed, ping attempt %s." \ % str(ext_ping_count)) # Reset external ping counter else: ext_ping_count = 0 if ext_ping_count == 3: # Fire off notifications threading.Thread(target=notification_handler.notify_timeline, kwargs=dict(notify_action='extdown')).start()
def delete_all_history(self, section_id=None): monitor_db = database.MonitorDatabase() try: if section_id.isdigit(): logger.info(u"PlexPy Libraries :: Deleting all history for library id %s from database." % section_id) session_history_media_info_del = \ monitor_db.action('DELETE FROM ' 'session_history_media_info ' 'WHERE session_history_media_info.id IN (SELECT session_history_media_info.id ' 'FROM session_history_media_info ' 'JOIN session_history_metadata ON session_history_media_info.id = session_history_metadata.id ' 'WHERE session_history_metadata.section_id = ?)', [section_id]) session_history_del = \ monitor_db.action('DELETE FROM ' 'session_history ' 'WHERE session_history.id IN (SELECT session_history.id ' 'FROM session_history ' 'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' 'WHERE session_history_metadata.section_id = ?)', [section_id]) session_history_metadata_del = \ monitor_db.action('DELETE FROM ' 'session_history_metadata ' 'WHERE session_history_metadata.section_id = ?', [section_id]) return 'Deleted all items for section_id %s.' % section_id else: return 'Unable to delete items, section_id not valid.' except Exception as e: logger.warn(u"PlexPy Libraries :: Unable to execute database query for delete_all_history: %s." % e)
def shutdown(restart=False, update=False): cherrypy.engine.exit() SCHED.shutdown(wait=False) CONFIG.write() if not restart and not update: logger.info('PlexPy is shutting down...') if update: logger.info('PlexPy is updating...') try: versioncheck.update() except Exception as e: logger.warn('PlexPy failed to update: %s. Restarting.', e) if CREATEPID: logger.info('Removing pidfile %s', PIDFILE) os.remove(PIDFILE) if restart: logger.info('PlexPy is restarting...') exe = sys.executable args = [exe, FULL_PATH] args += ARGS if '--nolaunch' not in args: args += ['--nolaunch'] logger.info('Restarting PlexPy with %s', args) os.execv(exe, args) os._exit(0)
def action(self, query, args=None, return_last_id=False): if query is None: return with db_lock: sql_result = None attempts = 0 while attempts < 5: try: with self.connection as c: if args is None: sql_result = c.execute(query) else: sql_result = c.execute(query, args) # Our transaction was successful, leave the loop break except sqlite3.OperationalError, e: if "unable to open database file" in e.message or "database is locked" in e.message: logger.warn('Database Error: %s', e) attempts += 1 time.sleep(1) else: logger.error('Database error: %s', e) raise except sqlite3.DatabaseError, e: logger.error('Fatal Error executing %s :: %s', query, e) raise
def get_season_children(self, rating_key=''): episode_data = self.get_episode_list(rating_key, output_format='xml') episode_list = [] xml_head = episode_data.getElementsByTagName('MediaContainer') if not xml_head: logger.warn("Error parsing XML for Plex session data.") return None for a in xml_head: if a.getAttribute('size'): if a.getAttribute('size') == '0': logger.debug(u"No episode data.") episode_list = {'episode_count': '0', 'episode_list': [] } return episode_list if a.getElementsByTagName('Video'): result_data = a.getElementsByTagName('Video') for result in result_data: episode_output = {'rating_key': helpers.get_xml_attr(result, 'ratingKey'), 'index': helpers.get_xml_attr(result, 'index'), 'title': helpers.get_xml_attr(result, 'title'), 'thumb': helpers.get_xml_attr(result, 'thumb') } episode_list.append(episode_output) output = {'episode_count': helpers.get_xml_attr(xml_head[0], 'size'), 'title': helpers.get_xml_attr(xml_head[0], 'title2'), 'episode_list': episode_list } return output
def shutdown(restart=False, update=False): cherrypy.engine.exit() SCHED.shutdown(wait=False) # Clear any sessions in the db - Not sure yet if we should do this. More testing required # logger.debug(u'Clearing Plex sessions.') # monitor.drop_session_db() CONFIG.write() if not restart and not update: logger.info('PlexPy is shutting down...') if update: logger.info('PlexPy is updating...') try: versioncheck.update() except Exception as e: logger.warn('PlexPy failed to update: %s. Restarting.', e) if CREATEPID: logger.info('Removing pidfile %s', PIDFILE) os.remove(PIDFILE) if restart: logger.info('PlexPy is restarting...') popen_list = [sys.executable, FULL_PATH] popen_list += ARGS if '--nolaunch' not in popen_list: popen_list += ['--nolaunch'] logger.info('Restarting PlexPy with %s', popen_list) subprocess.Popen(popen_list, cwd=os.getcwd()) os._exit(0)
def _sendhttp(self, host, command): 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 as e: logger.warn('Error opening Plex url: %s' % e) return response = handle.read().decode(plexpy.SYS_ENCODING) return response
def get_current_activity(self): session_data = self.get_sessions(output_format='xml') session_list = [] xml_head = session_data.getElementsByTagName('MediaContainer') if not xml_head: logger.warn("Error parsing XML for Plex session data.") return None for a in xml_head: if a.getAttribute('size'): if a.getAttribute('size') == '0': session_list = {'stream_count': '0', 'sessions': [] } return session_list if a.getElementsByTagName('Track'): session_data = a.getElementsByTagName('Track') session_type = 'track' for session in session_data: session_output = self.get_session_each(session_type, session) session_list.append(session_output) if a.getElementsByTagName('Video'): session_data = a.getElementsByTagName('Video') session_type = 'video' for session in session_data: session_output = self.get_session_each(session_type, session) session_list.append(session_output) output = {'stream_count': helpers.get_xml_attr(xml_head[0], 'size'), 'sessions': session_list } return output
def uploadToImgur(imgPath, imgTitle=''): from plexpy import logger client_id = '743b1a443ccd2b0' img_url = '' try: with open(imgPath, 'rb') as imgFile: img = imgFile.read() except IOError as e: logger.error(u"PlexPy Helpers :: Unable to read image file for Imgur: %s" % e) return img_url headers = {'Authorization': 'Client-ID %s' % client_id} data = {'type': 'base64', 'image': base64.b64encode(img)} if imgTitle: data['title'] = imgTitle data['name'] = imgTitle + '.jpg' request = urllib2.Request('https://api.imgur.com/3/image', headers=headers, data=urllib.urlencode(data)) response = urllib2.urlopen(request) response = json.loads(response.read()) if response.get('status') == 200: logger.debug(u"PlexPy Helpers :: Image uploaded to Imgur.") img_url = response.get('data').get('link', '') elif response.get('status') >= 400 and response.get('status') < 500: logger.warn(u"PlexPy Helpers :: Unable to upload image to Imgur: %s" % response.reason) else: logger.warn(u"PlexPy Helpers :: Unable to upload image to Imgur.") return img_url
def notify(self, subject, message): message = MIMEText(message, "plain", "utf-8") message["Subject"] = subject message["From"] = email.utils.formataddr(("PlexPy", plexpy.CONFIG.EMAIL_FROM)) message["To"] = plexpy.CONFIG.EMAIL_TO try: mailserver = smtplib.SMTP(plexpy.CONFIG.EMAIL_SMTP_SERVER, plexpy.CONFIG.EMAIL_SMTP_PORT) if plexpy.CONFIG.EMAIL_TLS: mailserver.starttls() mailserver.ehlo() if plexpy.CONFIG.EMAIL_SMTP_USER: mailserver.login(plexpy.CONFIG.EMAIL_SMTP_USER, plexpy.CONFIG.EMAIL_SMTP_PASSWORD) mailserver.sendmail(plexpy.CONFIG.EMAIL_FROM, plexpy.CONFIG.EMAIL_TO, message.as_string()) mailserver.quit() return True except Exception, e: logger.warn("Error sending Email: %s" % e) return False
def get_player_stats(self, user_id=None): monitor_db = database.MonitorDatabase() player_stats = [] result_id = 0 try: if str(user_id).isdigit(): query = 'SELECT player, COUNT(player) as player_count, platform ' \ 'FROM session_history ' \ 'WHERE user_id = ? ' \ 'GROUP BY player ' \ 'ORDER BY player_count DESC' result = monitor_db.select(query, args=[user_id]) else: result = [] except Exception as e: logger.warn(u"PlexPy Users :: Unable to execute database query for get_player_stats: %s." % e) result = [] for item in result: # Rename Mystery platform names platform_type = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform']) row = {'player_name': item['player'], 'platform_type': platform_type, 'total_plays': item['player_count'], 'result_id': result_id } player_stats.append(row) result_id += 1 return player_stats
def get_user_platform_stats(self, user=None, user_id=None): monitor_db = database.MonitorDatabase() platform_stats = [] result_id = 0 try: if user_id: query = 'SELECT player, COUNT(player) as player_count, platform ' \ 'FROM session_history ' \ 'WHERE user_id = ? ' \ 'GROUP BY player ' \ 'ORDER BY player_count DESC' result = monitor_db.select(query, args=[user_id]) else: query = 'SELECT player, COUNT(player) as player_count, platform ' \ 'FROM session_history ' \ 'WHERE user = ? ' \ 'GROUP BY player ' \ 'ORDER BY player_count DESC' result = monitor_db.select(query, args=[user]) except: logger.warn("Unable to execute database query.") return None for item in result: row = {'platform_name': item[0], 'platform_type': item[2], 'total_plays': item[1], 'result_id': result_id } platform_stats.append(row) result_id += 1 return platform_stats
def user(self, user=None): try: data_factory = datafactory.DataFactory() user_details = data_factory.get_user_details(user=user) except: logger.warn("Unable to retrieve friendly name for user %s " % user) return serve_template(templatename="user.html", title="User", data=user_details)
def user(self, user=None): try: plex_watch = plexwatch.PlexWatch() user_details = plex_watch.get_user_details(user) except: logger.warn("Unable to retrieve friendly name for user %s " % user) return serve_template(templatename="user.html", title="User", data=user_details)
def get_current_activity(self): session_data = self.get_sessions() session_list = [] try: xml_parse = minidom.parseString(session_data) except Exception, e: logger.warn("Error parsing XML for Plex session data: %s" % e) return None
def get_season_children(self, rating_key=''): episode_data = self.get_episode_list(rating_key) episode_list = [] try: xml_parse = minidom.parseString(episode_data) except Exception, e: logger.warn("Error parsing XML for Plex session data: %s" % e) return None
def get_server_pref(self, pref=None, **kwargs): pms_connect = pmsconnect.PmsConnect() result = pms_connect.get_server_pref(pref=pref) if result: return result else: logger.warn("Unable to retrieve data.")
def get_metadata_details(self, rating_key=''): metadata = self.get_metadata(rating_key) metadata_list = [] try: xml_parse = minidom.parseString(metadata) except Exception, e: logger.warn("Error parsing XML for Plex metadata: %s" % e) return None
def get_plex_log(self, window=1000, **kwargs): log_lines = [] try: log_lines = {"data": log_reader.get_log_tail(window=window)} except: logger.warn("Unable to retrieve Plex Logs.") cherrypy.response.headers["Content-type"] = "application/json" return json.dumps(log_lines)
def get_recently_added_details(self, count='0'): recent = self.get_recently_added(count) recents_list = [] try: xml_parse = minidom.parseString(recent) except Exception, e: logger.warn("Error parsing XML for Plex recently added: %s" % e) return None
def delete_all_history(self): monitor_db = database.MonitorDatabase() try: logger.info( u"Tautulli Servers :: %s: Deleting all history from database." % self.CONFIG.PMS_NAME) query = 'SELECT session_key FROM sessions WHERE server_id = ?' result = monitor_db.select(query, [self.CONFIG.ID]) ap = activity_processor.ActivityProcessor(server=self) for item in result: ap.delete_session(session_key=item['session_key']) activity_handler.delete_metadata_cache( session_key=item['session_key'], server=self) sessions_del = \ monitor_db.action('DELETE FROM ' 'sessions ' 'WHERE server_id = ?', [self.CONFIG.ID]) session_history_media_info_del = \ monitor_db.action('DELETE FROM ' 'session_history_media_info ' 'WHERE server_id = ?', [self.CONFIG.ID]) session_history_metadata_del = \ monitor_db.action('DELETE FROM ' 'session_history_metadata ' 'WHERE server_id = ?', [self.CONFIG.ID]) session_history_del = \ monitor_db.action('DELETE FROM ' 'session_history ' 'WHERE server_id = ?', [self.CONFIG.ID]) recently_added_del = \ monitor_db.action('DELETE FROM ' 'recently_added ' 'WHERE server_id = ?', [self.CONFIG.ID]) themoviedb_lookup_del = \ monitor_db.action('DELETE FROM ' 'themoviedb_lookup ' 'WHERE server_id = ?', [self.CONFIG.ID]) tvmaze_lookup_del = \ monitor_db.action('DELETE FROM ' 'tvmaze_lookup ' 'WHERE server_id = ?', [self.CONFIG.ID]) return True except Exception as e: logger.warn( u"Tautulli Servers :: %s: Unable to execute database query for delete_all_history: %s." % (self.CONFIG.PMS_NAME, e)) return False
def delete_sessions(): logger.debug( u"Tautulli Database :: Clearing temporary sessions from database.") monitor_db = MonitorDatabase() try: monitor_db.action('DELETE FROM sessions') monitor_db.action('VACUUM') return True except Exception as e: logger.warn( u"Tautulli Database :: Unable to clear temporary sessions from database: %s." % e) return False
def get_token(self): plextv_response = self.get_plex_auth(output_format='xml') if plextv_response: xml_head = plextv_response.getElementsByTagName('user') if not xml_head: logger.warn("Error parsing XML for Plex.tv token: %s" % e) return [] auth_token = xml_head[0].getAttribute('authenticationToken') return auth_token else: return []
def delete_login_log(self): monitor_db = database.MonitorDatabase() try: logger.info( u"Tautulli Users :: Clearing login logs from database.") monitor_db.action('DELETE FROM user_login') monitor_db.action('VACUUM') return True except Exception as e: logger.warn( u"Tautulli Users :: Unable to execute database query for delete_login_log: %s." % e) return False
def set_last_seen(device_token=None): db = database.MonitorDatabase() last_seen = int(time.time()) try: result = db.action( 'UPDATE mobile_devices SET last_seen = ? WHERE device_token = ?', args=[last_seen, device_token]) except Exception as e: logger.warn( u"Tautulli MobileApp :: Failed to set last_seen time for device: %s." % e) return
def get_user_unique_ips(self, kwargs=None, custom_where=None): data_tables = datatables.DataTables() columns = ['session_history.started as last_seen', 'session_history.ip_address as ip_address', 'COUNT(session_history.id) as play_count', 'session_history.player as platform', 'session_history_metadata.full_title as last_watched', 'session_history.user as user', 'session_history.user_id as user_id' ] try: query = data_tables.ssp_query(table_name='session_history', columns=columns, custom_where=custom_where, group_by=['ip_address'], join_types=['JOIN'], join_tables=['session_history_metadata'], join_evals=[['session_history.id', 'session_history_metadata.id']], kwargs=kwargs) except: logger.warn("Unable to execute database query.") return {'recordsFiltered': 0, 'recordsTotal': 0, 'draw': 0, 'data': 'null', 'error': 'Unable to execute database query.'} results = query['result'] rows = [] for item in results: row = {"last_seen": item['last_seen'], "ip_address": item['ip_address'], "play_count": item['play_count'], "platform": item['platform'], "last_watched": item['last_watched'] } rows.append(row) dict = {'recordsFiltered': query['filteredCount'], 'recordsTotal': query['totalCount'], 'data': rows, 'draw': query['draw'] } return dict
def get_user_details(user_id=user_id, user=user): monitor_db = database.MonitorDatabase() try: if str(user_id).isdigit(): query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \ 'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history ' \ 'FROM users ' \ 'WHERE user_id = ? ' result = monitor_db.select(query, args=[user_id]) elif user: query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \ 'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history ' \ 'FROM users ' \ 'WHERE username = ? ' result = monitor_db.select(query, args=[user]) else: result = [] except Exception as e: logger.warn(u"PlexPy Users :: Unable to execute database query for get_details: %s." % e) result = [] user_details = {} if result: for item in result: if item['friendly_name']: friendly_name = item['friendly_name'] else: friendly_name = item['username'] if item['custom_thumb'] and item['custom_thumb'] != item['user_thumb']: user_thumb = item['custom_thumb'] elif item['user_thumb']: user_thumb = item['user_thumb'] else: user_thumb = common.DEFAULT_USER_THUMB user_details = {'user_id': item['user_id'], 'username': item['username'], 'friendly_name': friendly_name, 'user_thumb': user_thumb, 'email': item['email'], 'is_home_user': item['is_home_user'], 'is_allow_sync': item['is_allow_sync'], 'is_restricted': item['is_restricted'], 'do_notify': item['do_notify'], 'keep_history': item['keep_history'] } return user_details
def get_watch_time_stats(self, user_id=None): monitor_db = database.MonitorDatabase() time_queries = [1, 7, 30, 0] user_watch_time_stats = [] for days in time_queries: try: if days > 0: if str(user_id).isdigit(): query = 'SELECT (SUM(stopped - started) - ' \ ' SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \ 'COUNT(id) AS total_plays ' \ 'FROM session_history ' \ 'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \ 'AND user_id = ?' % days result = monitor_db.select(query, args=[user_id]) else: result = [] else: if str(user_id).isdigit(): query = 'SELECT (SUM(stopped - started) - ' \ ' SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \ 'COUNT(id) AS total_plays ' \ 'FROM session_history ' \ 'WHERE user_id = ?' result = monitor_db.select(query, args=[user_id]) else: result = [] except Exception as e: logger.warn(u"PlexPy Users :: Unable to execute database query for get_watch_time_stats: %s." % e) result = [] for item in result: if item['total_time']: total_time = item['total_time'] total_plays = item['total_plays'] else: total_time = 0 total_plays = 0 row = {'query_days': days, 'total_time': total_time, 'total_plays': total_plays } user_watch_time_stats.append(row) return user_watch_time_stats
def get_recently_watched(self, user_id=None, limit='10'): monitor_db = database.MonitorDatabase() recently_watched = [] if not limit.isdigit(): limit = '10' try: if str(user_id).isdigit(): query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, session_history.parent_rating_key, ' \ 'title, parent_title, grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \ 'year, started, user ' \ 'FROM session_history_metadata ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \ 'WHERE user_id = ? ' \ 'GROUP BY (CASE WHEN session_history.media_type = "track" THEN session_history.parent_rating_key ' \ ' ELSE session_history.rating_key END) ' \ 'ORDER BY started DESC LIMIT ?' result = monitor_db.select(query, args=[user_id, limit]) else: result = [] except Exception as e: logger.warn(u"PlexPy Users :: Unable to execute database query for get_recently_watched: %s." % e) result = [] for row in result: if row['media_type'] == 'episode' and row['parent_thumb']: thumb = row['parent_thumb'] elif row['media_type'] == 'episode': thumb = row['grandparent_thumb'] else: thumb = row['thumb'] recent_output = {'row_id': row['id'], 'media_type': row['media_type'], 'rating_key': row['rating_key'], 'title': row['title'], 'parent_title': row['parent_title'], 'grandparent_title': row['grandparent_title'], 'thumb': thumb, 'media_index': row['media_index'], 'parent_media_index': row['parent_media_index'], 'year': row['year'], 'time': row['started'], 'user': row['user'] } recently_watched.append(recent_output) return recently_watched
def osxnotifyregister(self, app): cherrypy.response.headers[ 'Cache-Control'] = "max-age=0,no-cache,no-store" from 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
def get_server_identity(self): identity = self.get_local_server_identity(output_format='xml') xml_head = identity.getElementsByTagName('MediaContainer') if not xml_head: logger.warn("Error parsing XML for Plex server identity.") return None server_identity = {} for a in xml_head: server_identity = {"machine_identifier": helpers.get_xml_attr(a, 'machineIdentifier'), "version": helpers.get_xml_attr(a, 'version') } return server_identity
def add_newsletter_config(agent_id=None, **kwargs): if str(agent_id).isdigit(): agent_id = int(agent_id) else: logger.error( u"Tautulli Newsletters :: Unable to add new newsletter: invalid agent_id %s." % agent_id) return False agent = next( (a for a in available_newsletter_agents() if a['id'] == agent_id), None) if not agent: logger.error( u"Tautulli Newsletters :: Unable to retrieve new newsletter agent: invalid agent_id %s." % agent_id) return False agent_class = get_agent_class(agent_id=agent['id']) keys = {'id': None} values = { 'agent_id': agent['id'], 'agent_name': agent['name'], 'agent_label': agent['label'], 'id_name': '', 'friendly_name': '', 'newsletter_config': json.dumps(agent_class.config), 'email_config': json.dumps(agent_class.email_config), 'subject': agent_class.subject, 'body': agent_class.body, 'message': agent_class.message } db = database.MonitorDatabase() try: db.upsert(table_name='newsletters', key_dict=keys, value_dict=values) newsletter_id = db.last_insert_id() logger.info( u"Tautulli Newsletters :: Added new newsletter agent: %s (newsletter_id %s)." % (agent['label'], newsletter_id)) blacklist_logger() return newsletter_id except Exception as e: logger.warn( u"Tautulli Newsletters :: Unable to add newsletter agent: %s." % e) return False
def get_player_stats(self, user_id=None, grouping=None): if not session.allow_session_user(user_id): return [] if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES monitor_db = database.MonitorDatabase() player_stats = [] result_id = 0 group_by = 'reference_id' if grouping else 'id' try: if str(user_id).isdigit(): query = 'SELECT player, COUNT(DISTINCT %s) as player_count, platform ' \ 'FROM session_history ' \ 'WHERE user_id = ? ' \ 'GROUP BY player ' \ 'ORDER BY player_count DESC' % group_by result = monitor_db.select(query, args=[user_id]) else: result = [] except Exception as e: logger.warn( u"Tautulli Users :: Unable to execute database query for get_player_stats: %s." % e) result = [] for item in result: # Rename Mystery platform names platform = common.PLATFORM_NAME_OVERRIDES.get( item['platform'], item['platform']) platform_name = next((v for k, v in common.PLATFORM_NAMES.items() if k in platform.lower()), 'default') row = { 'player_name': item['player'], 'platform': platform, 'platform_name': platform_name, 'total_plays': item['player_count'], 'result_id': result_id } player_stats.append(row) result_id += 1 return player_stats
def get_current_activity(self, **kwargs): try: pms_connect = pmsconnect.PmsConnect() result = pms_connect.get_current_activity() except: return serve_template(templatename="current_activity.html", data=None) if result: return serve_template(templatename="current_activity.html", data=result) else: return serve_template(templatename="current_activity.html", data=None) logger.warn('Unable to retrieve data.')
def daemonize(): if threading.activeCount() != 1: logger.warn( 'There are %r active threads. Daemonizing may cause' ' strange behavior.', threading.enumerate()) sys.stdout.flush() sys.stderr.flush() # Do first fork try: pid = os.fork() # @UndefinedVariable - only available in UNIX if pid != 0: sys.exit(0) except OSError, e: raise RuntimeError("1st fork failed: %s [%d]", e.strerror, e.errno)
def delete(self, user_id=None): monitor_db = database.MonitorDatabase() try: if str(user_id).isdigit(): self.delete_all_history(user_id) logger.info(u"PlexPy DataFactory :: Deleting user with id %s from database." % user_id) monitor_db.action('UPDATE users SET deleted_user = 1 WHERE user_id = ?', [user_id]) monitor_db.action('UPDATE users SET keep_history = 0 WHERE user_id = ?', [user_id]) monitor_db.action('UPDATE users SET do_notify = 0 WHERE user_id = ?', [user_id]) return 'Deleted user with id %s.' % user_id else: return 'Unable to delete user, user_id not valid.' except Exception as e: logger.warn(u"PlexPy Users :: Unable to execute database query for delete: %s." % e)
def get_server_urls(self, include_https=True): if plexpy.CONFIG.PMS_IDENTIFIER: server_id = plexpy.CONFIG.PMS_IDENTIFIER else: logger.error('PlexPy PlexTV connector :: Unable to retrieve server identity.') return [] plextv_resources = self.get_plextv_resources(include_https=include_https) server_urls = [] try: xml_parse = minidom.parseString(plextv_resources) except Exception, e: logger.warn("Error parsing XML for Plex resources: %s" % e) return []
def undelete(self, section_id=None, section_name=None): monitor_db = database.MonitorDatabase() try: if section_id and section_id.isdigit(): logger.info( u"PlexPy Libraries :: Re-adding library with id %s to database." % section_id) monitor_db.action( 'UPDATE library_sections SET deleted_section = 0 WHERE section_id = ?', [section_id]) monitor_db.action( 'UPDATE library_sections SET keep_history = 1 WHERE section_id = ?', [section_id]) monitor_db.action( 'UPDATE library_sections SET do_notify = 1 WHERE section_id = ?', [section_id]) monitor_db.action( 'UPDATE library_sections SET do_notify_created = 1 WHERE section_id = ?', [section_id]) return 'Re-added library with id %s.' % section_id elif section_name: logger.info( u"PlexPy Libraries :: Re-adding library with name %s to database." % section_name) monitor_db.action( 'UPDATE library_sections SET deleted_section = 0 WHERE section_name = ?', [section_name]) monitor_db.action( 'UPDATE library_sections SET keep_history = 1 WHERE section_name = ?', [section_name]) monitor_db.action( 'UPDATE library_sections SET do_notify = 1 WHERE section_name = ?', [section_name]) monitor_db.action( 'UPDATE library_sections SET do_notify_created = 1 WHERE section_name = ?', [section_name]) return 'Re-added library with section_name %s.' % section_name else: return 'Unable to re-add library, section_id or section_name not valid.' except Exception as e: logger.warn( u"PlexPy Libraries :: Unable to execute database query for undelete: %s." % e)
def check_server_response(): with monitor_lock: pms_connect = pmsconnect.PmsConnect() server_response = pms_connect.get_server_response() global ext_ping_count # Check for remote access if server_response: mapping_state = server_response['mapping_state'] mapping_error = server_response['mapping_error'] # Check if the port is mapped if not mapping_state == 'mapped': ext_ping_count += 1 logger.warn(u"PlexPy Monitor :: Plex remote access port not mapped, ping attempt %s." \ % str(ext_ping_count)) # Check if the port is open elif mapping_error == 'unreachable': ext_ping_count += 1 logger.warn(u"PlexPy Monitor :: Plex remote access port mapped, but mapping failed, ping attempt %s." \ % str(ext_ping_count)) # Reset external ping counter else: if ext_ping_count >= 3: logger.info( u"PlexPy Monitor :: Plex remote access is back up.") # Check if any notification agents have notifications enabled if any(d['on_extup'] for d in notifiers.available_notification_agents()): # Fire off notifications threading.Thread( target=notification_handler.notify_timeline, kwargs=dict(notify_action='extup')).start() ext_ping_count = 0 if ext_ping_count == 3: # Check if any notification agents have notifications enabled if any(d['on_extdown'] for d in notifiers.available_notification_agents()): # Fire off notifications threading.Thread(target=notification_handler.notify_timeline, kwargs=dict(notify_action='extdown')).start()
def _getSync(self, machine_id=None, user_id=None, **kwargs): pms_connect = pmsconnect.PmsConnect() server_id = pms_connect.get_server_identity() plex_tv = plextv.PlexTV() if not machine_id: result = plex_tv.get_synced_items(machine_id=server_id['machine_identifier'], user_id=user_id) else: result = plex_tv.get_synced_items(machine_id=machine_id, user_id=user_id) if result: self.data = result return result else: self.msg = 'Unable to retrieve sync data for user' logger.warn('Unable to retrieve sync data for user.')
def process(self, opcode, data): if opcode not in self.opcode_data: return False try: logger.websocket_debug(data) info = json.loads(data.decode('utf-8')) except Exception as e: logger.warn(u"Tautulli WebSocket :: %s: Error decoding message from websocket: %s" % (self.server.CONFIG.PMS_NAME, e)) logger.websocket_error(data) return False info = info.get('NotificationContainer', info) type = info.get('type') if not type: return False if type == 'playing': time_line = info.get('PlaySessionStateNotification', info.get('_children', {})) if not time_line: logger.debug(u"Tautulli WebSocket :: %s: Session found but unable to get timeline data." % self.server.CONFIG.PMS_NAME) return False try: activity = activity_handler.ActivityHandler(self.server, timeline=time_line[0]) activity.process() except Exception as e: logger.error(u"Tautulli WebSocket :: %s: Failed to process session data: %s." % (self.server.CONFIG.PMS_NAME, e)) if type == 'timeline': time_line = info.get('TimelineEntry', info.get('_children', {})) if not time_line: logger.debug(u"Tautulli WebSocket :: %s: Timeline event found but unable to get timeline data." % self.server.CONFIG.PMS_NAME) return False try: activity = activity_handler.TimelineHandler(self.server, timeline=time_line[0]) activity.process() except Exception as e: logger.error(u"Tautulli WebSocket :: %s: Failed to process timeline data: %s." % (self.server.CONFIG.PMS_NAME, e)) return True
def checkGithub(): plexpy.COMMITS_BEHIND = 0 # Get the latest version available from github logger.info('Retrieving latest version information from GitHub') url = 'https://api.github.com/repos/%s/plexpy/commits/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_BRANCH) version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict) if version is None: logger.warn('Could not get the latest version from GitHub. Are you running a local development version?') return plexpy.CURRENT_VERSION plexpy.LATEST_VERSION = version['sha'] logger.debug("Latest version is %s", plexpy.LATEST_VERSION) # See how many commits behind we are if not plexpy.CURRENT_VERSION: logger.info('You are running an unknown version of PlexPy. Run the updater to identify your version') return plexpy.LATEST_VERSION if plexpy.LATEST_VERSION == plexpy.CURRENT_VERSION: logger.info('PlexPy is up to date') return plexpy.LATEST_VERSION logger.info('Comparing currently installed version with latest GitHub version') url = 'https://api.github.com/repos/%s/plexpy/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.LATEST_VERSION, plexpy.CURRENT_VERSION) commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict) if commits is None: logger.warn('Could not get commits behind from GitHub.') return plexpy.LATEST_VERSION try: plexpy.COMMITS_BEHIND = int(commits['behind_by']) logger.debug("In total, %d commits behind", plexpy.COMMITS_BEHIND) except KeyError: logger.info('Cannot compare versions. Are you running a local development version?') plexpy.COMMITS_BEHIND = 0 if plexpy.COMMITS_BEHIND > 0: logger.info('New version is available. You are %s commits behind' % plexpy.COMMITS_BEHIND) elif plexpy.COMMITS_BEHIND == 0: logger.info('PlexPy is up to date') return plexpy.LATEST_VERSION
def get_server_times(self): servers = self.get_plextv_server_list(output_format='xml') server_times = [] try: xml_head = servers.getElementsByTagName('Server') except: logger.warn("Error parsing XML for Plex servers.") return [] for a in xml_head: if helpers.get_xml_attr(a, 'machineIdentifier') == plexpy.CONFIG.PMS_IDENTIFIER: server_times.append({"created_at": helpers.get_xml_attr(a, 'createdAt'), "updated_at": helpers.get_xml_attr(a, 'updatedAt') }) break return server_times
def get_plexpass_status(self): account_data = self.get_plextv_user_details(output_format='xml') try: subscription = account_data.getElementsByTagName('subscription') except Exception as e: logger.warn( u"Tautulli PlexTV :: Unable to parse XML for get_plexpass_status: %s." % e) return False if subscription and helpers.get_xml_attr(subscription[0], 'active') == '1': return True else: logger.debug( u"Tautulli PlexTV :: Plex Pass subscription not found.") return False
def get_user_player_stats(self, user=None, user_id=None): monitor_db = database.MonitorDatabase() player_stats = [] result_id = 0 try: if user_id: query = 'SELECT player, COUNT(player) as player_count, platform ' \ 'FROM session_history ' \ 'WHERE user_id = ? ' \ 'GROUP BY player ' \ 'ORDER BY player_count DESC' result = monitor_db.select(query, args=[user_id]) else: query = 'SELECT player, COUNT(player) as player_count, platform ' \ 'FROM session_history ' \ 'WHERE user = ? ' \ 'GROUP BY player ' \ 'ORDER BY player_count DESC' result = monitor_db.select(query, args=[user]) except: logger.warn("Unable to execute database query.") return None for item in result: # Rename Mystery platform names platform_names = { 'Mystery 3': 'Playstation 3', 'Mystery 4': 'Playstation 4', 'Mystery 5': 'Xbox 360' } platform_type = platform_names.get(item[2], item[2]) row = { 'player_name': item[0], 'platform_type': platform_type, 'total_plays': item[1], 'result_id': result_id } player_stats.append(row) result_id += 1 return player_stats
def process(opcode, data): from plexpy import activity_handler if opcode not in opcode_data: return False try: info = json.loads(data) except Exception as ex: logger.warn( u'PlexPy WebSocket :: Error decoding message from websocket: %s' % ex) logger.debug(data) return False type = info.get('type') if not type: return False if type == 'playing': # logger.debug('%s.playing %s' % (name, info)) try: time_line = info.get('_children') except: logger.debug( u"PlexPy WebSocket :: Session found but unable to get timeline data." ) return False activity = activity_handler.ActivityHandler(timeline=time_line[0]) activity.process() #if type == 'timeline': # try: # time_line = info.get('_children') # except: # logger.debug(u"PlexPy WebSocket :: Timeline event found but unable to get timeline data.") # return False # activity = activity_handler.TimelineHandler(timeline=time_line[0]) # activity.process() return True
def get_library_details(section_id=section_id): monitor_db = database.MonitorDatabase() try: if str(section_id).isdigit(): query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \ 'thumb AS library_thumb, custom_thumb_url AS custom_thumb, art, ' \ 'do_notify, do_notify_created, keep_history ' \ 'FROM library_sections ' \ 'WHERE section_id = ? ' result = monitor_db.select(query, args=[section_id]) else: result = [] except Exception as e: logger.warn( u"PlexPy Libraries :: Unable to execute database query for get_details: %s." % e) result = [] library_details = {} if result: for item in result: if item['custom_thumb'] and item['custom_thumb'] != item[ 'library_thumb']: library_thumb = item['custom_thumb'] elif item['library_thumb']: library_thumb = item['library_thumb'] else: library_thumb = common.DEFAULT_COVER_THUMB library_details = { 'section_id': item['section_id'], 'section_name': item['section_name'], 'section_type': item['section_type'], 'library_thumb': library_thumb, 'library_art': item['art'], 'count': item['count'], 'parent_count': item['parent_count'], 'child_count': item['child_count'], 'do_notify': item['do_notify'], 'do_notify_created': item['do_notify_created'], 'keep_history': item['keep_history'] } return library_details
def delete_all_users(self): monitor_db = database.MonitorDatabase() try: logger.info( u"Tautulli Servers :: %s: Deleting all user tokens from database." % self.CONFIG.PMS_NAME) user_shared_libraries_del = \ monitor_db.action('DELETE FROM ' 'user_shared_libraries ' 'WHERE server_id = ?', [self.CONFIG.ID]) return True except Exception as e: logger.warn( u"Tautulli Servers :: %s: Unable to execute database query for delete_all_users: %s." % (self.CONFIG.PMS_NAME, e)) return False
def get_cloud_server_status(self, server): cloud_status = self.cloud_server_status(output_format='xml') try: status_info = cloud_status.getElementsByTagName('info') except Exception as e: logger.warn( u"Tautulli PlexTV :: Unable to parse XML for get_cloud_server_status: %s." % e) return False for info in status_info: servers = info.getElementsByTagName('server') for s in servers: if helpers.get_xml_attr(s, 'address') == server.CONFIG.PMS_IP: if helpers.get_xml_attr(info, 'running') == '1': return True else: return False
def notify(self, title, subtitle=None, text=None, sound=True, image=None): 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") if image: source_img = self.AppKit.NSImage.alloc( ).initByReferencingFile_(image) notification.setContentImage_(source_img) #notification.set_identityImage_(source_img) notification.setHasActionButton_(False) notification_center = NSUserNotificationCenter.defaultUserNotificationCenter( ) notification_center.deliverNotification_(notification) del pool return True except Exception as e: logger.warn('Error sending OS X Notification: %s' % e) return False