mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-19 04:49:36 -07:00
Merge 0cc69c275b
into a7660d5c03
This commit is contained in:
commit
c3f13c3a13
13 changed files with 1253 additions and 13 deletions
|
@ -44,6 +44,7 @@
|
|||
<li role="presentation"><a id="nav-tabs-plays" href="#tabs-plays" aria-controls="tabs-plays" data-toggle="tab" role="tab">Media Type</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-stream" href="#tabs-stream" aria-controls="tabs-stream" data-toggle="tab" role="tab">Stream Type</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-total" href="#tabs-total" aria-controls="tabs-total" data-toggle="tab" role="tab">Play Totals</a></li>
|
||||
<li role="presentation"><a id="nav-tabs-library-statistics" href="#tabs-library-statistics" aria-controls="tabs-library-statistics" data-toggle="tab" role="tab">Library Statistics</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-plays">
|
||||
|
@ -225,6 +226,68 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-library-statistics">
|
||||
<div id="section-library-graphs">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4><i class="fa fa-history"></i> Daily addition by media type <small>Last <span class="days">30</span> days</small></h4>
|
||||
<p class="help-block">
|
||||
The total addition count of shows, seasons, episodes and movies per day.
|
||||
</p>
|
||||
<div class="graphs-instance">
|
||||
<div class="watch-chart" id="graph_additions_by_day">
|
||||
<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="col-md-12">
|
||||
<h4><i class="fa fa-signal"></i> Library Growth <small>Last <span class="days">30</span> days</small></h4>
|
||||
<p class="help-block">
|
||||
The overall library growth by the library stats per day.
|
||||
</p>
|
||||
<div class="graphs-instance">
|
||||
<div class="watch-chart" id="library_growth_by_day">
|
||||
<div class="graphs-load"><i class="fa fa-refresh fa-spin"></i> Loading chart...</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h4><i class="fa fa-television"></i> Addition count by media type <small>Last <span class="days">30</span> days</small></h4>
|
||||
<p class="help-block">
|
||||
The combined total of tv, movies, and music added to the server.
|
||||
</p>
|
||||
<div class="graphs-instance">
|
||||
<div class="watch-chart" id="graph_additions_by_media_type" style="float: left;">
|
||||
<div class="graphs-load">
|
||||
<i class="fa fa-refresh fa-spin"></i> Loading chart...
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4><i class="fa fa-television"></i> Addition count by source resolution <small>Last <span class="days">30</span> days</small></h4>
|
||||
<p class="help-block">
|
||||
The combined total of tv, movies, and music added to the server by source resolution.
|
||||
</p>
|
||||
<div class="graphs-instance">
|
||||
<div class="watch-chart" id="graph_additions_by_source_resolution" style="float: left;">
|
||||
<div class="graphs-load">
|
||||
<i class="fa fa-refresh fa-spin"></i> Loading chart...
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -246,6 +309,8 @@
|
|||
|
||||
<script>
|
||||
var selected_user_id = null;
|
||||
var _Charts = [];
|
||||
var _enableChartSync = false;
|
||||
|
||||
// Modal popup dialog
|
||||
function selectHandler(selectedDate, selectedSeries) {
|
||||
|
@ -304,15 +369,154 @@
|
|||
return data_series.map(function(s) {
|
||||
var obj = Object.assign({}, s);
|
||||
obj.visible = (chart_visibility[s.name] !== false);
|
||||
if(chart_name == "graph_additions_by_day" || chart_name == "library_growth_by_day") {
|
||||
if(obj.name == "Episodes") {
|
||||
obj.yAxis = 1;
|
||||
} else if(obj.name == "Tracks") {
|
||||
obj.yAxis = 2;
|
||||
}
|
||||
obj.softThreshold = true;
|
||||
}
|
||||
return obj
|
||||
});
|
||||
}
|
||||
|
||||
var syncLinks = {
|
||||
'library_growth_by_day': 'graph_additions_by_day',
|
||||
'graph_additions_by_day': 'library_growth_by_day'
|
||||
}
|
||||
|
||||
function syncGraphs(instance, chart_name, series_name, browserEvent) {
|
||||
var otherChart = _Charts.find(c => c['renderTo'].id == syncLinks[chart_name]);
|
||||
var sisterSeries = otherChart.series.find(s => s['name'] == series_name);
|
||||
|
||||
// Chart Visibility in storage - needs to be done before setting axis visibility
|
||||
setGraphVisibility(syncLinks[chart_name], otherChart.series, series_name);
|
||||
|
||||
// Axis visibility
|
||||
if (sisterSeries) {
|
||||
if (instance.visible) {
|
||||
sisterSeries.hide();
|
||||
} else {
|
||||
sisterSeries.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Axis name visibility
|
||||
if (series_name == "Episodes") {
|
||||
document.getElementsByClassName("highcharts-yaxis-title")[0].innerHTML = (instance.visible) ? "" : "Episodes";
|
||||
document.getElementsByClassName("highcharts-yaxis-title")[2].innerHTML = (instance.visible) ? "" : "Episodes";
|
||||
} else if(series_name == "Tracks") {
|
||||
document.getElementsByClassName("highcharts-yaxis-title")[1].innerHTML = (instance.visible) ? "" : "Tracks";
|
||||
document.getElementsByClassName("highcharts-yaxis-title")[3].innerHTML = (instance.visible) ? "" : "Tracks";
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
if(browserEvent) {
|
||||
//syncTooltip(browserEvent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Sync selected day (tooltip position)
|
||||
// https://jsfiddle.net/BlackLabel/5wq9sdbp
|
||||
['mousemove', 'touchmove', 'touchstart', 'mouseleave'].forEach(function (eventType) {
|
||||
document.getElementById('section-library-graphs').addEventListener(
|
||||
eventType,
|
||||
function(e) {
|
||||
if(_enableChartSync) {
|
||||
syncTooltip(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
function syncTooltip(e) {
|
||||
var chart, otherChart,
|
||||
point, points,
|
||||
i, event;
|
||||
|
||||
for (i = 0; i < Highcharts.charts.length; i = i + 1) {
|
||||
chart = _Charts[i];
|
||||
if(chart == undefined || chart.options.chart.type != "line" || !syncLinks.hasOwnProperty(chart.renderTo.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find coordinates within the chart
|
||||
event = chart.pointer.normalize(e);
|
||||
const isMouseLeave = e.type == "mouseleave";
|
||||
otherChart = _Charts.find(c => c['renderTo'].id == syncLinks[chart.renderTo.id]);
|
||||
|
||||
// Get the hovered points
|
||||
points = [];
|
||||
chart.series.forEach(function (series, idx) {
|
||||
var _point = series.searchPoint(event, true);
|
||||
if(isMouseLeave) {
|
||||
if(_point) {
|
||||
_point.setState('');
|
||||
points.push(_point);
|
||||
}
|
||||
} else {
|
||||
if(_point && _point.series.visible == true) {
|
||||
points.push(_point);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(points.length && !points.includes(undefined)) {
|
||||
number = 0;
|
||||
if(isMouseLeave) {
|
||||
chart.tooltip.hide();
|
||||
chart.xAxis[0].hideCrosshair();
|
||||
} else {
|
||||
chart.tooltip.refresh(points);
|
||||
chart.xAxis[0].drawCrosshair(e, points[0]);
|
||||
//chart.xAxis[0].drawCrosshair(e, points[points.length]);
|
||||
|
||||
pointsChart2 = []
|
||||
}
|
||||
|
||||
otherChart.series.forEach(function(_series, idx) {
|
||||
if(_series.visible) {
|
||||
try {
|
||||
var _point = _series.points[points[number].x];
|
||||
if(isMouseLeave) {
|
||||
_point.setState('');
|
||||
} else {
|
||||
pointsChart2.push(_point);
|
||||
}
|
||||
number++;
|
||||
} catch {
|
||||
// Graph render issue. Normalizes by itself so no additional measures required.
|
||||
// No effect on the user.
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(isMouseLeave) {
|
||||
otherChart.tooltip.hide();
|
||||
otherChart.xAxis[0].hideCrosshair();
|
||||
} else {
|
||||
otherChart.tooltip.refresh(pointsChart2);
|
||||
otherChart.xAxis[0].drawCrosshair(e, pointsChart2[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a matching chart is found the other will be retrieved through the syncLink so no additional iteration is required.
|
||||
**/
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function setGraphVisibility(chart_name, data_series, series_name) {
|
||||
var chart_key = 'HighCharts_' + chart_name;
|
||||
|
||||
var chart_visibility = data_series.map(function(s) {
|
||||
return {name: s.name, visible: (s.name === series_name) ? !s.visible : s.visible}
|
||||
var _visible = (s.name === series_name) ? !s.visible : s.visible;
|
||||
return {
|
||||
name: s.name, visible: _visible
|
||||
}
|
||||
});
|
||||
|
||||
setLocalStorage(chart_key, JSON.stringify(chart_visibility));
|
||||
|
@ -320,6 +524,12 @@
|
|||
|
||||
function getGraphColors(data_series) {
|
||||
var colors = {
|
||||
'Artists': '#461d1d',
|
||||
'Albums': '#9b4141',
|
||||
'Tracks': '#f06464',
|
||||
'Shows': '#ff3300',
|
||||
'Seasons': '#ff6600',
|
||||
'Episodes': '#ff9933',
|
||||
'TV': '#E5A00D',
|
||||
'Movies': '#FFFFFF',
|
||||
'Music': '#F06464',
|
||||
|
@ -336,6 +546,10 @@
|
|||
return series_colors;
|
||||
}
|
||||
</script>
|
||||
<script src="${http_root}js/graphs/library_additions_by_media_type.js${cache_param}"></script>
|
||||
<script src="${http_root}js/graphs/library_growth_by_day.js${cache_param}"></script>
|
||||
<script src="${http_root}js/graphs/library_additions_by_source_resolution.js${cache_param}"></script>
|
||||
<script src="${http_root}js/graphs/library_additions_by_day.js${cache_param}"></script>
|
||||
<script src="${http_root}js/graphs/plays_by_day.js${cache_param}"></script>
|
||||
<script src="${http_root}js/graphs/plays_by_dayofweek.js${cache_param}"></script>
|
||||
<script src="${http_root}js/graphs/plays_by_hourofday.js${cache_param}"></script>
|
||||
|
@ -368,6 +582,8 @@
|
|||
case '#tabs-3':
|
||||
current_tab = '#tabs-total'
|
||||
break
|
||||
case '#tabs-4':
|
||||
current_tab = '#tabs-library-statistics'
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -436,6 +652,8 @@
|
|||
function loadGraphsTab1(time_range, yaxis) {
|
||||
$('#days-selection').show();
|
||||
$('#months-selection').hide();
|
||||
$('#user-selection').show();
|
||||
$('#yaxis-selection').show();
|
||||
|
||||
setGraphFormat(yaxis);
|
||||
|
||||
|
@ -512,7 +730,7 @@
|
|||
$.ajax({
|
||||
url: "get_plays_by_top_10_users",
|
||||
type: 'get',
|
||||
data: { time_range: time_range, y_axis: yaxis, user_id: selected_user_id },
|
||||
data: { time_range: time_range, y_axis: yaxis, user_id: selected_user_id },
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
if (yaxis === 'duration') { dataSecondsToHours(data); }
|
||||
|
@ -522,13 +740,15 @@
|
|||
var hc_plays_by_user = new Highcharts.Chart(hc_plays_by_user_options);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$('#nav-tabs-plays').tab('show');
|
||||
}
|
||||
|
||||
function loadGraphsTab2(time_range, yaxis) {
|
||||
$('#days-selection').show();
|
||||
$('#months-selection').hide();
|
||||
$('#user-selection').show();
|
||||
$('#yaxis-selection').show();
|
||||
|
||||
setGraphFormat(yaxis);
|
||||
|
||||
|
@ -649,6 +869,8 @@
|
|||
function loadGraphsTab3(time_range, yaxis) {
|
||||
$('#days-selection').hide();
|
||||
$('#months-selection').show();
|
||||
$('#user-selection').show();
|
||||
$('#yaxis-selection').show();
|
||||
|
||||
setGraphFormat(yaxis);
|
||||
|
||||
|
@ -670,6 +892,132 @@
|
|||
$('#nav-tabs-total').tab('show');
|
||||
}
|
||||
|
||||
function loadGraphsTab4(time_range, yaxis) {
|
||||
$('#days-selection').show();
|
||||
$('#months-selection').hide();
|
||||
$('#user-selection').hide();
|
||||
$('#yaxis-selection').hide();
|
||||
|
||||
// Fixed as graph uses own measurement 'addition count' with formatting of 'plays'
|
||||
setGraphFormat("plays", "ddd D MMM YY");
|
||||
|
||||
var _graph_1_call = $.ajax({
|
||||
url: "get_additions_by_date",
|
||||
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_library_additions_by_day_options.xAxis.plotBands.push({
|
||||
from: i-0.5,
|
||||
to: i+0.5,
|
||||
color: 'rgba(80,80,80,0.3)'
|
||||
});
|
||||
}
|
||||
});
|
||||
hc_library_additions_by_day_options.yAxis.min = 0;
|
||||
hc_library_additions_by_day_options.xAxis.categories = dateArray;
|
||||
hc_library_additions_by_day_options.series = getGraphVisibility(hc_library_additions_by_day_options.chart.renderTo, data.series);
|
||||
hc_library_additions_by_day_options.colors = getGraphColors(data.series);
|
||||
const hc_library_additions_by_day = new Highcharts.Chart(hc_library_additions_by_day_options);
|
||||
}
|
||||
});
|
||||
|
||||
var _graph_2_call = $.ajax({
|
||||
url: "get_additions_by_date",
|
||||
type: 'get',
|
||||
data: { time_range: time_range, growth: 1 },
|
||||
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_library_growth_by_day_options.xAxis.plotBands.push({
|
||||
from: i-0.5,
|
||||
to: i+0.5,
|
||||
color: 'rgba(80,80,80,0.3)'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
hc_library_growth_by_day_options.yAxis.min = 0;
|
||||
hc_library_growth_by_day_options.xAxis.categories = dateArray;
|
||||
hc_library_growth_by_day_options.series = getGraphVisibility(hc_library_growth_by_day_options.chart.renderTo, data.series);
|
||||
hc_library_growth_by_day_options.colors = getGraphColors(data.series);
|
||||
const hc_library_growth_by_day = new Highcharts.Chart(hc_library_growth_by_day_options);
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: "get_additions_by_media_type",
|
||||
type: 'get',
|
||||
data: { time_range: time_range },
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
hc_library_additions_by_media_type_options.xAxis.categories = data.categories;
|
||||
hc_library_additions_by_media_type_options.series = getGraphVisibility(hc_library_additions_by_media_type_options.chart.renderTo, data.series);
|
||||
hc_library_additions_by_media_type_options.colors = getGraphColors(data.series);
|
||||
const hc_library_additions_by_media_type = new Highcharts.Chart(hc_library_additions_by_media_type_options);
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: "get_additions_by_resolution",
|
||||
type: 'get',
|
||||
data: { time_range: time_range },
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
hc_library_additions_by_source_resolution_options.xAxis.categories = data.categories;
|
||||
hc_library_additions_by_source_resolution_options.series = getGraphVisibility(hc_library_additions_by_source_resolution_options.chart.renderTo, data.series);
|
||||
hc_library_additions_by_source_resolution_options.colors = getGraphColors(data.series);
|
||||
const hc_library_additions_by_source_resolution = new Highcharts.Chart(hc_library_additions_by_source_resolution_options);
|
||||
}
|
||||
});
|
||||
|
||||
$.when(_graph_1_call, _graph_2_call).then(function(a1, a2) {
|
||||
// Define charts.
|
||||
if(Highcharts.charts.includes(undefined)) {
|
||||
_Charts = Highcharts.charts.filter(chart => chart !== undefined);
|
||||
} else {
|
||||
_Charts = Highcharts.charts;
|
||||
}
|
||||
|
||||
// Both graphs loaded successful. Prepare sync.
|
||||
chart = _Charts.find(c => c['renderTo'].id == "graph_additions_by_day");
|
||||
otherChart = _Charts.find(c => c['renderTo'].id == syncLinks["graph_additions_by_day"]);
|
||||
|
||||
/**
|
||||
* Sync different series to fix possible misconfiguration/data that leads
|
||||
* to a series having different states in the two additions_by_day graphs.
|
||||
**/
|
||||
chart.series.forEach(function(series) {
|
||||
_othSeries = otherChart.series.find(s => s['name'] == series.name);
|
||||
if(!series.visible == _othSeries.visible) {
|
||||
syncGraphs(series, syncLinks["graph_additions_by_day"], series.name);
|
||||
}
|
||||
});
|
||||
|
||||
// Enable sync.
|
||||
_enableChartSync = true;
|
||||
});
|
||||
|
||||
$('#nav-tabs-library-statistics').tab('show')
|
||||
}
|
||||
|
||||
// Set initial state
|
||||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
if (current_tab === '#tabs-library-statistics') { loadGraphsTab4(current_day_range, yaxis); }
|
||||
|
||||
// Tab1 opened
|
||||
$('#nav-tabs-plays').on('shown.bs.tab', function (e) {
|
||||
e.preventDefault();
|
||||
|
@ -694,6 +1042,14 @@
|
|||
loadGraphsTab3(current_month_range, yaxis);
|
||||
});
|
||||
|
||||
// Tab4 opened
|
||||
$('#nav-tabs-library-statistics').on('shown.bs.tab', function (e) {
|
||||
e.preventDefault();
|
||||
current_tab = $(this).attr('href');
|
||||
setLocalStorage('graph_tab', current_tab.replace('#',''));
|
||||
loadGraphsTab4(current_day_range, yaxis);
|
||||
});
|
||||
|
||||
// Date range changed
|
||||
$('#graph-days').tooltip({ container: 'body', placement: 'top', html: true });
|
||||
$('#graph-days').on('change', function() {
|
||||
|
@ -702,6 +1058,7 @@
|
|||
setLocalStorage('graph_days', current_day_range);
|
||||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-library-statistics') { loadGraphsTab4(current_day_range, yaxis); }
|
||||
$('.days').text(current_day_range);
|
||||
});
|
||||
|
||||
|
@ -732,6 +1089,7 @@
|
|||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
//GraphTab4 not needed as no user relevant graph is included here -> may change in the future?
|
||||
});
|
||||
|
||||
// Y-axis changed
|
||||
|
@ -741,14 +1099,15 @@
|
|||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); }
|
||||
if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); }
|
||||
//GraphTab4 not needed as Addition Count is used for the Y-Axis
|
||||
});
|
||||
|
||||
function setGraphFormat(type) {
|
||||
function setGraphFormat(type, tooltipFormat) {
|
||||
if (type === 'plays') {
|
||||
yaxis_format = function() { return this.value; };
|
||||
tooltip_format = function() {
|
||||
if (moment(this.x, 'X').isValid() && (this.x > 946684800)) {
|
||||
var s = '<b>'+ moment(this.x).format('ddd MMM D') +'</b>';
|
||||
var s = tooltipFormat ? '<b>'+ moment(this.x).format(tooltipFormat) +'</b>' : '<b>'+ moment(this.x).format('ddd MMM D') +'</b>';
|
||||
} else {
|
||||
var s = '<b>'+ this.x +'</b>';
|
||||
}
|
||||
|
@ -800,6 +1159,8 @@
|
|||
}
|
||||
|
||||
hc_plays_by_day_options.xAxis.plotBands = [];
|
||||
hc_library_additions_by_day_options.xAxis.plotBands = [];
|
||||
hc_library_growth_by_day_options.xAxis.plotBands = [];
|
||||
hc_plays_by_stream_type_options.xAxis.plotBands = [];
|
||||
hc_concurrent_streams_by_stream_type_options.xAxis.plotBands = [];
|
||||
|
||||
|
@ -812,6 +1173,14 @@
|
|||
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;
|
||||
hc_library_additions_by_day_options.yAxis[0].labels.formatter = yaxis_format;
|
||||
hc_library_additions_by_day_options.yAxis[1].labels.formatter = yaxis_format;
|
||||
hc_library_additions_by_day_options.yAxis[2].labels.formatter = yaxis_format;
|
||||
hc_library_growth_by_day_options.yAxis[0].labels.formatter = yaxis_format;
|
||||
hc_library_growth_by_day_options.yAxis[1].labels.formatter = yaxis_format;
|
||||
hc_library_growth_by_day_options.yAxis[2].labels.formatter = yaxis_format;
|
||||
hc_library_additions_by_media_type_options.yAxis.labels.formatter = yaxis_format;
|
||||
hc_library_additions_by_source_resolution_options.yAxis.labels.formatter = yaxis_format;
|
||||
hc_plays_by_user_by_stream_type_options.yAxis.labels.formatter = yaxis_format;
|
||||
hc_plays_by_month_options.yAxis.labels.formatter = yaxis_format;
|
||||
|
||||
|
@ -819,6 +1188,10 @@
|
|||
hc_plays_by_dayofweek_options.tooltip.formatter = tooltip_format;
|
||||
hc_plays_by_hourofday_options.tooltip.formatter = tooltip_format;
|
||||
hc_plays_by_platform_options.tooltip.formatter = tooltip_format;
|
||||
hc_library_additions_by_media_type_options.tooltip.formatter = tooltip_format;
|
||||
hc_library_additions_by_source_resolution_options.tooltip.formatter = tooltip_format;
|
||||
hc_library_additions_by_day_options.tooltip.formatter = tooltip_format;
|
||||
hc_library_growth_by_day_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_source_resolution_options.tooltip.formatter = tooltip_format;
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
var hc_library_additions_by_day_options = {
|
||||
chart: {
|
||||
type: 'line',
|
||||
backgroundColor: 'rgba(0,0,0,0)',
|
||||
renderTo: 'graph_additions_by_day'
|
||||
},
|
||||
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: {
|
||||
allowPointSelect: false,
|
||||
threshold: 0,
|
||||
events: {
|
||||
legendItemClick: function(event) {
|
||||
syncGraphs(this, this.chart.renderTo.id, this.name, event.browserEvent);
|
||||
setGraphVisibility(this.chart.renderTo.id, this.chart.series, this.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
formatter: function() {
|
||||
return moment(this.value).format("YY MMM D");
|
||||
},
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
},
|
||||
categories: [{}],
|
||||
plotBands: []
|
||||
},
|
||||
yAxis: [{
|
||||
title: {
|
||||
text: null
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
title: {
|
||||
text: 'Episodes'
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
},
|
||||
opposite: true
|
||||
}, {
|
||||
title: {
|
||||
text: 'Tracks'
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
},
|
||||
opposite: true
|
||||
}],
|
||||
tooltip: {
|
||||
shared: true,
|
||||
crosshairs: true
|
||||
},
|
||||
series: [{}]
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
var hc_library_additions_by_media_type_options = {
|
||||
chart: {
|
||||
type: 'column',
|
||||
backgroundColor: 'rgba(0,0,0,0)',
|
||||
renderTo: 'graph_additions_by_media_type'
|
||||
},
|
||||
title: {
|
||||
text: ''
|
||||
},
|
||||
legend: {
|
||||
enabled: true,
|
||||
itemStyle: {
|
||||
font: '9pt "Open Sans", sans-serif',
|
||||
color: '#A0A0A0'
|
||||
},
|
||||
itemHoverStyle: {
|
||||
color: '#FFF'
|
||||
},
|
||||
itemHiddenStyle: {
|
||||
color: '#444'
|
||||
}
|
||||
},
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
xAxis: {
|
||||
categories: [{}],
|
||||
labels: {
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
title: {
|
||||
text: null
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
},
|
||||
stackLabels: {
|
||||
enabled: false,
|
||||
style: {
|
||||
color: '#fff'
|
||||
}
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
column: {
|
||||
borderWidth: 0,
|
||||
stacking: 'normal',
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
style: {
|
||||
color: '#000'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: {
|
||||
events: {
|
||||
legendItemClick: function () {
|
||||
setGraphVisibility(this.chart.renderTo.id, this.chart.series, this.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
shared: true
|
||||
},
|
||||
series: [{}]
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
var hc_library_additions_by_source_resolution_options = {
|
||||
chart: {
|
||||
type: 'column',
|
||||
backgroundColor: 'rgba(0,0,0,0)',
|
||||
renderTo: 'graph_additions_by_source_resolution'
|
||||
},
|
||||
title: {
|
||||
text: ''
|
||||
},
|
||||
legend: {
|
||||
enabled: true,
|
||||
itemStyle: {
|
||||
font: '9pt "Open Sans", sans-serif',
|
||||
color: '#A0A0A0'
|
||||
},
|
||||
itemHoverStyle: {
|
||||
color: '#FFF'
|
||||
},
|
||||
itemHiddenStyle: {
|
||||
color: '#444'
|
||||
}
|
||||
},
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
xAxis: {
|
||||
categories: [{}],
|
||||
labels: {
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
title: {
|
||||
text: null
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
},
|
||||
stackLabels: {
|
||||
enabled: false,
|
||||
style: {
|
||||
color: '#fff'
|
||||
}
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
column: {
|
||||
borderWidth: 0,
|
||||
stacking: 'normal',
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
style: {
|
||||
color: '#000'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: {
|
||||
events: {
|
||||
legendItemClick: function () {
|
||||
setGraphVisibility(this.chart.renderTo.id, this.chart.series, this.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
shared: true
|
||||
},
|
||||
series: [{}]
|
||||
};
|
86
data/interfaces/default/js/graphs/library_growth_by_day.js
Normal file
86
data/interfaces/default/js/graphs/library_growth_by_day.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
var hc_library_growth_by_day_options = {
|
||||
chart: {
|
||||
type: 'line',
|
||||
backgroundColor: 'rgba(0,0,0,0)',
|
||||
renderTo: 'library_growth_by_day'
|
||||
},
|
||||
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: {
|
||||
allowPointSelect: false,
|
||||
threshold: 0,
|
||||
events: {
|
||||
legendItemClick: function(event) {
|
||||
syncGraphs(this, this.chart.renderTo.id, this.name, event.browserEvent);
|
||||
setGraphVisibility(this.chart.renderTo.id, this.chart.series, this.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
formatter: function() {
|
||||
return moment(this.value).format("YY MMM D");
|
||||
},
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
},
|
||||
categories: [{}],
|
||||
plotBands: []
|
||||
},
|
||||
yAxis: [{
|
||||
title: {
|
||||
text: null
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
title: {
|
||||
text: 'Episodes'
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
},
|
||||
opposite: true
|
||||
}, {
|
||||
title: {
|
||||
text: 'Tracks'
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
color: '#aaa'
|
||||
}
|
||||
},
|
||||
opposite: true
|
||||
}],
|
||||
tooltip: {
|
||||
shared: true,
|
||||
crosshairs: true
|
||||
},
|
||||
series: [{}]
|
||||
};
|
|
@ -976,6 +976,25 @@
|
|||
<p class="help-block">Refresh the libraries list when Tautulli starts.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group advanced-setting">
|
||||
<label for="refresh_library_stats_data_interval">Library Statistics Data Refresh Interval</label>
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" data-parsley-type="integer" id="refresh_library_stats_data_interval" name="refresh_library_stats_data_interval" value="${config['refresh_library_stats_data_interval']}" size="5" data-parsley-range="[6,24]" data-parsley-trigger="change" data-parsley-errors-container="#refresh_library_stats_data_interval_error" required>
|
||||
</div>
|
||||
<div id="refresh_library_stats_data_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||
</div>
|
||||
<p class="help-block">The interval (in hours) Tautulli will request an update of all media items from your Plex Media Server.</p>
|
||||
<p class="help-block">Minimum 6, maximum 24, default 12.</p>
|
||||
<p class="help-block">This process, depending on your library sizes, can take multiple minutes up to half an hour.</p>
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
<label>
|
||||
<input type="checkbox" id="refresh_library_stats_data_on_startup" name="refresh_library_stats_data_on_startup" value="1" ${config['refresh_library_stats_data_on_startup']}> Refresh Library Statistics Data on Startup
|
||||
</label>
|
||||
<p class="help-block">Refresh the library statistics data when Tautulli starts.</p>
|
||||
</div>
|
||||
|
||||
<div class="padded-header">
|
||||
<h3>Plex.tv Authentication</h3>
|
||||
</div>
|
||||
|
|
|
@ -485,11 +485,15 @@ def initialize_scheduler():
|
|||
# Refresh the users list and libraries list
|
||||
user_hours = CONFIG.REFRESH_USERS_INTERVAL if 1 <= CONFIG.REFRESH_USERS_INTERVAL <= 24 else 12
|
||||
library_hours = CONFIG.REFRESH_LIBRARIES_INTERVAL if 1 <= CONFIG.REFRESH_LIBRARIES_INTERVAL <= 24 else 12
|
||||
library_stats_data_hours = CONFIG.REFRESH_LIBRARY_STATS_DATA_INTERVAL if 6 <= CONFIG.REFRESH_LIBRARY_STATS_DATA_INTERVAL <= 24 else 12
|
||||
|
||||
schedule_job(users.refresh_users, 'Refresh users list',
|
||||
hours=user_hours, minutes=0, seconds=0)
|
||||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||
hours=library_hours, minutes=0, seconds=0)
|
||||
|
||||
schedule_job(libraries.refresh_library_statistics, 'Refresh libraries statistics data',
|
||||
hours=library_stats_data_hours, minutes=0, seconds=0)
|
||||
|
||||
schedule_job(activity_pinger.connect_server, 'Check for server response',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
@ -509,6 +513,9 @@ def initialize_scheduler():
|
|||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
schedule_job(libraries.refresh_library_statistics, 'Refresh libraries statistics data',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
# Schedule job to reconnect server
|
||||
schedule_job(activity_pinger.connect_server, 'Check for server response',
|
||||
hours=0, minutes=0, seconds=30, args=(False,))
|
||||
|
@ -611,6 +618,9 @@ def startup_refresh():
|
|||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARIES_ON_STARTUP:
|
||||
libraries.refresh_libraries()
|
||||
|
||||
# Refresh the library stats data on startup
|
||||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARY_STATS_DATA_ON_STARTUP:
|
||||
libraries.refresh_library_statistics()
|
||||
|
||||
def sig_handler(signum=None, frame=None):
|
||||
if signum is not None:
|
||||
|
@ -821,6 +831,14 @@ def dbcheck():
|
|||
"media_info TEXT)"
|
||||
)
|
||||
|
||||
# library_stats_items table :: This table keeps record of all added items
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS library_stats_items (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
'added_at INTEGER, updated_at INTEGER, last_viewed_at INTEGER, pms_identifier TEXT, section_id INTEGER, '
|
||||
'library_name TEXT, rating_key INTEGER, parent_rating_key INTEGER, grandparent_rating_key INTEGER, '
|
||||
'media_type TEXT, media_info TEXT, user_ratings TEXT)'
|
||||
)
|
||||
|
||||
# mobile_devices table :: This table keeps record of devices linked with the mobile app
|
||||
c_db.execute(
|
||||
"CREATE TABLE IF NOT EXISTS mobile_devices (id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
|
@ -2716,6 +2734,20 @@ def dbcheck():
|
|||
"ON session_history_media_info (transcode_decision)"
|
||||
)
|
||||
|
||||
# Create library_stats_items table indices
|
||||
c_db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS "idx_library_stats_items_media_type" '
|
||||
'ON "library_stats_items" ("media_type")'
|
||||
)
|
||||
c_db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS "idx_library_stats_items_added_at" '
|
||||
'ON "library_stats_items" ("added_at")'
|
||||
)
|
||||
c_db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS "idx_library_stats_items_rating_key" '
|
||||
'ON "library_stats_items" ("rating_key")'
|
||||
)
|
||||
|
||||
# Create lookup table indices
|
||||
c_db.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_tvmaze_lookup "
|
||||
|
|
|
@ -188,6 +188,8 @@ _CONFIG_DEFINITIONS = {
|
|||
'PLEXPY_AUTO_UPDATE': (int, 'General', 0),
|
||||
'REFRESH_LIBRARIES_INTERVAL': (int, 'Monitoring', 12),
|
||||
'REFRESH_LIBRARIES_ON_STARTUP': (int, 'Monitoring', 1),
|
||||
'REFRESH_LIBRARY_STATS_DATA_INTERVAL': (int, 'Monitoring', 12),
|
||||
'REFRESH_LIBRARY_STATS_DATA_ON_STARTUP': (int, 'Monitoring', 1),
|
||||
'REFRESH_USERS_INTERVAL': (int, 'Monitoring', 12),
|
||||
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
|
||||
'SESSION_DB_WRITE_ATTEMPTS': (int, 'Advanced', 5),
|
||||
|
@ -299,6 +301,7 @@ SETTINGS = [
|
|||
'PMS_VERSION',
|
||||
'PMS_WEB_URL',
|
||||
'REFRESH_LIBRARIES_INTERVAL',
|
||||
'REFRESH_LIBRARY_STATS_DATA_INTERVAL',
|
||||
'REFRESH_USERS_INTERVAL',
|
||||
'SHOW_ADVANCED_SETTINGS',
|
||||
'TIME_FORMAT',
|
||||
|
@ -338,6 +341,7 @@ CHECKED_SETTINGS = [
|
|||
'PLEXPY_AUTO_UPDATE',
|
||||
'PMS_URL_MANUAL',
|
||||
'REFRESH_LIBRARIES_ON_STARTUP',
|
||||
'REFRESH_LIBRARY_STATS_DATA_ON_STARTUP',
|
||||
'REFRESH_USERS_ON_STARTUP',
|
||||
'SYS_TRAY_ICON',
|
||||
'THEMOVIEDB_LOOKUP',
|
||||
|
@ -709,3 +713,8 @@ class Config(object):
|
|||
self.ANON_REDIRECT_DYNAMIC = 1
|
||||
|
||||
self.CONFIG_VERSION = 22
|
||||
if self.CONFIG_VERSION == 22:
|
||||
self.REFRESH_LIBRARY_STATS_DATA_INTERVAL = 12
|
||||
self.REFRESH_LIBRARY_STATS_DATA_ON_STARTUP = 1
|
||||
|
||||
self.CONFIG_VERSION = 23
|
||||
|
|
|
@ -2473,3 +2473,52 @@ class DataFactory(object):
|
|||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_library_stats_item(self, rating_key=''):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
if rating_key:
|
||||
try:
|
||||
query = 'SELECT * FROM library_stats_items WHERE rating_key = ?'
|
||||
result = monitor_db.select(query=query, args=[rating_key])
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_library_stats_item: %s." % e)
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
|
||||
return result
|
||||
|
||||
def set_library_stats_item(self, rating_key='', created_at=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
metadata = pms_connect.get_metadata_details(rating_key=rating_key, skip_cache=True, media_info=True)
|
||||
|
||||
keys = {'rating_key': metadata['rating_key']}
|
||||
|
||||
_addedAt = metadata['added_at']
|
||||
# Catch media items which have a timestamp when their corresponding library did not existed
|
||||
added_at = _addedAt if _addedAt > created_at else created_at
|
||||
|
||||
values = {'added_at': added_at,
|
||||
'updated_at': metadata['updated_at'],
|
||||
'last_viewed_at': metadata['last_viewed_at'],
|
||||
'section_id': metadata['section_id'],
|
||||
'library_name': metadata['library_name'],
|
||||
'parent_rating_key': metadata['parent_rating_key'],
|
||||
'grandparent_rating_key': metadata['grandparent_rating_key'],
|
||||
'media_type': metadata['media_type'],
|
||||
'media_info': json.dumps(metadata['media_info']),
|
||||
# TODO json array with ratings from all users
|
||||
'user_ratings': ''
|
||||
}
|
||||
|
||||
try:
|
||||
monitor_db.upsert(table_name='library_stats_items', key_dict=keys, value_dict=values)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for set_library_stats_item: %s." % e)
|
||||
return False
|
||||
|
||||
return True
|
327
plexpy/graphs.py
327
plexpy/graphs.py
|
@ -387,6 +387,333 @@ class Graphs(object):
|
|||
'series': series_output}
|
||||
return output
|
||||
|
||||
def get_total_additions_per_day(self, time_range='30', growth=False):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
time_range = helpers.cast_to_int(time_range) or 30
|
||||
timestamp = helpers.timestamp() - time_range * 24 * 60 * 60
|
||||
|
||||
join_statement = ' AS lsi JOIN library_sections AS ls ON ' \
|
||||
'lsi.section_id = ls.section_id AND lsi.library_name = ls.section_name ' \
|
||||
'AND ls.is_active = 1 AND ls.deleted_section = 0 '
|
||||
|
||||
try:
|
||||
if growth:
|
||||
query = 'SELECT ' \
|
||||
'0 AS date_added, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "show" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "season" THEN 1 ELSE 0 END) AS season_count, ' \
|
||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS episode_count, ' \
|
||||
'SUM(CASE WHEN media_type = "artist" THEN 1 ELSE 0 END) AS artist_count, ' \
|
||||
'SUM(CASE WHEN media_type = "album" THEN 1 ELSE 0 END) AS album_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS track_count ' \
|
||||
'FROM library_stats_items %s' \
|
||||
'WHERE added_at < %s ' \
|
||||
'UNION ALL ' \
|
||||
'SELECT ' \
|
||||
'date(added_at, "unixepoch", "localtime") AS date_added, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "movie" THEN rating_key ELSE NULL END) AS movie_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "show" THEN grandparent_rating_key ELSE NULL END) AS tv_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "season" THEN parent_rating_key ELSE NULL END) AS season_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "episode" THEN rating_key ELSE NULL END) AS episode_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "artist" THEN grandparent_rating_key ELSE NULL END) AS artist_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "album" THEN parent_rating_key ELSE NULL END) AS album_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "track" THEN rating_key ELSE NULL END) AS track_count ' \
|
||||
'FROM library_stats_items %s' \
|
||||
'WHERE added_at >= %s ' \
|
||||
'GROUP BY date_added ' \
|
||||
'ORDER BY date_added' % (join_statement, timestamp,
|
||||
join_statement, timestamp)
|
||||
|
||||
result = monitor_db.select(query)
|
||||
else:
|
||||
query = 'SELECT ' \
|
||||
'date(added_at, "unixepoch", "localtime") AS date_added, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "movie" THEN rating_key ELSE NULL END) AS movie_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "show" THEN grandparent_rating_key ELSE NULL END) AS tv_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "season" THEN parent_rating_key ELSE NULL END) AS season_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "episode" THEN rating_key ELSE NULL END) AS episode_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "artist" THEN grandparent_rating_key ELSE NULL END) AS artist_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "album" THEN parent_rating_key ELSE NULL END) AS album_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "track" THEN rating_key ELSE NULL END) AS track_count ' \
|
||||
'FROM library_stats_items %s' \
|
||||
'WHERE added_at >= %s ' \
|
||||
'GROUP BY date_added ' \
|
||||
'ORDER BY date_added' % (join_statement, timestamp)
|
||||
|
||||
result = monitor_db.select(query)
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_additions_per_day: %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 = []
|
||||
series_5 = []
|
||||
series_6 = []
|
||||
series_7 = []
|
||||
|
||||
if growth:
|
||||
base_value_1 = result[0]['movie_count'] if result[0]['movie_count'] else 0
|
||||
base_value_2 = result[0]['tv_count'] if result[0]['tv_count'] else 0
|
||||
base_value_3 = result[0]['season_count'] if result[0]['season_count'] else 0
|
||||
base_value_4 = result[0]['episode_count'] if result[0]['episode_count'] else 0
|
||||
base_value_5 = result[0]['artist_count'] if result[0]['artist_count'] else 0
|
||||
base_value_6 = result[0]['album_count'] if result[0]['album_count'] else 0
|
||||
base_value_7 = result[0]['track_count'] if result[0]['track_count'] else 0
|
||||
|
||||
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
|
||||
series_5_value = 0
|
||||
series_6_value = 0
|
||||
series_7_value = 0
|
||||
|
||||
for item in result:
|
||||
if date_string == item['date_added']:
|
||||
series_1_value = item['movie_count'] if item['movie_count'] else 0
|
||||
series_2_value = item['tv_count'] if item['tv_count'] else 0
|
||||
series_3_value = item['season_count'] if item['season_count'] else 0
|
||||
series_4_value = item['episode_count'] if item['episode_count'] else 0
|
||||
series_5_value = item['artist_count'] if item['artist_count'] else 0
|
||||
series_6_value = item['album_count'] if item['album_count'] else 0
|
||||
series_7_value = item['track_count'] if item['track_count'] else 0
|
||||
continue
|
||||
|
||||
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_5.append(series_5_value)
|
||||
series_6.append(series_6_value)
|
||||
series_7.append(series_7_value)
|
||||
|
||||
if growth:
|
||||
for idx, day in enumerate(series_1):
|
||||
series_1[idx] = base_value_1 + day
|
||||
base_value_1 += day
|
||||
for idx, day in enumerate(series_2):
|
||||
series_2[idx] = base_value_2 + day
|
||||
base_value_2 += day
|
||||
for idx, day in enumerate(series_3):
|
||||
series_3[idx] = base_value_3 + day
|
||||
base_value_3 += day
|
||||
for idx, day in enumerate(series_4):
|
||||
series_4[idx] = base_value_4 + day
|
||||
base_value_4 += day
|
||||
for idx, day in enumerate(series_5):
|
||||
series_5[idx] = base_value_5 + day
|
||||
base_value_5 += day
|
||||
for idx, day in enumerate(series_6):
|
||||
series_6[idx] = base_value_6 + day
|
||||
base_value_6 += day
|
||||
for idx, day in enumerate(series_7):
|
||||
series_7[idx] = base_value_7 + day
|
||||
base_value_7 += day
|
||||
|
||||
series_1_output = {'name': 'Movies',
|
||||
'data': series_1}
|
||||
series_2_output = {'name': 'Shows',
|
||||
'data': series_2}
|
||||
series_3_output = {'name': 'Seasons',
|
||||
'data': series_3}
|
||||
series_4_output = {'name': 'Episodes',
|
||||
'data': series_4}
|
||||
series_5_output = {'name': 'Artists',
|
||||
'data': series_5}
|
||||
series_6_output = {'name': 'Albums',
|
||||
'data': series_6}
|
||||
series_7_output = {'name': 'Tracks',
|
||||
'data': series_7}
|
||||
|
||||
series_output = []
|
||||
if libraries.has_library_type('movie'):
|
||||
series_output.append(series_1_output)
|
||||
if libraries.has_library_type('show'):
|
||||
series_output.append(series_2_output)
|
||||
series_output.append(series_3_output)
|
||||
series_output.append(series_4_output)
|
||||
if libraries.has_library_type('artist'):
|
||||
series_output.append(series_5_output)
|
||||
series_output.append(series_6_output)
|
||||
series_output.append(series_7_output)
|
||||
|
||||
output = {'categories': categories,
|
||||
'series': series_output}
|
||||
|
||||
return output
|
||||
|
||||
def get_total_additions_by_media_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
|
||||
|
||||
try:
|
||||
query = 'SELECT ' \
|
||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "show" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "season" THEN 1 ELSE 0 END) AS season_count, ' \
|
||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS episode_count, ' \
|
||||
'SUM(CASE WHEN media_type = "artist" THEN 1 ELSE 0 END) AS artist_count, ' \
|
||||
'SUM(CASE WHEN media_type = "album" THEN 1 ELSE 0 END) AS album_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS track_count ' \
|
||||
'FROM library_stats_items AS lsi JOIN library_sections AS ls ON ' \
|
||||
'lsi.section_id = ls.section_id AND lsi.library_name = ls.section_name ' \
|
||||
'AND ls.is_active = 1 AND ls.deleted_section = 0 ' \
|
||||
'WHERE added_at >= %s' % timestamp
|
||||
|
||||
result = monitor_db.select(query)
|
||||
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_additions_by_media_type: %s." % e)
|
||||
return None
|
||||
|
||||
categories = ["Movies", "TV", "Music"]
|
||||
_catCount = len(categories)
|
||||
|
||||
series_1 = [None] * _catCount
|
||||
series_2 = [None] * _catCount
|
||||
series_3 = [None] * _catCount
|
||||
series_4 = [None] * _catCount
|
||||
series_5 = [None] * _catCount
|
||||
series_6 = [None] * _catCount
|
||||
series_7 = [None] * _catCount
|
||||
|
||||
content = result[0]
|
||||
|
||||
for idx, item in enumerate(categories):
|
||||
if idx == 0:
|
||||
series_1[idx] = content['movie_count']
|
||||
elif idx == 1:
|
||||
series_2[idx] = content['tv_count']
|
||||
series_3[idx] = content['season_count']
|
||||
series_4[idx] = content['episode_count']
|
||||
else:
|
||||
series_5[idx] = content['artist_count']
|
||||
series_6[idx] = content['album_count']
|
||||
series_7[idx] = content['track_count']
|
||||
|
||||
series_1_output = {'name': 'Movies',
|
||||
'data': series_1}
|
||||
series_2_output = {'name': 'Shows',
|
||||
'data': series_2}
|
||||
series_3_output = {'name': 'Seasons',
|
||||
'data': series_3}
|
||||
series_4_output = {'name': 'Episodes',
|
||||
'data': series_4}
|
||||
series_5_output = {'name': 'Artists',
|
||||
'data': series_5}
|
||||
series_6_output = {'name': 'Albums',
|
||||
'data': series_6}
|
||||
series_7_output = {'name': 'Tracks',
|
||||
'data': series_7}
|
||||
|
||||
series_output = []
|
||||
if libraries.has_library_type('movie'):
|
||||
series_output.append(series_1_output)
|
||||
if libraries.has_library_type('show'):
|
||||
series_output.append(series_2_output)
|
||||
series_output.append(series_3_output)
|
||||
series_output.append(series_4_output)
|
||||
if libraries.has_library_type('artist'):
|
||||
series_output.append(series_5_output)
|
||||
series_output.append(series_6_output)
|
||||
series_output.append(series_7_output)
|
||||
|
||||
output = {'categories': categories,
|
||||
'series': series_output}
|
||||
|
||||
return output
|
||||
|
||||
def get_total_additions_by_resolution(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
|
||||
|
||||
resolution = '(CASE WHEN media_info LIKE \'%"video_resolution": "4K"%\' THEN "1_4K" ' \
|
||||
'WHEN media_info LIKE \'%"video_resolution": "1080"%\' THEN "2_1080" ' \
|
||||
'WHEN media_info LIKE \'%"video_resolution": "720"%\' THEN "3_720" ' \
|
||||
'WHEN media_info LIKE \'%"video_resolution": "576"%\' THEN "4_576" ' \
|
||||
'WHEN media_info LIKE \'%"video_resolution": "480"%\' THEN "5_480" ' \
|
||||
'WHEN media_info LIKE \'%"video_resolution": "sd"%\' THEN "6_SD" ELSE "7_Unknown" END) AS resolution '
|
||||
|
||||
join_statement = ' AS lsi JOIN library_sections AS ls ON ' \
|
||||
'lsi.section_id = ls.section_id AND lsi.library_name = ls.section_name ' \
|
||||
'AND ls.is_active = 1 AND ls.deleted_section = 0 '
|
||||
|
||||
try:
|
||||
#Change queries for show and episode so they also get a resolution like before? -> Cool for the user,
|
||||
# but it doesn't make real sense as show/seasons itself have no resolution
|
||||
query = 'SELECT ' \
|
||||
'%s, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "movie" THEN rating_key ELSE NULL END) AS movie_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "show" THEN grandparent_rating_key ELSE NULL END) AS tv_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "season" THEN parent_rating_key ELSE NULL END) AS season_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "episode" THEN rating_key ELSE NULL END) AS episode_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "artist" THEN grandparent_rating_key ELSE NULL END) AS artist_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "album" THEN parent_rating_key ELSE NULL END) AS album_count, ' \
|
||||
'COUNT(DISTINCT CASE WHEN media_type = "track" THEN rating_key ELSE NULL END) AS track_count ' \
|
||||
'FROM library_stats_items %s' \
|
||||
'WHERE added_at >= %s AND (media_type = "movie" OR media_type = "episode") ' \
|
||||
'GROUP BY resolution ' \
|
||||
'ORDER BY resolution' % (resolution, join_statement, timestamp)
|
||||
|
||||
result = monitor_db.select(query)
|
||||
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_additions_by_resolution: %s." % e)
|
||||
return None
|
||||
|
||||
categories = []
|
||||
series_1 = []
|
||||
series_2 = []
|
||||
series_3 = []
|
||||
series_4 = []
|
||||
|
||||
for idx, item in enumerate(result):
|
||||
#remove sorting indicators (like 1_%)
|
||||
categories.append(item['resolution'][2:])
|
||||
|
||||
series_1.append(item['movie_count'])
|
||||
series_2.append(item['tv_count'])
|
||||
series_3.append(item['season_count'])
|
||||
series_4.append(item['episode_count'])
|
||||
|
||||
series_1_output = {'name': 'Movies',
|
||||
'data': series_1}
|
||||
series_2_output = {'name': 'Shows',
|
||||
'data': series_2}
|
||||
series_3_output = {'name': 'Seasons',
|
||||
'data': series_3}
|
||||
series_4_output = {'name': 'Episodes',
|
||||
'data': series_4}
|
||||
|
||||
series_output = []
|
||||
if libraries.has_library_type('movie'):
|
||||
series_output.append(series_1_output)
|
||||
if libraries.has_library_type('show'):
|
||||
series_output.append(series_2_output)
|
||||
series_output.append(series_3_output)
|
||||
series_output.append(series_4_output)
|
||||
|
||||
output = {'categories': categories,
|
||||
'series': series_output}
|
||||
return output
|
||||
|
||||
def get_total_plays_per_month(self, time_range='12', y_axis='plays', user_id=None, grouping=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ if plexpy.PYTHON2:
|
|||
import pmsconnect
|
||||
import session
|
||||
import users
|
||||
import datafactory
|
||||
from plex import Plex
|
||||
else:
|
||||
from plexpy import common
|
||||
|
@ -47,6 +48,7 @@ else:
|
|||
from plexpy import session
|
||||
from plexpy import users
|
||||
from plexpy.plex import Plex
|
||||
from plexpy import datafactory
|
||||
|
||||
|
||||
def refresh_libraries():
|
||||
|
@ -109,6 +111,59 @@ def refresh_libraries():
|
|||
logger.warn("Tautulli Libraries :: Unable to refresh libraries list.")
|
||||
return False
|
||||
|
||||
def refresh_library_statistics():
|
||||
logger.info("Tautulli Library Statistics :: Requesting library statistics data refresh...")
|
||||
|
||||
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
||||
if not server_id:
|
||||
logger.error("Tautulli Library Statistics :: No PMS identifier, cannot refresh data. Verify server in settings.")
|
||||
return
|
||||
|
||||
library_sections = pmsconnect.PmsConnect().get_library_details()
|
||||
|
||||
if library_sections:
|
||||
ratingKeys = {}
|
||||
|
||||
_pms = pmsconnect.PmsConnect()
|
||||
_datafactory = datafactory.DataFactory()
|
||||
|
||||
for section in library_sections:
|
||||
if section['created_at'] and section['is_active']:
|
||||
section_type = section['section_type']
|
||||
|
||||
# Push Data to library_sections table
|
||||
# Placed here as statistics should represent current library status (be in sync)
|
||||
# initial run: 16min for 16000 item (movies + shows + seasons + episodes + track + album + artist)
|
||||
# update run: 8min -,-
|
||||
_resultSet = []
|
||||
_resultSet.append(_pms.get_library_children_details(section_id=section['section_id'], section_type=section['section_type'], get_media_info=False))
|
||||
|
||||
# Add additional library contents for easier filtering at graph queries
|
||||
if section_type == 'show':
|
||||
_resultSet.append(_pms.get_library_children_details(section_id=section['section_id'], section_type='season', get_media_info=False))
|
||||
_resultSet.append(_pms.get_library_children_details(section_id=section['section_id'], section_type='episode', get_media_info=False))
|
||||
|
||||
if section_type == 'artist':
|
||||
_resultSet.append(_pms.get_library_children_details(section_id=section['section_id'], section_type='album', get_media_info=False))
|
||||
_resultSet.append(_pms.get_library_children_details(section_id=section['section_id'], section_type='track', get_media_info=False))
|
||||
|
||||
for result in _resultSet:
|
||||
for item in result['children_list']:
|
||||
if item['rating_key'] not in ratingKeys:
|
||||
ratingKeys[item['rating_key']] = section['created_at']
|
||||
elif not section['created_at']:
|
||||
logger.warn("Tautulli Library Statistics :: Library " + library['section_name'] + " skipped, because of no created_at timestamp!")
|
||||
|
||||
ratingKeys = sorted(ratingKeys.items())
|
||||
|
||||
for key, createdAt in ratingKeys:
|
||||
_datafactory.set_library_stats_item(rating_key=key, created_at=createdAt)
|
||||
|
||||
logger.info("Tautulli Library Statistics :: Data refreshed.")
|
||||
return True
|
||||
else:
|
||||
logger.warn("Tautulli Library Statistics :: Unable to refresh data.")
|
||||
return False
|
||||
|
||||
def add_live_tv_library(refresh=False):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
|
|
@ -2766,6 +2766,7 @@ class PmsConnect(object):
|
|||
libraries_output = {'section_id': helpers.get_xml_attr(result, 'key'),
|
||||
'section_type': helpers.get_xml_attr(result, 'type'),
|
||||
'section_name': helpers.get_xml_attr(result, 'title'),
|
||||
'created_at': helpers.get_xml_attr(result, 'createdAt'),
|
||||
'agent': helpers.get_xml_attr(result, 'agent'),
|
||||
'thumb': helpers.get_xml_attr(result, 'thumb'),
|
||||
'art': helpers.get_xml_attr(result, 'art')
|
||||
|
@ -2944,6 +2945,7 @@ class PmsConnect(object):
|
|||
'agent': library['agent'],
|
||||
'thumb': library['thumb'],
|
||||
'art': library['art'],
|
||||
'created_at': library['created_at'],
|
||||
'count': children_list['library_count'],
|
||||
'is_active': 1
|
||||
}
|
||||
|
|
|
@ -2505,6 +2505,48 @@ class WebInterface(object):
|
|||
else:
|
||||
logger.warn("Unable to retrieve data for get_plays_by_top_10_users.")
|
||||
return result
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
#called additions instead of adds so it isn't blocked by adblockers...
|
||||
def get_additions_by_media_type(self, time_range='30'):
|
||||
graph = graphs.Graphs()
|
||||
result = graph.get_total_additions_by_media_type(time_range=time_range)
|
||||
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
logger.warn("Unable to retrieve data for get_additions_by_media_type.")
|
||||
return result
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
#called additions instead of adds so it isn't blocked by adblockers...
|
||||
def get_additions_by_resolution(self, time_range='30'):
|
||||
graph = graphs.Graphs()
|
||||
result = graph.get_total_additions_by_resolution(time_range=time_range)
|
||||
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
logger.warn("Unable to retrieve data for get_additions_by_resolution.")
|
||||
return result
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
#called additions instead of adds so it isn't blocked by adblockers...
|
||||
def get_additions_by_date(self, time_range='30', growth=False):
|
||||
graph = graphs.Graphs()
|
||||
result = graph.get_total_additions_per_day(time_range=time_range, growth=growth)
|
||||
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
logger.warn("Unable to retrieve data for get_additions_by_date.")
|
||||
return result
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
|
@ -3248,14 +3290,28 @@ class WebInterface(object):
|
|||
first_run = True
|
||||
server_changed = True
|
||||
|
||||
if not first_run:
|
||||
for checked_config in config.CHECKED_SETTINGS:
|
||||
checked_config = checked_config.lower()
|
||||
if checked_config not in kwargs:
|
||||
# checked items should be zero or one. if they were not sent then the item was not checked
|
||||
kwargs[checked_config] = 0
|
||||
else:
|
||||
kwargs[checked_config] = 1
|
||||
checked_configs = [
|
||||
"launch_browser", "launch_startup", "enable_https", "https_create_cert",
|
||||
"api_enabled", "freeze_db", "check_github",
|
||||
"group_history_tables",
|
||||
"pms_url_manual", "week_start_monday",
|
||||
"refresh_libraries_on_startup", "refresh_users_on_startup",
|
||||
"notify_consecutive", "notify_recently_added_upgrade",
|
||||
"notify_group_recently_added_grandparent", "notify_group_recently_added_parent",
|
||||
"notify_new_device_initial_only",
|
||||
"notify_server_update_repeat", "notify_plexpy_update_repeat",
|
||||
"monitor_pms_updates", "get_file_sizes", "log_blacklist",
|
||||
"allow_guest_access", "cache_images", "http_proxy", "notify_concurrent_by_ip",
|
||||
"history_table_activity", "plexpy_auto_update",
|
||||
"themoviedb_lookup", "tvmaze_lookup", "musicbrainz_lookup", "http_plex_admin",
|
||||
"newsletter_self_hosted", "newsletter_inline_styles", "sys_tray_icon"
|
||||
]
|
||||
for checked_config in checked_configs:
|
||||
if checked_config not in kwargs:
|
||||
# checked items should be zero or one. if they were not sent then the item was not checked
|
||||
kwargs[checked_config] = 0
|
||||
else:
|
||||
kwargs[checked_config] = 1
|
||||
|
||||
# If http password exists in config, do not overwrite when blank value received
|
||||
if kwargs.get('http_password') == ' ':
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue