mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-22 22:23:36 -07:00
initial commit
This commit is contained in:
parent
3c996f01a9
commit
3b18432788
5 changed files with 257 additions and 1 deletions
|
@ -138,6 +138,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h4><i class="fa fa-video-camera"></i> Daily concurrent stream count</span> by stream type <small>Last <span class="days">30</span> days</small></h4>
|
||||||
|
<p class="help-block">
|
||||||
|
The total count of concurrent streams of tv, movies, and music by the transcode decision.
|
||||||
|
</p>
|
||||||
|
<div class="graphs-instance">
|
||||||
|
<div class="watch-chart" id="graph_concurrent_streams_by_stream_type">
|
||||||
|
<div class="graphs-load"><i class="fa fa-refresh fa-spin"></i> Loading chart...</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h4><i class="fa fa-expand-arrows-alt"></i> <span class="yaxis-text">Play count</span> by source resolution <small>Last <span class="days">30</span> days</small></h4>
|
<h4><i class="fa fa-expand-arrows-alt"></i> <span class="yaxis-text">Play count</span> by source resolution <small>Last <span class="days">30</span> days</small></h4>
|
||||||
|
@ -327,6 +341,7 @@
|
||||||
<script src="${http_root}js/graphs/plays_by_platform.js${cache_param}"></script>
|
<script src="${http_root}js/graphs/plays_by_platform.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/graphs/plays_by_user.js${cache_param}"></script>
|
<script src="${http_root}js/graphs/plays_by_user.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/graphs/plays_by_stream_type.js${cache_param}"></script>
|
<script src="${http_root}js/graphs/plays_by_stream_type.js${cache_param}"></script>
|
||||||
|
<script src="${http_root}js/graphs/concurrent_streams_by_stream_type.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/graphs/plays_by_source_resolution.js${cache_param}"></script>
|
<script src="${http_root}js/graphs/plays_by_source_resolution.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/graphs/plays_by_stream_resolution.js${cache_param}"></script>
|
<script src="${http_root}js/graphs/plays_by_stream_resolution.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/graphs/plays_by_platform_by_stream_type.js${cache_param}"></script>
|
<script src="${http_root}js/graphs/plays_by_platform_by_stream_type.js${cache_param}"></script>
|
||||||
|
@ -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({
|
$.ajax({
|
||||||
url: "get_plays_by_source_resolution",
|
url: "get_plays_by_source_resolution",
|
||||||
type: 'get',
|
type: 'get',
|
||||||
|
@ -727,6 +769,7 @@
|
||||||
|
|
||||||
hc_plays_by_day_options.xAxis.plotBands = [];
|
hc_plays_by_day_options.xAxis.plotBands = [];
|
||||||
hc_plays_by_stream_type_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_day_options.yAxis.labels.formatter = yaxis_format;
|
||||||
hc_plays_by_dayofweek_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_platform_options.yAxis.labels.formatter = yaxis_format;
|
||||||
hc_plays_by_user_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_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_source_resolution_options.yAxis.labels.formatter = yaxis_format;
|
||||||
hc_plays_by_stream_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;
|
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_platform_options.tooltip.formatter = tooltip_format;
|
||||||
hc_plays_by_user_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_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_source_resolution_options.tooltip.formatter = tooltip_format;
|
||||||
hc_plays_by_stream_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;
|
hc_plays_by_platform_by_stream_type_options.tooltip.formatter = tooltip_format;
|
||||||
|
|
|
@ -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: [{}]
|
||||||
|
};
|
|
@ -22,7 +22,7 @@ from future.builtins import object
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import datetime
|
import datetime
|
||||||
|
import itertools
|
||||||
import plexpy
|
import plexpy
|
||||||
if plexpy.PYTHON2:
|
if plexpy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
|
@ -854,6 +854,103 @@ class Graphs(object):
|
||||||
'series': [series_1_output, series_2_output, series_3_output]}
|
'series': [series_1_output, series_2_output, series_3_output]}
|
||||||
return 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):
|
def get_total_plays_by_source_resolution(self, time_range='30', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ import datetime
|
||||||
from functools import reduce, wraps
|
from functools import reduce, wraps
|
||||||
import hashlib
|
import hashlib
|
||||||
import imghdr
|
import imghdr
|
||||||
|
from itertools import groupby
|
||||||
from future.moves.itertools import islice, zip_longest
|
from future.moves.itertools import islice, zip_longest
|
||||||
import ipwhois
|
import ipwhois
|
||||||
import ipwhois.exceptions
|
import ipwhois.exceptions
|
||||||
|
@ -1240,6 +1241,11 @@ def grouper(iterable, n, fillvalue=None):
|
||||||
args = [iter(iterable)] * n
|
args = [iter(iterable)] * n
|
||||||
return zip_longest(fillvalue=fillvalue, *args)
|
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):
|
def chunk(it, size):
|
||||||
it = iter(it)
|
it = iter(it)
|
||||||
|
|
|
@ -2519,6 +2519,43 @@ class WebInterface(object):
|
||||||
logger.warn("Unable to retrieve data for get_plays_by_stream_type.")
|
logger.warn("Unable to retrieve data for get_plays_by_stream_type.")
|
||||||
return result
|
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.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth()
|
@requireAuth()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue