diff --git a/data/interfaces/default/base.html b/data/interfaces/default/base.html index d0ec31bf..8d721dad 100644 --- a/data/interfaces/default/base.html +++ b/data/interfaces/default/base.html @@ -57,6 +57,18 @@ from plexpy import version
-
+
  Loading season list...
% elif data['type'] == 'season': @@ -272,7 +285,7 @@ DOCUMENTATION :: END
-
+
  Loading episode list...
% elif data['type'] == 'artist': @@ -283,7 +296,7 @@ DOCUMENTATION :: END
-
+
  Loading album list...
% elif data['type'] == 'album': @@ -294,7 +307,7 @@ DOCUMENTATION :: END
-
+
  Loading track list...
% endif @@ -361,11 +374,122 @@ DOCUMENTATION :: END % else:
-
-

- Error retrieving item data. This media may not be available in the Plex Media Server database - anymore. -

+
+
+
+
+ % if query: + % if query['media_type'] == 'movie': + Movies + + ${query['title']} + % elif query['media_type'] == 'show': + TV Shows + + ${query['grandparent_title']} + % elif query['media_type'] == 'season': + + + + + Season ${query['parent_media_index']} + % elif query['media_type'] == 'episode': + + + + + Season ${query['parent_media_index']} + + Episode ${query['media_index']} - ${query['title']} + % elif query['media_type'] == 'artist': + Music + + ${query['grandparent_title']} + % elif query['media_type'] == 'album': + + + ${query['grandparent_title']} + + ${query['parent_title']} + % elif query['media_type'] == 'track': + + + + + ${query['parent_title']} + + Track ${query['media_index']} - ${query['title']} + % endif + % endif +
+
+
+
+
+

+ Error retrieving item metadata. This media item is not available in the Plex Media Server library. +

+ % if query: +

+ If the item has been moved, please select the correct match below to update the PlexPy database. +

+ % endif +
+
+
+
+ % if query: +
+
+ Search Results for ${query['query_string']} +
+
+
+
  Loading search results...
+
+ + % endif +
+
@@ -381,13 +505,6 @@ DOCUMENTATION :: END % if data: -% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'episode': - -% endif % if data['type'] == 'show' or data['type'] == 'artist': % endif +% if data['rating']: + +% endif +% elif query: + % endif diff --git a/data/interfaces/default/info_search_results_list.html b/data/interfaces/default/info_search_results_list.html new file mode 100644 index 00000000..bd09e068 --- /dev/null +++ b/data/interfaces/default/info_search_results_list.html @@ -0,0 +1,224 @@ +<%doc> +USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE + +For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/ + +Filename: info_children_list.html +Version: 0.1 +Variable names: data [list] + +data :: Usable parameters + +== Global keys == +results_count Returns the number of search results. +results_list Returns a dictionary of search result types. + +data['results_list'] :: Usable paramaters + +== media_type keys == +movie Returns an array of movie results +show Returns an array of show results +season Returns an array of season results +episode Returns an array of episode results +artist Returns an array of artist results +album Returns an array of album results +track Returns an array of track results + +data['results_list'][media_type] :: Usable paramaters + +== Global keys == +rating_key Returns the unique identifier for the media item. +type Returns the type of media. Either 'movie', 'show', 'season', 'episode', 'artist', 'album', or 'track'. +art Returns the location of the item's artwork +title Returns the name of the movie, show, episode, artist, album, or track. +duration Returns the standard runtime of the media. +content_rating Returns the age rating for the media. +summary Returns a brief description of the media plot. +grandparent_title Returns the name of the show, or artist. +parent_index Returns the index number of the season. +index Returns the index number of the episode, or track. +parent_thumb Returns the location of the item's thumbnail. Use with pms_image_proxy. +writers Returns an array of writers. +thumb Returns the location of the item's thumbnail. Use with pms_image_proxy. +parent_title Returns the name of the show, or artist. +rating Returns the 5 star rating value for the movie. Between 1 and 5. +year Returns the release year of the movie, or show. +genres Returns an array of genres. +actors Returns an array of actors. +directors Returns an array of directors. +studio Returns the name of the studio. +originally_available_at Returns the air date of the item. + +DOCUMENTATION :: END + + +% if data != None: +% if data['results_count'] > 0: +% if 'movie' in data['results_list'] and data['results_list']['movie']: +
+
+

