def on_disconnect(): if plexpy.PLEX_SERVER_UP is None: plexpy.PLEX_SERVER_UP = False if plexpy.PLEX_SERVER_UP: logger.info( "Tautulli WebSocket :: Unable to get a response from the server, Plex server is down." ) plexpy.PLEX_SERVER_UP = False logger.debug( "Tautulli WebSocket :: Scheduling Plex server down callback in %d seconds.", plexpy.CONFIG.NOTIFY_SERVER_CONNECTION_THRESHOLD) activity_handler.schedule_callback( 'on_intdown', func=on_intdown, seconds=plexpy.CONFIG.NOTIFY_SERVER_CONNECTION_THRESHOLD) activity_processor.ActivityProcessor().set_temp_stopped() plexpy.initialize_scheduler()
def on_resume(self): if self.is_valid_session(): logger.debug("Tautulli ActivityHandler :: Session %s 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 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_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 force_stop_stream(session_key, title, user): ap = activity_processor.ActivityProcessor() 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( "Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue" % (session['session_key'], session['rating_key'])) ap.delete_session(row_id=row_id) delete_metadata_cache(session_key) else: session['write_attempts'] += 1 if session['write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS: logger.warn("Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \ "Will try again in 30 seconds. Write attempt %s." % (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(session_key), func=force_stop_stream, args=[session_key, session['full_title'], session['user']], seconds=30) else: logger.warn("Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \ "Removing session from the database. Write attempt %s." % (session['session_key'], session['rating_key'], str(session['write_attempts']))) logger.info( "Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue" % (session['session_key'], session['rating_key'])) ap.delete_session(session_key=session_key) delete_metadata_cache(session_key)
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_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.get_session_key()), func=force_stop_stream, args=[ self.get_session_key(), db_session['full_title'], db_session['user'] ], 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'] last_rating_key_websocket = db_session['rating_key_websocket'] last_guid = db_session['guid'] this_guid = last_guid # Check guid for live TV metadata every 60 seconds if db_session['live'] and helpers.timestamp( ) - db_session['stopped'] > 60: metadata = self.get_metadata(skip_cache=True) if metadata: this_guid = metadata['guid'] # Make sure the same item is being played if (this_rating_key == last_rating_key or this_rating_key == last_rating_key_websocket or this_live_uuid == last_live_uuid) \ and this_guid == last_guid: # 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 helpers.timestamp() - 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 == 'error': self.on_error() 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( "Tautulli ActivityHandler :: Session %s watched." % 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() ap.set_session_state(session_key=self.get_session_key(), state=self.timeline['state'], view_offset=self.timeline['viewOffset'], stopped=helpers.timestamp())
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: stream['write_attempts'] += 1 if stream[ 'write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS: logger.warn(u"PlexPy Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \ "Will try again on the next pass. Write attempt %s." % (stream['session_key'], stream['rating_key'], str(stream['write_attempts']))) monitor_db.action( 'UPDATE sessions SET write_attempts = ? ' 'WHERE session_key = ? AND rating_key = ?', [ stream['write_attempts'], stream['session_key'], stream['rating_key'] ]) else: logger.warn(u"PlexPy Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \ "Removing session from the database. Write attempt %s." % (stream['session_key'], stream['rating_key'], str(stream['write_attempts']))) 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']]) # 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 with ratingKey %s." % (session['session_key'], session['rating_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()
def import_from_plexivity(database_file=None, table_name=None, import_ignore_interval=0): try: connection = sqlite3.connect(database_file, timeout=20) connection.row_factory = sqlite3.Row except sqlite3.OperationalError: logger.error("Tautulli Importer :: Invalid filename.") return None except ValueError: logger.error("Tautulli Importer :: Invalid filename.") return None try: connection.execute('SELECT xml from %s' % table_name) except sqlite3.OperationalError: logger.error("Tautulli Importer :: Database specified does not contain the required fields.") return None logger.debug("Tautulli Importer :: Plexivity data import in progress...") database.set_is_importing(True) ap = activity_processor.ActivityProcessor() user_data = users.Users() # Get the latest friends list so we can pull user id's try: users.refresh_users() except: logger.debug("Tautulli Importer :: Unable to refresh the users list. Aborting import.") return None query = 'SELECT id AS id, ' \ 'time AS started, ' \ 'stopped, ' \ 'null AS user_id, ' \ 'user, ' \ 'ip_address, ' \ 'paused_counter, ' \ 'platform AS player, ' \ 'null AS platform, ' \ 'null as machine_id, ' \ 'null AS media_type, ' \ 'null AS view_offset, ' \ 'xml, ' \ 'rating as content_rating,' \ 'summary,' \ 'title AS full_title,' \ '(case when orig_title_ep = "n/a" then orig_title else ' \ 'orig_title_ep end) as title,' \ '(case when orig_title_ep != "n/a" 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 Plexivity db xml field. extracted_xml = extract_plexivity_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("Tautulli Importer :: Skipping record with id %s due to malformed xml." % str(row['id'])) continue # Skip line if we don't have a ratingKey to work with #if not row['rating_key']: # logger.error("Tautulli Importer :: Skipping record due to null ratingKey.") # 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': arrow.get(row['started']).timestamp(), 'stopped': arrow.get(row['stopped']).timestamp(), 'rating_key': extracted_xml['rating_key'], 'title': row['title'], 'parent_title': extracted_xml['parent_title'], 'grandparent_title': row['grandparent_title'], 'original_title': extracted_xml['original_title'], 'full_title': row['full_title'], 'user_id': user_id, 'user': row['user'], 'ip_address': row['ip_address'] if row['ip_address'] else extracted_xml['ip_address'], 'paused_counter': row['paused_counter'], 'player': row['player'], 'platform': extracted_xml['platform'], 'machine_id': extracted_xml['machine_id'], 'parent_rating_key': extracted_xml['parent_rating_key'], 'grandparent_rating_key': extracted_xml['grandparent_rating_key'], 'media_type': extracted_xml['media_type'], 'view_offset': extracted_xml['view_offset'], 'section_id': extracted_xml['section_id'], 'video_decision': extracted_xml['video_decision'], 'audio_decision': extracted_xml['audio_decision'], 'transcode_decision': extracted_xml['transcode_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': extracted_xml['rating_key'], 'parent_rating_key': extracted_xml['parent_rating_key'], 'grandparent_rating_key': extracted_xml['grandparent_rating_key'], 'title': row['title'], 'parent_title': extracted_xml['parent_title'], 'grandparent_title': row['grandparent_title'], 'original_title': extracted_xml['original_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'], 'directors': extracted_xml['directors'], 'writers': extracted_xml['writers'], 'actors': extracted_xml['actors'], 'genres': extracted_xml['genres'], 'studio': extracted_xml['studio'], 'labels': extracted_xml['labels'], 'full_title': row['full_title'], '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'] } # 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("Tautulli Importer :: Item has bad rating_key: %s" % session_history_metadata['rating_key']) import_users() logger.debug("Tautulli Importer :: Plexivity data import complete.") database.set_is_importing(False)
def update_db_session(self, session=None): # Update our session temp table values monitor_proc = activity_processor.ActivityProcessor() monitor_proc.write_session(session=session, notify=False)
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: # Re-schedule the callback to reset the 5 minutes timer schedule_callback('session_key-{}'.format( self.get_session_key()), func=force_stop_stream, args=[self.get_session_key()], minutes=5) 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': # Update the session in our temp session table # if the last set temporary stopped time exceeds 15 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_state == 'paused' and this_state == 'playing': self.on_resume() elif this_state == 'stopped': self.on_stop() elif this_state == 'buffering': self.on_buffer() elif this_state == 'paused': # Update the session last_paused timestamp self.on_pause(still_paused=True) # 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 :: Session %s watched." % 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() # Schedule a callback to force stop a stale stream 5 minutes later schedule_callback('session_key-{}'.format( self.get_session_key()), func=force_stop_stream, args=[self.get_session_key()], minutes=5)
def check_active_sessions(ws_request=False): with monitor_lock: monitor_db = database.MonitorDatabase() monitor_process = activity_processor.ActivityProcessor() db_streams = monitor_process.get_sessions() # Clear the metadata cache for stream in db_streams: activity_handler.delete_metadata_cache(stream['session_key']) pms_connect = pmsconnect.PmsConnect() session_list = pms_connect.get_current_activity() logger.debug(u"Tautulli 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 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 :: Session %s paused." % 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 :: Session %s resumed." % 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 :: 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'] ]) 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 :: 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'] ]) plexpy.NOTIFY_QUEUE.put({ 'stream_data': stream.copy(), 'notify_action': 'on_buffer' }) logger.debug( u"Tautulli 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['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 :: Session %s stopped." % 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 :: Removing sessionKey %s ratingKey %s from session queue" % (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 :: Failed to write sessionKey %s ratingKey %s to the database. " \ "Will try again on the next pass. Write attempt %s." % (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 :: Failed to write sessionKey %s ratingKey %s to the database. " \ "Removing session from the database. Write attempt %s." % (stream['session_key'], stream['rating_key'], str(stream['write_attempts']))) logger.debug( u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue" % (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 :: Session %s started by user %s with ratingKey %s." % (session['session_key'], session['user_id'], session['rating_key'])) else: logger.debug(u"Tautulli Monitor :: Unable to read session list.")
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()