diff --git a/PlexPy.py b/PlexPy.py index 4b4ad54d..4d8d953f 100755 --- a/PlexPy.py +++ b/PlexPy.py @@ -198,7 +198,7 @@ def main(): except: logger.warn(u"Websocket :: Unable to open connection.") # Fallback to polling - plexpy.CONFIG.MONITORING_USE_WEBSOCKET = 0 + plexpy.POLLING_FAILOVER = True plexpy.initialize_scheduler() # Open webbrowser diff --git a/data/interfaces/default/current_activity.html b/data/interfaces/default/current_activity.html index 00a96048..7a741026 100644 --- a/data/interfaces/default/current_activity.html +++ b/data/interfaces/default/current_activity.html @@ -19,7 +19,7 @@ session_key Returns a unique session id for the active stream rating_key Returns the unique identifier for the media item. media_index Returns the index of the media item. parent_media_index Returns the index of the media item's parent. -type Returns the type of session. Either 'track', 'episode' or 'movie'. +media_type Returns the type of session. Either 'track', 'episode' or 'movie'. thumb Returns the location of the item's thumbnail. Use with pms_image_proxy. bif_thumb Returns the location of the item's bif thumbnail. Use with pms_image_proxy. art Returns the location of the item's artwork @@ -67,21 +67,21 @@ DOCUMENTATION :: END % if data['stream_count'] != '0': % for a in data['sessions']:
- % if a['type'] == 'movie' or a['type'] == 'episode' or a['type'] == 'track': + % if a['media_type'] == 'movie' or a['media_type'] == 'episode' or a['media_type'] == 'track': % endif
- % if a['type'] == 'movie' and not a['indexes']: + % if a['media_type'] == 'movie' and not a['indexes']:
- % elif a['type'] == 'episode' and not a['indexes']: + % elif a['media_type'] == 'episode' and not a['indexes']:
% elif a['indexes']: % else: - % if a['type'] == 'track': + % if a['media_type'] == 'track':
- % elif a['type'] == 'clip': + % elif a['media_type'] == 'clip': % if a['art'][:4] == 'http':
% elif a['thumb'][:4] == 'http': @@ -93,7 +93,7 @@ DOCUMENTATION :: END
% endif % endif - % elif a['type'] == 'photo': + % elif a['media_type'] == 'photo':
% else:
@@ -116,7 +116,7 @@ DOCUMENTATION :: END State  Buffering % endif
- % if a['type'] == 'track': + % if a['media_type'] == 'track': % if a['audio_decision'] == 'direct play': Stream  Direct Play % elif a['audio_decision'] == 'copy': @@ -137,7 +137,7 @@ DOCUMENTATION :: END % elif a['audio_decision'] != 'transcode': Audio  Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch) % endif - % elif a['type'] == 'episode' or a['type'] == 'movie' or a['type'] == 'clip': + % elif a['media_type'] == 'episode' or a['media_type'] == 'movie' or a['media_type'] == 'clip': % if a['video_decision'] == 'direct play' and a['audio_decision'] == 'direct play': Stream  Direct Play % elif a['video_decision'] == 'copy' and a['audio_decision'] == 'copy': @@ -166,7 +166,7 @@ DOCUMENTATION :: END % elif a['audio_decision'] == 'transcode': Audio  Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch) % endif - % elif a['type'] == 'photo': + % elif a['media_type'] == 'photo': % if a['video_decision'] == 'direct play': Stream  Direct Play % elif a['video_decision'] == 'copy': @@ -184,15 +184,15 @@ DOCUMENTATION :: END
- % if a['type'] != 'photo': + % if a['media_type'] != 'photo':
- ${a['progress']}/${a['duration']} + ${a['view_offset']}/${a['duration']}
% endif - % if a['type'] == 'movie' or a['type'] == 'episode' or a['type'] == 'track': + % if a['media_type'] == 'movie' or a['media_type'] == 'episode' or a['media_type'] == 'track':
% endif
@@ -213,28 +213,28 @@ DOCUMENTATION :: END % elif a['state'] == 'buffering':   % endif - % if a['type'] == 'episode': + % if a['media_type'] == 'episode': ${a['grandparent_title']} - ${a['title']} - % elif a['type'] == 'movie': + % elif a['media_type'] == 'movie': ${a['title']} - % elif a['type'] == 'clip': + % elif a['media_type'] == 'clip': ${a['title']} - % elif a['type'] == 'track': + % elif a['media_type'] == 'track': ${a['grandparent_title']} - ${a['title']} - % elif a['type'] == 'photo': + % elif a['media_type'] == 'photo': ${a['parent_title']} % else: ${a['title']} % endif
- % if a['type'] == 'episode': + % if a['media_type'] == 'episode': S${a['parent_media_index']} · E${a['media_index']} - % elif a['type'] == 'movie': + % elif a['media_type'] == 'movie': ${a['year']} - % elif a['type'] == 'track': + % elif a['media_type'] == 'track': ${a['parent_title']} - % elif a['type'] == 'photo': + % elif a['media_type'] == 'photo': ${a['title']} % else: ${a['year']} diff --git a/plexpy/__init__.py b/plexpy/__init__.py index 202687c2..8f497d50 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -361,7 +361,7 @@ def dbcheck(): 'audio_channels INTEGER, transcode_protocol TEXT, transcode_container TEXT, ' 'transcode_video_codec TEXT, transcode_audio_codec TEXT, transcode_audio_channels INTEGER,' 'transcode_width INTEGER, transcode_height INTEGER, buffer_count INTEGER DEFAULT 0, ' - 'buffer_last_triggered INTEGER)' + 'buffer_last_triggered INTEGER, last_paused INTEGER)' ) # session_history table :: This is a history table which logs essential stream details @@ -609,6 +609,15 @@ def dbcheck(): 'ALTER TABLE users ADD COLUMN custom_avatar_url TEXT' ) + # Upgrade sessions table from earlier versions + try: + c_db.execute('SELECT last_paused from sessions') + except sqlite3.OperationalError: + logger.debug(u"Altering database. Updating database table sessions.") + c_db.execute( + 'ALTER TABLE sessions ADD COLUMN last_paused INTEGER' + ) + # Add "Local" user to database as default unauthenticated user. result = c_db.execute('SELECT id FROM users WHERE username = "Local"') if not result.fetchone(): diff --git a/plexpy/activity_handler.py b/plexpy/activity_handler.py new file mode 100644 index 00000000..a3bffeb5 --- /dev/null +++ b/plexpy/activity_handler.py @@ -0,0 +1,160 @@ +# This file is part of PlexPy. +# +# PlexPy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PlexPy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PlexPy. If not, see . + +import time +from plexpy import logger, datafactory, pmsconnect, monitor, threading, notification_handler + + +class ActivityHandler(object): + + def __init__(self, timeline): + self.timeline = timeline + + def is_valid_session(self): + if 'sessionKey' in self.timeline: + if str(self.timeline['sessionKey']).isdigit(): + return True + + return False + + def get_session_key(self): + if self.is_valid_session(): + return int(self.timeline['sessionKey']) + + return None + + def get_live_session(self): + pms_connect = pmsconnect.PmsConnect() + session_list = pms_connect.get_current_activity() + + for session in session_list['sessions']: + if int(session['session_key']) == self.get_session_key(): + return session + + return None + + def update_db_session(self): + # Update our session temp table values + monitor_proc = monitor.MonitorProcessing() + monitor_proc.write_session(self.get_live_session()) + + def on_start(self): + if self.is_valid_session(): + logger.debug(u"PlexPy ActivityHandler :: Session %s has started." % str(self.get_session_key())) + + # Fire off notifications + threading.Thread(target=notification_handler.notify, + kwargs=dict(stream_data=self.get_live_session(), notify_action='play')).start() + + # Write the new session to our temp session table + self.update_db_session() + + def on_stop(self): + 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 + data_factory = datafactory.DataFactory() + data_factory.set_session_last_paused(session_key=self.get_session_key(), timestamp=None) + + # Update the session state and viewOffset + data_factory.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 = data_factory.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='stop')).start() + + # Write it to the history table + monitor_proc = monitor.MonitorProcessing() + monitor_proc.write_session_history(session=db_session) + + # Remove the session from our temp session table + data_factory.delete_session(session_key=self.get_session_key()) + + def on_buffer(self): + pass + + 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 + data_factory = datafactory.DataFactory() + data_factory.set_session_last_paused(session_key=self.get_session_key(), timestamp=int(time.time())) + + # Update the session state and viewOffset + data_factory.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 = data_factory.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, time_line=None): + 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 + data_factory = datafactory.DataFactory() + data_factory.set_session_last_paused(session_key=self.get_session_key(), timestamp=None) + + # Update the session state and viewOffset + data_factory.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 = data_factory.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='resume')).start() + + # This function receives events from our websocket connection + def process(self): + if self.is_valid_session(): + data_factory = datafactory.DataFactory() + db_session = data_factory.get_session_by_key(session_key=self.get_session_key()) + + # If we already have this session in the temp table, check for state changes + if db_session: + this_state = self.timeline['state'] + last_state = db_session['state'] + + if this_state != last_state: + # logger.debug(u"PlexPy ActivityHandler :: Last state %s :: Current state %s" % + # (last_state, this_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() + # else: + # logger.debug(u"PlexPy ActivityHandler :: Session %s state has not changed." % + # self.get_session_key()) + else: + # We don't have this session in our table yet, start a new one. + # logger.debug(u"PlexPy ActivityHandler :: Session %s has started." % self.get_session_key()) + self.on_start() \ No newline at end of file diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index 40e57092..6fa10a9a 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -778,3 +778,66 @@ class DataFactory(object): return 'Deleted all items for user_id %s.' % user_id else: return 'Unable to delete items. Input user_id not valid.' + + def get_session_by_key(self, session_key=None): + monitor_db = database.MonitorDatabase() + + if str(session_key).isdigit(): + result = 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 WHERE session_key = ? LIMIT 1', args=[session_key]) + for session in result: + if session: + return session + + return None + + def set_session_state(self, session_key=None, state=None, view_offset=0): + monitor_db = database.MonitorDatabase() + + if str(session_key).isdigit() and str(view_offset).isdigit(): + values = {'view_offset': int(view_offset)} + if state: + values['state'] = state + + keys = {'session_key': session_key} + result = monitor_db.upsert('sessions', values, keys) + + return result + + return None + + def delete_session(self, session_key=None): + monitor_db = database.MonitorDatabase() + + if str(session_key).isdigit(): + monitor_db.action('DELETE FROM sessions WHERE session_key = ?', [session_key]) + + def set_session_last_paused(self, session_key=None, timestamp=None): + import time + + if str(session_key).isdigit(): + monitor_db = database.MonitorDatabase() + + result = monitor_db.select('SELECT last_paused, paused_counter ' + 'FROM sessions ' + 'WHERE session_key = ?', args=[session_key]) + + paused_counter = 0 + for session in result: + if session['last_paused']: + paused_offset = int(time.time()) - int(session['last_paused']) + paused_counter = int(session['paused_counter']) + int(paused_offset) + + values = {'state': 'playing', + 'last_paused': timestamp, + 'paused_counter': paused_counter} + keys = {'session_key': session_key} + monitor_db.upsert('sessions', values, keys) \ No newline at end of file diff --git a/plexpy/monitor.py b/plexpy/monitor.py index d6b68c24..571bc36b 100644 --- a/plexpy/monitor.py +++ b/plexpy/monitor.py @@ -22,7 +22,8 @@ import time monitor_lock = threading.Lock() -def check_active_sessions(): + +def check_active_sessions(ws_request=False): with monitor_lock: pms_connect = pmsconnect.PmsConnect() @@ -42,7 +43,8 @@ def check_active_sessions(): '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 ' + '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']) @@ -59,19 +61,22 @@ def check_active_sessions(): # 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': + + 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. + # 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, @@ -160,21 +165,6 @@ def check_active_sessions(): logger.debug(u"PlexPy Monitor :: Unable to read session list.") -def get_last_state_by_session(session_key=None): - monitor_db = database.MonitorDatabase() - - if str(session_key).isdigit(): - logger.debug(u"PlexPy Monitor :: Checking state for sessionKey %s..." % str(session_key)) - query = 'SELECT state FROM sessions WHERE session_key = ? LIMIT 1' - result = monitor_db.select(query, args=[session_key]) - - if result: - return result[0] - - logger.debug(u"PlexPy Monitor :: No session with key %s is active." % str(session_key)) - return False - - class MonitorProcessing(object): def __init__(self): @@ -184,7 +174,7 @@ class MonitorProcessing(object): values = {'session_key': session['session_key'], 'rating_key': session['rating_key'], - 'media_type': session['type'], + 'media_type': session['media_type'], 'state': session['state'], 'user_id': session['user_id'], 'user': session['user'], @@ -197,7 +187,7 @@ class MonitorProcessing(object): 'platform': session['platform'], 'parent_rating_key': session['parent_rating_key'], 'grandparent_rating_key': session['grandparent_rating_key'], - 'view_offset': session['progress'], + 'view_offset': session['view_offset'], 'duration': session['duration'], 'video_decision': session['video_decision'], 'audio_decision': session['audio_decision'], diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py index 0fa2e061..3b58adb5 100644 --- a/plexpy/pmsconnect.py +++ b/plexpy/pmsconnect.py @@ -705,9 +705,9 @@ class PmsConnect(object): 'transcode_container': transcode_container, 'transcode_protocol': transcode_protocol, 'duration': duration, - 'progress': progress, + 'view_offset': progress, 'progress_percent': str(helpers.get_percent(progress, duration)), - 'type': 'track', + 'media_type': 'track', 'indexes': 0 } @@ -826,14 +826,14 @@ class PmsConnect(object): 'transcode_container': transcode_container, 'transcode_protocol': transcode_protocol, 'duration': duration, - 'progress': progress, + 'view_offset': progress, 'progress_percent': str(helpers.get_percent(progress, duration)), 'indexes': use_indexes } if helpers.get_xml_attr(session, 'ratingKey').isdigit(): - session_output['type'] = helpers.get_xml_attr(session, 'type') + session_output['media_type'] = helpers.get_xml_attr(session, 'type') else: - session_output['type'] = 'clip' + session_output['media_type'] = 'clip' elif helpers.get_xml_attr(session, 'type') == 'movie': session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'), @@ -882,14 +882,14 @@ class PmsConnect(object): 'transcode_container': transcode_container, 'transcode_protocol': transcode_protocol, 'duration': duration, - 'progress': progress, + 'view_offset': progress, 'progress_percent': str(helpers.get_percent(progress, duration)), 'indexes': use_indexes } if helpers.get_xml_attr(session, 'ratingKey').isdigit(): - session_output['type'] = helpers.get_xml_attr(session, 'type') + session_output['media_type'] = helpers.get_xml_attr(session, 'type') else: - session_output['type'] = 'clip' + session_output['media_type'] = 'clip' elif helpers.get_xml_attr(session, 'type') == 'clip': session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'), @@ -938,9 +938,9 @@ class PmsConnect(object): 'transcode_container': transcode_container, 'transcode_protocol': transcode_protocol, 'duration': duration, - 'progress': progress, + 'view_offset': progress, 'progress_percent': str(helpers.get_percent(progress, duration)), - 'type': helpers.get_xml_attr(session, 'type'), + 'media_type': helpers.get_xml_attr(session, 'type'), 'indexes': 0 } @@ -1027,9 +1027,9 @@ class PmsConnect(object): 'transcode_container': transcode_container, 'transcode_protocol': transcode_protocol, 'duration': '', - 'progress': '', + 'view_offset': '', 'progress_percent': '100', - 'type': 'photo', + 'media_type': 'photo', 'indexes': 0 } @@ -1083,7 +1083,6 @@ class PmsConnect(object): } children_list.append(children_output) - output = {'children_count': helpers.get_xml_attr(xml_head[0], 'size'), 'children_type': helpers.get_xml_attr(xml_head[0], 'viewGroup'), 'title': helpers.get_xml_attr(xml_head[0], 'title2'), diff --git a/plexpy/web_socket.py b/plexpy/web_socket.py index f8ee16bd..62536a6e 100644 --- a/plexpy/web_socket.py +++ b/plexpy/web_socket.py @@ -15,7 +15,7 @@ # Mostly borrowed from https://github.com/trakt/Plex-Trakt-Scrobbler -from plexpy import logger +from plexpy import logger, monitor import threading import plexpy @@ -28,6 +28,9 @@ opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) def start_thread(): + # Check for any existing sessions on start up + monitor.check_active_sessions(ws_request=True) + # Start the websocket listener on it's own thread threading.Thread(target=run).start() @@ -53,9 +56,9 @@ def run(): ws = create_connection(uri) reconnects = 0 ws_connected = True - logger.debug(u'PlexPy WebSocket :: Ready') + logger.info(u'PlexPy WebSocket :: Ready') except IOError, e: - logger.info(u'PlexPy WebSocket :: %s.' % e) + logger.error(u'PlexPy WebSocket :: %s.' % e) reconnects += 1 time.sleep(5) @@ -73,7 +76,7 @@ def run(): if reconnects > 1: time.sleep(2 * (reconnects - 1)) - logger.info(u'PlexPy WebSocket :: Connection has closed, reconnecting...') + logger.warn(u'PlexPy WebSocket :: Connection has closed, reconnecting...') try: ws = create_connection(uri) except IOError, e: @@ -108,7 +111,7 @@ def receive(ws): def process(opcode, data): - from plexpy import monitor + from plexpy import activity_handler if opcode not in opcode_data: return False @@ -126,19 +129,14 @@ def process(opcode, data): return False if type == 'playing': - logger.debug('%s.playing %s' % (name, info)) + # 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 - last_session_state = monitor.get_last_state_by_session(time_line[0]['sessionKey']) - session_state = time_line[0]['state'] - - if last_session_state != session_state: - monitor.check_active_sessions() - else: - logger.debug(u"PlexPy WebSocket :: Session %s state has not changed." % time_line[0]['sessionKey']) + activity = activity_handler.ActivityHandler(timeline=time_line[0]) + activity.process() return True