From 8791babf8efc960c7f29693e6f4d6b6f9c61ac29 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Tue, 15 Sep 2015 23:34:00 -0700 Subject: [PATCH 01/19] Add settings for grouping in history table --- data/interfaces/default/settings.html | 42 +++++++++++++++++---------- plexpy/config.py | 1 + plexpy/webserve.py | 6 ++-- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index d13e8700..9afca8e9 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -34,14 +34,15 @@ available_notification_agents = notifiers.available_notification_agents()
@@ -83,6 +84,15 @@ available_notification_agents = notifiers.available_notification_agents()

Set your preferred time format. Click here to see the parameter list.

+
+ +

Group successive play history as a single entry in tables.

+
+

+ +

Homepage Statistics

@@ -115,7 +125,7 @@ available_notification_agents = notifiers.available_notification_agents()

-
+

Web Interface

@@ -164,7 +174,7 @@ available_notification_agents = notifiers.available_notification_agents()

-
+

Authentication

@@ -216,7 +226,7 @@ available_notification_agents = notifiers.available_notification_agents()

-
+

Plex Media Server

@@ -272,7 +282,7 @@ available_notification_agents = notifiers.available_notification_agents()
-
+

Plex.tv Authentication

@@ -315,7 +325,7 @@ available_notification_agents = notifiers.available_notification_agents()

-
+

Extra Settings

@@ -334,7 +344,7 @@ available_notification_agents = notifiers.available_notification_agents()

-
+

Monitoring Settings

@@ -416,7 +426,7 @@ available_notification_agents = notifiers.available_notification_agents()

-
+

Global Notification Toggles

@@ -566,7 +576,7 @@ available_notification_agents = notifiers.available_notification_agents()

-
+

Notification Agents

