diff --git a/data/interfaces/default/js/tables/user_ips.js b/data/interfaces/default/js/tables/user_ips.js index e73c7635..084b0378 100644 --- a/data/interfaces/default/js/tables/user_ips.js +++ b/data/interfaces/default/js/tables/user_ips.js @@ -167,7 +167,7 @@ $('.user_ip_table').on('click', 'td.modal-control', function () { function showStreamDetails() { $.ajax({ url: 'get_stream_data', - data: { row_id: rowData['id'], user: rowData['friendly_name'] }, + data: { row_id: rowData['history_row_id'], user: rowData['friendly_name'] }, cache: false, async: true, complete: function (xhr, status) { diff --git a/data/interfaces/default/js/tables/users.js b/data/interfaces/default/js/tables/users.js index 58918d46..15c64fbc 100644 --- a/data/interfaces/default/js/tables/users.js +++ b/data/interfaces/default/js/tables/users.js @@ -44,8 +44,8 @@ users_list_table_options = { "data": null, "createdCell": function (td, cellData, rowData, row, col) { $(td).html('
' + - ' ' + - '   ' + + ' ' + + '   ' + ' ' + ' ' + '
'); @@ -77,7 +77,7 @@ users_list_table_options = { "data": "friendly_name", "createdCell": function (td, cellData, rowData, row, col) { if (cellData !== null && cellData !== '') { - $(td).html('
' + + $(td).html('
' + '' + cellData + '' + '' + '
'); @@ -256,10 +256,10 @@ users_list_table_options = { }, "rowCallback": function (row, rowData) { if ($.inArray(rowData['user_id'], users_to_delete) !== -1) { - $(row).find('button.delete-user[data-id="' + rowData['user_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); + $(row).find('button.delete-user[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); } if ($.inArray(rowData['user_id'], users_to_purge) !== -1) { - $(row).find('button.purge-user[data-id="' + rowData['user_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); + $(row).find('button.purge-user[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); } } } @@ -270,7 +270,7 @@ $('#users_list_table').on('click', 'td.modal-control', function () { var rowData = row.data(); $.get('get_stream_data', { - row_id: rowData['id'], + row_id: rowData['history_row_id'], user: rowData['friendly_name'] }).then(function (jqXHR) { $("#info-modal").html(jqXHR); @@ -328,11 +328,11 @@ $('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > butto var row = users_list_table.row(tr); var rowData = row.data(); - var index_delete = $.inArray(rowData['user_id'], users_to_delete); - var index_purge = $.inArray(rowData['user_id'], users_to_purge); + var index_delete = $.inArray(rowData['row_id'], users_to_delete); + var index_purge = $.inArray(rowData['row_id'], users_to_purge); if (index_delete === -1) { - users_to_delete.push(rowData['user_id']); + users_to_delete.push(rowData['row_id']); if (index_purge === -1) { tr.find('button.purge-user').click(); } @@ -351,11 +351,11 @@ $('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > butto var row = users_list_table.row(tr); var rowData = row.data(); - var index_delete = $.inArray(rowData['user_id'], users_to_delete); - var index_purge = $.inArray(rowData['user_id'], users_to_purge); + var index_delete = $.inArray(rowData['row_id'], users_to_delete); + var index_purge = $.inArray(rowData['row_id'], users_to_purge); if (index_purge === -1) { - users_to_purge.push(rowData['user_id']); + users_to_purge.push(rowData['row_id']); } else { users_to_purge.splice(index_purge, 1); if (index_delete != -1) { diff --git a/data/interfaces/default/users.html b/data/interfaces/default/users.html index 79f264e3..f3595148 100644 --- a/data/interfaces/default/users.html +++ b/data/interfaces/default/users.html @@ -119,14 +119,14 @@ }); if (users_to_delete.length > 0) { - $('#users-to-delete').prepend('

Are you REALLY sure you want to delete and purge all history for the following users:

') + $('#users-to-delete').prepend('

Are you REALLY sure you want to delete and purge all history for the following users:

'); for (var i = 0; i < users_to_delete.length; i++) { $('#users-to-delete').append('
  • ' + $('div[data-id=' + users_to_delete[i] + '] > input').val() + '
  • '); } } if (users_to_purge.length > 0) { - $('#users-to-purge').prepend('

    Are you REALLY sure you want to purge all history for the following users:

    ') + $('#users-to-purge').prepend('

    Are you REALLY sure you want to purge all history for the following users:

    '); for (var i = 0; i < users_to_purge.length; i++) { $('#users-to-purge').append('
  • ' + $('div[data-id=' + users_to_purge[i] + '] > input').val() + '
  • '); } @@ -134,31 +134,27 @@ $('#confirm-modal-delete').modal(); $('#confirm-modal-delete').one('click', '#confirm-delete', function () { - users_to_delete.forEach(function(row, idx) { - $.ajax({ - url: 'delete_user', - type: 'POST', - data: { user_id: row }, - cache: false, - async: true, - success: function (data) { - var msg = "User deleted"; - showMsg(msg, false, true, 2000); - } - }); + $.ajax({ + url: 'delete_user', + type: 'POST', + data: { row_ids: users_to_delete.join(',') }, + cache: false, + async: true, + success: function (data) { + var msg = "User deleted"; + showMsg(msg, false, true, 2000); + } }); - users_to_purge.forEach(function(row, idx) { - $.ajax({ - url: 'delete_all_user_history', - type: 'POST', - data: { user_id: row }, - cache: false, - async: true, - success: function (data) { - var msg = "User history purged"; - showMsg(msg, false, true, 2000); - } - }); + $.ajax({ + url: 'delete_all_user_history', + type: 'POST', + data: { row_ids: users_to_purge.join(',') }, + cache: false, + async: true, + success: function (data) { + var msg = "User history purged"; + showMsg(msg, false, true, 2000); + } }); users_list_table.draw(); }); @@ -192,7 +188,7 @@ complete: function (xhr, status) { var result = $.parseJSON(xhr.responseText); var msg = result.message; - if (result.result == 'success') { + if (result.result === 'success') { showMsg(' ' + msg, false, true, 2000, false); users_list_table.draw(); } else { diff --git a/plexpy/database.py b/plexpy/database.py index 203142eb..b55deaab 100644 --- a/plexpy/database.py +++ b/plexpy/database.py @@ -58,14 +58,6 @@ def delete_recently_added(): return clear_table('recently_added') -def delete_session_history_rows(row_ids=None): - if row_ids: - for table in ('session_history', 'session_history_media_info', 'session_history_metadata'): - delete_rows_from_table(table=table, row_ids=row_ids) - return True - return False - - def delete_rows_from_table(table, row_ids): if row_ids and isinstance(row_ids, basestring): row_ids = map(helpers.cast_to_int, row_ids.split(',')) @@ -76,6 +68,26 @@ def delete_rows_from_table(table, row_ids): monitor_db.action(query, row_ids) +def delete_session_history_rows(row_ids=None): + if row_ids: + for table in ('session_history', 'session_history_media_info', 'session_history_metadata'): + delete_rows_from_table(table=table, row_ids=row_ids) + return True + return False + + +def delete_user_history(user_id=None): + if str(user_id).isdigit(): + monitor_db = MonitorDatabase() + + # Get all history associated with the user_id + result = monitor_db.select('SELECT id FROM session_history WHERE user_id = ?', [user_id]) + row_ids = [row['id'] for row in result] + + logger.info(u"Tautulli Database :: Deleting all history for user id %s from database." % user_id) + return delete_session_history_rows(row_ids=row_ids) + + def db_filename(filename=FILENAME): """ Returns the filepath to the db """ diff --git a/plexpy/users.py b/plexpy/users.py index ddac44fa..93efc810 100644 --- a/plexpy/users.py +++ b/plexpy/users.py @@ -99,7 +99,8 @@ class Users(object): group_by = 'session_history.reference_id' if grouping else 'session_history.id' - columns = ['users.user_id', + columns = ['users.id AS row_id', + 'users.user_id', '(CASE WHEN users.friendly_name IS NULL OR TRIM(users.friendly_name) = "" \ THEN users.username ELSE users.friendly_name END) AS friendly_name', 'users.thumb AS user_thumb', @@ -109,7 +110,7 @@ class Users(object): 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_seen', - 'MAX(session_history.id) AS id', + 'MAX(session_history.id) AS history_row_id', 'session_history_metadata.full_title AS last_played', 'session_history.ip_address', 'session_history.platform', @@ -128,10 +129,10 @@ class Users(object): 'session_history_metadata.originally_available_at', 'session_history_metadata.guid', 'session_history_media_info.transcode_decision', - 'users.do_notify as do_notify', - 'users.keep_history as keep_history', - 'users.allow_guest as allow_guest', - 'users.is_active as is_active' + 'users.do_notify AS do_notify', + 'users.keep_history AS keep_history', + 'users.allow_guest AS allow_guest', + 'users.is_active AS is_active' ] try: query = data_tables.ssp_query(table_name='users', @@ -173,14 +174,15 @@ class Users(object): # Rename Mystery platform names platform = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform']) - row = {'user_id': item['user_id'], + row = {'row_id': item['row_id'], + 'user_id': item['user_id'], 'friendly_name': item['friendly_name'], 'user_thumb': user_thumb, 'plays': item['plays'], 'duration': item['duration'], 'last_seen': item['last_seen'], 'last_played': item['last_played'], - 'id': item['id'], + 'history_row_id': item['history_row_id'], 'ip_address': item['ip_address'], 'platform': platform, 'player': item['player'], @@ -225,7 +227,7 @@ class Users(object): custom_where = ['users.user_id', user_id] - columns = ['session_history.id', + columns = ['session_history.id AS history_row_id', 'MAX(session_history.started) AS last_seen', 'session_history.ip_address', 'COUNT(session_history.id) AS play_count', @@ -285,7 +287,7 @@ class Users(object): # Rename Mystery platform names platform = common.PLATFORM_NAME_OVERRIDES.get(item["platform"], item["platform"]) - row = {'id': item['id'], + row = {'history_row_id': item['history_row_id'], 'last_seen': item['last_seen'], 'ip_address': item['ip_address'], 'play_count': item['play_count'], @@ -334,7 +336,8 @@ class Users(object): logger.warn(u"Tautulli Users :: Unable to execute database query for set_config: %s." % e) def get_details(self, user_id=None, user=None, email=None): - default_return = {'user_id': 0, + default_return = {'row_id': 0, + 'user_id': 0, 'username': 'Local', 'friendly_name': 'Local', 'user_thumb': common.DEFAULT_USER_THUMB, @@ -359,7 +362,8 @@ class Users(object): try: if str(user_id).isdigit(): - query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \ + query = 'SELECT id AS row_id, user_id, username, friendly_name, ' \ + 'thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \ 'email, is_active, is_admin, is_home_user, is_allow_sync, is_restricted, ' \ 'do_notify, keep_history, deleted_user, ' \ 'allow_guest, shared_libraries ' \ @@ -367,7 +371,8 @@ class Users(object): 'WHERE user_id = ? ' result = monitor_db.select(query, args=[user_id]) elif user: - query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \ + query = 'SELECT id AS row_id, user_id, username, friendly_name, ' \ + 'thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \ 'email, is_active, is_admin, is_home_user, is_allow_sync, is_restricted, ' \ 'do_notify, keep_history, deleted_user, ' \ 'allow_guest, shared_libraries ' \ @@ -375,7 +380,8 @@ class Users(object): 'WHERE username = ? COLLATE NOCASE ' result = monitor_db.select(query, args=[user]) elif email: - query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \ + query = 'SELECT id AS row_id, user_id, username, friendly_name, ' \ + 'thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \ 'email, is_active, is_admin, is_home_user, is_allow_sync, is_restricted, ' \ 'do_notify, keep_history, deleted_user, ' \ 'allow_guest, shared_libraries ' \ @@ -407,7 +413,8 @@ class Users(object): shared_libraries = tuple(item['shared_libraries'].split(';')) if item['shared_libraries'] else () - user_details = {'user_id': item['user_id'], + user_details = {'row_id': item['row_id'], + 'user_id': item['user_id'], 'username': item['username'], 'friendly_name': friendly_name, 'user_thumb': user_thumb, @@ -619,7 +626,7 @@ class Users(object): monitor_db = database.MonitorDatabase() try: - query = 'SELECT user_id, username, friendly_name, thumb, custom_avatar_url, email, ' \ + query = 'SELECT id AS row_id, user_id, username, friendly_name, thumb, custom_avatar_url, email, ' \ 'is_active, is_admin, is_home_user, is_allow_sync, is_restricted, ' \ 'do_notify, keep_history, allow_guest, server_token, shared_libraries, ' \ 'filter_all, filter_movies, filter_tv, filter_music, filter_photos ' \ @@ -631,7 +638,8 @@ class Users(object): users = [] for item in result: - user = {'user_id': item['user_id'], + user = {'row_id': item['row_id'], + 'user_id': item['user_id'], 'username': item['username'], 'friendly_name': item['friendly_name'] or item['username'], 'thumb': item['custom_avatar_url'] or item['thumb'], @@ -656,53 +664,38 @@ class Users(object): return users - def delete_all_history(self, user_id=None): + def delete(self, user_id=None, row_ids=None, purge_only=False): monitor_db = database.MonitorDatabase() - try: - if str(user_id).isdigit(): - logger.info(u"Tautulli Users :: Deleting all history for user id %s from database." % user_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 ON session_history_media_info.id = session_history.id ' - 'WHERE session_history.user_id = ?)', [user_id]) - session_history_metadata_del = \ - monitor_db.action('DELETE FROM ' - 'session_history_metadata ' - 'WHERE session_history_metadata.id IN (SELECT session_history_metadata.id ' - 'FROM session_history_metadata ' - 'JOIN session_history ON session_history_metadata.id = session_history.id ' - 'WHERE session_history.user_id = ?)', [user_id]) - session_history_del = \ - monitor_db.action('DELETE FROM ' - 'session_history ' - 'WHERE session_history.user_id = ?', [user_id]) + if row_ids and row_ids is not None: + row_ids = map(helpers.cast_to_int, row_ids.split(',')) - return 'Deleted all items for user_id %s.' % user_id + # Get the user_ids corresponding to the row_ids + result = monitor_db.select('SELECT user_id FROM users ' + 'WHERE id IN ({})'.format(','.join(['?'] * len(row_ids))), row_ids) + user_ids = [user['user_id'] for user in result] + + success = [] + for user_id in user_ids: + success.append(self.delete(user_id=user_id, purge_only=purge_only)) + return all(success) + + elif str(user_id).isdigit(): + database.delete_user_history(user_id=user_id) + if purge_only: + return True else: - return 'Unable to delete items. Input user_id not valid.' - except Exception as e: - logger.warn(u"Tautulli Users :: Unable to execute database query for delete_all_history: %s." % e) + logger.info(u"Tautulli Users :: Deleting user with user_id %s from database." % user_id) + try: + monitor_db.action('UPDATE users ' + 'SET deleted_user = 1, keep_history = 0, do_notify = 0 ' + 'WHERE user_id = ?', [user_id]) + return True + except Exception as e: + logger.warn(u"Tautulli Users :: Unable to execute database query for delete: %s." % e) - def delete(self, user_id=None): - monitor_db = database.MonitorDatabase() - - try: - if str(user_id).isdigit(): - self.delete_all_history(user_id) - logger.info(u"Tautulli Users :: Deleting user with id %s from database." % user_id) - monitor_db.action('UPDATE users SET deleted_user = 1 WHERE user_id = ?', [user_id]) - monitor_db.action('UPDATE users SET keep_history = 0 WHERE user_id = ?', [user_id]) - monitor_db.action('UPDATE users SET do_notify = 0 WHERE user_id = ?', [user_id]) - - return 'Deleted user with id %s.' % user_id - else: - return 'Unable to delete user, user_id not valid.' - except Exception as e: - logger.warn(u"Tautulli Users :: Unable to execute database query for delete: %s." % e) + else: + return False def undelete(self, user_id=None, username=None): monitor_db = database.MonitorDatabase() diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 5329a755..16015876 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -1417,6 +1417,7 @@ class WebInterface(object): "is_home_user": 1, "is_restricted": 0, "keep_history": 1, + "row_id": 1, "shared_libraries": ["10", "1", "4", "5", "15", "20", "2"], "user_id": 133788, "user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar", @@ -1529,7 +1530,7 @@ class WebInterface(object): @cherrypy.tools.json_out() @requireAuth(member_of("admin")) @addtoapi() - def delete_all_user_history(self, user_id, **kwargs): + def delete_all_user_history(self, user_id=None, row_ids=None, **kwargs): """ Delete all Tautulli history for a specific user. ``` @@ -1537,25 +1538,27 @@ class WebInterface(object): user_id (str): The id of the Plex user Optional parameters: - None + row_ids (str): Comma separated row ids to delete, e.g. "2,3,8" Returns: None ``` """ - if user_id: + if user_id or row_ids: user_data = users.Users() - delete_row = user_data.delete_all_history(user_id=user_id) - if delete_row: - return {'message': delete_row} + success = user_data.delete(user_id=user_id, row_ids=row_ids, purge_only=True) + if success: + return {'result': 'success', 'message': 'Deleted user history.'} + else: + return {'result': 'error', 'message': 'Failed to delete user(s) history.'} else: - return {'message': 'no data received'} + return {'result': 'error', 'message': 'No user id or row ids received.'} @cherrypy.expose @cherrypy.tools.json_out() @requireAuth(member_of("admin")) @addtoapi() - def delete_user(self, user_id, **kwargs): + def delete_user(self, user_id=None, row_ids=None, **kwargs): """ Delete a user from Tautulli. Also erases all history for the user. ``` @@ -1563,19 +1566,21 @@ class WebInterface(object): user_id (str): The id of the Plex user Optional parameters: - None + row_ids (str): Comma separated row ids to delete, e.g. "2,3,8" Returns: None ``` """ - if user_id: + if user_id or row_ids: user_data = users.Users() - delete_row = user_data.delete(user_id=user_id) - if delete_row: - return {'message': delete_row} + success = user_data.delete(user_id=user_id, row_ids=row_ids) + if success: + return {'result': 'success', 'message': 'Deleted user.'} + else: + return {'result': 'error', 'message': 'Failed to delete user(s).'} else: - return {'message': 'no data received'} + return {'result': 'error', 'message': 'No user id or row ids received.'} @cherrypy.expose @cherrypy.tools.json_out() @@ -5427,6 +5432,7 @@ class WebInterface(object): "is_home_user": 1, "is_restricted": 0, "keep_history": 1, + "row_id": 1, "server_token": "PU9cMuQZxJKFBtGqHk68", "shared_libraries": "1;2;3", "thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",