# -*- coding: utf-8 -*- # 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 # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Tautulli 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 Tautulli. If not, see . from __future__ import absolute_import from __future__ import unicode_literals from builtins import str from builtins import next from builtins import object import json import os import plexpy from plexpy import common from plexpy import database from plexpy import datatables from plexpy import helpers from plexpy import logger from plexpy import plextv from plexpy import pmsconnect from plexpy import session def refresh_libraries(): logger.info("Tautulli Libraries :: Requesting libraries list refresh...") server_id = plexpy.CONFIG.PMS_IDENTIFIER if not server_id: logger.error("Tautulli Libraries :: No PMS identifier, cannot refresh libraries. Verify server in settings.") return library_sections = pmsconnect.PmsConnect().get_library_details() if library_sections: monitor_db = database.MonitorDatabase() library_keys = [] new_keys = [] for section in library_sections: section_keys = {'server_id': server_id, 'section_id': section['section_id']} section_values = {'server_id': server_id, 'section_id': section['section_id'], 'section_name': section['section_name'], 'section_type': section['section_type'], 'agent': section['agent'], 'thumb': section['thumb'], 'art': section['art'], 'count': section['count'], 'parent_count': section.get('parent_count', None), 'child_count': section.get('child_count', None), } result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values) library_keys.append(section['section_id']) if result == 'insert': new_keys.append(section['section_id']) if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']: plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys) plexpy.CONFIG.write() else: new_keys = plexpy.CONFIG.HOME_LIBRARY_CARDS + new_keys plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', new_keys) plexpy.CONFIG.write() #if plexpy.CONFIG.UPDATE_SECTION_IDS == 1 or plexpy.CONFIG.UPDATE_SECTION_IDS == -1: # # Start library section_id update on it's own thread # threading.Thread(target=libraries.update_section_ids).start() #if plexpy.CONFIG.UPDATE_LABELS == 1 or plexpy.CONFIG.UPDATE_LABELS == -1: # # Start library labels update on it's own thread # threading.Thread(target=libraries.update_labels).start() logger.info("Tautulli Libraries :: Libraries list refreshed.") return True else: logger.warn("Tautulli Libraries :: Unable to refresh libraries list.") return False def add_live_tv_library(): if not plexpy.CONFIG.ADD_LIVE_TV_LIBRARY: return logger.info(u"Tautulli Libraries :: Adding Live TV library to the database.") monitor_db = database.MonitorDatabase() section_keys = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER, 'section_id': common.LIVE_TV_SECTION_ID} section_values = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER, 'section_id': common.LIVE_TV_SECTION_ID, 'section_name': common.LIVE_TV_SECTION_NAME, 'section_type': 'live' } result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values) if result == 'insert': plexpy.CONFIG.__setattr__('ADD_LIVE_TV_LIBRARY', 0) plexpy.CONFIG.write() def has_library_type(section_type): monitor_db = database.MonitorDatabase() query = 'SELECT * FROM library_sections WHERE section_type = ? AND deleted_section = 0' args = [section_type] result = monitor_db.select_single(query=query, args=args) return bool(result) def update_section_ids(): plexpy.CONFIG.UPDATE_SECTION_IDS = -1 monitor_db = database.MonitorDatabase() try: query = 'SELECT id, rating_key, grandparent_rating_key, media_type ' \ 'FROM session_history_metadata WHERE section_id IS NULL' history_results = monitor_db.select(query=query) query = 'SELECT section_id, section_type FROM library_sections' library_results = monitor_db.select(query=query) except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for update_section_ids: %s." % e) logger.warn("Tautulli Libraries :: Unable to update section_id's in database.") plexpy.CONFIG.UPDATE_SECTION_IDS = 1 plexpy.CONFIG.write() return None if not history_results: plexpy.CONFIG.UPDATE_SECTION_IDS = 0 plexpy.CONFIG.write() return None logger.debug("Tautulli Libraries :: Updating section_id's in database.") # Get rating_key: section_id mapping pairs key_mappings = {} pms_connect = pmsconnect.PmsConnect() for library in library_results: section_id = library['section_id'] section_type = library['section_type'] if section_type != 'photo': library_children = pms_connect.get_library_children_details(section_id=section_id, section_type=section_type) if library_children: children_list = library_children['children_list'] key_mappings.update({child['rating_key']: child['section_id'] for child in children_list}) else: logger.warn("Tautulli Libraries :: Unable to get a list of library items for section_id %s." % section_id) error_keys = set() for item in history_results: rating_key = item['grandparent_rating_key'] if item['media_type'] != 'movie' else item['rating_key'] section_id = key_mappings.get(str(rating_key), None) if section_id: try: section_keys = {'id': item['id']} section_values = {'section_id': section_id} monitor_db.upsert('session_history_metadata', key_dict=section_keys, value_dict=section_values) except: error_keys.add(item['rating_key']) else: error_keys.add(item['rating_key']) if error_keys: logger.info("Tautulli Libraries :: Updated all section_id's in database except for rating_keys: %s." % ', '.join(str(key) for key in error_keys)) else: logger.info("Tautulli Libraries :: Updated all section_id's in database.") plexpy.CONFIG.UPDATE_SECTION_IDS = 0 plexpy.CONFIG.write() return True def update_labels(): plexpy.CONFIG.UPDATE_LABELS = -1 monitor_db = database.MonitorDatabase() try: query = 'SELECT section_id, section_type FROM library_sections' library_results = monitor_db.select(query=query) except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for update_labels: %s." % e) logger.warn("Tautulli Libraries :: Unable to update labels in database.") plexpy.CONFIG.UPDATE_LABELS = 1 plexpy.CONFIG.write() return None if not library_results: plexpy.CONFIG.UPDATE_LABELS = 0 plexpy.CONFIG.write() return None logger.debug("Tautulli Libraries :: Updating labels in database.") # Get rating_key: section_id mapping pairs key_mappings = {} pms_connect = pmsconnect.PmsConnect() for library in library_results: section_id = library['section_id'] section_type = library['section_type'] if section_type != 'photo': library_children = [] library_labels = pms_connect.get_library_label_details(section_id=section_id) if library_labels: for label in library_labels: library_children = pms_connect.get_library_children_details(section_id=section_id, section_type=section_type, label_key=label['label_key']) if library_children: children_list = library_children['children_list'] # rating_key_list = [child['rating_key'] for child in children_list] for rating_key in [child['rating_key'] for child in children_list]: if key_mappings.get(rating_key): key_mappings[rating_key].append(label['label_title']) else: key_mappings[rating_key] = [label['label_title']] else: logger.warn("Tautulli Libraries :: Unable to get a list of library items for section_id %s." % section_id) error_keys = set() for rating_key, labels in key_mappings.items(): try: labels = ';'.join(labels) monitor_db.action('UPDATE session_history_metadata SET labels = ? ' 'WHERE rating_key = ? OR parent_rating_key = ? OR grandparent_rating_key = ? ', args=[labels, rating_key, rating_key, rating_key]) except: error_keys.add(rating_key) if error_keys: logger.info("Tautulli Libraries :: Updated all labels in database except for rating_keys: %s." % ', '.join(str(key) for key in error_keys)) else: logger.info("Tautulli Libraries :: Updated all labels in database.") plexpy.CONFIG.UPDATE_LABELS = 0 plexpy.CONFIG.write() return True class Libraries(object): def __init__(self): pass def get_datatables_list(self, kwargs=None, grouping=None): default_return = {'recordsFiltered': 0, 'recordsTotal': 0, 'draw': 0, 'data': 'null', 'error': 'Unable to execute database query.'} data_tables = datatables.DataTables() custom_where = [['library_sections.deleted_section', 0]] if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES if session.get_session_shared_libraries(): custom_where.append(['library_sections.section_id', session.get_session_shared_libraries()]) group_by = 'session_history.reference_id' if grouping else 'session_history.id' columns = ['library_sections.section_id', 'library_sections.section_name', 'library_sections.section_type', 'library_sections.count', 'library_sections.parent_count', 'library_sections.child_count', 'library_sections.thumb AS library_thumb', 'library_sections.custom_thumb_url AS custom_thumb', 'library_sections.art', 'COUNT(DISTINCT %s) AS plays' % group_by, 'SUM(CASE WHEN session_history.stopped > 0 THEN (session_history.stopped - session_history.started) \ ELSE 0 END) - SUM(CASE WHEN session_history.paused_counter IS NULL THEN 0 ELSE \ session_history.paused_counter END) AS duration', 'MAX(session_history.started) AS last_accessed', 'MAX(session_history.id) AS id', 'session_history_metadata.full_title AS last_played', 'session_history.rating_key', 'session_history_metadata.media_type', 'session_history_metadata.thumb', 'session_history_metadata.parent_thumb', 'session_history_metadata.grandparent_thumb', 'session_history_metadata.parent_title', 'session_history_metadata.year', 'session_history_metadata.media_index', 'session_history_metadata.parent_media_index', 'session_history_metadata.content_rating', 'session_history_metadata.labels', 'session_history_metadata.live', 'session_history_metadata.added_at', 'session_history_metadata.originally_available_at', 'session_history_metadata.guid', 'library_sections.do_notify', 'library_sections.do_notify_created', 'library_sections.keep_history' ] try: query = data_tables.ssp_query(table_name='library_sections', columns=columns, custom_where=custom_where, group_by=['library_sections.server_id', 'library_sections.section_id'], join_types=['LEFT OUTER JOIN', 'LEFT OUTER JOIN', 'LEFT OUTER JOIN'], join_tables=['session_history_metadata', 'session_history', 'session_history_media_info'], join_evals=[['session_history_metadata.section_id', 'library_sections.section_id'], ['session_history_metadata.id', 'session_history.id'], ['session_history_metadata.id', 'session_history_media_info.id']], kwargs=kwargs) except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for get_list: %s." % e) return default_return result = query['result'] rows = [] for item in result: if item['media_type'] == 'episode' and item['parent_thumb']: thumb = item['parent_thumb'] elif item['media_type'] == 'episode': thumb = item['grandparent_thumb'] else: thumb = item['thumb'] if item['custom_thumb'] and item['custom_thumb'] != item['library_thumb']: library_thumb = item['custom_thumb'] elif item['library_thumb']: library_thumb = item['library_thumb'] else: library_thumb = common.DEFAULT_COVER_THUMB row = {'section_id': item['section_id'], 'section_name': item['section_name'], 'section_type': item['section_type'], 'count': item['count'], 'parent_count': item['parent_count'], 'child_count': item['child_count'], 'library_thumb': library_thumb, 'library_art': item['art'], 'plays': item['plays'], 'duration': item['duration'], 'last_accessed': item['last_accessed'], 'id': item['id'], 'last_played': item['last_played'], 'rating_key': item['rating_key'], 'media_type': item['media_type'], 'thumb': thumb, 'parent_title': item['parent_title'], 'year': item['year'], 'media_index': item['media_index'], 'parent_media_index': item['parent_media_index'], 'content_rating': item['content_rating'], 'labels': item['labels'].split(';') if item['labels'] else (), 'live': item['live'], 'originally_available_at': item['originally_available_at'], 'guid': item['guid'], 'do_notify': helpers.checked(item['do_notify']), 'do_notify_created': helpers.checked(item['do_notify_created']), 'keep_history': helpers.checked(item['keep_history']) } rows.append(row) dict = {'recordsFiltered': query['filteredCount'], 'recordsTotal': query['totalCount'], 'data': session.mask_session_info(rows), 'draw': query['draw'] } return dict def get_datatables_media_info(self, section_id=None, section_type=None, rating_key=None, refresh=False, kwargs=None): default_return = {'recordsFiltered': 0, 'recordsTotal': 0, 'draw': 0, 'data': 'null', 'error': 'Unable to execute database query.'} if not session.allow_session_library(section_id): return default_return if section_id and not str(section_id).isdigit(): logger.warn("Tautulli Libraries :: Datatable media info called but invalid section_id provided.") return default_return elif rating_key and not str(rating_key).isdigit(): logger.warn("Tautulli Libraries :: Datatable media info called but invalid rating_key provided.") return default_return elif not section_id and not rating_key: logger.warn("Tautulli Libraries :: Datatable media info called but no input provided.") return default_return # Get the library details library_details = self.get_details(section_id=section_id) if library_details['section_id'] == None: logger.debug("Tautulli Libraries :: Library section_id %s not found." % section_id) return default_return if not section_type: section_type = library_details['section_type'] # Get play counts from the database monitor_db = database.MonitorDatabase() if plexpy.CONFIG.GROUP_HISTORY_TABLES: count_by = 'reference_id' else: count_by = 'id' if section_type == 'show' or section_type == 'artist': group_by = 'grandparent_rating_key' elif section_type == 'season' or section_type == 'album': group_by = 'parent_rating_key' else: group_by = 'rating_key' try: query = 'SELECT MAX(session_history.started) AS last_played, COUNT(DISTINCT session_history.%s) AS play_count, ' \ 'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key ' \ 'FROM session_history ' \ 'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' \ 'WHERE session_history_metadata.section_id = ? ' \ 'GROUP BY session_history.%s ' % (count_by, group_by) result = monitor_db.select(query, args=[section_id]) except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for get_datatables_media_info2: %s." % e) return default_return watched_list = {} for item in result: watched_list[str(item[group_by])] = {'last_played': item['last_played'], 'play_count': item['play_count']} rows = [] # Import media info cache from json file if rating_key: try: inFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s-%s.json' % (section_id, rating_key)) with open(inFilePath, 'r') as inFile: rows = json.load(inFile) library_count = len(rows) except IOError as e: #logger.debug("Tautulli Libraries :: No JSON file for rating_key %s." % rating_key) #logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for rating_key %s." % rating_key) pass elif section_id: try: inFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s.json' % section_id) with open(inFilePath, 'r') as inFile: rows = json.load(inFile) library_count = len(rows) except IOError as e: #logger.debug("Tautulli Libraries :: No JSON file for library section_id %s." % section_id) #logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for section_id %s." % section_id) pass # If no cache was imported, get all library children items cached_items = {d['rating_key']: d['file_size'] for d in rows} if not refresh else {} if refresh or not rows: pms_connect = pmsconnect.PmsConnect() if rating_key: library_children = pms_connect.get_library_children_details(rating_key=rating_key, get_media_info=True) elif section_id: library_children = pms_connect.get_library_children_details(section_id=section_id, section_type=section_type, get_media_info=True) if library_children: library_count = library_children['library_count'] children_list = library_children['children_list'] else: logger.warn("Tautulli Libraries :: Unable to get a list of library items.") return default_return new_rows = [] for item in children_list: ## TODO: Check list of media info items, currently only grabs first item cached_file_size = cached_items.get(item['rating_key'], None) file_size = cached_file_size if cached_file_size else item.get('file_size', '') row = {'section_id': library_details['section_id'], 'section_type': library_details['section_type'], 'added_at': item['added_at'], 'media_type': item['media_type'], 'rating_key': item['rating_key'], 'parent_rating_key': item['parent_rating_key'], 'grandparent_rating_key': item['grandparent_rating_key'], 'title': item['title'], 'sort_title': item['sort_title'] or item['title'], 'year': item['year'], 'media_index': item['media_index'], 'parent_media_index': item['parent_media_index'], 'thumb': item['thumb'], 'container': item.get('container', ''), 'bitrate': item.get('bitrate', ''), 'video_codec': item.get('video_codec', ''), 'video_resolution': item.get('video_resolution', ''), 'video_framerate': item.get('video_framerate', ''), 'audio_codec': item.get('audio_codec', ''), 'audio_channels': item.get('audio_channels', ''), 'file_size': file_size } new_rows.append(row) rows = new_rows if not rows: return default_return # Cache the media info to a json file if rating_key: try: outFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s-%s.json' % (section_id, rating_key)) with open(outFilePath, 'w') as outFile: json.dump(rows, outFile) except IOError as e: logger.debug("Tautulli Libraries :: Unable to create cache file for rating_key %s." % rating_key) elif section_id: try: outFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s.json' % section_id) with open(outFilePath, 'w') as outFile: json.dump(rows, outFile) except IOError as e: logger.debug("Tautulli Libraries :: Unable to create cache file for section_id %s." % section_id) # Update the last_played and play_count for item in rows: watched_item = watched_list.get(item['rating_key'], None) if watched_item: item['last_played'] = watched_item['last_played'] item['play_count'] = watched_item['play_count'] else: item['last_played'] = None item['play_count'] = None results = [] # Get datatables JSON data if kwargs.get('json_data'): json_data = helpers.process_json_kwargs(json_kwargs=kwargs.get('json_data')) #print json_data # Search results search_value = json_data['search']['value'].lower() if search_value: searchable_columns = [d['data'] for d in json_data['columns'] if d['searchable']] + ['title'] for row in rows: for k,v in row.items(): if k in searchable_columns and search_value in v.lower(): results.append(row) break else: results = rows filtered_count = len(results) # Sort results results = sorted(results, key=lambda k: k['sort_title'].lower()) sort_order = json_data['order'] for order in reversed(sort_order): sort_key = json_data['columns'][int(order['column'])]['data'] reverse = True if order['dir'] == 'desc' else False if rating_key and sort_key == 'sort_title': results = sorted(results, key=lambda k: helpers.cast_to_int(k['media_index']), reverse=reverse) elif sort_key in ('file_size', 'bitrate', 'added_at', 'last_played', 'play_count'): results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key]), reverse=reverse) elif sort_key == 'video_resolution': results = sorted(results, key=lambda k: helpers.cast_to_int(k[sort_key].replace('4k', '2160p').rstrip('p')), reverse=reverse) else: results = sorted(results, key=lambda k: k[sort_key].lower(), reverse=reverse) total_file_size = sum([helpers.cast_to_int(d['file_size']) for d in results]) # Paginate results results = results[json_data['start']:(json_data['start'] + json_data['length'])] filtered_file_size = sum([helpers.cast_to_int(d['file_size']) for d in results]) dict = {'recordsFiltered': filtered_count, 'recordsTotal': library_count, 'data': results, 'draw': int(json_data['draw']), 'filtered_file_size': filtered_file_size, 'total_file_size': total_file_size } return dict def get_media_info_file_sizes(self, section_id=None, rating_key=None): if not session.allow_session_library(section_id): return False if section_id and not str(section_id).isdigit(): logger.warn("Tautulli Libraries :: Datatable media info file size called but invalid section_id provided.") return False elif rating_key and not str(rating_key).isdigit(): logger.warn("Tautulli Libraries :: Datatable media info file size called but invalid rating_key provided.") return False # Get the library details library_details = self.get_details(section_id=section_id) if library_details['section_id'] == None: logger.debug("Tautulli Libraries :: Library section_id %s not found." % section_id) return False if library_details['section_type'] == 'photo': return False rows = [] # Import media info cache from json file if rating_key: #logger.debug("Tautulli Libraries :: Getting file sizes for rating_key %s." % rating_key) try: inFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s-%s.json' % (section_id, rating_key)) with open(inFilePath, 'r') as inFile: rows = json.load(inFile) except IOError as e: #logger.debug("Tautulli Libraries :: No JSON file for rating_key %s." % rating_key) #logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for rating_key %s." % rating_key) pass elif section_id: logger.debug("Tautulli Libraries :: Getting file sizes for section_id %s." % section_id) try: inFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s.json' % section_id) with open(inFilePath, 'r') as inFile: rows = json.load(inFile) except IOError as e: #logger.debug("Tautulli Libraries :: No JSON file for library section_id %s." % section_id) #logger.debug("Tautulli Libraries :: Refreshing data and creating new JSON file for section_id %s." % section_id) pass # Get the total file size for each item pms_connect = pmsconnect.PmsConnect() for item in rows: if item['rating_key'] and not item['file_size']: file_size = 0 metadata = pms_connect.get_metadata_children_details(rating_key=item['rating_key'], get_children=True) for child_metadata in metadata: ## TODO: Check list of media info items, currently only grabs first item media_info = media_part_info = {} if 'media_info' in child_metadata and len(child_metadata['media_info']) > 0: media_info = child_metadata['media_info'][0] if 'parts' in media_info and len (media_info['parts']) > 0: media_part_info = next((p for p in media_info['parts'] if p['selected']), media_info['parts'][0]) file_size += helpers.cast_to_int(media_part_info.get('file_size', 0)) item['file_size'] = file_size # Cache the media info to a json file if rating_key: try: outFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s-%s.json' % (section_id, rating_key)) with open(outFilePath, 'w') as outFile: json.dump(rows, outFile) except IOError as e: logger.debug("Tautulli Libraries :: Unable to create cache file with file sizes for rating_key %s." % rating_key) elif section_id: try: outFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s.json' % section_id) with open(outFilePath, 'w') as outFile: json.dump(rows, outFile) except IOError as e: logger.debug("Tautulli Libraries :: Unable to create cache file with file sizes for section_id %s." % section_id) if rating_key: #logger.debug("Tautulli Libraries :: File sizes updated for rating_key %s." % rating_key) pass elif section_id: logger.debug("Tautulli Libraries :: File sizes updated for section_id %s." % section_id) return True def set_config(self, section_id=None, custom_thumb='', custom_art='', do_notify=1, keep_history=1, do_notify_created=1): if section_id: monitor_db = database.MonitorDatabase() key_dict = {'section_id': section_id} value_dict = {'custom_thumb_url': custom_thumb, 'custom_art_url': custom_art, 'do_notify': do_notify, 'do_notify_created': do_notify_created, 'keep_history': keep_history} try: monitor_db.upsert('library_sections', value_dict, key_dict) except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for set_config: %s." % e) def get_details(self, section_id=None): default_return = {'section_id': 0, 'section_name': 'Local', 'section_type': '', 'library_thumb': common.DEFAULT_COVER_THUMB, 'library_art': '', 'count': 0, 'parent_count': 0, 'child_count': 0, 'do_notify': 0, 'do_notify_created': 0, 'keep_history': 1, 'deleted_section': 0 } if not section_id: return default_return def get_library_details(section_id=section_id): monitor_db = database.MonitorDatabase() try: if str(section_id).isdigit(): query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \ 'thumb AS library_thumb, custom_thumb_url AS custom_thumb, art AS library_art, ' \ 'custom_art_url AS custom_art, ' \ 'do_notify, do_notify_created, keep_history, deleted_section ' \ 'FROM library_sections ' \ 'WHERE section_id = ? ' result = monitor_db.select(query, args=[section_id]) else: result = [] except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for get_details: %s." % e) result = [] library_details = {} if result: for item in result: if item['custom_thumb'] and item['custom_thumb'] != item['library_thumb']: library_thumb = item['custom_thumb'] elif item['library_thumb']: library_thumb = item['library_thumb'] else: library_thumb = common.DEFAULT_COVER_THUMB if item['custom_art'] and item['custom_art'] != item['library_art']: library_art = item['custom_art'] else: library_art = item['library_art'] library_details = {'section_id': item['section_id'], 'section_name': item['section_name'], 'section_type': item['section_type'], 'library_thumb': library_thumb, 'library_art': library_art, 'count': item['count'], 'parent_count': item['parent_count'], 'child_count': item['child_count'], 'do_notify': item['do_notify'], 'do_notify_created': item['do_notify_created'], 'keep_history': item['keep_history'], 'deleted_section': item['deleted_section'] } return library_details library_details = get_library_details(section_id=section_id) if library_details: return library_details else: logger.warn("Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh." % section_id) # Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet refresh_libraries() library_details = get_library_details(section_id=section_id) if library_details: return library_details else: logger.warn("Tautulli Users :: Unable to retrieve library %s from database. Returning 'Local' library." % section_id) # If there is no library data we must return something return default_return def get_watch_time_stats(self, section_id=None, grouping=None): if not session.allow_session_library(section_id): return [] if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES monitor_db = database.MonitorDatabase() time_queries = [1, 7, 30, 0] library_watch_time_stats = [] group_by = 'session_history.reference_id' if grouping else 'session_history.id' for days in time_queries: try: if days > 0: if str(section_id).isdigit(): query = 'SELECT (SUM(stopped - started) - ' \ 'SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END)) AS total_time, ' \ 'COUNT(DISTINCT %s) AS total_plays ' \ 'FROM session_history ' \ 'JOIN session_history_metadata ON session_history_metadata.id = session_history.id ' \ 'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \ 'AND section_id = ?' % (group_by, days) result = monitor_db.select(query, args=[section_id]) else: result = [] else: if str(section_id).isdigit(): query = 'SELECT (SUM(stopped - started) - ' \ 'SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END)) AS total_time, ' \ 'COUNT(DISTINCT %s) AS total_plays ' \ 'FROM session_history ' \ 'JOIN session_history_metadata ON session_history_metadata.id = session_history.id ' \ 'WHERE section_id = ?' % group_by result = monitor_db.select(query, args=[section_id]) else: result = [] except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for get_watch_time_stats: %s." % e) result = [] for item in result: if item['total_time']: total_time = item['total_time'] total_plays = item['total_plays'] else: total_time = 0 total_plays = 0 row = {'query_days': days, 'total_time': total_time, 'total_plays': total_plays } library_watch_time_stats.append(row) return library_watch_time_stats def get_user_stats(self, section_id=None, grouping=None): if not session.allow_session_library(section_id): return [] if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES monitor_db = database.MonitorDatabase() user_stats = [] group_by = 'session_history.reference_id' if grouping else 'session_history.id' try: if str(section_id).isdigit(): query = 'SELECT (CASE WHEN users.friendly_name IS NULL OR TRIM(users.friendly_name) = "" ' \ 'THEN users.username ELSE users.friendly_name END) AS friendly_name, ' \ 'users.user_id, users.thumb, COUNT(DISTINCT %s) AS user_count ' \ 'FROM session_history ' \ 'JOIN session_history_metadata ON session_history_metadata.id = session_history.id ' \ 'JOIN users ON users.user_id = session_history.user_id ' \ 'WHERE section_id = ? ' \ 'GROUP BY users.user_id ' \ 'ORDER BY user_count DESC' % group_by result = monitor_db.select(query, args=[section_id]) else: result = [] except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for get_user_stats: %s." % e) result = [] for item in result: row = {'friendly_name': item['friendly_name'], 'user_id': item['user_id'], 'user_thumb': item['thumb'], 'total_plays': item['user_count'] } user_stats.append(row) return session.mask_session_info(user_stats, mask_metadata=False) def get_recently_watched(self, section_id=None, limit='10'): if not session.allow_session_library(section_id): return [] monitor_db = database.MonitorDatabase() recently_watched = [] if not limit.isdigit(): limit = '10' try: if str(section_id).isdigit(): query = 'SELECT session_history.id, session_history.media_type, guid, ' \ 'session_history.rating_key, session_history.parent_rating_key, session_history.grandparent_rating_key, ' \ 'title, parent_title, grandparent_title, original_title, ' \ 'thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, ' \ 'year, originally_available_at, added_at, live, started, user, content_rating, labels, section_id ' \ 'FROM session_history_metadata ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \ 'WHERE section_id = ? ' \ 'GROUP BY session_history.rating_key ' \ 'ORDER BY started DESC LIMIT ?' result = monitor_db.select(query, args=[section_id, limit]) else: result = [] except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for get_recently_watched: %s." % e) result = [] for row in result: if row['media_type'] == 'episode' and row['parent_thumb']: thumb = row['parent_thumb'] elif row['media_type'] == 'episode': thumb = row['grandparent_thumb'] else: thumb = row['thumb'] recent_output = {'row_id': row['id'], 'media_type': row['media_type'], 'rating_key': row['rating_key'], 'parent_rating_key': row['parent_rating_key'], 'grandparent_rating_key': row['grandparent_rating_key'], 'title': row['title'], 'parent_title': row['parent_title'], 'grandparent_title': row['grandparent_title'], 'original_title': row['original_title'], 'thumb': thumb, 'media_index': row['media_index'], 'parent_media_index': row['parent_media_index'], 'year': row['year'], 'originally_available_at': row['originally_available_at'], 'live': row['live'], 'guid': row['guid'], 'time': row['started'], 'user': row['user'], 'section_id': row['section_id'], 'content_rating': row['content_rating'], 'labels': row['labels'].split(';') if row['labels'] else (), } recently_watched.append(recent_output) return session.mask_session_info(recently_watched) def get_sections(self): monitor_db = database.MonitorDatabase() try: query = 'SELECT section_id, section_name, section_type, agent ' \ 'FROM library_sections WHERE deleted_section = 0' result = monitor_db.select(query=query) except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for get_sections: %s." % e) return None libraries = [] for item in result: library = {'section_id': item['section_id'], 'section_name': item['section_name'], 'section_type': item['section_type'], 'agent': item['agent'] } libraries.append(library) return libraries def delete_all_history(self, section_id=None): monitor_db = database.MonitorDatabase() try: if section_id.isdigit(): logger.info("Tautulli Libraries :: Deleting all history for library id %s from database." % section_id) session_history_media_info_del = \ monitor_db.action('DELETE FROM ' 'session_history_media_info ' 'WHERE session_history_media_info.id IN (SELECT session_history_media_info.id ' 'FROM session_history_media_info ' 'JOIN session_history_metadata ON session_history_media_info.id = session_history_metadata.id ' 'WHERE session_history_metadata.section_id = ?)', [section_id]) session_history_del = \ monitor_db.action('DELETE FROM ' 'session_history ' 'WHERE session_history.id IN (SELECT session_history.id ' 'FROM session_history ' 'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' 'WHERE session_history_metadata.section_id = ?)', [section_id]) session_history_metadata_del = \ monitor_db.action('DELETE FROM ' 'session_history_metadata ' 'WHERE session_history_metadata.section_id = ?', [section_id]) return 'Deleted all items for section_id %s.' % section_id else: return 'Unable to delete items, section_id not valid.' except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for delete_all_history: %s." % e) def delete(self, section_id=None): monitor_db = database.MonitorDatabase() try: if section_id.isdigit(): self.delete_all_history(section_id) logger.info("Tautulli Libraries :: Deleting library with id %s from database." % section_id) monitor_db.action('UPDATE library_sections SET deleted_section = 1 WHERE section_id = ?', [section_id]) monitor_db.action('UPDATE library_sections SET keep_history = 0 WHERE section_id = ?', [section_id]) monitor_db.action('UPDATE library_sections SET do_notify = 0 WHERE section_id = ?', [section_id]) monitor_db.action('UPDATE library_sections SET do_notify_created = 0 WHERE section_id = ?', [section_id]) library_cards = plexpy.CONFIG.HOME_LIBRARY_CARDS if section_id in library_cards: library_cards.remove(section_id) plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_cards) plexpy.CONFIG.write() return 'Deleted library with id %s.' % section_id else: return 'Unable to delete library, section_id not valid.' except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for delete: %s." % e) def undelete(self, section_id=None, section_name=None): monitor_db = database.MonitorDatabase() try: if section_id and section_id.isdigit(): query = 'SELECT * FROM library_sections WHERE section_id = ?' result = monitor_db.select(query=query, args=[section_id]) if result: logger.info("Tautulli Libraries :: Re-adding library with id %s to database." % section_id) monitor_db.action('UPDATE library_sections SET deleted_section = 0 WHERE section_id = ?', [section_id]) monitor_db.action('UPDATE library_sections SET keep_history = 1 WHERE section_id = ?', [section_id]) monitor_db.action('UPDATE library_sections SET do_notify = 1 WHERE section_id = ?', [section_id]) monitor_db.action('UPDATE library_sections SET do_notify_created = 1 WHERE section_id = ?', [section_id]) return True else: return False elif section_name: query = 'SELECT * FROM library_sections WHERE section_name = ?' result = monitor_db.select(query=query, args=[section_name]) if result: logger.info("Tautulli Libraries :: Re-adding library with name %s to database." % section_name) monitor_db.action('UPDATE library_sections SET deleted_section = 0 WHERE section_name = ?', [section_name]) monitor_db.action('UPDATE library_sections SET keep_history = 1 WHERE section_name = ?', [section_name]) monitor_db.action('UPDATE library_sections SET do_notify = 1 WHERE section_name = ?', [section_name]) monitor_db.action('UPDATE library_sections SET do_notify_created = 1 WHERE section_name = ?', [section_name]) return True else: return False except Exception as e: logger.warn("Tautulli Libraries :: Unable to execute database query for undelete: %s." % e) def delete_media_info_cache(self, section_id=None): import os try: if section_id.isdigit(): [os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, f)) for f in os.listdir(plexpy.CONFIG.CACHE_DIR) if f.startswith('media_info_%s' % section_id) and f.endswith('.json')] logger.debug("Tautulli Libraries :: Deleted media info table cache for section_id %s." % section_id) return 'Deleted media info table cache for library with id %s.' % section_id else: return 'Unable to delete media info table cache, section_id not valid.' except Exception as e: logger.warn("Tautulli Libraries :: Unable to delete media info table cache: %s." % e) def delete_duplicate_libraries(self): monitor_db = database.MonitorDatabase() # Refresh the PMS_URL to make sure the server_id is updated plextv.get_server_resources() server_id = plexpy.CONFIG.PMS_IDENTIFIER try: logger.debug("Tautulli Libraries :: Deleting libraries where server_id does not match %s." % server_id) monitor_db.action('DELETE FROM library_sections WHERE server_id != ?', [server_id]) return 'Deleted duplicate libraries from the database.' except Exception as e: logger.warn("Tautulli Libraries :: Unable to delete duplicate libraries: %s." % e) def update_libraries_db_notify(): logger.info("Tautulli Libraries :: Upgrading library notification toggles...") # Set flag first in case something fails we don't want to keep re-adding the notifiers plexpy.CONFIG.__setattr__('UPDATE_LIBRARIES_DB_NOTIFY', 0) plexpy.CONFIG.write() libraries = Libraries() sections = libraries.get_sections() for section in sections: section_details = libraries.get_details(section['section_id']) if (section_details['do_notify'] == 1 and (section_details['section_type'] == 'movie' and not plexpy.CONFIG.MOVIE_NOTIFY_ENABLE) or (section_details['section_type'] == 'show' and not plexpy.CONFIG.TV_NOTIFY_ENABLE) or (section_details['section_type'] == 'artist' and not plexpy.CONFIG.MUSIC_NOTIFY_ENABLE)): do_notify = 0 else: do_notify = section_details['do_notify'] if (section_details['keep_history'] == 1 and (section_details['section_type'] == 'movie' and not plexpy.CONFIG.MOVIE_LOGGING_ENABLE) or (section_details['section_type'] == 'show' and not plexpy.CONFIG.TV_LOGGING_ENABLE) or (section_details['section_type'] == 'artist' and not plexpy.CONFIG.MUSIC_LOGGING_ENABLE)): keep_history = 0 else: keep_history = section_details['keep_history'] libraries.set_config(section_id=section_details['section_id'], custom_thumb=section_details['library_thumb'], do_notify=do_notify, keep_history=keep_history, do_notify_created=section_details['do_notify_created'])