diff --git a/data/interfaces/default/current_activity_instance.html b/data/interfaces/default/current_activity_instance.html index b7bfa25c..608810ba 100644 --- a/data/interfaces/default/current_activity_instance.html +++ b/data/interfaces/default/current_activity_instance.html @@ -162,7 +162,15 @@ DOCUMENTATION :: END
  • Optimized
    - ${data['optimized_version_profile']} + ${data['optimized_version_profile']} (${data['optimized_version_title']}) +
    +
  • + % endif + % if data['synced_version'] == 1: +
  • +
    Synced
    +
    + ${data['synced_version_profile']}
  • % endif @@ -185,7 +193,7 @@ DOCUMENTATION :: END % elif data['transcode_decision'] == 'copy': Direct Stream % else: - Direct Play ${'(Synced)' if data['synced_version'] == 1 else ''} + Direct Play % endif @@ -250,7 +258,7 @@ DOCUMENTATION :: END % elif data['stream_subtitle_decision'] == 'burn': Burn (${data['subtitle_codec'].upper()}) % else: - Direct Play (${data['subtitle_codec'].upper()}) + Direct Play (${data['stream_subtitle_codec'].upper() if data['synced_version'] else data['subtitle_codec'].upper()}) % endif % else: None diff --git a/data/interfaces/default/index.html b/data/interfaces/default/index.html index a76e48b1..6d722f98 100644 --- a/data/interfaces/default/index.html +++ b/data/interfaces/default/index.html @@ -257,7 +257,7 @@ if (!(current_activity)) { % if _session['user_group'] == 'admin': - var msg_settings = ' Verify your server in the settings.'; + var msg_settings = ' Verify your server connection in the settings.'; % else: var msg_settings = '' % endif @@ -357,7 +357,7 @@ } else if (s.transcode_decision === 'copy') { transcode_decision = 'Direct Stream'; } else { - transcode_decision = 'Direct Play' + ((s.synced_version == 1) ? ' (Synced)' : ''); + transcode_decision = 'Direct Play'; } $('#transcode_decision-' + key).html(transcode_decision); @@ -434,7 +434,7 @@ } else if (s.stream_subtitle_decision === 'burn') { subtitle_decision = 'Burn (' + s.subtitle_codec.toUpperCase() + ')'; } else { - subtitle_decision = 'Direct Play (' + s.subtitle_codec.toUpperCase() + ')'; + subtitle_decision = 'Direct Play (' + ((s.synced_version == '1') ? s.stream_subtitle_codec.toUpperCase() : s.subtitle_codec.toUpperCase()) + ')'; } } $('#subtitle_decision-' + key).html(subtitle_decision); @@ -453,7 +453,8 @@ } else { $('#stream_quality-' + key).html(s.quality_profile); } - $('#optimized_version-' + key).html(s.optimized_version_profile); + $('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')'); + $('#synced_quality_profile-' + key).html(s.synced_quality_profile); if (s.media_type != 'photo' && parseInt(s.bandwidth)) { var bw = parseInt(s.bandwidth); diff --git a/data/interfaces/default/stream_data.html b/data/interfaces/default/stream_data.html index a890efe0..548845d9 100644 --- a/data/interfaces/default/stream_data.html +++ b/data/interfaces/default/stream_data.html @@ -98,14 +98,14 @@ DOCUMENTATION :: END Optimized Version - - ${data['optimized_version_profile']} + ${data['optimized_version_profile']}
    (${data['optimized_version_title']}) % endif % if data['synced_version'] == 1: Synced Version - - yes + ${data['synced_version_profile']} % endif diff --git a/plexpy/__init__.py b/plexpy/__init__.py index 47ffe411..ec243428 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -450,7 +450,8 @@ def dbcheck(): 'transcode_protocol TEXT, transcode_container TEXT, ' 'transcode_video_codec TEXT, transcode_audio_codec TEXT, transcode_audio_channels INTEGER,' 'transcode_width INTEGER, transcode_height INTEGER, ' - 'optimized_version INTEGER, optimized_version_profile TEXT, synced_version INTEGER, ' + 'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title TEXT, ' + 'synced_version INTEGER, synced_version_profile TEXT, ' 'buffer_count INTEGER DEFAULT 0, buffer_last_triggered INTEGER, last_paused INTEGER, write_attempts INTEGER DEFAULT 0, ' 'raw_stream_info TEXT)' ) @@ -482,7 +483,8 @@ def dbcheck(): 'stream_video_framerate TEXT, ' 'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, ' 'stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, stream_subtitle_container TEXT, stream_subtitle_forced INTEGER, ' - 'subtitles INTEGER, subtitle_codec TEXT, synced_version INTEGER, optimized_version INTEGER, optimized_version_profile TEXT)' + 'subtitles INTEGER, subtitle_codec TEXT, synced_version INTEGER, synced_version_profile TEXT, ' + 'optimized_version INTEGER, optimized_version_profile TEXT, optimized_version_title TEXT)' ) # session_history_metadata table :: This is a table which logs each session's media metadata @@ -892,6 +894,15 @@ def dbcheck(): 'ALTER TABLE sessions ADD COLUMN raw_stream_info TEXT' ) + # Upgrade sessions table from earlier versions + try: + c_db.execute('SELECT video_height FROM sessions') + except sqlite3.OperationalError: + logger.debug(u"Altering database. Updating database table sessions.") + c_db.execute( + 'ALTER TABLE sessions ADD COLUMN video_height INTEGER' + ) + # Upgrade sessions table from earlier versions try: c_db.execute('SELECT subtitles FROM sessions') @@ -900,13 +911,17 @@ def dbcheck(): c_db.execute( 'ALTER TABLE sessions ADD COLUMN subtitles INTEGER' ) + # Upgrade sessions table from earlier versions try: - c_db.execute('SELECT video_height FROM sessions') + c_db.execute('SELECT synced_version_profile FROM sessions') except sqlite3.OperationalError: logger.debug(u"Altering database. Updating database table sessions.") c_db.execute( - 'ALTER TABLE sessions ADD COLUMN video_height INTEGER' + 'ALTER TABLE sessions ADD COLUMN synced_version_profile TEXT' + ) + c_db.execute( + 'ALTER TABLE sessions ADD COLUMN optimized_version_title TEXT' ) # Upgrade session_history table from earlier versions @@ -1139,6 +1154,18 @@ def dbcheck(): 'ALTER TABLE session_history_media_info ADD COLUMN subtitle_codec TEXT ' ) + # Upgrade session_history_media_info table from earlier versions + try: + c_db.execute('SELECT synced_version_profile FROM session_history_media_info') + except sqlite3.OperationalError: + logger.debug(u"Altering database. Updating database table session_history_media_info.") + c_db.execute( + 'ALTER TABLE session_history_media_info ADD COLUMN synced_version_profile TEXT ' + ) + c_db.execute( + 'ALTER TABLE session_history_media_info ADD COLUMN optimized_version_title TEXT ' + ) + # Upgrade users table from earlier versions try: c_db.execute('SELECT do_notify FROM users') diff --git a/plexpy/activity_processor.py b/plexpy/activity_processor.py index b62cd4fc..87bccc90 100644 --- a/plexpy/activity_processor.py +++ b/plexpy/activity_processor.py @@ -90,9 +90,11 @@ class ActivityProcessor(object): 'transcode_audio_channels': session.get('transcode_audio_channels', ''), 'transcode_width': session.get('stream_video_width', ''), 'transcode_height': session.get('stream_video_height', ''), + 'synced_version': session.get('synced_version', ''), + 'synced_version_profile': session.get('synced_version_profile', ''), 'optimized_version': session.get('optimized_version', ''), 'optimized_version_profile': session.get('optimized_version_profile', ''), - 'synced_version': session.get('synced_version', ''), + 'optimized_version_title': session.get('optimized_version_title', ''), 'stream_bitrate': session.get('stream_bitrate', ''), 'stream_video_resolution': session.get('stream_video_resolution', ''), 'quality_profile': session.get('quality_profile', ''), @@ -110,6 +112,7 @@ class ActivityProcessor(object): 'stream_audio_channels': session.get('stream_audio_channels', ''), 'stream_subtitle_decision': session.get('stream_subtitle_decision', ''), 'stream_subtitle_codec': session.get('stream_subtitle_codec', ''), + 'subtitles': session.get('subtitles', ''), 'raw_stream_info': json.dumps(session), 'stopped': int(time.time()) } @@ -356,6 +359,8 @@ class ActivityProcessor(object): 'stream_subtitle_forced': session['stream_subtitle_forced'], 'subtitles': session['subtitles'], 'synced_version': session['synced_version'], + 'synced_version_profile': session['synced_version_profile'], + 'synced_version_title': session['synced_version_title'], 'optimized_version': session['optimized_version'], 'optimized_version_profile': session['optimized_version_profile'] } diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index ce9bbfa1..856645ac 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -872,8 +872,10 @@ class DataFactory(object): user_cond = 'AND %s.user_id = %s ' % (table, session.get_session_user_id()) if row_id: - query = 'SELECT bitrate, video_resolution, optimized_version, optimized_version_profile, synced_version, ' \ - 'container, video_codec, video_Bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \ + query = 'SELECT bitrate, video_resolution, ' \ + 'optimized_version, optimized_version_profile, optimized_version_title, ' \ + 'synced_version, synced_version_profile, ' \ + 'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \ 'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \ 'stream_bitrate, stream_video_resolution, quality_profile, stream_container_decision, stream_container, ' \ 'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \ @@ -887,8 +889,10 @@ class DataFactory(object): 'WHERE session_history_media_info.id = ? %s' % user_cond result = monitor_db.select(query, args=[row_id]) elif session_key: - query = 'SELECT bitrate, video_resolution, optimized_version, optimized_version_profile, synced_version, ' \ - 'container, video_codec, video_Bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \ + query = 'SELECT bitrate, video_resolution, ' \ + 'optimized_version, optimized_version_profile, optimized_version_title, ' \ + 'synced_version, synced_version_profile, ' \ + 'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \ 'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \ 'stream_bitrate, stream_video_resolution, quality_profile, stream_container_decision, stream_container, ' \ 'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \ @@ -909,7 +913,9 @@ class DataFactory(object): 'video_resolution': item['video_resolution'], 'optimized_version': item['optimized_version'], 'optimized_version_profile': item['optimized_version_profile'], + 'optimized_version_title': item['optimized_version_title'], 'synced_version': item['synced_version'], + 'synced_version_profile': item['synced_version_profile'], 'container': item['container'], 'video_codec': item['video_codec'], 'video_bitrate': item['video_bitrate'], diff --git a/plexpy/plextv.py b/plexpy/plextv.py index 80ae3b6b..3782fc1c 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -425,7 +425,7 @@ class PlexTV(object): return users_list - def get_synced_items(self, machine_id=None, user_id=None): + def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None, rating_key_filter=None): sync_list = self.get_plextv_sync_lists(machine_id) user_data = users.Users() @@ -446,9 +446,15 @@ class PlexTV(object): logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items.") else: for a in xml_head: - sync_id = helpers.get_xml_attr(a, 'id') client_id = helpers.get_xml_attr(a, 'clientIdentifier') + + # Filter by client_id + if client_id_filter and client_id_filter != client_id: + continue + + sync_id = helpers.get_xml_attr(a, 'id') sync_device = a.getElementsByTagName('Device') + for device in sync_device: device_user_id = helpers.get_xml_attr(device, 'userID') try: @@ -467,12 +473,23 @@ class PlexTV(object): device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt') # Filter by user_id - if user_id and user_id != device_user_id: + if user_id_filter and user_id_filter != device_user_id: continue for synced in a.getElementsByTagName('SyncItems'): sync_item = synced.getElementsByTagName('SyncItem') for item in sync_item: + + for location in item.getElementsByTagName('Location'): + clean_uri = helpers.get_xml_attr(location, 'uri').split('%2F') + + rating_key = next((clean_uri[(idx + 1) % len(clean_uri)] + for idx, item in enumerate(clean_uri) if item == 'metadata'), None) + + # Filter by rating_key + if rating_key_filter and rating_key_filter != rating_key: + continue + sync_id = helpers.get_xml_attr(item, 'id') sync_version = helpers.get_xml_attr(item, 'version') sync_root_title = helpers.get_xml_attr(item, 'rootTitle') @@ -501,12 +518,6 @@ class PlexTV(object): settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality') settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution') - for location in item.getElementsByTagName('Location'): - clean_uri = helpers.get_xml_attr(location, 'uri').split('%2F') - - rating_key = next((clean_uri[(idx + 1) % len(clean_uri)] - for idx, item in enumerate(clean_uri) if item == 'metadata'), None) - sync_details = {"device_name": helpers.sanitize(device_name), "platform": helpers.sanitize(device_platform), "username": helpers.sanitize(device_username), diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py index 4f255d9b..96f5e224 100644 --- a/plexpy/pmsconnect.py +++ b/plexpy/pmsconnect.py @@ -24,6 +24,7 @@ import helpers import http_handler import libraries import logger +import plextv import session import users @@ -590,7 +591,7 @@ class PmsConnect(object): return output - def get_metadata_details(self, rating_key=''): + def get_metadata_details(self, rating_key='', sync_id=''): """ Return processed and validated metadata list for requested item. @@ -598,7 +599,10 @@ class PmsConnect(object): Output: array """ - metadata = self.get_metadata(str(rating_key), output_format='xml') + if rating_key: + metadata = self.get_metadata(str(rating_key), output_format='xml') + elif sync_id: + metadata = self.get_sync_item(str(sync_id), output_format='xml') try: xml_head = metadata.getElementsByTagName('MediaContainer') @@ -1322,6 +1326,7 @@ class PmsConnect(object): # Get the source media type media_type = helpers.get_xml_attr(session, 'type') + rating_key = helpers.get_xml_attr(session, 'ratingKey') # Get the user details user_info = session.getElementsByTagName('User')[0] @@ -1348,7 +1353,7 @@ class PmsConnect(object): 'product_version': helpers.get_xml_attr(player_info, 'version'), 'profile': helpers.get_xml_attr(player_info, 'profile'), 'player': helpers.get_xml_attr(player_info, 'title') or helpers.get_xml_attr(player_info, 'product'), - 'machine_id': helpers.get_xml_attr(player_info, 'machineIdentifier').rstrip('_Video').rstrip('_Track'), + 'machine_id': helpers.get_xml_attr(player_info, 'machineIdentifier'), 'state': helpers.get_xml_attr(player_info, 'state'), 'local': helpers.get_xml_attr(player_info, 'local') } @@ -1431,14 +1436,29 @@ class PmsConnect(object): # Determine if a synced version is being played if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \ and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play': - synced_version = 1 + plex_tv = plextv.PlexTV() + synced_items = plex_tv.get_synced_items(machine_id=plexpy.CONFIG.PMS_IDENTIFIER, + client_id_filter=player_details['machine_id'], + rating_key_filter=rating_key) + if synced_items: + sync_id = synced_items[0]['sync_id'] + synced_xml = self.get_sync_item(sync_id=sync_id, output_format='xml') + synced_xml_head = synced_xml.getElementsByTagName('MediaContainer') + if synced_xml_head[0].getElementsByTagName('Track'): + synced_session_data = synced_xml_head[0].getElementsByTagName('Track')[0] + elif synced_xml_head[0].getElementsByTagName('Video'): + synced_session_data = synced_xml_head[0].getElementsByTagName('Video')[0] else: - synced_version = 0 + sync_id = None # Figure out which version is being played - media_info_all = session.getElementsByTagName('Media') + if sync_id: + media_info_all = synced_session_data.getElementsByTagName('Media') + else: + media_info_all = session.getElementsByTagName('Media') stream_media_info = next((m for m in media_info_all if helpers.get_xml_attr(m, 'selected') == '1'), media_info_all[0]) - stream_media_parts_info = stream_media_info.getElementsByTagName('Part')[0] + part_info_all = stream_media_info.getElementsByTagName('Part') + stream_media_parts_info = next((p for p in part_info_all if helpers.get_xml_attr(p, 'selected') == '1'), part_info_all[0]) # Get the stream details video_stream_info = audio_stream_info = subtitle_stream_info = None @@ -1495,6 +1515,7 @@ class PmsConnect(object): if subtitle_stream_info: subtitle_id = helpers.get_xml_attr(subtitle_stream_info, 'id') + subtitle_selected = helpers.get_xml_attr(subtitle_stream_info, 'selected') subtitle_details = {'stream_subtitle_codec': helpers.get_xml_attr(subtitle_stream_info, 'codec'), 'stream_subtitle_container': helpers.get_xml_attr(subtitle_stream_info, 'container'), 'stream_subtitle_format': helpers.get_xml_attr(subtitle_stream_info, 'format'), @@ -1544,14 +1565,14 @@ class PmsConnect(object): 'stream_video_height': helpers.get_xml_attr(stream_media_info, 'height'), 'stream_video_width': helpers.get_xml_attr(stream_media_info, 'width'), 'stream_duration': helpers.get_xml_attr(stream_media_info, 'duration') or helpers.get_xml_attr(session, 'duration'), - 'stream_container_decision': helpers.get_xml_attr(stream_media_parts_info, 'decision').replace('directplay', 'direct play'), + 'stream_container_decision': 'direct play' if sync_id else helpers.get_xml_attr(stream_media_parts_info, 'decision').replace('directplay', 'direct play'), 'transcode_decision': transcode_decision, 'optimized_version': 1 if helpers.get_xml_attr(stream_media_info, 'proxyType') == '42' else 0, - 'optimized_version_profile': helpers.get_xml_attr(stream_media_info, 'title'), - 'synced_version': synced_version, + 'optimized_version_title': helpers.get_xml_attr(stream_media_info, 'title'), + 'synced_version': 1 if sync_id else 0, 'indexes': 1 if indexes == 'sd' else 0, 'bif_thumb': bif_thumb, - 'subtitles': 1 if subtitle_id else 0 + 'subtitles': 1 if subtitle_id and subtitle_selected else 0 } # Get the source media info @@ -1617,7 +1638,10 @@ class PmsConnect(object): media_id = helpers.get_xml_attr(stream_media_info, 'id') part_id = helpers.get_xml_attr(stream_media_parts_info, 'id') - metadata_details = self.get_metadata_details(rating_key=helpers.get_xml_attr(session, 'ratingKey')) + if sync_id: + metadata_details = self.get_metadata_details(sync_id=sync_id) + else: + metadata_details = self.get_metadata_details(rating_key=rating_key) # Get the media info, fallback to first item if match id is not found source_medias = metadata_details.pop('media_info', []) @@ -1682,7 +1706,22 @@ class PmsConnect(object): quality_profile = common.VIDEO_QUALITY_PROFILES[quailtiy_bitrate] except ValueError: quality_profile = 'Original' - + + if sync_id: + try: + synced_bitrate = min(b for b in common.VIDEO_QUALITY_PROFILES if source_bitrate <= b) + synced_version_profile = common.VIDEO_QUALITY_PROFILES[synced_bitrate] + except ValueError: + synced_version_profile = 'Original' + else: + synced_version_profile = '' + + if stream_details['optimized_version']: + optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1), + plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'], source_media_details['video_resolution'])) + else: + optimized_version_profile = '' + elif media_type == 'track' and 'stream_bitrate' in stream_details: stream_bitrate = helpers.cast_to_int(stream_details['stream_bitrate']) source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate')) @@ -1693,11 +1732,26 @@ class PmsConnect(object): except ValueError: quality_profile = 'Original' + if sync_id: + try: + synced_bitrate = min(b for b in common.AUDIO_QUALITY_PROFILES if source_bitrate <= b) + synced_version_profile = common.AUDIO_QUALITY_PROFILES[synced_bitrate] + except ValueError: + synced_version_profile = 'Original' + else: + synced_version_profile = '' + + optimized_version_profile = '' + elif media_type == 'photo': quality_profile = 'Original' + synced_version_profile = '' + optimized_version_profile = '' else: quality_profile = 'Unknown' + synced_version_profile = '' + optimized_version_profile = '' # Entire session output (single dict for backwards compatibility) session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'), @@ -1705,6 +1759,8 @@ class PmsConnect(object): 'view_offset': view_offset, 'progress_percent': str(helpers.get_percent(view_offset, stream_details['stream_duration'])), 'quality_profile': quality_profile, + 'synced_version_profile': synced_version_profile, + 'optimized_version_profile': optimized_version_profile, 'user': user_details['username'], # Keep for backwards compatibility 'channel_stream': channel_stream } diff --git a/plexpy/webserve.py b/plexpy/webserve.py index da4086ab..dd6939fe 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -2195,7 +2195,7 @@ class WebInterface(object): machine_id = plexpy.CONFIG.PMS_IDENTIFIER plex_tv = plextv.PlexTV() - result = plex_tv.get_synced_items(machine_id=machine_id, user_id=user_id) + result = plex_tv.get_synced_items(machine_id=machine_id, user_id_filter=user_id) if result: output = {"data": result} @@ -4560,7 +4560,7 @@ class WebInterface(object): ``` """ plex_tv = plextv.PlexTV() - result = plex_tv.get_synced_items(machine_id=machine_id, user_id=user_id) + result = plex_tv.get_synced_items(machine_id=machine_id, user_id_filter=user_id) if result: return result