Movies

+
+ +
+% endif +% if 'show' in data['results_list'] and data['results_list']['show']: +
+
+

TV Shows

+
+ +
+% endif +% if 'season' in data['results_list'] and data['results_list']['season']: +
+
+

Seasons

+
+ +
+% endif +% if 'episode' in data['results_list'] and data['results_list']['episode']: +
+
+

Episodes

+
+ +
+% endif +% if 'artist' in data['results_list'] and data['results_list']['artist']: +
+
+

Artists

+
+ +
+% endif +% if 'album' in data['results_list'] and data['results_list']['album']: +
+
+

Albums

+
+ +
+% endif +% if 'track' in data['results_list'] and data['results_list']['track']: +
+
+

Tracks

+
+ +
+% endif +% else: +
+ No search results found. +
+% endif +% endif + diff --git a/data/interfaces/default/search.html b/data/interfaces/default/search.html new file mode 100644 index 00000000..957b2878 --- /dev/null +++ b/data/interfaces/default/search.html @@ -0,0 +1,41 @@ +<%inherit file="base.html"/> + +<%def name="headIncludes()"> + + +<%def name="headerIncludes()"> + + +<%def name="body()"> +
+
+
+ Search Results + % if query: + for ${query} + % endif + +
+
+
+
  Loading search results...
