diff --git a/data/interfaces/default/graphs.html b/data/interfaces/default/graphs.html
index 8435df20..8f16f59a 100644
--- a/data/interfaces/default/graphs.html
+++ b/data/interfaces/default/graphs.html
@@ -137,6 +137,20 @@
+
Play count by source resolution Last 30 days
@@ -312,7 +326,8 @@
'Live TV': '#19A0D7',
'Direct Play': '#E5A00D',
'Direct Stream': '#FFFFFF',
- 'Transcode': '#F06464'
+ 'Transcode': '#F06464',
+ 'Max. Concurrent Streams': '#1014FC'
};
var series_colors = [];
$.each(data_series, function(index, series) {
@@ -327,6 +342,7 @@
+
@@ -540,6 +556,33 @@
}
});
+ $.ajax({
+ url: "get_concurrent_streams_by_stream_type",
+ type: 'get',
+ data: { time_range: time_range, user_id: selected_user_id },
+ dataType: "json",
+ success: function(data) {
+ var dateArray = [];
+ $.each(data.categories, function (i, day) {
+ dateArray.push(moment(day, 'YYYY-MM-DD').valueOf());
+ // Highlight the weekend
+ if ((moment(day, 'YYYY-MM-DD').format('ddd') == 'Sat') ||
+ (moment(day, 'YYYY-MM-DD').format('ddd') == 'Sun')) {
+ hc_plays_by_day_options.xAxis.plotBands.push({
+ from: i-0.5,
+ to: i+0.5,
+ color: 'rgba(80,80,80,0.3)'
+ });
+ }
+ });
+ hc_concurrent_streams_by_stream_type_options.yAxis.min = 0;
+ hc_concurrent_streams_by_stream_type_options.xAxis.categories = dateArray;
+ hc_concurrent_streams_by_stream_type_options.series = getGraphVisibility(hc_concurrent_streams_by_stream_type_options.chart.renderTo, data.series);
+ hc_concurrent_streams_by_stream_type_options.colors = getGraphColors(data.series);
+ var hc_plays_by_stream_type = new Highcharts.Chart(hc_concurrent_streams_by_stream_type_options);
+ }
+ });
+
$.ajax({
url: "get_plays_by_source_resolution",
type: 'get',
@@ -754,6 +797,7 @@
hc_plays_by_day_options.xAxis.plotBands = [];
hc_plays_by_stream_type_options.xAxis.plotBands = [];
+ hc_concurrent_streams_by_stream_type_options.xAxis.plotBands = [];
hc_plays_by_day_options.yAxis.labels.formatter = yaxis_format;
hc_plays_by_dayofweek_options.yAxis.labels.formatter = yaxis_format;
diff --git a/data/interfaces/default/js/graphs/concurrent_streams_by_stream_type.js b/data/interfaces/default/js/graphs/concurrent_streams_by_stream_type.js
new file mode 100644
index 00000000..623e4735
--- /dev/null
+++ b/data/interfaces/default/js/graphs/concurrent_streams_by_stream_type.js
@@ -0,0 +1,76 @@
+var formatter_function = function() {
+ if (moment(this.x, 'X').isValid() && (this.x > 946684800)) {
+ var s = ''+ moment(this.x).format('ddd MMM D') +'';
+ } else {
+ var s = ''+ this.x +'';
+ }
+ $.each(this.points, function(i, point) {
+ s += '
'+point.series.name+': '+point.y;
+ });
+ return s;
+};
+
+var hc_concurrent_streams_by_stream_type_options = {
+ chart: {
+ type: 'line',
+ backgroundColor: 'rgba(0,0,0,0)',
+ renderTo: 'graph_concurrent_streams_by_stream_type'
+ },
+ title: {
+ text: ''
+ },
+ legend: {
+ enabled: true,
+ itemStyle: {
+ font: '9pt "Open Sans", sans-serif',
+ color: '#A0A0A0'
+ },
+ itemHoverStyle: {
+ color: '#FFF'
+ },
+ itemHiddenStyle: {
+ color: '#444'
+ }
+ },
+ credits: {
+ enabled: false
+ },
+ plotOptions: {
+ series: {
+ events: {
+ legendItemClick: function() {
+ setGraphVisibility(this.chart.renderTo.id, this.chart.series, this.name);
+ }
+ }
+ }
+ },
+ xAxis: {
+ type: 'datetime',
+ labels: {
+ formatter: function() {
+ return moment(this.value).format("MMM D");
+ },
+ style: {
+ color: '#aaa'
+ }
+ },
+ categories: [{}],
+ plotBands: []
+ },
+ yAxis: {
+ title: {
+ text: null
+ },
+ labels: {
+ style: {
+ color: '#aaa'
+ }
+ }
+ },
+ tooltip: {
+ shared: true,
+ crosshairs: true,
+ formatter: formatter_function
+ },
+ series: [{}]
+};
\ No newline at end of file
diff --git a/plexpy/graphs.py b/plexpy/graphs.py
index 58a199c0..82db9d82 100644
--- a/plexpy/graphs.py
+++ b/plexpy/graphs.py
@@ -22,7 +22,6 @@ from future.builtins import object
import arrow
import datetime
-
import plexpy
if plexpy.PYTHON2:
import common
@@ -826,6 +825,102 @@ class Graphs(object):
'series': [series_1_output, series_2_output, series_3_output]}
return output
+ def get_total_concurrent_streams_per_stream_type(self, time_range='30', user_id=None):
+ monitor_db = database.MonitorDatabase()
+
+ time_range = helpers.cast_to_int(time_range) or 30
+ timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
+
+ user_cond = self._make_user_cond(user_id, 'WHERE')
+
+ def calc_most_concurrent(result):
+ times = []
+ for item in result:
+ times.append({'time': str(item['started']) + 'B', 'count': 1})
+ times.append({'time': str(item['stopped']) + 'A', 'count': -1})
+ times = sorted(times, key=lambda k: k['time'])
+
+ count = 0
+ final_count = 0
+ last_count = 0
+
+ for d in times:
+ if d['count'] == 1:
+ count += d['count']
+ else:
+ if count >= last_count:
+ last_count = count
+ final_count = count
+ count += d['count']
+
+ return final_count
+
+ try:
+ query = 'SELECT sh.date_played, sh.started, sh.stopped, shmi.transcode_decision ' \
+ 'FROM (SELECT *, ' \
+ 'date(started, "unixepoch", "localtime") AS date_played ' \
+ 'FROM session_history %s) AS sh ' \
+ 'JOIN session_history_media_info AS shmi ON sh.id = shmi.id ' \
+ 'WHERE sh.stopped >= %s ' \
+ 'ORDER BY sh.date_played' % (user_cond, timestamp)
+
+ result = monitor_db.select(query)
+ except Exception as e:
+ logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_per_stream_type: %s." % e)
+ return None
+
+ # create our date range as some days may not have any data
+ # but we still want to display them
+ base = datetime.date.today()
+ date_list = [base - datetime.timedelta(days=x) for x in range(0, int(time_range))]
+
+ categories = []
+ series_1 = []
+ series_2 = []
+ series_3 = []
+ series_4 = []
+
+ grouped_result_by_stream_type = helpers.group_by_keys(result, ('date_played','transcode_decision'))
+ grouped_result_by_day = helpers.group_by_keys(result, ['date_played'])
+
+ for date_item in sorted(date_list):
+ date_string = date_item.strftime('%Y-%m-%d')
+ categories.append(date_string)
+ series_1_value = 0
+ series_2_value = 0
+ series_3_value = 0
+ series_4_value = 0
+
+ for item in grouped_result_by_stream_type:
+ if item['key'] == (date_string,'direct play'):
+ series_1_value = calc_most_concurrent(item['value'])
+ elif item['key'] == (date_string,'copy'):
+ series_2_value = calc_most_concurrent(item['value'])
+ elif item['key'] == (date_string,'transcode'):
+ series_3_value = calc_most_concurrent(item['value'])
+
+ for item in grouped_result_by_day:
+ if item['key'] == date_string:
+ series_4_value = calc_most_concurrent(item['value'])
+
+ series_1.append(series_1_value)
+ series_2.append(series_2_value)
+ series_3.append(series_3_value)
+ series_4.append(series_4_value)
+
+ series_1_output = {'name': 'Direct Play',
+ 'data': series_1}
+ series_2_output = {'name': 'Direct Stream',
+ 'data': series_2}
+ series_3_output = {'name': 'Transcode',
+ 'data': series_3}
+ series_4_output = {'name': 'Max. Concurrent Streams',
+ 'data': series_4}
+
+ output = {'categories': categories,
+ 'series': [series_1_output, series_2_output, series_3_output, series_4_output]}
+ return output
+
def get_total_plays_by_source_resolution(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
monitor_db = database.MonitorDatabase()
@@ -1169,15 +1264,16 @@ class Graphs(object):
return output
- def _make_user_cond(self, user_id):
+ def _make_user_cond(self, user_id, cond_prefix='AND'):
"""
Expects user_id to be a comma-separated list of ints.
"""
user_cond = ''
+
if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()):
- user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id()
+ user_cond = cond_prefix + ' session_history.user_id = %s ' % session.get_session_user_id()
elif user_id:
user_ids = helpers.split_strip(user_id)
if all(id.isdigit() for id in user_ids):
- user_cond = 'AND session_history.user_id IN (%s) ' % ','.join(user_ids)
+ user_cond =cond_prefix + ' session_history.user_id IN (%s) ' % ','.join(user_ids)
return user_cond
diff --git a/plexpy/helpers.py b/plexpy/helpers.py
index 085dfc12..4512f0b6 100644
--- a/plexpy/helpers.py
+++ b/plexpy/helpers.py
@@ -32,6 +32,7 @@ import datetime
from functools import reduce, wraps
import hashlib
import imghdr
+from itertools import groupby
from future.moves.itertools import islice, zip_longest
from ipaddress import ip_address, ip_network, IPv4Address
import ipwhois
@@ -1241,6 +1242,11 @@ def grouper(iterable, n, fillvalue=None):
args = [iter(iterable)] * n
return zip_longest(fillvalue=fillvalue, *args)
+def group_by_keys(iterable, keys):
+ key_function = operator.itemgetter(*keys)
+
+ sorted_iterable = sorted(iterable, key=key_function)
+ return[{'key': key, 'value': list(group)} for key, group in groupby(sorted_iterable, key_function)]
def chunk(it, size):
it = iter(it)
diff --git a/plexpy/webserve.py b/plexpy/webserve.py
index b643f84b..196bbe5b 100644
--- a/plexpy/webserve.py
+++ b/plexpy/webserve.py
@@ -2549,6 +2549,44 @@ class WebInterface(object):
logger.warn("Unable to retrieve data for get_plays_by_stream_type.")
return result
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ @requireAuth()
+ @addtoapi()
+ def get_concurrent_streams_by_stream_type(self, time_range='30', user_id=None, **kwargs):
+ """ Get graph data for concurrent streams by stream type by date.
+
+ ```
+ Required parameters:
+ None
+
+ Optional parameters:
+ time_range (str): The number of days of data to return
+ user_id (str): Comma separated list of user id to filter the data
+
+ Returns:
+ json:
+ {"categories":
+ ["YYYY-MM-DD", "YYYY-MM-DD", ...]
+ "series":
+ [{"name": "Direct Play", "data": [...]}
+ {"name": "Direct Stream", "data": [...]},
+ {"name": "Transcode", "data": [...]},
+ {"name": "Max. Concurrent Streams", "data": [...]}
+ ]
+ }
+ ```
+ """
+
+ graph = graphs.Graphs()
+ result = graph.get_total_concurrent_streams_per_stream_type(time_range=time_range, user_id=user_id)
+
+ if result:
+ return result
+ else:
+ logger.warn("Unable to retrieve data for get_concurrent_streams_by_stream_type.")
+ return result
+
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth()