diff --git a/data/interfaces/default/graphs.html b/data/interfaces/default/graphs.html
index 2e71bd76..b74820c8 100644
--- a/data/interfaces/default/graphs.html
+++ b/data/interfaces/default/graphs.html
@@ -138,6 +138,20 @@
+
Play count by source resolution Last 30 days
@@ -327,6 +341,7 @@
+
@@ -519,6 +534,33 @@
}
});
+ $.ajax({
+ url: "get_concurrent_streams_by_stream_type",
+ type: 'get',
+ data: { time_range: time_range },
+ 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',
@@ -727,6 +769,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;
@@ -734,6 +777,7 @@
hc_plays_by_platform_options.yAxis.labels.formatter = yaxis_format;
hc_plays_by_user_options.yAxis.labels.formatter = yaxis_format;
hc_plays_by_stream_type_options.yAxis.labels.formatter = yaxis_format;
+ hc_concurrent_streams_by_stream_type_options.yAxis.labels.formatter = yaxis_format;
hc_plays_by_source_resolution_options.yAxis.labels.formatter = yaxis_format;
hc_plays_by_stream_resolution_options.yAxis.labels.formatter = yaxis_format;
hc_plays_by_platform_by_stream_type_options.yAxis.labels.formatter = yaxis_format;
@@ -746,6 +790,7 @@
hc_plays_by_platform_options.tooltip.formatter = tooltip_format;
hc_plays_by_user_options.tooltip.formatter = tooltip_format;
hc_plays_by_stream_type_options.tooltip.formatter = tooltip_format;
+ hc_concurrent_streams_by_stream_type_options.tooltip.formatter = tooltip_format;
hc_plays_by_source_resolution_options.tooltip.formatter = tooltip_format;
hc_plays_by_stream_resolution_options.tooltip.formatter = tooltip_format;
hc_plays_by_platform_by_stream_type_options.tooltip.formatter = tooltip_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..42cd4eaa
--- /dev/null
+++ b/data/interfaces/default/js/graphs/concurrent_streams_by_stream_type.js
@@ -0,0 +1,71 @@
+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: {
+ cursor: 'pointer',
+ point: {
+ events: {
+ click: function() {
+ selectHandler(this.category, this.series.name);
+ }
+ }
+ },
+ 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
+ },
+ series: [{}]
+};
\ No newline at end of file
diff --git a/plexpy/graphs.py b/plexpy/graphs.py
index e9afa704..0727c66a 100644
--- a/plexpy/graphs.py
+++ b/plexpy/graphs.py
@@ -22,7 +22,7 @@ from future.builtins import object
import arrow
import datetime
-
+import itertools
import plexpy
if plexpy.PYTHON2:
import common
@@ -854,6 +854,103 @@ 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'):
+ monitor_db = database.MonitorDatabase()
+
+ time_range = helpers.cast_to_int(time_range) or 30
+ timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
+
+ def calc_most_concurrent(result):
+ '''
+ Function to calculate most concurrent streams
+ Input: Stat title, SQLite query result
+ Output: Dict {title, count, started, stopped}
+ '''
+ 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
+ last_count = 0
+ last_start = ''
+ concurrent = { 'count': 0,
+ 'started': None,
+ 'stopped': None
+ }
+
+ for d in times:
+ if d['count'] == 1:
+ count += d['count']
+ if count >= last_count:
+ last_start = d['time']
+ else:
+ if count >= last_count:
+ last_count = count
+ concurrent['count'] = count
+ concurrent['started'] = last_start[:-1]
+ concurrent['stopped'] = d['time'][:-1]
+ count += d['count']
+
+ return concurrent
+
+ 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) AS sh ' \
+ 'JOIN session_history_media_info AS shmi ON sh.id = shmi.id ' \
+ 'WHERE sh.stopped >= %s ' \
+ 'ORDER BY sh.date_played' % 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 = []
+
+ grouped_result = helpers.group_by_keys(result, ('date_played','transcode_decision'))
+
+ 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
+
+ for item in grouped_result:
+ if item['key'] == (date_string,'direct play'):
+ series_1_value = calc_most_concurrent(item['value'])['count']
+ elif item['key'] == (date_string,'copy'):
+ series_2_value = calc_most_concurrent(item['value'])['count']
+ elif item['key'] == (date_string,'transcode'):
+ series_3_value = calc_most_concurrent(item['value'])['count']
+
+ series_1.append(series_1_value)
+ series_2.append(series_2_value)
+ series_3.append(series_3_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}
+
+ output = {'categories': categories,
+ 'series': [series_1_output, series_2_output, series_3_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()
diff --git a/plexpy/helpers.py b/plexpy/helpers.py
index b0995849..43d096b4 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
import ipwhois
import ipwhois.exceptions
@@ -1240,6 +1241,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 76c44243..753c3c7d 100644
--- a/plexpy/webserve.py
+++ b/plexpy/webserve.py
@@ -2519,6 +2519,43 @@ 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', **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
+
+ Returns:
+ json:
+ {"categories":
+ ["YYYY-MM-DD", "YYYY-MM-DD", ...]
+ "series":
+ [{"name": "Direct Play", "data": [...]}
+ {"name": "Direct Stream", "data": [...]},
+ {"name": "Transcode", "data": [...]}
+ ]
+ }
+ ```
+ """
+ grouping = helpers.bool_true(grouping, return_none=True)
+
+ graph = graphs.Graphs()
+ result = graph.get_total_concurrent_streams_per_stream_type(time_range=time_range)
+
+ 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()