def on_stop(self, force_stop=False): if self.is_valid_session(): logger.debug(u"PlexPy ActivityHandler :: Session %s has stopped." % str(self.get_session_key())) # Set the session last_paused timestamp ap = activity_processor.ActivityProcessor() ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=None) # Update the session state and viewOffset # Set force_stop to true to disable the state set if not force_stop: ap.set_session_state(session_key=self.get_session_key(), state=self.timeline['state'], view_offset=self.timeline['viewOffset'], stopped=int(time.time())) # Retrieve the session data from our temp table db_session = ap.get_session_by_key(session_key=self.get_session_key()) # Check if any notification agents have notifications enabled if any(d['on_stop'] for d in notifiers.available_notification_agents()): # Fire off notifications threading.Thread(target=notification_handler.notify, kwargs=dict(stream_data=db_session, notify_action='stop')).start() # Write it to the history table monitor_proc = activity_processor.ActivityProcessor() monitor_proc.write_session_history(session=db_session) # Remove the session from our temp session table logger.debug(u"PlexPy ActivityHandler :: Removing session %s from session queue" % str(self.get_session_key())) ap.delete_session(session_key=self.get_session_key())
def force_stop_stream(session_key, title, user, server_name, server): ap = activity_processor.ActivityProcessor(server=server) session = ap.get_session_by_key(session_key=session_key) row_id = ap.write_session_history(session=session) if row_id: # If session is written to the database successfully, remove the session from the session table logger.info(u"Tautulli ActivityHandler :: %s: Removing stale stream with sessionKey %s ratingKey %s from session queue" % (server.CONFIG.PMS_NAME, session['session_key'], session['rating_key'])) ap.delete_session(row_id=row_id) delete_metadata_cache(session_key, server) else: session['write_attempts'] += 1 if session['write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS: logger.warn(u"Tautulli ActivityHandler :: %s: Failed to write stream with sessionKey %s ratingKey %s to the database. " \ "Will try again in 30 seconds. Write attempt %s." % (server.CONFIG.PMS_NAME, session['session_key'], session['rating_key'], str(session['write_attempts']))) ap.increment_write_attempts(session_key=session_key) # Reschedule for 30 seconds later schedule_callback('session_key-{}-{}'.format(server.CONFIG.ID, session_key), func=force_stop_stream, args=[session_key, session['full_title'], session['user'], server.CONFIG.PMS_NAME, server], seconds=30) else: logger.warn(u"Tautulli ActivityHandler :: %s: Failed to write stream with sessionKey %s ratingKey %s to the database. " \ "Removing session from the database. Write attempt %s." % (server.CONFIG.PMS_NAME, session['session_key'], session['rating_key'], str(session['write_attempts']))) logger.info(u"Tautulli ActivityHandler :: %s: Removing stale stream with sessionKey %s ratingKey %s from session queue" % (server.CONFIG.PMS_NAME, session['session_key'], session['rating_key'])) ap.delete_session(session_key=session_key) delete_metadata_cache(session_key, server)
def on_buffer(self): if self.is_valid_session(): ap = activity_processor.ActivityProcessor(server=self.server) # Increment our buffer count ap.increment_session_buffer_count(session_key=self.get_session_key()) # Get our current buffer count current_buffer_count = ap.get_session_buffer_count(self.get_session_key()) logger.debug(u"Tautulli ActivityHandler :: %s: Session %s buffer count is %s." % (self.server.CONFIG.PMS_NAME, self.get_session_key(), current_buffer_count)) # Get our last triggered time buffer_last_triggered = ap.get_session_buffer_trigger_time(self.get_session_key()) # Update the session state and viewOffset self.update_db_session() time_since_last_trigger = None if buffer_last_triggered: logger.debug(u"Tautulli ActivityHandler :: %s: Session %s buffer last triggered at %s." % (self.server.CONFIG.PMS_NAME, self.get_session_key(), buffer_last_triggered)) time_since_last_trigger = int(time.time()) - int(buffer_last_triggered) if (current_buffer_count >= plexpy.CONFIG.BUFFER_THRESHOLD and time_since_last_trigger is None) or \ (time_since_last_trigger is not None and time_since_last_trigger >= plexpy.CONFIG.BUFFER_WAIT): ap.set_session_buffer_trigger_time(session_key=self.get_session_key()) # Retrieve the session data from our temp table db_session = ap.get_session_by_key(session_key=self.get_session_key()) plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_buffer'})
def on_buffer(self): if self.is_valid_session(): logger.debug(u"PlexPy ActivityHandler :: Session %s is buffering." % self.get_session_key()) ap = activity_processor.ActivityProcessor() db_stream = ap.get_session_by_key(session_key=self.get_session_key()) # Increment our buffer count ap.increment_session_buffer_count(session_key=self.get_session_key()) # Get our current buffer count current_buffer_count = ap.get_session_buffer_count(self.get_session_key()) logger.debug(u"PlexPy ActivityHandler :: Session %s buffer count is %s." % (self.get_session_key(), current_buffer_count)) # Get our last triggered time buffer_last_triggered = ap.get_session_buffer_trigger_time(self.get_session_key()) time_since_last_trigger = 0 if buffer_last_triggered: logger.debug(u"PlexPy ActivityHandler :: Session %s buffer last triggered at %s." % (self.get_session_key(), buffer_last_triggered)) time_since_last_trigger = int(time.time()) - int(buffer_last_triggered) if plexpy.CONFIG.BUFFER_THRESHOLD > 0 and (current_buffer_count >= plexpy.CONFIG.BUFFER_THRESHOLD and \ time_since_last_trigger == 0 or time_since_last_trigger >= plexpy.CONFIG.BUFFER_WAIT): ap.set_session_buffer_trigger_time(session_key=self.get_session_key()) # Check if any notification agents have notifications enabled if any(d['on_buffer'] for d in notifiers.available_notification_agents()): threading.Thread(target=notification_handler.notify, kwargs=dict(stream_data=db_stream, notify_action='buffer')).start()
def on_disconnect(self): if self.server.PLEX_SERVER_UP is None or self.server.PLEX_SERVER_UP: logger.info(u"Tautulli WebSocket :: %s: Unable to get a response from the server, Plex server is down." % self.server.CONFIG.PMS_NAME) plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown', 'server_id': self.server.CONFIG.ID}) self.server.PLEX_SERVER_UP = False activity_processor.ActivityProcessor(server=self.server).set_temp_stopped() self.server.initialize_scheduler()
def update_db_session(self, session=None): if session is None: session = self.get_live_session() if session: # Update our session temp table values ap = activity_processor.ActivityProcessor(server=self.server) ap.write_session(session=session, notify=False) self.set_session_state()
def process(self): if self.is_valid_session(): ap = activity_processor.ActivityProcessor() db_session = ap.get_session_by_key(session_key=self.get_session_key()) this_state = self.timeline['state'] this_key = str(self.timeline['ratingKey']) # If we already have this session in the temp table, check for state changes if db_session: last_state = db_session['state'] last_key = str(db_session['rating_key']) # Make sure the same item is being played if this_key == last_key: # Update the session state and viewOffset if this_state == 'playing': ap.set_session_state(session_key=self.get_session_key(), state=this_state, view_offset=self.timeline['viewOffset']) # Start our state checks if this_state != last_state: if this_state == 'paused': self.on_pause() elif last_state == 'paused' and this_state == 'playing': self.on_resume() elif this_state == 'stopped': self.on_stop() elif this_state == 'buffering': self.on_buffer() # If a client doesn't register stop events (I'm looking at you PHT!) check if the ratingKey has changed else: # Manually stop and start # Set force_stop so that we don't overwrite our last viewOffset self.on_stop(force_stop=True) self.on_start() # Monitor if the stream has reached the watch percentage for notifications # The only purpose of this is for notifications # Check if any notification agents have notifications enabled notify_agents = [d['id'] for d in notifiers.available_notification_agents() if d['on_watched']] # Get the current states for notifications from our db notified_agents = [d['agent_id'] for d in notification_handler.get_notify_state(session=db_session) if d['notify_action'] == 'watched'] if notify_agents else [] if any(a not in notified_agents for a in notify_agents): progress_percent = helpers.get_percent(self.timeline['viewOffset'], db_session['duration']) if progress_percent >= plexpy.CONFIG.NOTIFY_WATCHED_PERCENT and this_state != 'buffering': # Rather not put this on it's own thread so we know it completes before our next event. notification_handler.notify(stream_data=db_session, notify_action='watched') else: # We don't have this session in our table yet, start a new one. if this_state != 'buffering': self.on_start()
def process(self): if self.is_valid_session(): from plexpy import helpers ap = activity_processor.ActivityProcessor() db_session = ap.get_session_by_key( session_key=self.get_session_key()) this_state = self.timeline['state'] this_key = str(self.timeline['ratingKey']) # If we already have this session in the temp table, check for state changes if db_session: last_state = db_session['state'] last_key = str(db_session['rating_key']) # Make sure the same item is being played if this_key == last_key: # Update the session state and viewOffset if this_state == 'playing': ap.set_session_state( session_key=self.get_session_key(), state=this_state, view_offset=self.timeline['viewOffset']) # Start our state checks if this_state != last_state: if this_state == 'paused': self.on_pause() elif last_state == 'paused' and this_state == 'playing': self.on_resume() elif this_state == 'stopped': self.on_stop() elif this_state == 'buffering': self.on_buffer() # If a client doesn't register stop events (I'm looking at you PHT!) check if the ratingKey has changed else: # Manually stop and start # Set force_stop so that we don't overwrite our last viewOffset self.on_stop(force_stop=True) self.on_start() # Monitor if the stream has reached the watch percentage for notifications # The only purpose of this is for notifications progress_percent = helpers.get_percent( self.timeline['viewOffset'], db_session['duration']) if progress_percent >= plexpy.CONFIG.NOTIFY_WATCHED_PERCENT and this_state != 'buffering': threading.Thread(target=notification_handler.notify, kwargs=dict( stream_data=db_session, notify_action='watched')).start() else: # We don't have this session in our table yet, start a new one. if this_state != 'buffering': self.on_start()
def on_change(self): if self.is_valid_session(): logger.debug(u"Tautulli ActivityHandler :: %s: Session %s has changed transcode decision." % (self.server.CONFIG.PMS_NAME, str(self.get_session_key()))) # Update the session state and viewOffset self.update_db_session() # Retrieve the session data from our temp table ap = activity_processor.ActivityProcessor(server=self.server) db_session = ap.get_session_by_key(session_key=self.get_session_key()) plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_change'})
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 on_stop(self, force_stop=False): if self.is_valid_session(): logger.debug(u"Tautulli ActivityHandler :: %s: Session %s %sstopped." % (self.server.CONFIG.PMS_NAME, str(self.get_session_key()), 'force ' if force_stop else '')) # Set the session last_paused timestamp ap = activity_processor.ActivityProcessor(server=self.server) ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=None) # Update the session state and viewOffset # Set force_stop to true to disable the state set if not force_stop: self.set_session_state() # Retrieve the session data from our temp table db_session = ap.get_session_by_key(session_key=self.get_session_key()) plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_stop'}) # Write it to the history table monitor_proc = activity_processor.ActivityProcessor(server=self.server) row_id = monitor_proc.write_session_history(session=db_session) if row_id: schedule_callback('session_key-{}-{}'.format(self.server.CONFIG.ID, self.get_session_key()), remove_job=True) # Remove the session from our temp session table logger.debug(u"Tautulli ActivityHandler :: %s: Removing sessionKey %s ratingKey %s from session queue" % (self.server.CONFIG.PMS_NAME, str(self.get_session_key()), str(self.get_rating_key()))) ap.delete_session(row_id=row_id) delete_metadata_cache(self.get_session_key(), self.server) else: schedule_callback('session_key-{}-{}'.format(self.server.CONFIG.ID, self.get_session_key()), func=force_stop_stream, args=[self.get_session_key(), db_session['full_title'], db_session['user'], self.server.CONFIG.PMS_NAME, self.server], seconds=30)
def on_resume(self): if self.is_valid_session(): logger.debug(u"Tautulli ActivityHandler :: %s: Session %s resumed." % (self.server.CONFIG.PMS_NAME, str(self.get_session_key()))) # Set the session last_paused timestamp ap = activity_processor.ActivityProcessor(server=self.server) ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=None) # Update the session state and viewOffset self.update_db_session() # Retrieve the session data from our temp table db_session = ap.get_session_by_key(session_key=self.get_session_key()) plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_resume'})
def on_pause(self): if self.is_valid_session(): logger.debug(u"PlexPy ActivityHandler :: Session %s has been paused." % str(self.get_session_key())) # Set the session last_paused timestamp ap = activity_processor.ActivityProcessor() ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=int(time.time())) # Update the session state and viewOffset ap.set_session_state(session_key=self.get_session_key(), state=self.timeline['state'], view_offset=self.timeline['viewOffset']) # Retrieve the session data from our temp table db_session = ap.get_session_by_key(session_key=self.get_session_key()) # Fire off notifications threading.Thread(target=notification_handler.notify, kwargs=dict(stream_data=db_session, notify_action='pause')).start()
def on_resume(self): if self.is_valid_session(): logger.debug(u"PlexPy ActivityHandler :: Session %s has been resumed." % str(self.get_session_key())) # Set the session last_paused timestamp ap = activity_processor.ActivityProcessor() ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=None) # Update the session state and viewOffset ap.set_session_state(session_key=self.get_session_key(), state=self.timeline['state'], view_offset=self.timeline['viewOffset']) # Retrieve the session data from our temp table db_session = ap.get_session_by_key(session_key=self.get_session_key()) # Check if any notification agents have notifications enabled if any(d['on_resume'] for d in notifiers.available_notification_agents()): # Fire off notifications threading.Thread(target=notification_handler.notify, kwargs=dict(stream_data=db_session, notify_action='resume')).start()
def import_from_plexwatch(database=None, table_name=None, import_ignore_interval=0): try: connection = sqlite3.connect(database, timeout=20) connection.row_factory = sqlite3.Row except sqlite3.OperationalError: logger.error(u"PlexPy Importer :: Invalid filename.") return None except ValueError: logger.error(u"PlexPy Importer :: Invalid filename.") return None try: connection.execute('SELECT ratingKey from %s' % table_name) except sqlite3.OperationalError: logger.error( u"PlexPy Importer :: Database specified does not contain the required fields." ) return None logger.debug(u"PlexPy Importer :: PlexWatch data import in progress...") logger.debug( u"PlexPy Importer :: Disabling monitoring while import in progress.") plexpy.schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions', hours=0, minutes=0, seconds=0) plexpy.schedule_job(activity_pinger.check_recently_added, 'Check for recently added items', hours=0, minutes=0, seconds=0) plexpy.schedule_job(activity_pinger.check_server_response, 'Check for server response', hours=0, minutes=0, seconds=0) ap = activity_processor.ActivityProcessor() user_data = users.Users() # Get the latest friends list so we can pull user id's try: plextv.refresh_users() except: logger.debug( u"PlexPy Importer :: Unable to refresh the users list. Aborting import." ) return None query = 'SELECT time AS started, ' \ 'stopped, ' \ 'cast(ratingKey as text) AS rating_key, ' \ 'null AS user_id, ' \ 'user, ' \ 'ip_address, ' \ 'paused_counter, ' \ 'platform AS player, ' \ 'null AS platform, ' \ 'null as machine_id, ' \ 'parentRatingKey as parent_rating_key, ' \ 'grandparentRatingKey as grandparent_rating_key, ' \ 'null AS media_type, ' \ 'null AS view_offset, ' \ 'xml, ' \ 'rating as content_rating,' \ 'summary,' \ 'title AS full_title,' \ '(case when orig_title_ep = "" then orig_title else ' \ 'orig_title_ep end) as title,' \ '(case when orig_title_ep != "" then orig_title else ' \ 'null end) as grandparent_title ' \ 'FROM ' + table_name + ' ORDER BY id' result = connection.execute(query) for row in result: # Extract the xml from the Plexwatch db xml field. extracted_xml = extract_plexwatch_xml(row['xml']) # If we get back None from our xml extractor skip over the record and log error. if not extracted_xml: logger.error( u"PlexPy Importer :: Skipping record with ratingKey %s due to malformed xml." % str(row['rating_key'])) continue # Skip line if we don't have a ratingKey to work with if not row['rating_key']: logger.error( u"PlexPy Importer :: Skipping record due to null ratingRey.") continue # If the user_id no longer exists in the friends list, pull it from the xml. if user_data.get_user_id(user=row['user']): user_id = user_data.get_user_id(user=row['user']) else: user_id = extracted_xml['user_id'] session_history = { 'started': row['started'], 'stopped': row['stopped'], 'rating_key': row['rating_key'], 'title': row['title'], 'parent_title': extracted_xml['parent_title'], 'grandparent_title': row['grandparent_title'], 'user_id': user_id, 'user': row['user'], 'ip_address': row['ip_address'], 'paused_counter': row['paused_counter'], 'player': row['player'], 'platform': extracted_xml['platform'], 'machine_id': extracted_xml['machine_id'], 'parent_rating_key': row['parent_rating_key'], 'grandparent_rating_key': row['grandparent_rating_key'], 'media_type': extracted_xml['media_type'], 'view_offset': extracted_xml['view_offset'], 'video_decision': extracted_xml['video_decision'], 'audio_decision': extracted_xml['audio_decision'], 'duration': extracted_xml['duration'], 'width': extracted_xml['width'], 'height': extracted_xml['height'], 'container': extracted_xml['container'], 'video_codec': extracted_xml['video_codec'], 'audio_codec': extracted_xml['audio_codec'], 'bitrate': extracted_xml['bitrate'], 'video_resolution': extracted_xml['video_resolution'], 'video_framerate': extracted_xml['video_framerate'], 'aspect_ratio': extracted_xml['aspect_ratio'], 'audio_channels': extracted_xml['audio_channels'], 'transcode_protocol': extracted_xml['transcode_protocol'], 'transcode_container': extracted_xml['transcode_container'], 'transcode_video_codec': extracted_xml['transcode_video_codec'], 'transcode_audio_codec': extracted_xml['transcode_audio_codec'], 'transcode_audio_channels': extracted_xml['transcode_audio_channels'], 'transcode_width': extracted_xml['transcode_width'], 'transcode_height': extracted_xml['transcode_height'] } session_history_metadata = { 'rating_key': helpers.latinToAscii(row['rating_key']), 'parent_rating_key': row['parent_rating_key'], 'grandparent_rating_key': row['grandparent_rating_key'], 'title': row['title'], 'parent_title': extracted_xml['parent_title'], 'grandparent_title': row['grandparent_title'], 'media_index': extracted_xml['media_index'], 'parent_media_index': extracted_xml['parent_media_index'], 'thumb': extracted_xml['thumb'], 'parent_thumb': extracted_xml['parent_thumb'], 'grandparent_thumb': extracted_xml['grandparent_thumb'], 'art': extracted_xml['art'], 'media_type': extracted_xml['media_type'], 'year': extracted_xml['year'], 'originally_available_at': extracted_xml['originally_available_at'], 'added_at': extracted_xml['added_at'], 'updated_at': extracted_xml['updated_at'], 'last_viewed_at': extracted_xml['last_viewed_at'], 'content_rating': row['content_rating'], 'summary': row['summary'], 'tagline': extracted_xml['tagline'], 'rating': extracted_xml['rating'], 'duration': extracted_xml['duration'], 'guid': extracted_xml['guid'], 'section_id': extracted_xml['section_id'], 'directors': extracted_xml['directors'], 'writers': extracted_xml['writers'], 'actors': extracted_xml['actors'], 'genres': extracted_xml['genres'], 'studio': extracted_xml['studio'], 'full_title': row['full_title'] } # On older versions of PMS, "clip" items were still classified as "movie" and had bad ratingKey values # Just make sure that the ratingKey is indeed an integer if session_history_metadata['rating_key'].isdigit(): ap.write_session_history( session=session_history, import_metadata=session_history_metadata, is_import=True, import_ignore_interval=import_ignore_interval) else: logger.debug(u"PlexPy Importer :: Item has bad rating_key: %s" % session_history_metadata['rating_key']) logger.debug(u"PlexPy Importer :: PlexWatch data import complete.") import_users() logger.debug(u"PlexPy Importer :: Re-enabling monitoring.") plexpy.initialize_scheduler()
def check_active_sessions(ws_request=False): with monitor_lock: pms_connect = pmsconnect.PmsConnect() session_list = pms_connect.get_current_activity() monitor_db = database.MonitorDatabase() monitor_process = activity_processor.ActivityProcessor() # logger.debug(u"PlexPy Monitor :: Checking for active streams.") if session_list: media_container = session_list['sessions'] # Check our temp table for what we must do with the new streams db_streams = monitor_db.select( 'SELECT started, session_key, rating_key, media_type, title, parent_title, ' 'grandparent_title, user_id, user, friendly_name, ip_address, player, ' 'platform, machine_id, parent_rating_key, grandparent_rating_key, state, ' 'view_offset, duration, video_decision, audio_decision, width, height, ' 'container, video_codec, audio_codec, bitrate, video_resolution, ' 'video_framerate, aspect_ratio, audio_channels, transcode_protocol, ' 'transcode_container, transcode_video_codec, transcode_audio_codec, ' 'transcode_audio_channels, transcode_width, transcode_height, ' 'paused_counter, last_paused ' 'FROM sessions') for stream in db_streams: if any(d['session_key'] == str(stream['session_key']) and d['rating_key'] == str(stream['rating_key']) for d in media_container): # The user's session is still active for session in media_container: if session['session_key'] == str(stream['session_key']) and \ session['rating_key'] == str(stream['rating_key']): # The user is still playing the same media item # Here we can check the play states if session['state'] != stream['state']: if session['state'] == 'paused': # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread( target=notification_handler.notify, kwargs=dict( stream_data=stream, notify_action='pause')).start() if session['state'] == 'playing' and stream[ 'state'] == 'paused': # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread( target=notification_handler.notify, kwargs=dict( stream_data=stream, notify_action='resume')).start() if stream['state'] == 'paused' and not ws_request: # The stream is still paused so we need to increment the paused_counter # Using the set config parameter as the interval, probably not the most accurate but # it will have to do for now. If it's a websocket request don't use this method. paused_counter = int( stream['paused_counter'] ) + plexpy.CONFIG.MONITORING_INTERVAL monitor_db.action( 'UPDATE sessions SET paused_counter = ? ' 'WHERE session_key = ? AND rating_key = ?', [ paused_counter, stream['session_key'], stream['rating_key'] ]) if session[ 'state'] == 'buffering' and plexpy.CONFIG.BUFFER_THRESHOLD > 0: # The stream is buffering so we need to increment the buffer_count # We're going just increment on every monitor ping, # would be difficult to keep track otherwise monitor_db.action( 'UPDATE sessions SET buffer_count = buffer_count + 1 ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) # Check the current buffer count and last buffer to determine if we should notify buffer_values = monitor_db.select( 'SELECT buffer_count, buffer_last_triggered ' 'FROM sessions ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) if buffer_values[0][ 'buffer_count'] >= plexpy.CONFIG.BUFFER_THRESHOLD: # Push any notifications - # Push it on it's own thread so we don't hold up our db actions # Our first buffer notification if buffer_values[0][ 'buffer_count'] == plexpy.CONFIG.BUFFER_THRESHOLD: logger.info( u"PlexPy Monitor :: User '%s' has triggered a buffer warning." % stream['user']) # Set the buffer trigger time monitor_db.action( 'UPDATE sessions ' 'SET buffer_last_triggered = strftime("%s","now") ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) threading.Thread( target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='buffer' )).start() else: # Subsequent buffer notifications after wait time if int(time.time()) > buffer_values[0]['buffer_last_triggered'] + \ plexpy.CONFIG.BUFFER_WAIT: logger.info( u"PlexPy Monitor :: User '%s' has triggered multiple buffer warnings." % stream['user']) # Set the buffer trigger time monitor_db.action( 'UPDATE sessions ' 'SET buffer_last_triggered = strftime("%s","now") ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) threading.Thread( target=notification_handler. notify, kwargs=dict( stream_data=stream, notify_action='buffer' )).start() logger.debug( u"PlexPy Monitor :: Stream buffering. Count is now %s. Last triggered %s." % (buffer_values[0][0], buffer_values[0][1])) # Check if the user has reached the offset in the media we defined as the "watched" percent # Don't trigger if state is buffer as some clients push the progress to the end when # buffering on start. if session['view_offset'] and session[ 'duration'] and session[ 'state'] != 'buffering': if helpers.get_percent( session['view_offset'], session['duration'] ) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT: # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread( target=notification_handler.notify, kwargs=dict( stream_data=stream, notify_action='watched')).start() else: # The user has stopped playing a stream logger.debug( u"PlexPy Monitor :: Removing sessionKey %s ratingKey %s from session queue" % (stream['session_key'], stream['rating_key'])) monitor_db.action( 'DELETE FROM sessions WHERE session_key = ? AND rating_key = ?', [stream['session_key'], stream['rating_key']]) # Check if the user has reached the offset in the media we defined as the "watched" percent if stream['view_offset'] and stream['duration']: if helpers.get_percent( stream['view_offset'], stream['duration'] ) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT: # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread( target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='watched')).start() # Push any notifications - Push it on it's own thread so we don't hold up our db actions threading.Thread(target=notification_handler.notify, kwargs=dict( stream_data=stream, notify_action='stop')).start() # Write the item history on playback stop monitor_process.write_session_history(session=stream) # Process the newly received session data for session in media_container: monitor_process.write_session(session) else: logger.debug(u"PlexPy Monitor :: Unable to read session list.")
def check_active_sessions(server=None, ws_request=False): if server.WS and server.WS.WS_CONNECTION and server.WS.WS_CONNECTION.connected: with server.monitor_lock: session_list = server.get_current_activity() monitor_db = database.MonitorDatabase() monitor_process = activity_processor.ActivityProcessor(server) logger.debug( u"Tautulli Monitor :: %s: Checking for active streams." % server.CONFIG.PMS_NAME) if session_list: media_container = session_list['sessions'] # Check our temp table for what we must do with the new streams db_streams = monitor_process.get_sessions() for stream in db_streams: if any(d['session_key'] == str(stream['session_key']) and d['rating_key'] == str(stream['rating_key']) for d in media_container): # The user's session is still active for session in media_container: if session['session_key'] == str(stream['session_key']) and \ session['rating_key'] == str(stream['rating_key']): # The user is still playing the same media item # Here we can check the play states if session['state'] != stream['state']: if session['state'] == 'paused': logger.debug( u"Tautulli Monitor :: %s: Session %s paused." % (server.CONFIG.PMS_NAME, stream['session_key'])) plexpy.NOTIFY_QUEUE.put({ 'stream_data': stream.copy(), 'notify_action': 'on_pause' }) if session['state'] == 'playing' and stream[ 'state'] == 'paused': logger.debug( u"Tautulli Monitor :: %s: Session %s resumed." % (server.CONFIG.PMS_NAME, stream['session_key'])) plexpy.NOTIFY_QUEUE.put({ 'stream_data': stream.copy(), 'notify_action': 'on_resume' }) if stream[ 'state'] == 'paused' and not ws_request: # The stream is still paused so we need to increment the paused_counter # Using the set config parameter as the interval, probably not the most accurate but # it will have to do for now. If it's a websocket request don't use this method. paused_counter = int( stream['paused_counter'] ) + plexpy.CONFIG.MONITORING_INTERVAL monitor_db.action( 'UPDATE sessions SET paused_counter = ? ' 'WHERE session_key = ? AND rating_key = ?', [ paused_counter, stream['session_key'], stream['rating_key'] ]) if session[ 'state'] == 'buffering' and plexpy.CONFIG.BUFFER_THRESHOLD > 0: # The stream is buffering so we need to increment the buffer_count # We're going just increment on every monitor ping, # would be difficult to keep track otherwise monitor_db.action( 'UPDATE sessions SET buffer_count = buffer_count + 1 ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) # Check the current buffer count and last buffer to determine if we should notify buffer_values = monitor_db.select( 'SELECT buffer_count, buffer_last_triggered ' 'FROM sessions ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) if buffer_values[0][ 'buffer_count'] >= plexpy.CONFIG.BUFFER_THRESHOLD: # Push any notifications - # Push it on it's own thread so we don't hold up our db actions # Our first buffer notification if buffer_values[0][ 'buffer_count'] == plexpy.CONFIG.BUFFER_THRESHOLD: logger.info( u"Tautulli Monitor :: %s: User '%s' has triggered a buffer warning." % (server.CONFIG.PMS_NAME, stream['user'])) # Set the buffer trigger time monitor_db.action( 'UPDATE sessions ' 'SET buffer_last_triggered = strftime("%s","now") ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) plexpy.NOTIFY_QUEUE.put({ 'stream_data': stream.copy(), 'notify_action': 'on_buffer' }) else: # Subsequent buffer notifications after wait time if int(time.time()) > buffer_values[0]['buffer_last_triggered'] + \ plexpy.CONFIG.BUFFER_WAIT: logger.info( u"Tautulli Monitor :: %s: User '%s' has triggered multiple buffer warnings." % (server.CONFIG.PMS_NAME, stream['user'])) # Set the buffer trigger time monitor_db.action( 'UPDATE sessions ' 'SET buffer_last_triggered = strftime("%s","now") ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) plexpy.NOTIFY_QUEUE.put({ 'stream_data': stream.copy(), 'notify_action': 'on_buffer' }) logger.debug( u"Tautulli Monitor :: %s: Session %s is buffering. Count is now %s. Last triggered %s." % (server.CONFIG.PMS_NAME, stream['session_key'], buffer_values[0]['buffer_count'], buffer_values[0] ['buffer_last_triggered'])) # Check if the user has reached the offset in the media we defined as the "watched" percent # Don't trigger if state is buffer as some clients push the progress to the end when # buffering on start. if session['state'] != 'buffering': progress_percent = helpers.get_percent( session['view_offset'], session['duration']) notify_states = notification_handler.get_notify_state( session=session) if (session['media_type'] == 'movie' and progress_percent >= plexpy.CONFIG.MOVIE_WATCHED_PERCENT or session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \ and not any(d['notify_action'] == 'on_watched' for d in notify_states): plexpy.NOTIFY_QUEUE.put({ 'stream_data': stream.copy(), 'notify_action': 'on_watched' }) else: # The user has stopped playing a stream if stream['state'] != 'stopped': logger.debug( u"Tautulli Monitor :: %s: Session %s stopped." % (server.CONFIG.PMS_NAME, stream['session_key'])) if not stream['stopped']: # Set the stream stop time stream['stopped'] = int(time.time()) monitor_db.action( 'UPDATE sessions SET stopped = ?, state = ? ' 'WHERE session_key = ? AND rating_key = ?', [ stream['stopped'], 'stopped', stream['session_key'], stream['rating_key'] ]) progress_percent = helpers.get_percent( stream['view_offset'], stream['duration']) notify_states = notification_handler.get_notify_state( session=stream) if (stream['media_type'] == 'movie' and progress_percent >= plexpy.CONFIG.MOVIE_WATCHED_PERCENT or stream['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or stream['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \ and not any(d['notify_action'] == 'on_watched' for d in notify_states): plexpy.NOTIFY_QUEUE.put({ 'stream_data': stream.copy(), 'notify_action': 'on_watched' }) plexpy.NOTIFY_QUEUE.put({ 'stream_data': stream.copy(), 'notify_action': 'on_stop' }) # Write the item history on playback stop row_id = monitor_process.write_session_history( session=stream) if row_id: # If session is written to the databaase successfully, remove the session from the session table logger.debug( u"Tautulli Monitor :: %s: Removing sessionKey %s ratingKey %s from session queue" % (server.CONFIG.PMS_NAME, stream['session_key'], stream['rating_key'])) monitor_process.delete_session(row_id=row_id) else: stream['write_attempts'] += 1 if stream[ 'write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS: logger.warn(u"Tautulli Monitor :: %s: Failed to write sessionKey %s ratingKey %s to the database. " \ "Will try again on the next pass. Write attempt %s." % (server.CONFIG.PMS_NAME, stream['session_key'], stream['rating_key'], str(stream['write_attempts']))) monitor_process.increment_write_attempts( session_key=stream['session_key']) else: logger.warn(u"Tautulli Monitor :: %s: Failed to write sessionKey %s ratingKey %s to the database. " \ "Removing session from the database. Write attempt %s." % (server.CONFIG.PMS_NAME, stream['session_key'], stream['rating_key'], str(stream['write_attempts']))) logger.debug( u"Tautulli Monitor :: %s: Removing sessionKey %s ratingKey %s from session queue" % (server.CONFIG.PMS_NAME, stream['session_key'], stream['rating_key'])) monitor_process.delete_session( session_key=stream['session_key']) # Process the newly received session data for session in media_container: new_session = monitor_process.write_session(session) if new_session: logger.debug( u"Tautulli Monitor :: %s: Session %s started by user %s with ratingKey %s." % (server.CONFIG.PMS_NAME, session['session_key'], session['user_id'], session['rating_key'])) else: logger.debug( u"Tautulli Monitor :: %s: Unable to read session list." % server.CONFIG.PMS_NAME)
def process(self): if self.is_valid_session(): ap = activity_processor.ActivityProcessor(server=self.server) db_session = ap.get_session_by_key(session_key=self.get_session_key()) this_state = self.timeline['state'] this_rating_key = str(self.timeline['ratingKey']) this_key = self.timeline['key'] this_transcode_key = self.timeline.get('transcodeSession', '') # Get the live tv session uuid this_live_uuid = this_key.split('/')[-1] if this_key.startswith('/livetv/sessions') else None # If we already have this session in the temp table, check for state changes if db_session: # Re-schedule the callback to reset the 5 minutes timer schedule_callback('session_key-{}-{}'.format(self.server.CONFIG.ID, self.get_session_key()), func=force_stop_stream, args=[self.get_session_key(), db_session['full_title'], db_session['user'], self.server.CONFIG.PMS_NAME, self.server], minutes=5) last_state = db_session['state'] last_rating_key = str(db_session['rating_key']) last_live_uuid = db_session['live_uuid'] last_transcode_key = db_session['transcode_key'].split('/')[-1] last_paused = db_session['last_paused'] buffer_count = db_session['buffer_count'] # Make sure the same item is being played if this_rating_key == last_rating_key or this_live_uuid == last_live_uuid: # Update the session state and viewOffset if this_state == 'playing': # Update the session in our temp session table # if the last set temporary stopped time exceeds 60 seconds if int(time.time()) - db_session['stopped'] > 60: self.update_db_session() # Start our state checks if this_state != last_state: if this_state == 'paused': self.on_pause() elif last_paused and this_state == 'playing': self.on_resume() elif this_state == 'stopped': self.on_stop() elif this_state == 'paused': # Update the session last_paused timestamp self.on_pause(still_paused=True) if this_state == 'buffering': self.on_buffer() if this_transcode_key != last_transcode_key and this_state != 'stopped': self.on_change() # If a client doesn't register stop events (I'm looking at you PHT!) check if the ratingKey has changed else: # Manually stop and start # Set force_stop so that we don't overwrite our last viewOffset self.on_stop(force_stop=True) self.on_start() # Monitor if the stream has reached the watch percentage for notifications # The only purpose of this is for notifications if not db_session['watched'] and this_state != 'buffering': progress_percent = helpers.get_percent(self.timeline['viewOffset'], db_session['duration']) watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT, 'episode': plexpy.CONFIG.TV_WATCHED_PERCENT, 'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT, 'clip': plexpy.CONFIG.TV_WATCHED_PERCENT } if progress_percent >= watched_percent.get(db_session['media_type'], 101): logger.debug(u"Tautulli ActivityHandler :: %s: Session %s watched." % (self.server.CONFIG.PMS_NAME, str(self.get_session_key()))) ap.set_watched(session_key=self.get_session_key()) watched_notifiers = notification_handler.get_notify_state_enabled( session=db_session, notify_action='on_watched', notified=False) for d in watched_notifiers: plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notifier_id': d['notifier_id'], 'notify_action': 'on_watched'}) else: # We don't have this session in our table yet, start a new one. if this_state != 'buffering': self.on_start()
def set_session_state(self): ap = activity_processor.ActivityProcessor(server=self.server) ap.set_session_state(session_key=self.get_session_key(), state=self.timeline['state'], view_offset=self.timeline['viewOffset'], stopped=int(time.time()))
def update_db_session(self): # Update our session temp table values monitor_proc = activity_processor.ActivityProcessor() monitor_proc.write_session(session=self.get_live_session(), notify=False)
def check_active_sessions(ws_request=False): with monitor_lock: pms_connect = pmsconnect.PmsConnect() session_list = pms_connect.get_current_activity() monitor_db = database.MonitorDatabase() monitor_process = activity_processor.ActivityProcessor() # logger.debug(u"PlexPy Monitor :: Checking for active streams.") global int_ping_count if session_list: if int_ping_count >= 3: logger.info( u"PlexPy Monitor :: The Plex Media Server is back up.") # Check if any notification agents have notifications enabled if any(d['on_intup'] for d in notifiers.available_notification_agents()): # Fire off notifications threading.Thread( target=notification_handler.notify_timeline, kwargs=dict(notify_action='intup')).start() int_ping_count = 0 media_container = session_list['sessions'] # Check our temp table for what we must do with the new streams db_streams = monitor_db.select('SELECT * FROM sessions') for stream in db_streams: if any(d['session_key'] == str(stream['session_key']) and d['rating_key'] == str(stream['rating_key']) for d in media_container): # The user's session is still active for session in media_container: if session['session_key'] == str(stream['session_key']) and \ session['rating_key'] == str(stream['rating_key']): # The user is still playing the same media item # Here we can check the play states if session['state'] != stream['state']: if session['state'] == 'paused': logger.debug( u"PlexPy Monitor :: Session %s has been paused." % stream['session_key']) # Check if any notification agents have notifications enabled if any(d['on_pause'] for d in notifiers. available_notification_agents()): # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread( target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='pause' )).start() if session['state'] == 'playing' and stream[ 'state'] == 'paused': logger.debug( u"PlexPy Monitor :: Session %s has been resumed." % stream['session_key']) # Check if any notification agents have notifications enabled if any(d['on_resume'] for d in notifiers. available_notification_agents()): # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread( target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='resume' )).start() if stream['state'] == 'paused' and not ws_request: # The stream is still paused so we need to increment the paused_counter # Using the set config parameter as the interval, probably not the most accurate but # it will have to do for now. If it's a websocket request don't use this method. paused_counter = int( stream['paused_counter'] ) + plexpy.CONFIG.MONITORING_INTERVAL monitor_db.action( 'UPDATE sessions SET paused_counter = ? ' 'WHERE session_key = ? AND rating_key = ?', [ paused_counter, stream['session_key'], stream['rating_key'] ]) if session[ 'state'] == 'buffering' and plexpy.CONFIG.BUFFER_THRESHOLD > 0: # The stream is buffering so we need to increment the buffer_count # We're going just increment on every monitor ping, # would be difficult to keep track otherwise monitor_db.action( 'UPDATE sessions SET buffer_count = buffer_count + 1 ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) # Check the current buffer count and last buffer to determine if we should notify buffer_values = monitor_db.select( 'SELECT buffer_count, buffer_last_triggered ' 'FROM sessions ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) if buffer_values[0][ 'buffer_count'] >= plexpy.CONFIG.BUFFER_THRESHOLD: # Push any notifications - # Push it on it's own thread so we don't hold up our db actions # Our first buffer notification if buffer_values[0][ 'buffer_count'] == plexpy.CONFIG.BUFFER_THRESHOLD: logger.info( u"PlexPy Monitor :: User '%s' has triggered a buffer warning." % stream['user']) # Set the buffer trigger time monitor_db.action( 'UPDATE sessions ' 'SET buffer_last_triggered = strftime("%s","now") ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) # Check if any notification agents have notifications enabled if any(d['on_buffer'] for d in notifiers. available_notification_agents( )): # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread( target=notification_handler. notify, kwargs=dict( stream_data=stream, notify_action='buffer' )).start() else: # Subsequent buffer notifications after wait time if int(time.time()) > buffer_values[0]['buffer_last_triggered'] + \ plexpy.CONFIG.BUFFER_WAIT: logger.info( u"PlexPy Monitor :: User '%s' has triggered multiple buffer warnings." % stream['user']) # Set the buffer trigger time monitor_db.action( 'UPDATE sessions ' 'SET buffer_last_triggered = strftime("%s","now") ' 'WHERE session_key = ? AND rating_key = ?', [ stream['session_key'], stream['rating_key'] ]) # Check if any notification agents have notifications enabled if any(d['on_buffer'] for d in notifiers. available_notification_agents( )): # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread( target=notification_handler .notify, kwargs=dict( stream_data=stream, notify_action='buffer' )).start() logger.debug( u"PlexPy Monitor :: Session %s is buffering. Count is now %s. Last triggered %s." % (stream['session_key'], buffer_values[0]['buffer_count'], buffer_values[0]['buffer_last_triggered']) ) # Check if the user has reached the offset in the media we defined as the "watched" percent # Don't trigger if state is buffer as some clients push the progress to the end when # buffering on start. if session['view_offset'] and session[ 'duration'] and session[ 'state'] != 'buffering': if helpers.get_percent( session['view_offset'], session['duration'] ) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT: # Check if any notification agents have notifications enabled if any(d['on_watched'] for d in notifiers. available_notification_agents()): # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread( target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='watched' )).start() else: # The user has stopped playing a stream if stream['state'] != 'stopped': logger.debug( u"PlexPy Monitor :: Session %s has stopped." % stream['session_key']) # Set the stream stop time stream['stopped'] = int(time.time()) monitor_db.action( 'UPDATE sessions SET stopped = ?, state = ? ' 'WHERE session_key = ? AND rating_key = ?', [ stream['stopped'], 'stopped', stream['session_key'], stream['rating_key'] ]) # Check if the user has reached the offset in the media we defined as the "watched" percent if stream['view_offset'] and stream['duration']: if helpers.get_percent( stream['view_offset'], stream['duration'] ) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT: # Check if any notification agents have notifications enabled if any(d['on_watched'] for d in notifiers. available_notification_agents()): # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread( target=notification_handler.notify, kwargs=dict( stream_data=stream, notify_action='watched')).start() # Check if any notification agents have notifications enabled if any(d['on_stop'] for d in notifiers.available_notification_agents()): # Push any notifications - Push it on it's own thread so we don't hold up our db actions threading.Thread( target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='stop')).start() # Write the item history on playback stop success = monitor_process.write_session_history( session=stream) if success: # If session is written to the databaase successfully, remove the session from the session table logger.debug( u"PlexPy Monitor :: Removing sessionKey %s ratingKey %s from session queue" % (stream['session_key'], stream['rating_key'])) monitor_db.action( 'DELETE FROM sessions WHERE session_key = ? AND rating_key = ?', [stream['session_key'], stream['rating_key']]) else: logger.warn(u"PlexPy Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \ "Will try again on the next pass." % (stream['session_key'], stream['rating_key'])) # Process the newly received session data for session in media_container: new_session = monitor_process.write_session(session) if new_session: logger.debug(u"PlexPy Monitor :: Session %s has started." % session['session_key']) else: logger.debug(u"PlexPy Monitor :: Unable to read session list.") int_ping_count += 1 logger.warn(u"PlexPy Monitor :: Unable to get an internal response from the server, ping attempt %s." \ % str(int_ping_count)) if int_ping_count == 3: # Check if any notification agents have notifications enabled if any(d['on_intdown'] for d in notifiers.available_notification_agents()): # Fire off notifications threading.Thread(target=notification_handler.notify_timeline, kwargs=dict(notify_action='intdown')).start()