diff --git a/plexpy/config.py b/plexpy/config.py index 51145685..5cf7b109 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -73,6 +73,7 @@ _CONFIG_DEFINITIONS = { 'GIT_BRANCH': (str, 'General', 'master'), 'GIT_PATH': (str, 'General', ''), 'GIT_USER': (str, 'General', 'drzoidberg33'), + 'GROUP_HISTORY_TABLES': (int, 'General', 0), 'GROWL_ENABLED': (int, 'Growl', 0), 'GROWL_HOST': (str, 'Growl', ''), 'GROWL_PASSWORD': (str, 'Growl', ''), diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 6bb6f9ac..d9840b61 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -464,7 +464,8 @@ class WebInterface(object): "home_stats_type": checked(plexpy.CONFIG.HOME_STATS_TYPE), "home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT, "buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD, - "buffer_wait": plexpy.CONFIG.BUFFER_WAIT + "buffer_wait": plexpy.CONFIG.BUFFER_WAIT, + "group_history_tables": checked(plexpy.CONFIG.GROUP_HISTORY_TABLES) } return serve_template(templatename="settings.html", title="Settings", config=config) @@ -485,7 +486,8 @@ class WebInterface(object): "tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start", "tv_notify_on_stop", "movie_notify_on_stop", "music_notify_on_stop", "tv_notify_on_pause", "movie_notify_on_pause", "music_notify_on_pause", "refresh_users_on_startup", - "ip_logging_enable", "video_logging_enable", "music_logging_enable", "pms_is_remote", "home_stats_type" + "ip_logging_enable", "video_logging_enable", "music_logging_enable", "pms_is_remote", "home_stats_type", + "group_history_tables" ] for checked_config in checked_configs: if checked_config not in kwargs: From 179eaf1bbe1ddc290433854b859d4a4719e23b9b Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Tue, 15 Sep 2015 23:34:42 -0700 Subject: [PATCH 02/19] Rewrite get_history query for grouping --- plexpy/datafactory.py | 176 +++++++++++++++++++++++++++--------------- plexpy/webserve.py | 12 ++- 2 files changed, 123 insertions(+), 65 deletions(-) diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index 4a988480..0ae6f576 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -26,56 +26,104 @@ class DataFactory(object): def __init__(self): pass - def get_history(self, kwargs=None, custom_where=None): + def get_history(self, kwargs=None, custom_where=None, grouping=0): data_tables = datatables.DataTables() + + group_by = 'group_start_id' if grouping else 'id' + + from_table = '(SELECT ' \ + ' /* Session info */ ' \ + ' (CASE ' \ + ' /* IF rating_key AND user are NOT EQUAL to previous row */ ' \ + ' WHEN t1.rating_key <> ( ' \ + ' SELECT rating_key FROM session_history WHERE id = ( ' \ + ' SELECT MAX(id) FROM session_history WHERE id < t1.id)) ' \ + ' AND t1.user <> ( ' \ + ' SELECT user FROM session_history WHERE id = ( ' \ + ' SELECT MAX(id) FROM session_history WHERE id < t1.id)) ' \ + ' /* THEN select the row */ ' \ + ' THEN t1.id ' \ + ' /* IF rating_key OR user are NOT EQUAL to previous row */ ' \ + ' WHEN ( ' \ + ' SELECT MIN(id) FROM session_history WHERE id > ( ' \ + ' SELECT MAX(id) FROM session_history ' \ + ' WHERE (rating_key <> t1.rating_key OR user <> t1.user) AND id < t1.id)) IS NULL /* First row */ ' \ + ' /* THEN select the first row */ ' \ + ' THEN (SELECT MIN(id) FROM session_history) ' \ + ' /* ELSE select the row where the rating key or user changed */ ' \ + ' ELSE (SELECT MIN(id) FROM session_history ' \ + ' WHERE id > (SELECT MAX(id) FROM session_history ' \ + ' WHERE (rating_key <> t1.rating_key OR user <> t1.user) AND id < t1.id)) ' \ + ' END) AS group_start_id, ' \ + ' t1.id, ' \ + ' t1.started as date, ' \ + ' t1.started, ' \ + ' t1.stopped, ' \ + ' (CASE WHEN t1.stopped > 0 THEN (t1.stopped - t1.started) ELSE 0 END) AS duration, ' \ + ' (CASE WHEN t1.paused_counter IS NULL THEN 0 ELSE t1.paused_counter END) AS paused_counter, ' \ + ' /* User and player info */ ' \ + ' t1.user_id, ' \ + ' t1.user, ' \ + ' (CASE WHEN t2.friendly_name IS NULL THEN t1.user ELSE t2.friendly_name END) as friendly_name, ' \ + ' t1.player, ' \ + ' t1.ip_address, ' \ + ' /* Metadata info */ ' \ + ' t3.media_type, ' \ + ' t3.rating_key, ' \ + ' t3.parent_rating_key, ' \ + ' t3.grandparent_rating_key, ' \ + ' t3.full_title, ' \ + ' t3.parent_title, ' \ + ' t3.year, ' \ + ' t3.media_index, ' \ + ' t3.parent_media_index, ' \ + ' t3.thumb, ' \ + ' t3.parent_thumb, ' \ + ' t3.grandparent_thumb, ' \ + ' /* Stream info */ ' \ + ' ((CASE WHEN t1.view_offset IS NULL THEN 0.1 ELSE t1.view_offset * 1.0 END) / ' \ + ' (CASE WHEN t3.duration IS NULL THEN 1.0 ELSE t3.duration * 1.0 END) * 100) as percent_complete, ' \ + ' t4.video_decision ' \ + 'FROM session_history AS t1 ' \ + ' LEFT OUTER JOIN users AS t2 ON t1.user_id = t2.user_id ' \ + ' JOIN session_history_metadata AS t3 ON t1.id = t3.id ' \ + ' JOIN session_history_media_info AS t4 ON t1.id = t4.id) ' - columns = ['session_history.id', - 'session_history.started as date', - '(CASE WHEN users.friendly_name IS NULL THEN session_history' - '.user ELSE users.friendly_name END) as friendly_name', - 'session_history.player', - 'session_history.ip_address', - 'session_history_metadata.full_title as full_title', - 'session_history_metadata.thumb', - 'session_history_metadata.parent_thumb', - 'session_history_metadata.grandparent_thumb', - 'session_history_metadata.media_index', - 'session_history_metadata.parent_media_index', - 'session_history_metadata.parent_title', - 'session_history_metadata.year', - 'session_history.started', - 'session_history.paused_counter', - 'session_history.stopped', - 'round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - \ - julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - \ - (CASE WHEN session_history.paused_counter IS NULL THEN 0 \ - ELSE session_history.paused_counter END) as duration', - '((CASE WHEN session_history.view_offset IS NULL THEN 0.1 ELSE \ - session_history.view_offset * 1.0 END) / \ - (CASE WHEN session_history_metadata.duration IS NULL THEN 1.0 ELSE \ - session_history_metadata.duration * 1.0 END) * 100) as percent_complete', - 'session_history.grandparent_rating_key as grandparent_rating_key', - 'session_history.parent_rating_key as parent_rating_key', - 'session_history.rating_key as rating_key', - 'session_history.user', - 'session_history_metadata.media_type', - 'session_history_media_info.video_decision', - 'session_history.user_id as user_id' + columns = ['group_start_id', + 'id', + 'date', + 'MIN(started) AS started', + 'MAX(stopped) AS stopped', + 'SUM(duration) - SUM(paused_counter) AS duration', + 'SUM(paused_counter) AS paused_counter', + 'user_id', + 'user', + 'friendly_name', + 'player', + 'ip_address', + 'media_type', + 'rating_key', + 'parent_rating_key', + 'grandparent_rating_key', + 'full_title', + 'parent_title', + 'year', + 'media_index', + 'parent_media_index', + 'thumb', + 'parent_thumb', + 'grandparent_thumb', + 'percent_complete', + 'video_decision', + 'COUNT(*) AS group_count' ] try: - query = data_tables.ssp_query(table_name='session_history', + query = data_tables.ssp_query(table_name=from_table, columns=columns, custom_where=custom_where, - group_by=[], - join_types=['LEFT OUTER JOIN', - 'JOIN', - 'JOIN'], - join_tables=['users', - 'session_history_metadata', - 'session_history_media_info'], - join_evals=[['session_history.user_id', 'users.user_id'], - ['session_history.id', 'session_history_metadata.id'], - ['session_history.id', 'session_history_media_info.id']], + group_by=[group_by], + join_types=[], + join_tables=[], kwargs=kwargs) except: logger.warn("Unable to execute database query.") @@ -86,7 +134,7 @@ class DataFactory(object): 'error': 'Unable to execute database query.'} history = query['result'] - + rows = [] for item in history: if item["media_type"] == 'episode' and item["parent_thumb"]: @@ -96,33 +144,35 @@ class DataFactory(object): else: thumb = item["thumb"] - row = {"id": item['id'], - "date": item['date'], - "friendly_name": item['friendly_name'], - "player": item["player"], - "ip_address": item["ip_address"], - "full_title": item["full_title"], - "thumb": thumb, - "media_index": item["media_index"], - "parent_media_index": item["parent_media_index"], - "parent_title": item["parent_title"], - "year": item["year"], + row = {"group_start_id": item["group_start_id"], + "id": item["id"], + "date": item["date"], "started": item["started"], - "paused_counter": item["paused_counter"], "stopped": item["stopped"], "duration": item["duration"], - "percent_complete": item["percent_complete"], - "grandparent_rating_key": item["grandparent_rating_key"], - "parent_rating_key": item["parent_rating_key"], - "rating_key": item["rating_key"], + "paused_counter": item["paused_counter"], + "user_id": item["user_id"], "user": item["user"], + "friendly_name": item["friendly_name"], + "player": item["player"], + "ip_address": item["ip_address"], "media_type": item["media_type"], + "rating_key": item["rating_key"], + "parent_rating_key": item["parent_rating_key"], + "grandparent_rating_key": item["grandparent_rating_key"], + "full_title": item["full_title"], + "parent_title": item["parent_title"], + "year": item["year"], + "media_index": item["media_index"], + "parent_media_index": item["parent_media_index"], + "thumb": thumb, "video_decision": item["video_decision"], - "user_id": item["user_id"] + "percent_complete": item["percent_complete"], + "group_count": item["group_count"] } rows.append(row) - + dict = {'recordsFiltered': query['filteredCount'], 'recordsTotal': query['totalCount'], 'data': rows, diff --git a/plexpy/webserve.py b/plexpy/webserve.py index d9840b61..d61355c2 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -569,7 +569,12 @@ class WebInterface(object): message=message, timer=timer, quote=quote) @cherrypy.expose - def get_history(self, user=None, user_id=None, **kwargs): + def get_history(self, user=None, user_id=None, grouping=0, **kwargs): + + if grouping == 'false': + grouping = 0 + else: + grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES custom_where=[] if user_id: @@ -588,9 +593,12 @@ class WebInterface(object): if 'start_date' in kwargs: start_date = kwargs.get('start_date', "") custom_where = [['strftime("%Y-%m-%d", datetime(date, "unixepoch", "localtime"))', start_date]] + if 'group_start_id' in kwargs: + group_start_id = kwargs.get('group_start_id', "") + custom_where = [['group_start_id', int(group_start_id)]] data_factory = datafactory.DataFactory() - history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where) + history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where, grouping=grouping) cherrypy.response.headers['Content-type'] = 'application/json' return json.dumps(history) From d6c21e173dd96b0435c249d47e5d811b4b416d0b Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Tue, 15 Sep 2015 23:35:07 -0700 Subject: [PATCH 03/19] Update history table to show grouped items --- data/interfaces/default/css/plexpy.css | 36 ++ .../default/js/tables/history_table.js | 311 ++++++++++++++++-- 2 files changed, 321 insertions(+), 26 deletions(-) diff --git a/data/interfaces/default/css/plexpy.css b/data/interfaces/default/css/plexpy.css index ff44dbea..2842814e 100644 --- a/data/interfaces/default/css/plexpy.css +++ b/data/interfaces/default/css/plexpy.css @@ -2350,4 +2350,40 @@ a .home-platforms-instance-list-oval:hover, .dashboard-instance { width: 100%; } +} + +table.display tr.shown + tr div.slider { + display: none; +} +table.display tr.shown + tr > td { + padding-top: 0; + padding-bottom: 0; +} +table.display tr.shown + tr:hover { + background-color: rgba(255,255,255,0); +} +table.display tr.shown + tr:hover a, +table.display tr.shown + tr td:hover a, +table.display tr.shown + tr .pagination > .active > a, +table.display tr.shown + tr .pagination > .active > a:hover { + color: #fff; +} +table.display tr.shown + tr table[id^='history_child'] td:hover a { + color: #F9AA03; +} +table.display tr.shown + tr .pagination > .disabled > a { + color: #444444; +} +table.display tr.shown + tr .pagination > li > a:hover { + color: #23527c; +} +table[id^='history_child'] { + margin-top: 0; + margin-left: -4px; + opacity: .6; +} +table[id^='history_child'] thead th { + line-height: 0; + height: 0 !important; + overflow: hidden; } \ No newline at end of file diff --git a/data/interfaces/default/js/tables/history_table.js b/data/interfaces/default/js/tables/history_table.js index 1cccbf9b..dcd94933 100644 --- a/data/interfaces/default/js/tables/history_table.js +++ b/data/interfaces/default/js/tables/history_table.js @@ -46,13 +46,17 @@ history_table_options = { "createdCell": function (td, cellData, rowData, row, col) { if (rowData['stopped'] === null) { $(td).html('Currently watching...'); + } else if (rowData['group_count'] > 1) { + date = moment(cellData, "X").format(date_format); + expand_history = ''; + $(td).html(''); } else { - $(td).html(moment(cellData,"X").format(date_format)); + $(td).html(moment(cellData, "X").format(date_format)); } }, "searchable": false, "width": "8%", - "className": "no-wrap" + "className": "no-wrap expand-history" }, { "targets": [2], @@ -83,7 +87,8 @@ history_table_options = { $(td).html('n/a'); } } else { - $(td).html(' ' + cellData + ''); + external_ip = ''; + $(td).html(''+ external_ip + cellData + ''); } } else { $(td).html('n/a'); @@ -105,7 +110,7 @@ history_table_options = { } else if (rowData['video_decision'] === 'direct play' || rowData['video_decision'] === '') { transcode_dec = ''; } - $(td).html(''); + $(td).html(''); } }, "width": "15%", @@ -121,16 +126,16 @@ history_table_options = { if (rowData['media_type'] === 'movie') { media_type = ''; thumb_popover = '' + cellData + ' (' + rowData['year'] + ')' - $(td).html(''); + $(td).html(''); } else if (rowData['media_type'] === 'episode') { media_type = ''; thumb_popover = '' + cellData + ' \ (S' + rowData['parent_media_index'] + '· E' + rowData['media_index'] + ')' - $(td).html(''); + $(td).html(''); } else if (rowData['media_type'] === 'track') { media_type = ''; thumb_popover = '' + cellData + ' (' + rowData['parent_title'] + ')' - $(td).html(''); + $(td).html(''); } else { $(td).html('' + cellData + ''); } @@ -155,7 +160,7 @@ history_table_options = { { "targets": [7], "data":"paused_counter", - "render": function ( data, type, full ) { + "render": function (data, type, full) { if (data !== null) { return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins'; } else { @@ -183,7 +188,7 @@ history_table_options = { { "targets": [9], "data":"duration", - "render": function ( data, type, full ) { + "render": function (data, type, full) { if (data !== null) { return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins'; } else { @@ -197,7 +202,7 @@ history_table_options = { { "targets": [10], "data":"percent_complete", - "render": function ( data, type, full ) { + "render": function (data, type, full) { if (data > 80) { return '' } else if (data > 40) { @@ -218,6 +223,8 @@ history_table_options = { $('#ajaxMsg').fadeOut(); // Create the tooltips. + $('.expand-history-tooltip').tooltip({ container: 'body' }); + $('.external-ip-tooltip').tooltip(); $('.transcode-tooltip').tooltip(); $('.media-type-tooltip').tooltip(); $('.watched-tooltip').tooltip(); @@ -231,24 +238,57 @@ history_table_options = { }); if ($('#row-edit-mode').hasClass('active')) { - $('.delete-control').each(function() { + $('.delete-control').each(function () { $(this).removeClass('hidden'); }); } + + history_table.rows().every(function () { + var rowData = this.data(); + if (rowData['group_count'] != 1 && rowData['group_start_id'] in history_child_table) { + // if grouped row and a child table was already created + this.child(childTableFormat(rowData)).show(); + createChildTable(this, rowData) + } + }); }, - "preDrawCallback": function(settings) { + "preDrawCallback": function (settings) { var msg = "
 Fetching rows...
"; showMsg(msg, false, false, 0) }, - "rowCallback": function (row, rowData) { - if ($.inArray(rowData['id'], history_to_delete) !== -1) { + "rowCallback": function (row, rowData, rowIndex) { + if (rowData['group_count'] == 1) { + // if no grouped rows simply toggle the delete button + if ($.inArray(rowData['id'], history_to_delete) !== -1) { + $(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); + } + } else { + // if grouped rows + // toggle the parent button to danger $(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); + // check if any child rows are not selected + for (var i = rowData['group_start_id']; i <= rowData['id']; i++) { + var index = $.inArray(i, history_to_delete); + if (index == -1) { + // if any child row is not selected, toggle parent button to warning + $(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); + break; + } + } } + + if (rowData['group_count'] != 1 && rowData['group_start_id'] in history_child_table) { + // if grouped row and a child table was already created + $(row).addClass('shown') + history_table.row(row).child(childTableFormat(rowData)).show(); + } + } } -$('#history_table').on('click', 'td.modal-control', function () { - var tr = $(this).parents('tr'); +// Parent table platform modal +$('#history_table').on('click', '> tbody > tr > td.modal-control', function () { + var tr = $(this).closest('tr'); var row = history_table.row( tr ); var rowData = row.data(); @@ -266,8 +306,9 @@ $('#history_table').on('click', 'td.modal-control', function () { showStreamDetails(); }); -$('#history_table').on('click', 'td.modal-control-ip', function () { - var tr = $(this).parents('tr'); +// Parent table ip address modal +$('#history_table').on('click', '> tbody > tr > td.modal-control-ip', function () { + var tr = $(this).closest('tr'); var row = history_table.row( tr ); var rowData = row.data(); @@ -288,16 +329,234 @@ $('#history_table').on('click', 'td.modal-control-ip', function () { getUserLocation(rowData['ip_address']); }); -$('#history_table').on('click', 'td.delete-control > button', function () { - var tr = $(this).parents('tr'); +// Parent table delete mode +$('#history_table').on('click', '> tbody > tr > td.delete-control > button', function () { + var tr = $(this).closest('tr'); var row = history_table.row( tr ); var rowData = row.data(); - var index = $.inArray(rowData['id'], history_to_delete); - if (index === -1) { - history_to_delete.push(rowData['id']); + if (rowData['group_count'] == 1) { + // if no grouped rows simply add or remove row from history_to_delete + var index = $.inArray(rowData['id'], history_to_delete); + if (index === -1) { + history_to_delete.push(rowData['id']); + } else { + history_to_delete.splice(index, 1); + } + $(this).toggleClass('btn-warning').toggleClass('btn-danger'); } else { - history_to_delete.splice(index, 1); + // if grouped rows + if ($(this).hasClass('btn-warning')) { + // add all grouped rows to history_to_delete + for (var i = rowData['group_start_id']; i <= rowData['id']; i++) { + var index = $.inArray(i, history_to_delete); + if (index == -1) { + history_to_delete.push(i); + } + } + $(this).toggleClass('btn-warning').toggleClass('btn-danger'); + if (row.child.isShown()) { + // if child table is visible, toggle all child buttons to danger + tr.next().find('td.delete-control > button.btn-warning').toggleClass('btn-warning').toggleClass('btn-danger'); + } + } else { + // remove all grouped rows to history_to_delete + for (var i = rowData['group_start_id']; i <= rowData['id']; i++) { + var index = $.inArray(i, history_to_delete); + if (index != -1) { + history_to_delete.splice(index, 1); + } + } + $(this).toggleClass('btn-warning').toggleClass('btn-danger'); + if (row.child.isShown()) { + // if child table is visible, toggle all child buttons to warning + tr.next().find('td.delete-control > button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); + } + } } - $(this).toggleClass('btn-warning').toggleClass('btn-danger'); -}); \ No newline at end of file +}); + +// Parent table expand detailed history +$('#history_table').on('click', '> tbody > tr > td.expand-history a', function () { + var tr = $(this).closest('tr'); + var row = history_table.row(tr); + var rowData = row.data(); + + if (row.child.isShown()) { + $('div.slider', row.child()).slideUp(function () { + row.child.hide(); + tr.removeClass('shown'); + delete history_child_table[rowData['group_start_id']]; + }); + } else { + tr.addClass('shown'); + row.child(childTableFormat(rowData)).show(); + createChildTable(row, rowData); + } +}); + + +// Initialize the detailed history child table options using the parent table options +function childTableOptions(rowData) { + history_child_options = history_table_options; + // Remove settings that are not necessary + history_child_options.searching = false; + history_child_options.lengthChange = false; + history_child_options.info = false; + history_child_options.pageLength = 10; + history_child_options.bStateSave = false; + history_child_options.ajax = { + "url": "get_history", + type: "post", + data: function (d) { + return { + 'json_data': JSON.stringify(d), + 'grouping': false, + 'group_start_id': rowData['group_start_id'] + }; + } + } + history_child_options.fnDrawCallback = function (settings) { + $('#ajaxMsg').fadeOut(); + + // Create the tooltips. + $('.expand-history-tooltip').tooltip({ container: 'body' }); + $('.external-ip-tooltip').tooltip(); + $('.transcode-tooltip').tooltip(); + $('.media-type-tooltip').tooltip(); + $('.watched-tooltip').tooltip(); + $('.thumb-tooltip').popover({ + html: true, + trigger: 'hover', + placement: 'right', + content: function () { + return '
'; + } + }); + + if ($('#row-edit-mode').hasClass('active')) { + $('.delete-control').each(function () { + $(this).removeClass('hidden'); + }); + } + + $(this).closest('div.slider').slideDown(); + } + + return history_child_options; +} + +// Format the detailed history child table +function childTableFormat(rowData) { + return '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
DeleteTimeUserIP AddressPlatformTitleStartedPausedStoppedDuration
' + + '
'; +} + +// Create the detailed history child table +history_child_table = {}; +function createChildTable(row, rowData) { + history_child_options = childTableOptions(rowData); + // initialize the child table + history_child_table[rowData['group_start_id']] = $('#history_child-' + rowData['group_start_id']).DataTable(history_child_options); + + // Set child table column visibility to match parent table + var visibility = history_table.columns().visible(); + for (var i = 0; i < visibility.length; i++) { + if (!(visibility[i])) { history_child_table[rowData['group_start_id']].column(i).visible(visibility[i]); } + } + history_table.on('column-visibility', function (e, settings, colIdx, visibility) { + if (row.child.isShown()) { + history_child_table[rowData['group_start_id']].column(colIdx).visible(visibility); + } + }); + + // Child table platform modal + $('#history_child-' + rowData['group_start_id']).on('click', 'td.modal-control', function () { + var tr = $(this).closest('tr'); + var childRow = history_child_table[rowData['group_start_id']].row(tr); + var childRowData = childRow.data(); + + function showStreamDetails() { + $.ajax({ + url: 'get_stream_data', + data: { row_id: childRowData['id'], user: childRowData['friendly_name'] }, + cache: false, + async: true, + complete: function (xhr, status) { + $("#info-modal").html(xhr.responseText); + } + }); + } + showStreamDetails(); + }); + + // Child table ip address modal + $('#history_child-' + rowData['group_start_id']).on('click', 'td.modal-control-ip', function () { + var tr = $(this).closest('tr'); + var childRow = history_child_table[rowData['group_start_id']].row(tr); + var childRowData = childRow.data(); + + function getUserLocation(ip_address) { + if (isPrivateIP(ip_address)) { + return "n/a" + } else { + $.ajax({ + url: 'get_ip_address_details', + data: { ip_address: ip_address }, + async: true, + complete: function (xhr, status) { + $("#ip-info-modal").html(xhr.responseText); + } + }); + } + } + getUserLocation(childRowData['ip_address']); + }); + + // Child table delete mode + $('#history_child-' + rowData['group_start_id']).on('click', 'td.delete-control > button', function () { + var tr = $(this).closest('tr'); + var childRow = history_child_table[rowData['group_start_id']].row(tr); + var childRowData = childRow.data(); + + // add or remove row from history_to_delete + var index = $.inArray(childRowData['id'], history_to_delete); + if (index === -1) { + history_to_delete.push(childRowData['id']); + } else { + history_to_delete.splice(index, 1); + } + $(this).toggleClass('btn-warning').toggleClass('btn-danger'); + + tr.parents('tr').prev().find('td.delete-control > button.btn-warning').toggleClass('btn-warning').toggleClass('btn-danger'); + // check if any child rows are not selected + for (var i = rowData['group_start_id']; i <= rowData['id']; i++) { + var index = $.inArray(i, history_to_delete); + if (index == -1) { + // if any child row is not selected, toggle parent button to warning + tr.parents('tr').prev().find('td.delete-control > button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); + break; + } + } + }); +} + From c1b5514789626ae819b6e2492fb1d9394293a886 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Wed, 16 Sep 2015 11:36:34 -0700 Subject: [PATCH 04/19] Add reference_id column to session_history * Also update database with values from previous versions --- plexpy/__init__.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/plexpy/__init__.py b/plexpy/__init__.py index ad02333c..9ffcdece 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -354,7 +354,7 @@ def dbcheck(): # session_history table :: This is a history table which logs essential stream details c_db.execute( - 'CREATE TABLE IF NOT EXISTS session_history (id INTEGER PRIMARY KEY AUTOINCREMENT, ' + 'CREATE TABLE IF NOT EXISTS session_history (id INTEGER PRIMARY KEY AUTOINCREMENT, reference_id INTEGER, ' 'started INTEGER, stopped INTEGER, rating_key INTEGER, user_id INTEGER, user TEXT, ' 'ip_address TEXT, paused_counter INTEGER DEFAULT 0, player TEXT, platform TEXT, machine_id TEXT, ' 'parent_rating_key INTEGER, grandparent_rating_key INTEGER, media_type TEXT, view_offset INTEGER DEFAULT 0)' @@ -603,6 +603,25 @@ def dbcheck(): logger.debug(u'User "Local" does not exist. Adding user.') c_db.execute('INSERT INTO users (user_id, username) VALUES (0, "Local")') + # Upgrade session_history table from earlier versions + try: + c_db.execute('SELECT reference_id from session_history') + except sqlite3.OperationalError: + logger.debug(u"Altering database. Updating database table session_history.") + c_db.execute( + 'ALTER TABLE session_history ADD COLUMN reference_id INTEGER DEFAULT 0' + ) + # SET reference_id to the first row where (rating_key != previous row OR user != previous row) + c_db.execute( + 'UPDATE session_history ' \ + 'SET reference_id = (SELECT (CASE WHEN (SELECT MIN(id) FROM session_history WHERE id > ( \ + SELECT MAX(id) FROM session_history WHERE (rating_key <> t1.rating_key OR user <> t1.user) AND id < t1.id)) IS NULL \ + THEN (SELECT MIN(id) FROM session_history) ELSE (SELECT MIN(id) FROM session_history WHERE id > ( \ + SELECT MAX(id) FROM session_history WHERE (rating_key <> t1.rating_key OR user <> t1.user) AND id < t1.id)) END) ' \ + 'FROM session_history AS t1 ' \ + 'WHERE t1.id = session_history.id) ' + ) + conn_db.commit() c_db.close() From 881142d4a18a76c7d2423dbb6ee990345cd5bd80 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Wed, 16 Sep 2015 11:37:19 -0700 Subject: [PATCH 05/19] Update get_history query to use the new reference_id --- .../default/js/tables/history_table.js | 36 +++--- plexpy/datafactory.py | 121 ++++++------------ plexpy/webserve.py | 6 +- 3 files changed, 57 insertions(+), 106 deletions(-) diff --git a/data/interfaces/default/js/tables/history_table.js b/data/interfaces/default/js/tables/history_table.js index dcd94933..053f994e 100644 --- a/data/interfaces/default/js/tables/history_table.js +++ b/data/interfaces/default/js/tables/history_table.js @@ -245,7 +245,7 @@ history_table_options = { history_table.rows().every(function () { var rowData = this.data(); - if (rowData['group_count'] != 1 && rowData['group_start_id'] in history_child_table) { + if (rowData['group_count'] != 1 && rowData['reference_id'] in history_child_table) { // if grouped row and a child table was already created this.child(childTableFormat(rowData)).show(); createChildTable(this, rowData) @@ -267,7 +267,7 @@ history_table_options = { // toggle the parent button to danger $(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); // check if any child rows are not selected - for (var i = rowData['group_start_id']; i <= rowData['id']; i++) { + for (var i = rowData['reference_id']; i <= rowData['id']; i++) { var index = $.inArray(i, history_to_delete); if (index == -1) { // if any child row is not selected, toggle parent button to warning @@ -277,7 +277,7 @@ history_table_options = { } } - if (rowData['group_count'] != 1 && rowData['group_start_id'] in history_child_table) { + if (rowData['group_count'] != 1 && rowData['reference_id'] in history_child_table) { // if grouped row and a child table was already created $(row).addClass('shown') history_table.row(row).child(childTableFormat(rowData)).show(); @@ -348,7 +348,7 @@ $('#history_table').on('click', '> tbody > tr > td.delete-control > button', fun // if grouped rows if ($(this).hasClass('btn-warning')) { // add all grouped rows to history_to_delete - for (var i = rowData['group_start_id']; i <= rowData['id']; i++) { + for (var i = rowData['reference_id']; i <= rowData['id']; i++) { var index = $.inArray(i, history_to_delete); if (index == -1) { history_to_delete.push(i); @@ -361,7 +361,7 @@ $('#history_table').on('click', '> tbody > tr > td.delete-control > button', fun } } else { // remove all grouped rows to history_to_delete - for (var i = rowData['group_start_id']; i <= rowData['id']; i++) { + for (var i = rowData['reference_id']; i <= rowData['id']; i++) { var index = $.inArray(i, history_to_delete); if (index != -1) { history_to_delete.splice(index, 1); @@ -386,7 +386,7 @@ $('#history_table').on('click', '> tbody > tr > td.expand-history a', function ( $('div.slider', row.child()).slideUp(function () { row.child.hide(); tr.removeClass('shown'); - delete history_child_table[rowData['group_start_id']]; + delete history_child_table[rowData['reference_id']]; }); } else { tr.addClass('shown'); @@ -412,7 +412,7 @@ function childTableOptions(rowData) { return { 'json_data': JSON.stringify(d), 'grouping': false, - 'group_start_id': rowData['group_start_id'] + 'reference_id': rowData['reference_id'] }; } } @@ -449,7 +449,7 @@ function childTableOptions(rowData) { // Format the detailed history child table function childTableFormat(rowData) { return '
' + - '' + + '
' + '' + '' + '' + @@ -476,23 +476,23 @@ history_child_table = {}; function createChildTable(row, rowData) { history_child_options = childTableOptions(rowData); // initialize the child table - history_child_table[rowData['group_start_id']] = $('#history_child-' + rowData['group_start_id']).DataTable(history_child_options); + history_child_table[rowData['reference_id']] = $('#history_child-' + rowData['reference_id']).DataTable(history_child_options); // Set child table column visibility to match parent table var visibility = history_table.columns().visible(); for (var i = 0; i < visibility.length; i++) { - if (!(visibility[i])) { history_child_table[rowData['group_start_id']].column(i).visible(visibility[i]); } + if (!(visibility[i])) { history_child_table[rowData['reference_id']].column(i).visible(visibility[i]); } } history_table.on('column-visibility', function (e, settings, colIdx, visibility) { if (row.child.isShown()) { - history_child_table[rowData['group_start_id']].column(colIdx).visible(visibility); + history_child_table[rowData['reference_id']].column(colIdx).visible(visibility); } }); // Child table platform modal - $('#history_child-' + rowData['group_start_id']).on('click', 'td.modal-control', function () { + $('#history_child-' + rowData['reference_id']).on('click', 'td.modal-control', function () { var tr = $(this).closest('tr'); - var childRow = history_child_table[rowData['group_start_id']].row(tr); + var childRow = history_child_table[rowData['reference_id']].row(tr); var childRowData = childRow.data(); function showStreamDetails() { @@ -510,9 +510,9 @@ function createChildTable(row, rowData) { }); // Child table ip address modal - $('#history_child-' + rowData['group_start_id']).on('click', 'td.modal-control-ip', function () { + $('#history_child-' + rowData['reference_id']).on('click', 'td.modal-control-ip', function () { var tr = $(this).closest('tr'); - var childRow = history_child_table[rowData['group_start_id']].row(tr); + var childRow = history_child_table[rowData['reference_id']].row(tr); var childRowData = childRow.data(); function getUserLocation(ip_address) { @@ -533,9 +533,9 @@ function createChildTable(row, rowData) { }); // Child table delete mode - $('#history_child-' + rowData['group_start_id']).on('click', 'td.delete-control > button', function () { + $('#history_child-' + rowData['reference_id']).on('click', 'td.delete-control > button', function () { var tr = $(this).closest('tr'); - var childRow = history_child_table[rowData['group_start_id']].row(tr); + var childRow = history_child_table[rowData['reference_id']].row(tr); var childRowData = childRow.data(); // add or remove row from history_to_delete @@ -549,7 +549,7 @@ function createChildTable(row, rowData) { tr.parents('tr').prev().find('td.delete-control > button.btn-warning').toggleClass('btn-warning').toggleClass('btn-danger'); // check if any child rows are not selected - for (var i = rowData['group_start_id']; i <= rowData['id']; i++) { + for (var i = rowData['reference_id']; i <= rowData['id']; i++) { var index = $.inArray(i, history_to_delete); if (index == -1) { // if any child row is not selected, toggle parent button to warning diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index 0ae6f576..50696f83 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -29,101 +29,52 @@ class DataFactory(object): def get_history(self, kwargs=None, custom_where=None, grouping=0): data_tables = datatables.DataTables() - group_by = 'group_start_id' if grouping else 'id' - - from_table = '(SELECT ' \ - ' /* Session info */ ' \ - ' (CASE ' \ - ' /* IF rating_key AND user are NOT EQUAL to previous row */ ' \ - ' WHEN t1.rating_key <> ( ' \ - ' SELECT rating_key FROM session_history WHERE id = ( ' \ - ' SELECT MAX(id) FROM session_history WHERE id < t1.id)) ' \ - ' AND t1.user <> ( ' \ - ' SELECT user FROM session_history WHERE id = ( ' \ - ' SELECT MAX(id) FROM session_history WHERE id < t1.id)) ' \ - ' /* THEN select the row */ ' \ - ' THEN t1.id ' \ - ' /* IF rating_key OR user are NOT EQUAL to previous row */ ' \ - ' WHEN ( ' \ - ' SELECT MIN(id) FROM session_history WHERE id > ( ' \ - ' SELECT MAX(id) FROM session_history ' \ - ' WHERE (rating_key <> t1.rating_key OR user <> t1.user) AND id < t1.id)) IS NULL /* First row */ ' \ - ' /* THEN select the first row */ ' \ - ' THEN (SELECT MIN(id) FROM session_history) ' \ - ' /* ELSE select the row where the rating key or user changed */ ' \ - ' ELSE (SELECT MIN(id) FROM session_history ' \ - ' WHERE id > (SELECT MAX(id) FROM session_history ' \ - ' WHERE (rating_key <> t1.rating_key OR user <> t1.user) AND id < t1.id)) ' \ - ' END) AS group_start_id, ' \ - ' t1.id, ' \ - ' t1.started as date, ' \ - ' t1.started, ' \ - ' t1.stopped, ' \ - ' (CASE WHEN t1.stopped > 0 THEN (t1.stopped - t1.started) ELSE 0 END) AS duration, ' \ - ' (CASE WHEN t1.paused_counter IS NULL THEN 0 ELSE t1.paused_counter END) AS paused_counter, ' \ - ' /* User and player info */ ' \ - ' t1.user_id, ' \ - ' t1.user, ' \ - ' (CASE WHEN t2.friendly_name IS NULL THEN t1.user ELSE t2.friendly_name END) as friendly_name, ' \ - ' t1.player, ' \ - ' t1.ip_address, ' \ - ' /* Metadata info */ ' \ - ' t3.media_type, ' \ - ' t3.rating_key, ' \ - ' t3.parent_rating_key, ' \ - ' t3.grandparent_rating_key, ' \ - ' t3.full_title, ' \ - ' t3.parent_title, ' \ - ' t3.year, ' \ - ' t3.media_index, ' \ - ' t3.parent_media_index, ' \ - ' t3.thumb, ' \ - ' t3.parent_thumb, ' \ - ' t3.grandparent_thumb, ' \ - ' /* Stream info */ ' \ - ' ((CASE WHEN t1.view_offset IS NULL THEN 0.1 ELSE t1.view_offset * 1.0 END) / ' \ - ' (CASE WHEN t3.duration IS NULL THEN 1.0 ELSE t3.duration * 1.0 END) * 100) as percent_complete, ' \ - ' t4.video_decision ' \ - 'FROM session_history AS t1 ' \ - ' LEFT OUTER JOIN users AS t2 ON t1.user_id = t2.user_id ' \ - ' JOIN session_history_metadata AS t3 ON t1.id = t3.id ' \ - ' JOIN session_history_media_info AS t4 ON t1.id = t4.id) ' + group_by = ['session_history.reference_id'] if grouping else ['session_history.id'] - columns = ['group_start_id', - 'id', - 'date', + columns = ['reference_id', + 'session_history.id', + 'started AS date', 'MIN(started) AS started', 'MAX(stopped) AS stopped', - 'SUM(duration) - SUM(paused_counter) AS duration', - 'SUM(paused_counter) AS paused_counter', - 'user_id', + 'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ELSE 0 END) - \ + SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) AS duration', + 'SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) AS paused_counter', + 'session_history.user_id', 'user', - 'friendly_name', + '(CASE WHEN users.friendly_name IS NULL THEN user ELSE users.friendly_name END) as friendly_name', 'player', 'ip_address', - 'media_type', - 'rating_key', - 'parent_rating_key', - 'grandparent_rating_key', - 'full_title', - 'parent_title', - 'year', - 'media_index', - 'parent_media_index', - 'thumb', - 'parent_thumb', - 'grandparent_thumb', - 'percent_complete', - 'video_decision', + 'session_history_metadata.media_type', + 'session_history_metadata.rating_key', + 'session_history_metadata.parent_rating_key', + 'session_history_metadata.grandparent_rating_key', + 'session_history_metadata.full_title', + 'session_history_metadata.parent_title', + 'session_history_metadata.year', + 'session_history_metadata.media_index', + 'session_history_metadata.parent_media_index', + 'session_history_metadata.thumb', + 'session_history_metadata.parent_thumb', + 'session_history_metadata.grandparent_thumb', + '((CASE WHEN view_offset IS NULL THEN 0.1 ELSE view_offset * 1.0 END) / \ + (CASE WHEN session_history_metadata.duration IS NULL THEN 1.0 ELSE session_history_metadata.duration * 1.0 END) * 100) AS percent_complete', + 'session_history_media_info.video_decision', 'COUNT(*) AS group_count' ] try: - query = data_tables.ssp_query(table_name=from_table, + query = data_tables.ssp_query(table_name='session_history', columns=columns, custom_where=custom_where, - group_by=[group_by], - join_types=[], - join_tables=[], + group_by=group_by, + join_types=['LEFT OUTER JOIN', + 'JOIN', + 'JOIN'], + join_tables=['users', + 'session_history_metadata', + 'session_history_media_info'], + join_evals=[['session_history.user_id', 'users.user_id'], + ['session_history.id', 'session_history_metadata.id'], + ['session_history.id', 'session_history_media_info.id']], kwargs=kwargs) except: logger.warn("Unable to execute database query.") @@ -144,7 +95,7 @@ class DataFactory(object): else: thumb = item["thumb"] - row = {"group_start_id": item["group_start_id"], + row = {"reference_id": item["reference_id"], "id": item["id"], "date": item["date"], "started": item["started"], diff --git a/plexpy/webserve.py b/plexpy/webserve.py index d61355c2..d0c1c5d9 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -593,9 +593,9 @@ class WebInterface(object): if 'start_date' in kwargs: start_date = kwargs.get('start_date', "") custom_where = [['strftime("%Y-%m-%d", datetime(date, "unixepoch", "localtime"))', start_date]] - if 'group_start_id' in kwargs: - group_start_id = kwargs.get('group_start_id', "") - custom_where = [['group_start_id', int(group_start_id)]] + if 'reference_id' in kwargs: + reference_id = kwargs.get('reference_id', "") + custom_where = [['reference_id', reference_id]] data_factory = datafactory.DataFactory() history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where, grouping=grouping) From c85ee3aec013e8dbff95783e1c3bec645ca2bd71 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Wed, 16 Sep 2015 11:37:50 -0700 Subject: [PATCH 06/19] Update write_session_history to include the reference_id --- plexpy/monitor.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/plexpy/monitor.py b/plexpy/monitor.py index 091fa575..2006114b 100644 --- a/plexpy/monitor.py +++ b/plexpy/monitor.py @@ -293,6 +293,29 @@ class MonitorProcessing(object): # logger.debug(u"PlexPy Monitor :: Writing session_history transaction...") self.db.action(query=query, args=args) + # Check if we should group the session + query = 'SELECT id, rating_key, user_id, reference_id FROM session_history ORDER BY id DESC LIMIT 2 ' + result = self.db.select(query) + + if len(result) == 2: + new_session = {'id': result[0][0], + 'rating_key': result[0][1], + 'user_id': result[0][2], + 'reference_id': result[0][3]} + prev_session = {'id': result[1][0], + 'rating_key': result[1][1], + 'user_id': result[1][2], + 'reference_id': result[1][3]} + + query = 'UPDATE session_history SET reference_id = ? WHERE id = ? ' + # If rating_key and user are the same in the previous session, then set the reference_id to the previous row + if prev_session['rating_key'] == new_session['rating_key'] and prev_session['user_id'] == new_session['user_id']: + args = [prev_session['reference_id'], new_session['id']] + else: + args = [new_session['id'], new_session['id']] + + self.db.action(query=query, args=args) + # logger.debug(u"PlexPy Monitor :: Successfully written history item, last id for session_history is %s" # % last_id) From fc75232519dc00a5ae50862707011511f1a1ebe4 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Wed, 16 Sep 2015 11:52:00 -0700 Subject: [PATCH 07/19] Change group logic to use user_id instead of user --- plexpy/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plexpy/__init__.py b/plexpy/__init__.py index 9ffcdece..0f402399 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -611,13 +611,13 @@ def dbcheck(): c_db.execute( 'ALTER TABLE session_history ADD COLUMN reference_id INTEGER DEFAULT 0' ) - # SET reference_id to the first row where (rating_key != previous row OR user != previous row) + # Set reference_id to the first row where (rating_key != previous row OR user_id != previous row) c_db.execute( 'UPDATE session_history ' \ 'SET reference_id = (SELECT (CASE WHEN (SELECT MIN(id) FROM session_history WHERE id > ( \ - SELECT MAX(id) FROM session_history WHERE (rating_key <> t1.rating_key OR user <> t1.user) AND id < t1.id)) IS NULL \ + SELECT MAX(id) FROM session_history WHERE (rating_key <> t1.rating_key OR user_id <> t1.user_id) AND id < t1.id)) IS NULL \ THEN (SELECT MIN(id) FROM session_history) ELSE (SELECT MIN(id) FROM session_history WHERE id > ( \ - SELECT MAX(id) FROM session_history WHERE (rating_key <> t1.rating_key OR user <> t1.user) AND id < t1.id)) END) ' \ + SELECT MAX(id) FROM session_history WHERE (rating_key <> t1.rating_key OR user_id <> t1.user_id) AND id < t1.id)) END) ' \ 'FROM session_history AS t1 ' \ 'WHERE t1.id = session_history.id) ' ) From adc808ac9fa9e057496f18a8c676542063088b89 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Wed, 16 Sep 2015 13:11:38 -0700 Subject: [PATCH 08/19] Fix ambiguous column names --- plexpy/datafactory.py | 2 +- plexpy/webserve.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index 50696f83..82cac9ae 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -31,7 +31,7 @@ class DataFactory(object): group_by = ['session_history.reference_id'] if grouping else ['session_history.id'] - columns = ['reference_id', + columns = ['session_history.reference_id', 'session_history.id', 'started AS date', 'MIN(started) AS started', diff --git a/plexpy/webserve.py b/plexpy/webserve.py index d0c1c5d9..3ed79246 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -583,19 +583,19 @@ class WebInterface(object): custom_where = [['user', user]] if 'rating_key' in kwargs: rating_key = kwargs.get('rating_key', "") - custom_where = [['rating_key', rating_key]] + custom_where = [['session_history.rating_key', rating_key]] if 'parent_rating_key' in kwargs: rating_key = kwargs.get('parent_rating_key', "") - custom_where = [['parent_rating_key', rating_key]] + custom_where = [['session_history.parent_rating_key', rating_key]] if 'grandparent_rating_key' in kwargs: rating_key = kwargs.get('grandparent_rating_key', "") - custom_where = [['grandparent_rating_key', rating_key]] + custom_where = [['session_history.grandparent_rating_key', rating_key]] if 'start_date' in kwargs: start_date = kwargs.get('start_date', "") custom_where = [['strftime("%Y-%m-%d", datetime(date, "unixepoch", "localtime"))', start_date]] if 'reference_id' in kwargs: reference_id = kwargs.get('reference_id', "") - custom_where = [['reference_id', reference_id]] + custom_where = [['session_history.reference_id', reference_id]] data_factory = datafactory.DataFactory() history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where, grouping=grouping) From 626e7fdf829bc514a90f384b2889b663e0ae2713 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Wed, 16 Sep 2015 13:30:05 -0700 Subject: [PATCH 09/19] Fix watched status to use watched percent specified in settings --- data/interfaces/default/js/tables/history_table.js | 6 +++--- plexpy/datafactory.py | 11 +++++++++-- plexpy/webserve.py | 4 +++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/data/interfaces/default/js/tables/history_table.js b/data/interfaces/default/js/tables/history_table.js index 053f994e..0f6e34cb 100644 --- a/data/interfaces/default/js/tables/history_table.js +++ b/data/interfaces/default/js/tables/history_table.js @@ -201,11 +201,11 @@ history_table_options = { }, { "targets": [10], - "data":"percent_complete", + "data": "watched_status", "render": function (data, type, full) { - if (data > 80) { + if (data == 1) { return '' - } else if (data > 40) { + } else if (data == 0.5) { return '' } else { return '' diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index 82cac9ae..c5dee357 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -26,7 +26,7 @@ class DataFactory(object): def __init__(self): pass - def get_history(self, kwargs=None, custom_where=None, grouping=0): + def get_history(self, kwargs=None, custom_where=None, grouping=0, watched_percent=85): data_tables = datatables.DataTables() group_by = ['session_history.reference_id'] if grouping else ['session_history.id'] @@ -95,6 +95,13 @@ class DataFactory(object): else: thumb = item["thumb"] + if item['percent_complete'] >= watched_percent: + watched_status = 1 + elif item['percent_complete'] >= watched_percent/2: + watched_status = 0.5 + else: + watched_status = 0 + row = {"reference_id": item["reference_id"], "id": item["id"], "date": item["date"], @@ -118,7 +125,7 @@ class DataFactory(object): "parent_media_index": item["parent_media_index"], "thumb": thumb, "video_decision": item["video_decision"], - "percent_complete": item["percent_complete"], + "watched_status": watched_status, "group_count": item["group_count"] } diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 3ed79246..1677374c 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -576,6 +576,8 @@ class WebInterface(object): else: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES + watched_percent = plexpy.CONFIG.NOTIFY_WATCHED_PERCENT + custom_where=[] if user_id: custom_where = [['user_id', user_id]] @@ -598,7 +600,7 @@ class WebInterface(object): custom_where = [['session_history.reference_id', reference_id]] data_factory = datafactory.DataFactory() - history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where, grouping=grouping) + history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where, grouping=grouping, watched_percent=watched_percent) cherrypy.response.headers['Content-type'] = 'application/json' return json.dumps(history) From 060c549259cef3cdfd2d21290ea42398dabe9a63 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Wed, 16 Sep 2015 13:40:47 -0700 Subject: [PATCH 10/19] Another ambiguous column name fix --- plexpy/datafactory.py | 2 +- plexpy/webserve.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index c5dee357..b9c9d559 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -40,7 +40,7 @@ class DataFactory(object): SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) AS duration', 'SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) AS paused_counter', 'session_history.user_id', - 'user', + 'session_history.user', '(CASE WHEN users.friendly_name IS NULL THEN user ELSE users.friendly_name END) as friendly_name', 'player', 'ip_address', diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 1677374c..4fde95eb 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -580,9 +580,9 @@ class WebInterface(object): custom_where=[] if user_id: - custom_where = [['user_id', user_id]] + custom_where = [['session_history.user_id', user_id]] elif user: - custom_where = [['user', user]] + custom_where = [['session_history.user', user]] if 'rating_key' in kwargs: rating_key = kwargs.get('rating_key', "") custom_where = [['session_history.rating_key', rating_key]] From 4fa70cb2349c7bc9b04ca40963e2f39af8afca67 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Fri, 18 Sep 2015 14:36:52 -0700 Subject: [PATCH 11/19] Update grouping logic * Check the user's previous row to match the rating key --- plexpy/__init__.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/plexpy/__init__.py b/plexpy/__init__.py index 0f402399..af357158 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -611,15 +611,19 @@ def dbcheck(): c_db.execute( 'ALTER TABLE session_history ADD COLUMN reference_id INTEGER DEFAULT 0' ) - # Set reference_id to the first row where (rating_key != previous row OR user_id != previous row) + # Set reference_id to the first row where (user_id = previous row, rating_key != previous row) and user_id = user_id c_db.execute( 'UPDATE session_history ' \ - 'SET reference_id = (SELECT (CASE WHEN (SELECT MIN(id) FROM session_history WHERE id > ( \ - SELECT MAX(id) FROM session_history WHERE (rating_key <> t1.rating_key OR user_id <> t1.user_id) AND id < t1.id)) IS NULL \ - THEN (SELECT MIN(id) FROM session_history) ELSE (SELECT MIN(id) FROM session_history WHERE id > ( \ - SELECT MAX(id) FROM session_history WHERE (rating_key <> t1.rating_key OR user_id <> t1.user_id) AND id < t1.id)) END) ' \ - 'FROM session_history AS t1 ' \ - 'WHERE t1.id = session_history.id) ' + 'SET reference_id = (SELECT (CASE \ + WHEN (SELECT MIN(id) FROM session_history WHERE id > ( \ + SELECT MAX(id) FROM session_history \ + WHERE (user_id = t1.user_id AND rating_key <> t1.rating_key AND id < t1.id)) AND user_id = t1.user_id) IS NULL \ + THEN (SELECT MIN(id) FROM session_history WHERE (user_id = t1.user_id)) \ + ELSE (SELECT MIN(id) FROM session_history WHERE id > ( \ + SELECT MAX(id) FROM session_history \ + WHERE (user_id = t1.user_id AND rating_key <> t1.rating_key AND id < t1.id)) AND user_id = t1.user_id) END) ' \ + 'FROM session_history AS t1 ' \ + 'WHERE t1.id = session_history.id) ' ) conn_db.commit() From 7c725ee4249c284a3ac116fca5823952cb474177 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Fri, 18 Sep 2015 15:00:51 -0700 Subject: [PATCH 12/19] Update grouping logic for new sessions * Check the user's previous row to match the rating key --- plexpy/monitor.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/plexpy/monitor.py b/plexpy/monitor.py index 2006114b..e29ecf47 100644 --- a/plexpy/monitor.py +++ b/plexpy/monitor.py @@ -293,28 +293,35 @@ class MonitorProcessing(object): # logger.debug(u"PlexPy Monitor :: Writing session_history transaction...") self.db.action(query=query, args=args) - # Check if we should group the session - query = 'SELECT id, rating_key, user_id, reference_id FROM session_history ORDER BY id DESC LIMIT 2 ' - result = self.db.select(query) + # Check if we should group the session, select the last two rows from the user + query = 'SELECT id, rating_key, user_id, reference_id FROM session_history \ + WHERE user_id = ? ORDER BY id DESC LIMIT 2 ' + + args = [session['user_id']] + + result = self.db.select(query=query, args=args) - if len(result) == 2: - new_session = {'id': result[0][0], - 'rating_key': result[0][1], - 'user_id': result[0][2], - 'reference_id': result[0][3]} + new_session = {'id': result[0][0], + 'rating_key': result[0][1], + 'user_id': result[0][2], + 'reference_id': result[0][3]} + + if len(result) == 1: + prev_session = None + else: prev_session = {'id': result[1][0], 'rating_key': result[1][1], 'user_id': result[1][2], 'reference_id': result[1][3]} - query = 'UPDATE session_history SET reference_id = ? WHERE id = ? ' - # If rating_key and user are the same in the previous session, then set the reference_id to the previous row - if prev_session['rating_key'] == new_session['rating_key'] and prev_session['user_id'] == new_session['user_id']: - args = [prev_session['reference_id'], new_session['id']] - else: - args = [new_session['id'], new_session['id']] + query = 'UPDATE session_history SET reference_id = ? WHERE id = ? ' + # If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id + if (prev_session is not None) and (prev_session['rating_key'] == new_session['rating_key']): + args = [prev_session['reference_id'], new_session['id']] + else: + args = [new_session['id'], new_session['id']] - self.db.action(query=query, args=args) + self.db.action(query=query, args=args) # logger.debug(u"PlexPy Monitor :: Successfully written history item, last id for session_history is %s" # % last_id) From b84f214030f25680e8548b7487bdbd547acf8a8f Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Sun, 27 Sep 2015 12:06:59 -0700 Subject: [PATCH 13/19] Align expand history icon --- data/interfaces/default/css/plexpy.css | 1 - data/interfaces/default/js/tables/history_table.js | 5 +++-- plexpy/datafactory.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data/interfaces/default/css/plexpy.css b/data/interfaces/default/css/plexpy.css index 2d623547..7398da6f 100644 --- a/data/interfaces/default/css/plexpy.css +++ b/data/interfaces/default/css/plexpy.css @@ -2378,7 +2378,6 @@ a .home-platforms-instance-list-oval:hover, width: 100%; } } - table.display tr.shown + tr div.slider { display: none; } diff --git a/data/interfaces/default/js/tables/history_table.js b/data/interfaces/default/js/tables/history_table.js index e6712654..2c499b0c 100644 --- a/data/interfaces/default/js/tables/history_table.js +++ b/data/interfaces/default/js/tables/history_table.js @@ -48,10 +48,11 @@ history_table_options = { $(td).html('Currently watching...'); } else if (rowData['group_count'] > 1) { date = moment(cellData, "X").format(date_format); - expand_history = ''; + expand_history = ''; $(td).html(''); } else { - $(td).html(moment(cellData, "X").format(date_format)); + date = moment(cellData, "X").format(date_format); + $(td).html(''); } }, "searchable": false, diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index 29549960..b6be65b8 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -59,7 +59,7 @@ class DataFactory(object): '((CASE WHEN view_offset IS NULL THEN 0.1 ELSE view_offset * 1.0 END) / \ (CASE WHEN session_history_metadata.duration IS NULL THEN 1.0 ELSE session_history_metadata.duration * 1.0 END) * 100) AS percent_complete', 'session_history_media_info.video_decision', - 'COUNT(*) AS group_count' + 'COUNT(*) AS group_count', 'session_history_media_info.audio_decision', 'session_history.user_id as user_id' ] @@ -128,7 +128,7 @@ class DataFactory(object): "thumb": thumb, "video_decision": item["video_decision"], "watched_status": watched_status, - "group_count": item["group_count"] + "group_count": item["group_count"], "audio_decision": item["audio_decision"], "user_id": item["user_id"] } From a05378934456c108aa530c8392910fcd4b523363 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Sun, 27 Sep 2015 13:59:58 -0700 Subject: [PATCH 14/19] Fix popover cut off by modal box --- data/interfaces/default/js/tables/history_table_modal.js | 1 + 1 file changed, 1 insertion(+) diff --git a/data/interfaces/default/js/tables/history_table_modal.js b/data/interfaces/default/js/tables/history_table_modal.js index c4536867..1080bdc3 100644 --- a/data/interfaces/default/js/tables/history_table_modal.js +++ b/data/interfaces/default/js/tables/history_table_modal.js @@ -126,6 +126,7 @@ history_table_modal_options = { $('.media-type-tooltip').tooltip(); $('.thumb-tooltip').popover({ html: true, + container: '#history-modal', trigger: 'hover', placement: 'right', content: function () { From d9d04f4857ae9516ecba655c6603d7a0a56ee509 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Sun, 27 Sep 2015 14:17:51 -0700 Subject: [PATCH 15/19] Remove href for non-expandable rows. --- data/interfaces/default/js/tables/history_table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/interfaces/default/js/tables/history_table.js b/data/interfaces/default/js/tables/history_table.js index 2c499b0c..84eae0ca 100644 --- a/data/interfaces/default/js/tables/history_table.js +++ b/data/interfaces/default/js/tables/history_table.js @@ -52,7 +52,7 @@ history_table_options = { $(td).html(''); } else { date = moment(cellData, "X").format(date_format); - $(td).html(''); + $(td).html('
 ' + date + '
'); } }, "searchable": false, From 960c281659439f36ae962a47d09dfb7e967a11d7 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Sun, 27 Sep 2015 14:42:38 -0700 Subject: [PATCH 16/19] Add icon change from plus to minus --- data/interfaces/default/js/tables/history_table.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/interfaces/default/js/tables/history_table.js b/data/interfaces/default/js/tables/history_table.js index 84eae0ca..530cdb89 100644 --- a/data/interfaces/default/js/tables/history_table.js +++ b/data/interfaces/default/js/tables/history_table.js @@ -248,6 +248,7 @@ history_table_options = { var rowData = this.data(); if (rowData['group_count'] != 1 && rowData['reference_id'] in history_child_table) { // if grouped row and a child table was already created + $(this.node()).find('i.fa').toggleClass('fa-plus-circle').toggleClass('fa-minus-circle'); this.child(childTableFormat(rowData)).show(); createChildTable(this, rowData) } @@ -382,6 +383,8 @@ $('#history_table').on('click', '> tbody > tr > td.expand-history a', function ( var tr = $(this).closest('tr'); var row = history_table.row(tr); var rowData = row.data(); + + $(this).find('i.fa').toggleClass('fa-plus-circle').toggleClass('fa-minus-circle'); if (row.child.isShown()) { $('div.slider', row.child()).slideUp(function () { From 200a85adcf3cbea55e22b258c5148db4c2c3e7dc Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Sun, 27 Sep 2015 14:43:08 -0700 Subject: [PATCH 17/19] Move code from monitor to activity_processor --- plexpy/activity_processor.py | 30 +++ plexpy/monitor.py | 438 ----------------------------------- 2 files changed, 30 insertions(+), 438 deletions(-) delete mode 100644 plexpy/monitor.py diff --git a/plexpy/activity_processor.py b/plexpy/activity_processor.py index ed926254..93c8f160 100644 --- a/plexpy/activity_processor.py +++ b/plexpy/activity_processor.py @@ -159,6 +159,36 @@ class ActivityProcessor(object): # logger.debug(u"PlexPy ActivityProcessor :: Writing session_history transaction...") self.db.action(query=query, args=args) + # Check if we should group the session, select the last two rows from the user + query = 'SELECT id, rating_key, user_id, reference_id FROM session_history \ + WHERE user_id = ? ORDER BY id DESC LIMIT 2 ' + + args = [session['user_id']] + + result = self.db.select(query=query, args=args) + + new_session = {'id': result[0][0], + 'rating_key': result[0][1], + 'user_id': result[0][2], + 'reference_id': result[0][3]} + + if len(result) == 1: + prev_session = None + else: + prev_session = {'id': result[1][0], + 'rating_key': result[1][1], + 'user_id': result[1][2], + 'reference_id': result[1][3]} + + query = 'UPDATE session_history SET reference_id = ? WHERE id = ? ' + # If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id + if (prev_session is not None) and (prev_session['rating_key'] == new_session['rating_key']): + args = [prev_session['reference_id'], new_session['id']] + else: + args = [new_session['id'], new_session['id']] + + self.db.action(query=query, args=args) + # logger.debug(u"PlexPy ActivityProcessor :: Successfully written history item, last id for session_history is %s" # % last_id) diff --git a/plexpy/monitor.py b/plexpy/monitor.py deleted file mode 100644 index e29ecf47..00000000 --- a/plexpy/monitor.py +++ /dev/null @@ -1,438 +0,0 @@ -# This file is part of PlexPy. -# -# PlexPy 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. -# -# PlexPy 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 PlexPy. If not, see . - -from plexpy import logger, pmsconnect, notification_handler, log_reader, common, database, helpers - -import threading -import plexpy -import re -import time - -monitor_lock = threading.Lock() - -def check_active_sessions(): - - with monitor_lock: - pms_connect = pmsconnect.PmsConnect() - session_list = pms_connect.get_current_activity() - monitor_db = database.MonitorDatabase() - monitor_process = MonitorProcessing() - # logger.debug(u"PlexPy Monitor :: Checking for active streams.") - - if session_list: - media_container = session_list['sessions'] - - # Check our temp table for what we must do with the new streams - db_streams = monitor_db.select('SELECT started, session_key, rating_key, media_type, title, parent_title, ' - 'grandparent_title, user_id, user, friendly_name, ip_address, player, ' - 'platform, machine_id, parent_rating_key, grandparent_rating_key, state, ' - 'view_offset, duration, video_decision, audio_decision, width, height, ' - 'container, video_codec, audio_codec, bitrate, video_resolution, ' - 'video_framerate, aspect_ratio, audio_channels, transcode_protocol, ' - 'transcode_container, transcode_video_codec, transcode_audio_codec, ' - 'transcode_audio_channels, transcode_width, transcode_height, paused_counter ' - 'FROM sessions') - for stream in db_streams: - if any(d['session_key'] == str(stream['session_key']) and d['rating_key'] == str(stream['rating_key']) - for d in media_container): - # The user's session is still active - for session in media_container: - if session['session_key'] == str(stream['session_key']) and \ - session['rating_key'] == str(stream['rating_key']): - # The user is still playing the same media item - # Here we can check the play states - if session['state'] != stream['state']: - if session['state'] == 'paused': - # Push any notifications - - # Push it on it's own thread so we don't hold up our db actions - threading.Thread(target=notification_handler.notify, - kwargs=dict(stream_data=stream, notify_action='pause')).start() - if session['state'] == 'playing' and stream['state'] == 'paused': - # Push any notifications - - # Push it on it's own thread so we don't hold up our db actions - threading.Thread(target=notification_handler.notify, - kwargs=dict(stream_data=stream, notify_action='resume')).start() - if stream['state'] == 'paused': - # The stream is still paused so we need to increment the paused_counter - # Using the set config parameter as the interval, probably not the most accurate but - # it will have to do for now. - paused_counter = int(stream['paused_counter']) + plexpy.CONFIG.MONITORING_INTERVAL - monitor_db.action('UPDATE sessions SET paused_counter = ? ' - 'WHERE session_key = ? AND rating_key = ?', - [paused_counter, stream['session_key'], stream['rating_key']]) - if session['state'] == 'buffering' and plexpy.CONFIG.BUFFER_THRESHOLD > 0: - # The stream is buffering so we need to increment the buffer_count - # We're going just increment on every monitor ping, - # would be difficult to keep track otherwise - monitor_db.action('UPDATE sessions SET buffer_count = buffer_count + 1 ' - 'WHERE session_key = ? AND rating_key = ?', - [stream['session_key'], stream['rating_key']]) - - # Check the current buffer count and last buffer to determine if we should notify - buffer_values = monitor_db.select('SELECT buffer_count, buffer_last_triggered ' - 'FROM sessions ' - 'WHERE session_key = ? AND rating_key = ?', - [stream['session_key'], stream['rating_key']]) - - if buffer_values[0]['buffer_count'] >= plexpy.CONFIG.BUFFER_THRESHOLD: - # Push any notifications - - # Push it on it's own thread so we don't hold up our db actions - # Our first buffer notification - if buffer_values[0]['buffer_count'] == plexpy.CONFIG.BUFFER_THRESHOLD: - logger.info(u"PlexPy Monitor :: User '%s' has triggered a buffer warning." - % stream['user']) - # Set the buffer trigger time - monitor_db.action('UPDATE sessions ' - 'SET buffer_last_triggered = strftime("%s","now") ' - 'WHERE session_key = ? AND rating_key = ?', - [stream['session_key'], stream['rating_key']]) - - threading.Thread(target=notification_handler.notify, - kwargs=dict(stream_data=stream, notify_action='buffer')).start() - else: - # Subsequent buffer notifications after wait time - if int(time.time()) > buffer_values[0]['buffer_last_triggered'] + \ - plexpy.CONFIG.BUFFER_WAIT: - logger.info(u"PlexPy Monitor :: User '%s' has triggered multiple buffer warnings." - % stream['user']) - # Set the buffer trigger time - monitor_db.action('UPDATE sessions ' - 'SET buffer_last_triggered = strftime("%s","now") ' - 'WHERE session_key = ? AND rating_key = ?', - [stream['session_key'], stream['rating_key']]) - - threading.Thread(target=notification_handler.notify, - kwargs=dict(stream_data=stream, notify_action='buffer')).start() - - logger.debug(u"PlexPy Monitor :: Stream buffering. Count is now %s. Last triggered %s." - % (buffer_values[0][0], buffer_values[0][1])) - - # Check if the user has reached the offset in the media we defined as the "watched" percent - # Don't trigger if state is buffer as some clients push the progress to the end when - # buffering on start. - if session['progress'] and session['duration'] and session['state'] != 'buffering': - if helpers.get_percent(session['progress'], - session['duration']) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT: - # Push any notifications - - # Push it on it's own thread so we don't hold up our db actions - threading.Thread(target=notification_handler.notify, - kwargs=dict(stream_data=stream, notify_action='watched')).start() - - else: - # The user has stopped playing a stream - logger.debug(u"PlexPy Monitor :: Removing sessionKey %s ratingKey %s from session queue" - % (stream['session_key'], stream['rating_key'])) - monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?', - [stream['session_key'], stream['rating_key']]) - - # Check if the user has reached the offset in the media we defined as the "watched" percent - if stream['view_offset'] and stream['duration']: - if helpers.get_percent(stream['view_offset'], - stream['duration']) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT: - # Push any notifications - - # Push it on it's own thread so we don't hold up our db actions - threading.Thread(target=notification_handler.notify, - kwargs=dict(stream_data=stream, notify_action='watched')).start() - - # Push any notifications - Push it on it's own thread so we don't hold up our db actions - threading.Thread(target=notification_handler.notify, - kwargs=dict(stream_data=stream, notify_action='stop')).start() - - # Write the item history on playback stop - monitor_process.write_session_history(session=stream) - - # Process the newly received session data - for session in media_container: - monitor_process.write_session(session) - else: - logger.debug(u"PlexPy Monitor :: Unable to read session list.") - - -class MonitorProcessing(object): - - def __init__(self): - self.db = database.MonitorDatabase() - - def write_session(self, session=None): - - values = {'session_key': session['session_key'], - 'rating_key': session['rating_key'], - 'media_type': session['type'], - 'state': session['state'], - 'user_id': session['user_id'], - 'user': session['user'], - 'machine_id': session['machine_id'], - 'title': session['title'], - 'parent_title': session['parent_title'], - 'grandparent_title': session['grandparent_title'], - 'friendly_name': session['friendly_name'], - 'player': session['player'], - 'platform': session['platform'], - 'parent_rating_key': session['parent_rating_key'], - 'grandparent_rating_key': session['grandparent_rating_key'], - 'view_offset': session['progress'], - 'duration': session['duration'], - 'video_decision': session['video_decision'], - 'audio_decision': session['audio_decision'], - 'width': session['width'], - 'height': session['height'], - 'container': session['container'], - 'video_codec': session['video_codec'], - 'audio_codec': session['audio_codec'], - 'bitrate': session['bitrate'], - 'video_resolution': session['video_resolution'], - 'video_framerate': session['video_framerate'], - 'aspect_ratio': session['aspect_ratio'], - 'audio_channels': session['audio_channels'], - 'transcode_protocol': session['transcode_protocol'], - 'transcode_container': session['transcode_container'], - 'transcode_video_codec': session['transcode_video_codec'], - 'transcode_audio_codec': session['transcode_audio_codec'], - 'transcode_audio_channels': session['transcode_audio_channels'], - 'transcode_width': session['transcode_width'], - 'transcode_height': session['transcode_height'] - } - - keys = {'session_key': session['session_key'], - 'rating_key': session['rating_key']} - - result = self.db.upsert('sessions', values, keys) - - if result == 'insert': - # Push any notifications - Push it on it's own thread so we don't hold up our db actions - threading.Thread(target=notification_handler.notify, - kwargs=dict(stream_data=values,notify_action='play')).start() - - started = int(time.time()) - - # Try and grab IP address from logs - if plexpy.CONFIG.IP_LOGGING_ENABLE and plexpy.CONFIG.PMS_LOGS_FOLDER: - ip_address = self.find_session_ip(rating_key=session['rating_key'], - machine_id=session['machine_id']) - else: - ip_address = None - - timestamp = {'started': started, - 'ip_address': ip_address} - - # If it's our first write then time stamp it. - self.db.upsert('sessions', timestamp, keys) - - def write_session_history(self, session=None, import_metadata=None, is_import=False, import_ignore_interval=0): - from plexpy import users - - user_data = users.Users() - user_details = user_data.get_user_friendly_name(user=session['user']) - - if session: - logging_enabled = False - - if is_import: - if str(session['stopped']).isdigit(): - stopped = session['stopped'] - else: - stopped = int(time.time()) - else: - stopped = int(time.time()) - - if plexpy.CONFIG.VIDEO_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \ - (session['media_type'] == 'movie' or session['media_type'] == 'episode'): - logging_enabled = True - elif plexpy.CONFIG.MUSIC_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \ - session['media_type'] == 'track': - logging_enabled = True - else: - logger.debug(u"PlexPy Monitor :: ratingKey %s not logged. Does not meet logging criteria. " - u"Media type is '%s'" % (session['rating_key'], session['media_type'])) - - if plexpy.CONFIG.LOGGING_IGNORE_INTERVAL and not is_import: - if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \ - (int(stopped) - session['started'] < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)): - logging_enabled = False - logger.debug(u"PlexPy Monitor :: Play duration for ratingKey %s is %s secs which is less than %s " - u"seconds, so we're not logging it." % - (session['rating_key'], str(int(stopped) - session['started']), - plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)) - elif is_import and import_ignore_interval: - if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \ - (int(stopped) - session['started'] < int(import_ignore_interval)): - logging_enabled = False - logger.debug(u"PlexPy Monitor :: Play duration for ratingKey %s is %s secs which is less than %s " - u"seconds, so we're not logging it." % - (session['rating_key'], str(int(stopped) - session['started']), - import_ignore_interval)) - - if not user_details['keep_history'] and not is_import: - logging_enabled = False - logger.debug(u"PlexPy Monitor :: History logging for user '%s' is disabled." % session['user']) - - if logging_enabled: - # logger.debug(u"PlexPy Monitor :: Attempting to write to session_history table...") - query = 'INSERT INTO session_history (started, stopped, rating_key, parent_rating_key, ' \ - 'grandparent_rating_key, media_type, user_id, user, ip_address, paused_counter, player, ' \ - 'platform, machine_id, view_offset) VALUES ' \ - '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - - args = [session['started'], stopped, session['rating_key'], session['parent_rating_key'], - session['grandparent_rating_key'], session['media_type'], session['user_id'], session['user'], - session['ip_address'], session['paused_counter'], session['player'], session['platform'], - session['machine_id'], session['view_offset']] - - # logger.debug(u"PlexPy Monitor :: Writing session_history transaction...") - self.db.action(query=query, args=args) - - # Check if we should group the session, select the last two rows from the user - query = 'SELECT id, rating_key, user_id, reference_id FROM session_history \ - WHERE user_id = ? ORDER BY id DESC LIMIT 2 ' - - args = [session['user_id']] - - result = self.db.select(query=query, args=args) - - new_session = {'id': result[0][0], - 'rating_key': result[0][1], - 'user_id': result[0][2], - 'reference_id': result[0][3]} - - if len(result) == 1: - prev_session = None - else: - prev_session = {'id': result[1][0], - 'rating_key': result[1][1], - 'user_id': result[1][2], - 'reference_id': result[1][3]} - - query = 'UPDATE session_history SET reference_id = ? WHERE id = ? ' - # If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id - if (prev_session is not None) and (prev_session['rating_key'] == new_session['rating_key']): - args = [prev_session['reference_id'], new_session['id']] - else: - args = [new_session['id'], new_session['id']] - - self.db.action(query=query, args=args) - - # logger.debug(u"PlexPy Monitor :: Successfully written history item, last id for session_history is %s" - # % last_id) - - # Write the session_history_media_info table - # logger.debug(u"PlexPy Monitor :: Attempting to write to session_history_media_info table...") - query = 'INSERT INTO session_history_media_info (id, rating_key, video_decision, audio_decision, ' \ - 'duration, width, height, container, video_codec, audio_codec, bitrate, video_resolution, ' \ - 'video_framerate, aspect_ratio, audio_channels, transcode_protocol, transcode_container, ' \ - 'transcode_video_codec, transcode_audio_codec, transcode_audio_channels, transcode_width, ' \ - 'transcode_height) VALUES ' \ - '(last_insert_rowid(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - - args = [session['rating_key'], session['video_decision'], session['audio_decision'], - session['duration'], session['width'], session['height'], session['container'], - session['video_codec'], session['audio_codec'], session['bitrate'], - session['video_resolution'], session['video_framerate'], session['aspect_ratio'], - session['audio_channels'], session['transcode_protocol'], session['transcode_container'], - session['transcode_video_codec'], session['transcode_audio_codec'], - session['transcode_audio_channels'], session['transcode_width'], session['transcode_height']] - - # logger.debug(u"PlexPy Monitor :: Writing session_history_media_info transaction...") - self.db.action(query=query, args=args) - - if not is_import: - logger.debug(u"PlexPy Monitor :: Fetching metadata for item ratingKey %s" % session['rating_key']) - pms_connect = pmsconnect.PmsConnect() - result = pms_connect.get_metadata_details(rating_key=str(session['rating_key'])) - metadata = result['metadata'] - else: - metadata = import_metadata - - # Write the session_history_metadata table - directors = ";".join(metadata['directors']) - writers = ";".join(metadata['writers']) - actors = ";".join(metadata['actors']) - genres = ";".join(metadata['genres']) - - # Build media item title - if session['media_type'] == 'episode' or session['media_type'] == 'track': - full_title = '%s - %s' % (metadata['grandparent_title'], metadata['title']) - elif session['media_type'] == 'movie': - full_title = metadata['title'] - else: - full_title = metadata['title'] - - # logger.debug(u"PlexPy Monitor :: Attempting to write to session_history_metadata table...") - query = 'INSERT INTO session_history_metadata (id, rating_key, parent_rating_key, ' \ - 'grandparent_rating_key, title, parent_title, grandparent_title, full_title, media_index, ' \ - 'parent_media_index, thumb, parent_thumb, grandparent_thumb, art, media_type, year, ' \ - 'originally_available_at, added_at, updated_at, last_viewed_at, content_rating, summary, ' \ - 'tagline, rating, duration, guid, directors, writers, actors, genres, studio) VALUES ' \ - '(last_insert_rowid(), ' \ - '?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - - args = [session['rating_key'], session['parent_rating_key'], session['grandparent_rating_key'], - session['title'], session['parent_title'], session['grandparent_title'], full_title, - metadata['index'], metadata['parent_index'], metadata['thumb'], metadata['parent_thumb'], - metadata['grandparent_thumb'], metadata['art'], session['media_type'], metadata['year'], - metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'], - metadata['last_viewed_at'], metadata['content_rating'], metadata['summary'], metadata['tagline'], - metadata['rating'], metadata['duration'], metadata['guid'], directors, writers, actors, genres, metadata['studio']] - - # logger.debug(u"PlexPy Monitor :: Writing session_history_metadata transaction...") - self.db.action(query=query, args=args) - - def find_session_ip(self, rating_key=None, machine_id=None): - - logger.debug(u"PlexPy Monitor :: Requesting log lines...") - log_lines = log_reader.get_log_tail(window=5000, parsed=False) - - rating_key_line = 'ratingKey=' + rating_key - rating_key_line_2 = 'metadata%2F' + rating_key - machine_id_line = 'session=' + machine_id - - for line in reversed(log_lines): - # We're good if we find a line with both machine id and rating key - # This is usually when there is a transcode session - if machine_id_line in line and (rating_key_line in line or rating_key_line_2 in line): - # Currently only checking for ipv4 addresses - ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}', line) - if ipv4: - # The logged IP will always be the first match and we don't want localhost entries - if ipv4[0] != '127.0.0.1': - logger.debug(u"PlexPy Monitor :: Matched IP address (%s) for stream ratingKey %s " - u"and machineIdentifier %s." - % (ipv4[0], rating_key, machine_id)) - return ipv4[0] - - logger.debug(u"PlexPy Monitor :: Unable to find IP address on first pass. " - u"Attempting fallback check in 5 seconds...") - - # Wait for the log to catch up and read in new lines - time.sleep(5) - - logger.debug(u"PlexPy Monitor :: Requesting log lines...") - log_lines = log_reader.get_log_tail(window=5000, parsed=False) - - for line in reversed(log_lines): - if 'GET /:/timeline' in line and (rating_key_line in line or rating_key_line_2 in line): - # Currently only checking for ipv4 addresses - # This method can return the wrong IP address if more than one user - # starts watching the same media item around the same time. - ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}', line) - if ipv4: - # The logged IP will always be the first match and we don't want localhost entries - if ipv4[0] != '127.0.0.1': - logger.debug(u"PlexPy Monitor :: Matched IP address (%s) for stream ratingKey %s." % - (ipv4[0], rating_key)) - return ipv4[0] - - logger.debug(u"PlexPy Monitor :: Unable to find IP address on fallback search. Not logging IP address.") - - return None From 8b525480162087f35bcd5e17b52997785600a242 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Sun, 27 Sep 2015 22:43:42 -0700 Subject: [PATCH 18/19] Fix delete mode to use list of child id's instead of loop --- .../default/js/tables/history_table.js | 37 ++++++++++--------- plexpy/datafactory.py | 8 ++-- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/data/interfaces/default/js/tables/history_table.js b/data/interfaces/default/js/tables/history_table.js index 530cdb89..ec086ba2 100644 --- a/data/interfaces/default/js/tables/history_table.js +++ b/data/interfaces/default/js/tables/history_table.js @@ -269,14 +269,13 @@ history_table_options = { // toggle the parent button to danger $(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); // check if any child rows are not selected - for (var i = rowData['reference_id']; i <= rowData['id']; i++) { - var index = $.inArray(i, history_to_delete); + var group_ids = rowData['group_ids'].split(',').map(Number); + group_ids.forEach(function (id) { + var index = $.inArray(id, history_to_delete); if (index == -1) { - // if any child row is not selected, toggle parent button to warning - $(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); - break; + $(row).find('button[data-id="' + rowData['id'] + '"]').addClass('btn-warning').removeClass('btn-danger'); } - } + }); } if (rowData['group_count'] != 1 && rowData['reference_id'] in history_child_table) { @@ -350,12 +349,13 @@ $('#history_table').on('click', '> tbody > tr > td.delete-control > button', fun // if grouped rows if ($(this).hasClass('btn-warning')) { // add all grouped rows to history_to_delete - for (var i = rowData['reference_id']; i <= rowData['id']; i++) { - var index = $.inArray(i, history_to_delete); + var group_ids = rowData['group_ids'].split(',').map(Number); + group_ids.forEach(function (id) { + var index = $.inArray(id, history_to_delete); if (index == -1) { - history_to_delete.push(i); + history_to_delete.push(id); } - } + }); $(this).toggleClass('btn-warning').toggleClass('btn-danger'); if (row.child.isShown()) { // if child table is visible, toggle all child buttons to danger @@ -363,12 +363,13 @@ $('#history_table').on('click', '> tbody > tr > td.delete-control > button', fun } } else { // remove all grouped rows to history_to_delete - for (var i = rowData['reference_id']; i <= rowData['id']; i++) { - var index = $.inArray(i, history_to_delete); + var group_ids = rowData['group_ids'].split(',').map(Number); + group_ids.forEach(function (id) { + var index = $.inArray(id, history_to_delete); if (index != -1) { history_to_delete.splice(index, 1); } - } + }); $(this).toggleClass('btn-warning').toggleClass('btn-danger'); if (row.child.isShown()) { // if child table is visible, toggle all child buttons to warning @@ -553,14 +554,14 @@ function createChildTable(row, rowData) { tr.parents('tr').prev().find('td.delete-control > button.btn-warning').toggleClass('btn-warning').toggleClass('btn-danger'); // check if any child rows are not selected - for (var i = rowData['reference_id']; i <= rowData['id']; i++) { - var index = $.inArray(i, history_to_delete); + var group_ids = rowData['group_ids'].split(',').map(Number); + group_ids.forEach(function (id) { + var index = $.inArray(id, history_to_delete); if (index == -1) { // if any child row is not selected, toggle parent button to warning - tr.parents('tr').prev().find('td.delete-control > button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); - break; + tr.parents('tr').prev().find('td.delete-control > button.btn-danger').addClass('btn-warning').removeClass('btn-danger'); } - } + }); }); } diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index dc53e9c8..421bc85e 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -59,9 +59,9 @@ class DataFactory(object): '((CASE WHEN view_offset IS NULL THEN 0.1 ELSE view_offset * 1.0 END) / \ (CASE WHEN session_history_metadata.duration IS NULL THEN 1.0 ELSE session_history_metadata.duration * 1.0 END) * 100) AS percent_complete', 'session_history_media_info.video_decision', - 'COUNT(*) AS group_count', 'session_history_media_info.audio_decision', - 'session_history.user_id as user_id' + 'COUNT(*) AS group_count', + 'GROUP_CONCAT(session_history.id) AS group_ids' ] try: query = data_tables.ssp_query(table_name='session_history', @@ -127,10 +127,10 @@ class DataFactory(object): "parent_media_index": item["parent_media_index"], "thumb": thumb, "video_decision": item["video_decision"], + "audio_decision": item["audio_decision"], "watched_status": watched_status, "group_count": item["group_count"], - "audio_decision": item["audio_decision"], - "user_id": item["user_id"] + "group_ids": item["group_ids"] } rows.append(row) From c1f32674dc5af22ef08954322a059f818396d099 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Mon, 28 Sep 2015 00:35:30 -0700 Subject: [PATCH 19/19] Change wording of group history in settings --- data/interfaces/default/settings.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index 2f3aa40b..ef9bb1ea 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -86,9 +86,9 @@ available_notification_agents = notifiers.available_notification_agents()
-

Group successive play history as a single entry in tables.

+

Group successive play history by the same user as a single entry in tables.

Delete