+
+
+ + +<%def name="javascriptIncludes()"> + + diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index b6be65b8..dc53e9c8 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -789,4 +789,204 @@ class DataFactory(object): return 'Deleted all items for user_id %s.' % user_id else: - return 'Unable to delete items. Input user_id not valid.' \ No newline at end of file + return 'Unable to delete items. Input user_id not valid.' + + def get_search_query(self, rating_key=''): + monitor_db = database.MonitorDatabase() + + if rating_key: + query = 'SELECT rating_key, parent_rating_key, grandparent_rating_key, title, parent_title, grandparent_title, ' \ + 'media_index, parent_media_index, year, media_type ' \ + 'FROM session_history_metadata ' \ + 'WHERE rating_key = ? ' \ + 'OR parent_rating_key = ? ' \ + 'OR grandparent_rating_key = ? ' \ + 'LIMIT 1' + result = monitor_db.select(query=query, args=[rating_key, rating_key, rating_key]) + else: + result = [] + + query = {} + query_string = None + media_type = None + + for item in result: + title = item['title'] + parent_title = item['parent_title'] + grandparent_title = item['grandparent_title'] + media_index = item['media_index'] + parent_media_index = item['parent_media_index'] + year = item['year'] + + if str(item['rating_key']) == rating_key: + query_string = item['title'] + media_type = item['media_type'] + + elif str(item['parent_rating_key']) == rating_key: + if item['media_type'] == 'episode': + query_string = item['grandparent_title'] + media_type = 'season' + elif item['media_type'] == 'track': + query_string = item['parent_title'] + media_type = 'album' + + elif str(item['grandparent_rating_key']) == rating_key: + if item['media_type'] == 'episode': + query_string = item['grandparent_title'] + media_type = 'show' + elif item['media_type'] == 'track': + query_string = item['grandparent_title'] + media_type = 'artist' + + if query_string and media_type: + query = {'query_string': query_string.replace('"', ''), + 'title': title, + 'parent_title': parent_title, + 'grandparent_title': grandparent_title, + 'media_index': media_index, + 'parent_media_index': parent_media_index, + 'year': year, + 'media_type': media_type, + 'rating_key': rating_key + } + else: + return None + + return query + + def get_rating_keys_list(self, rating_key='', media_type=''): + monitor_db = database.MonitorDatabase() + + if media_type == 'movie': + key_list = {0: {'rating_key': int(rating_key)}} + return key_list + + if media_type == 'artist' or media_type == 'album' or media_type == 'track': + match_type = 'title' + else: + match_type = 'index' + + # Get the grandparent rating key + try: + query = 'SELECT rating_key, parent_rating_key, grandparent_rating_key ' \ + 'FROM session_history_metadata ' \ + 'WHERE rating_key = ? ' \ + 'OR parent_rating_key = ? ' \ + 'OR grandparent_rating_key = ? ' \ + 'LIMIT 1' + result = monitor_db.select(query=query, args=[rating_key, rating_key, rating_key]) + + grandparent_rating_key = result[0]['grandparent_rating_key'] + + except: + logger.warn("Unable to execute database query.") + return {} + + query = 'SELECT rating_key, parent_rating_key, grandparent_rating_key, title, parent_title, grandparent_title, ' \ + 'media_index, parent_media_index ' \ + 'FROM session_history_metadata ' \ + 'WHERE {0} = ? ' \ + 'GROUP BY {1} ' + + # get grandparent_rating_keys + grandparents = {} + result = monitor_db.select(query=query.format('grandparent_rating_key', 'grandparent_rating_key'), + args=[grandparent_rating_key]) + for item in result: + # get parent_rating_keys + parents = {} + result = monitor_db.select(query=query.format('grandparent_rating_key', 'parent_rating_key'), + args=[item['grandparent_rating_key']]) + for item in result: + # get rating_keys + children = {} + result = monitor_db.select(query=query.format('parent_rating_key', 'rating_key'), + args=[item['parent_rating_key']]) + for item in result: + key = item['media_index'] + children.update({key: {'rating_key': item['rating_key']}}) + + key = item['parent_media_index'] if match_type == 'index' else item['parent_title'] + parents.update({key: + {'rating_key': item['parent_rating_key'], + 'children': children} + }) + + key = 0 if match_type == 'index' else item['grandparent_title'] + grandparents.update({key: + {'rating_key': item['grandparent_rating_key'], + 'children': parents} + }) + + key_list = grandparents + + return key_list + + def update_rating_key(self, old_key_list='', new_key_list='', media_type=''): + monitor_db = database.MonitorDatabase() + + # function to map rating keys pairs + def get_pairs(old, new): + pairs = {} + for k, v in old.iteritems(): + if k in new: + if v['rating_key'] != new[k]['rating_key']: + pairs.update({v['rating_key']: new[k]['rating_key']}) + if 'children' in old[k]: + pairs.update(get_pairs(old[k]['children'], new[k]['children'])) + + return pairs + + # map rating keys pairs + mapping = {} + if old_key_list and new_key_list: + mapping = get_pairs(old_key_list, new_key_list) + + if mapping: + logger.info(u"PlexPy DataFactory :: Updating rating keys in the database.") + for old_key, new_key in mapping.iteritems(): + # check rating_key (3 tables) + monitor_db.action('UPDATE session_history SET rating_key = ? WHERE rating_key = ?', + [new_key, old_key]) + monitor_db.action('UPDATE session_history_media_info SET rating_key = ? WHERE rating_key = ?', + [new_key, old_key]) + monitor_db.action('UPDATE session_history_metadata SET rating_key = ? WHERE rating_key = ?', + [new_key, old_key]) + + # check parent_rating_key (2 tables) + monitor_db.action('UPDATE session_history SET parent_rating_key = ? WHERE parent_rating_key = ?', + [new_key, old_key]) + monitor_db.action('UPDATE session_history_metadata SET parent_rating_key = ? WHERE parent_rating_key = ?', + [new_key, old_key]) + + # check grandparent_rating_key (2 tables) + monitor_db.action('UPDATE session_history SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?', + [new_key, old_key]) + monitor_db.action('UPDATE session_history_metadata SET grandparent_rating_key = ? WHERE grandparent_rating_key = ?', + [new_key, old_key]) + + # check thumb (1 table) + monitor_db.action('UPDATE session_history_metadata SET thumb = replace(thumb, ?, ?) \ + WHERE thumb LIKE "/library/metadata/%s/thumb/%%"' % old_key, + [old_key, new_key]) + + # check parent_thumb (1 table) + monitor_db.action('UPDATE session_history_metadata SET parent_thumb = replace(parent_thumb, ?, ?) \ + WHERE parent_thumb LIKE "/library/metadata/%s/thumb/%%"' % old_key, + [old_key, new_key]) + + # check grandparent_thumb (1 table) + monitor_db.action('UPDATE session_history_metadata SET grandparent_thumb = replace(grandparent_thumb, ?, ?) \ + WHERE grandparent_thumb LIKE "/library/metadata/%s/thumb/%%"' % old_key, + [old_key, new_key]) + + # check art (1 table) + monitor_db.action('UPDATE session_history_metadata SET art = replace(art, ?, ?) \ + WHERE art LIKE "/library/metadata/%s/art/%%"' % old_key, + [old_key, new_key]) + + return 'Updated rating key in database.' + else: + return 'No updated rating key needed in database. No changes were made.' + # for debugging + #return mapping \ No newline at end of file diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py index 3b58adb5..687725b4 100644 --- a/plexpy/pmsconnect.py +++ b/plexpy/pmsconnect.py @@ -17,6 +17,7 @@ from plexpy import logger, helpers, users, http_handler from urlparse import urlparse import plexpy +import urllib2 class PmsConnect(object): @@ -72,6 +73,23 @@ class PmsConnect(object): return request + """ + Return metadata for children of the request item. + + Parameters required: rating_key { Plex ratingKey } + Optional parameters: output_format { dict, json } + + Output: array + """ + def get_metadata_children(self, rating_key='', output_format=''): + uri = '/library/metadata/' + rating_key + '/children' + request = self.request_handler.make_request(uri=uri, + proto=self.protocol, + request_type='GET', + output_format=output_format) + + return request + """ Return list of recently added items. @@ -219,6 +237,22 @@ class PmsConnect(object): return request + """ + Return search results. + + Optional parameters: output_format { dict, json } + + Output: array + """ + def get_search(self, query='', track='', output_format=''): + uri = '/search?query=' + urllib2.quote(query.encode('utf8')) + track + request = self.request_handler.make_request(uri=uri, + proto=self.protocol, + request_type='GET', + output_format=output_format) + + return request + """ Return processed and validated list of recently added items. @@ -1340,3 +1374,202 @@ class PmsConnect(object): else: logger.error("Image proxy queries but no input received.") return None + + """ + Return processed list of search results. + + Output: array + """ + def get_search_results(self, query=''): + search_results = self.get_search(query=query, output_format='xml') + search_results_tracks = self.get_search(query=query, track='&type=10', output_format='xml') + + xml_head = [] + try: + try: + xml_head += search_results.getElementsByTagName('MediaContainer') + except: + pass + try: + xml_head += search_results_tracks.getElementsByTagName('MediaContainer') + except: + pass + except: + logger.warn("Unable to parse XML for get_search_result_details.") + return [] + + search_results_count = 0 + search_results_list = {'movie': [], + 'show': [], + 'season': [], + 'episode': [], + 'artist': [], + 'album': [], + 'track': [] + } + + totalSize = 0 + for a in xml_head: + if a.getAttribute('size'): + totalSize += int(a.getAttribute('size')) + if totalSize == 0: + logger.debug(u"No search results.") + search_results_list = {'results_count': search_results_count, + 'results_list': [] + } + return search_results_list + + for a in xml_head: + if a.getElementsByTagName('Video'): + result_data = a.getElementsByTagName('Video') + for result in result_data: + rating_key = helpers.get_xml_attr(result, 'ratingKey') + metadata = self.get_metadata_details(rating_key=rating_key) + if metadata['metadata']['type'] == 'movie': + search_results_list['movie'].append(metadata['metadata']) + elif metadata['metadata']['type'] == 'episode': + search_results_list['episode'].append(metadata['metadata']) + search_results_count += 1 + + if a.getElementsByTagName('Directory'): + result_data = a.getElementsByTagName('Directory') + for result in result_data: + rating_key = helpers.get_xml_attr(result, 'ratingKey') + metadata = self.get_metadata_details(rating_key=rating_key) + if metadata['metadata']['type'] == 'show': + search_results_list['show'].append(metadata['metadata']) + + show_seasons = self.get_item_children(rating_key=metadata['metadata']['rating_key']) + if show_seasons['children_count'] != '0': + for season in show_seasons['children_list']: + if season['rating_key']: + rating_key = season['rating_key'] + metadata = self.get_metadata_details(rating_key=rating_key) + search_results_list['season'].append(metadata['metadata']) + search_results_count += 1 + + elif metadata['metadata']['type'] == 'artist': + search_results_list['artist'].append(metadata['metadata']) + elif metadata['metadata']['type'] == 'album': + search_results_list['album'].append(metadata['metadata']) + search_results_count += 1 + + if a.getElementsByTagName('Track'): + result_data = a.getElementsByTagName('Track') + for result in result_data: + rating_key = helpers.get_xml_attr(result, 'ratingKey') + metadata = self.get_metadata_details(rating_key=rating_key) + search_results_list['track'].append(metadata['metadata']) + search_results_count += 1 + + output = {'results_count': search_results_count, + 'results_list': search_results_list + } + + return output + + """ + Return processed list of grandparent/parent/child rating keys. + + Output: array + """ + def get_rating_keys_list(self, rating_key='', media_type=''): + + if media_type == 'movie': + key_list = {0: {'rating_key': int(rating_key)}} + return key_list + + if media_type == 'artist' or media_type == 'album' or media_type == 'track': + match_type = 'title' + else: + match_type = 'index' + + # get grandparent rating key + if media_type == 'season' or media_type == 'album': + try: + metadata = self.get_metadata_details(rating_key=rating_key) + rating_key = metadata['metadata']['parent_rating_key'] + except: + logger.warn("Unable to get parent_rating_key for get_rating_keys_list.") + return {} + + elif media_type == 'episode' or media_type == 'track': + try: + metadata = self.get_metadata_details(rating_key=rating_key) + rating_key = metadata['metadata']['grandparent_rating_key'] + except: + logger.warn("Unable to get grandparent_rating_key for get_rating_keys_list.") + return {} + + # get parent_rating_keys + metadata = self.get_metadata_children(str(rating_key), output_format='xml') + + try: + xml_head = metadata.getElementsByTagName('MediaContainer') + except: + logger.warn("Unable to parse XML for get_rating_keys_list.") + return {} + + for a in xml_head: + if a.getAttribute('size'): + if a.getAttribute('size') == '0': + return {} + + title = helpers.get_xml_attr(a, 'title2') + + if a.getElementsByTagName('Directory'): + parents_metadata = a.getElementsByTagName('Directory') + else: + parents_metadata = [] + + parents = {} + for item in parents_metadata: + parent_rating_key = helpers.get_xml_attr(item, 'ratingKey') + parent_index = helpers.get_xml_attr(item, 'index') + parent_title = helpers.get_xml_attr(item, 'title') + + if parent_rating_key: + # get rating_keys + metadata = self.get_metadata_children(str(parent_rating_key), output_format='xml') + + try: + xml_head = metadata.getElementsByTagName('MediaContainer') + except: + logger.warn("Unable to parse XML for get_rating_keys_list.") + return {} + + for a in xml_head: + if a.getAttribute('size'): + if a.getAttribute('size') == '0': + return {} + + if a.getElementsByTagName('Video'): + children_metadata = a.getElementsByTagName('Video') + elif a.getElementsByTagName('Track'): + children_metadata = a.getElementsByTagName('Track') + else: + children_metadata = [] + + children = {} + for item in children_metadata: + child_rating_key = helpers.get_xml_attr(item, 'ratingKey') + child_index = helpers.get_xml_attr(item, 'index') + child_title = helpers.get_xml_attr(item, 'title') + + if child_rating_key: + key = int(child_index) + children.update({key: {'rating_key': int(child_rating_key)}}) + + key = int(parent_index) if match_type == 'index' else parent_title + parents.update({key: + {'rating_key': int(parent_rating_key), + 'children': children} + }) + + key = 0 if match_type == 'index' else title + key_list = {key: + {'rating_key': int(rating_key), + 'children': parents} + } + + return key_list \ No newline at end of file diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 3d1df8a4..46bb3dc2 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -761,6 +761,7 @@ class WebInterface(object): @cherrypy.expose def info(self, item_id=None, source=None, **kwargs): metadata = None + query = None config = { "pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER @@ -774,12 +775,15 @@ class WebInterface(object): result = pms_connect.get_metadata_details(rating_key=item_id) if result: metadata = result['metadata'] + else: + data_factory = datafactory.DataFactory() + query = data_factory.get_search_query(rating_key=item_id) if metadata: return serve_template(templatename="info.html", data=metadata, title="Info", config=config) else: logger.warn('Unable to retrieve data.') - return serve_template(templatename="info.html", data=None, title="Info") + return serve_template(templatename="info.html", data=None, query=query, title="Info") @cherrypy.expose def get_user_recently_watched(self, user=None, user_id=None, limit='10', **kwargs): @@ -1335,3 +1339,105 @@ class WebInterface(object): cherrypy.response.headers['Content-type'] = 'application/json' return json.dumps({'message': 'no data received'}) + @cherrypy.expose + def search(self, search_query=''): + query = search_query.replace('"', '') + + return serve_template(templatename="search.html", title="Search", query=query) + + @cherrypy.expose + def search_results(self, query, **kwargs): + + pms_connect = pmsconnect.PmsConnect() + result = pms_connect.get_search_results(query) + + if result: + cherrypy.response.headers['Content-type'] = 'application/json' + return json.dumps(result) + else: + logger.warn('Unable to retrieve data.') + + @cherrypy.expose + def get_search_results_children(self, query, media_type=None, season_index=None, **kwargs): + + pms_connect = pmsconnect.PmsConnect() + result = pms_connect.get_search_results(query) + + if media_type: + result['results_list'] = {media_type: result['results_list'][media_type]} + if media_type == 'season' and season_index: + for season in result['results_list']['season']: + if season['index'] == season_index: + result['results_list']['season'] = [season] + break + + if result: + return serve_template(templatename="info_search_results_list.html", data=result, title="Search Result List") + else: + logger.warn('Unable to retrieve data.') + return serve_template(templatename="info_search_results_list.html", data=None, title="Search Result List") + + @cherrypy.expose + def update_history_rating_key(self, old_rating_key, new_rating_key, media_type, **kwargs): + data_factory = datafactory.DataFactory() + pms_connect = pmsconnect.PmsConnect() + + old_key_list = data_factory.get_rating_keys_list(rating_key=old_rating_key, media_type=media_type) + new_key_list = pms_connect.get_rating_keys_list(rating_key=new_rating_key, media_type=media_type) + + update_db = data_factory.update_rating_key(old_key_list=old_key_list, + new_key_list=new_key_list, + media_type=media_type) + + if update_db: + cherrypy.response.headers['Content-type'] = 'application/json' + return json.dumps({'message': update_db}) + else: + cherrypy.response.headers['Content-type'] = 'application/json' + return json.dumps({'message': 'no data received'}) + + + # test code + @cherrypy.expose + def get_new_rating_keys(self, rating_key='', media_type='', **kwargs): + + pms_connect = pmsconnect.PmsConnect() + result = pms_connect.get_rating_keys_list(rating_key=rating_key, media_type=media_type) + + if result: + cherrypy.response.headers['Content-type'] = 'application/json' + return json.dumps(result) + else: + logger.warn('Unable to retrieve data.') + + @cherrypy.expose + def get_old_rating_keys(self, rating_key='', media_type='', **kwargs): + + data_factory = datafactory.DataFactory() + result = data_factory.get_rating_keys_list(rating_key=rating_key, media_type=media_type) + + if result: + cherrypy.response.headers['Content-type'] = 'application/json' + return json.dumps(result) + else: + logger.warn('Unable to retrieve data.') + + @cherrypy.expose + def get_map_rating_keys(self, old_rating_key, new_rating_key, media_type, **kwargs): + + data_factory = datafactory.DataFactory() + pms_connect = pmsconnect.PmsConnect() + + if new_rating_key: + old_key_list = data_factory.get_rating_keys_list(rating_key=old_rating_key, media_type=media_type) + new_key_list = pms_connect.get_rating_keys_list(rating_key=new_rating_key, media_type=media_type) + + result = data_factory.update_rating_key(old_key_list=old_key_list, + new_key_list=new_key_list, + media_type=media_type) + + if result: + cherrypy.response.headers['Content-type'] = 'application/json' + return json.dumps(result) + else: + logger.warn('Unable to retrieve data.')