From 458e89b8d73e62c321d126ea75c52f01468cebfe Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:41:41 -0700 Subject: [PATCH] Get live TV metadata from epg --- plexpy/activity_handler.py | 3 +- plexpy/activity_processor.py | 1 - plexpy/helpers.py | 6 +- plexpy/http_handler.py | 6 +- plexpy/pmsconnect.py | 113 +++++++++++++++++++++++++++++++++-- plexpy/webserve.py | 6 ++ 6 files changed, 124 insertions(+), 11 deletions(-) diff --git a/plexpy/activity_handler.py b/plexpy/activity_handler.py index ea25f0bd..5c572408 100644 --- a/plexpy/activity_handler.py +++ b/plexpy/activity_handler.py @@ -1,4 +1,4 @@ -# This file is part of Tautulli. +# This file is part of Tautulli. # # Tautulli is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -163,7 +163,6 @@ class ActivityHandler(object): # Set force_stop to true to disable the state set if not force_stop: # Set the view offset equal to the duration if it is within the last 10 seconds - # TODO: temporary workaround for missing livetv duration if self.db_session['duration'] > 0 and self.db_session['duration'] - self.view_offset <= 10000: view_offset = self.db_session['duration'] else: diff --git a/plexpy/activity_processor.py b/plexpy/activity_processor.py index 3ece3f11..0de08564 100644 --- a/plexpy/activity_processor.py +++ b/plexpy/activity_processor.py @@ -78,7 +78,6 @@ class ActivityProcessor(object): 'added_at': session.get('added_at', ''), 'guid': session.get('guid', ''), 'view_offset': session.get('view_offset', ''), - # TODO: temporary workaround for missing livetv duration 'duration': session.get('duration', '') or 0, 'video_decision': session.get('video_decision', ''), 'audio_decision': session.get('audio_decision', ''), diff --git a/plexpy/helpers.py b/plexpy/helpers.py index 04391204..dc83fcb3 100644 --- a/plexpy/helpers.py +++ b/plexpy/helpers.py @@ -242,10 +242,14 @@ def YMD_to_timestamp(ymd): return datetime.strptime(ymd, "%Y-%m-%d").timestamp() -def timestamp_to_YMDHMS(ts, sep=False): +def timestamp_to_YMDHMS(ts, sep=False, ymd=False): dt = timestamp_to_datetime(ts) if sep: + if ymd: + return dt.strftime("%Y-%m-%d") return dt.strftime("%Y-%m-%d %H:%M:%S") + if ymd: + return dt.strftime("%Y%m%d") return dt.strftime("%Y%m%d%H%M%S") diff --git a/plexpy/http_handler.py b/plexpy/http_handler.py index 79bb3562..3596fa12 100644 --- a/plexpy/http_handler.py +++ b/plexpy/http_handler.py @@ -82,6 +82,7 @@ class HTTPHandler(object): self.return_response = False self.return_type = False self.callback = None + self.raise_errors = True self.request_kwargs = {} def make_request(self, @@ -95,6 +96,7 @@ class HTTPHandler(object): no_token=False, timeout=None, callback=None, + raise_errors=True, **request_kwargs): """ Handle the HTTP requests. @@ -109,6 +111,7 @@ class HTTPHandler(object): self.return_response = return_response self.return_type = return_type self.callback = callback + self.raise_errors = raise_errors self.timeout = timeout or self.timeout self.request_kwargs = request_kwargs @@ -166,7 +169,8 @@ class HTTPHandler(object): try: r = self._session.request(self.request_type, url, headers=self.headers, data=self.data, timeout=self.timeout, verify=self.ssl_verify, **self.request_kwargs) - r.raise_for_status() + if self.raise_errors: + r.raise_for_status() except requests.exceptions.Timeout as e: err = True if not self._silent: diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py index 2e0ec22c..2662abf9 100644 --- a/plexpy/pmsconnect.py +++ b/plexpy/pmsconnect.py @@ -147,6 +147,23 @@ class PmsConnect(object): return request + def get_epg_metadata(self, epg_key='', output_format=''): + """ + Return epg metadata for request item. + + Parameters required: epg_key { Plex key } + Optional parameters: output_format { dict, json } + + Output: array + """ + uri = epg_key + request = self.request_handler.make_request(uri=uri, + request_type='GET', + output_format=output_format, + raise_errors=False) + + return request + def get_metadata_children(self, rating_key='', collection=False, output_format=''): """ Return metadata for children of the request item. @@ -515,6 +532,22 @@ class PmsConnect(object): return request + def get_dvrs(self, output_format=''): + """ + Return Plex dvrs. + + Parameters required: + Optional parameters: output_format { dict, json } + + Output: array + """ + uri = '/livetv/dvrs' + request = self.request_handler.make_request(uri=uri, + request_type='GET', + output_format=output_format) + + return request + def get_recently_added_details(self, start='0', count='0', media_type='', section_id=''): """ Return processed and validated list of recently added items. @@ -654,7 +687,7 @@ class PmsConnect(object): return output - def get_metadata_details(self, rating_key='', sync_id='', plex_guid='', section_id='', + def get_metadata_details(self, rating_key='', sync_id='', plex_guid='', epg_key='', section_id='', skip_cache=False, cache_key=None, return_cache=False, media_info=True): """ Return processed and validated metadata list for requested item. @@ -692,6 +725,8 @@ class PmsConnect(object): rating_key = plex_guid.rsplit('/', 1)[-1] plextv_metadata = PmsConnect(url='https://metadata.provider.plex.tv', token=plexpy.CONFIG.PMS_TOKEN) metadata_xml = plextv_metadata.get_metadata(rating_key, output_format='xml') + elif epg_key: + metadata_xml = self.get_epg_metadata(epg_key, output_format='xml') else: return metadata @@ -912,7 +947,10 @@ class PmsConnect(object): show_details = {} if plex_guid and parent_guid: show_details = self.get_metadata_details(plex_guid=parent_guid) - elif not plex_guid and parent_rating_key: + elif epg_key and parent_guid: + epg_key_root = epg_key.rsplit('/', maxsplit=1)[0] + show_details = self.get_metadata_details(epg_key=f"{epg_key_root}/{quote_plus(parent_guid)}") + elif not plex_guid and not epg_key and parent_rating_key: show_details = self.get_metadata_details(parent_rating_key) metadata = {'media_type': metadata_type, @@ -976,6 +1014,9 @@ class PmsConnect(object): show_details = {} if plex_guid and grandparent_guid: show_details = self.get_metadata_details(plex_guid=grandparent_guid) + elif epg_key and grandparent_guid: + epg_key_root = epg_key.rsplit('/', maxsplit=1)[0] + show_details = self.get_metadata_details(epg_key=f"{epg_key_root}/{quote_plus(grandparent_guid)}") elif not plex_guid and grandparent_rating_key: show_details = self.get_metadata_details(grandparent_rating_key) @@ -984,7 +1025,7 @@ class PmsConnect(object): parent_thumb = helpers.get_xml_attr(metadata_main, 'parentThumb') season_details = self.get_metadata_details(parent_rating_key) if parent_rating_key else {} - if not plex_guid and not parent_rating_key: + if not plex_guid and not epg_key and not parent_rating_key: # Try getting the parent_rating_key from the parent_thumb if parent_thumb.startswith('/library/metadata/'): parent_rating_key = parent_thumb.split('/')[3] @@ -1474,8 +1515,28 @@ class PmsConnect(object): else: return metadata + # Get additional metadata EPG provider + epg_metadata = None + if not epg_key and metadata['live']: + metadata['section_id'] = common.LIVE_TV_SECTION_ID + metadata['library_name'] = common.LIVE_TV_SECTION_NAME + + # Don't know the DVR key so need to try them all + for dvr in self.get_dvrs_list(): + epg_metadata = self.get_metadata_details(epg_key=f"/{dvr['epg_identifier']}/metadata/{quote_plus(metadata['guid'])}", media_info=True) + if epg_metadata: + metadata['epg_identifier'] = dvr['epg_identifier'] + keys_to_update = [ + 'content_rating', 'summary', 'duration', 'guid', + 'grandparent_title', 'grandparent_thumb', 'grandparent_guid', + 'parent_title', 'parent_thumb', 'parent_guid' + ] + for key in keys_to_update: + metadata[key] = epg_metadata[key] + metadata['originally_available_at'] = helpers.timestamp_to_YMDHMS(epg_metadata['media_info'][0]['begins_at'], sep=True, ymd=True) + break # Get additional metadata from metadata.provider.plex.tv - if not plex_guid and metadata['live']: + elif not plex_guid and metadata['live']: metadata['section_id'] = common.LIVE_TV_SECTION_ID metadata['library_name'] = common.LIVE_TV_SECTION_NAME @@ -1577,7 +1638,7 @@ class PmsConnect(object): audio_channels = helpers.get_xml_attr(media, 'audioChannels') - media_info = {'id': helpers.get_xml_attr(media, 'id'), + _media_info = {'id': helpers.get_xml_attr(media, 'id'), 'container': helpers.get_xml_attr(media, 'container'), 'bitrate': helpers.get_xml_attr(media, 'bitrate'), 'height': helpers.get_xml_attr(media, 'height'), @@ -1594,12 +1655,25 @@ class PmsConnect(object): 'audio_profile': helpers.get_xml_attr(media, 'audioProfile'), 'optimized_version': int(helpers.get_xml_attr(media, 'proxyType') == '42'), 'channel_call_sign': helpers.get_xml_attr(media, 'channelCallSign'), + 'channel_id': helpers.get_xml_attr(media, 'channelID'), 'channel_identifier': helpers.get_xml_attr(media, 'channelIdentifier'), + 'channel_title': helpers.get_xml_attr(media, 'channelTitle'), 'channel_thumb': helpers.get_xml_attr(media, 'channelThumb'), + 'channel_vcn': helpers.get_xml_attr(media, 'channelVcn'), + 'protocol': helpers.get_xml_attr(media, 'protocol'), # livetv + 'begins_at': helpers.cast_to_int(helpers.get_xml_attr(media, 'beginsAt')), # livetv + 'ends_at': helpers.cast_to_int(helpers.get_xml_attr(media, 'endsAt')), # livetv 'parts': parts } - medias.append(media_info) + if epg_metadata: + media_info_keys_to_update = [ + 'channel_id', 'channel_title', 'channel_vcn' + ] + for key in media_info_keys_to_update: + _media_info[key] = epg_metadata['media_info'][0][key] + + medias.append(_media_info) metadata['media_info'] = medias @@ -3325,3 +3399,30 @@ class PmsConnect(object): if not video_dynamic_range: return 'SDR' return '/'.join(video_dynamic_range) + + def get_dvrs_list(self): + dvrs_xml = self.get_dvrs(output_format='xml') + + try: + xml_head = dvrs_xml.getElementsByTagName('MediaContainer') + except Exception as e: + logger.warn("Tautulli Pmsconnect :: Unable to parse XML for get_dvrs_list: %s." % e) + + dvrs_output = [] + + for a in xml_head: + dvrs = a.getElementsByTagName('Dvr') + for dvr in dvrs: + dvr_info = { + 'key': helpers.get_xml_attr(dvr, 'key'), + 'uuid': helpers.get_xml_attr(dvr, 'uuid'), + 'language': helpers.get_xml_attr(dvr, 'language'), + 'lineup_title': helpers.get_xml_attr(dvr, 'lineupTitle'), + 'lineup': helpers.get_xml_attr(dvr, 'lineup'), + 'country': helpers.get_xml_attr(dvr, 'country'), + 'refreshedAt': helpers.get_xml_attr(dvr, 'refreshedAt'), + 'epg_identifier': helpers.get_xml_attr(dvr, 'epgIdentifier') + } + dvrs_output.append(dvr_info) + + return dvrs_output diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 1e73cca9..87196b12 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -5439,8 +5439,11 @@ class WebInterface(object): "audio_profile": "", "bitrate": "10617", "channel_call_sign": "", + "channel_id": "", "channel_identifier": "", + "channel_title": "", "channel_thumb": "", + "channel_vcn": "", "container": "mkv", "height": "1078", "id": "257925", @@ -5860,9 +5863,12 @@ class WebInterface(object): "bif_thumb": "/library/parts/274169/indexes/sd/1000", "bitrate": "10617", "channel_call_sign": "", + "channel_id": "", "channel_identifier": "", "channel_stream": 0, + "channel_title": "", "channel_thumb": "", + "channel_vcn": "", "children_count": "", "collections": [], "container": "mkv",