mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-19 12:59:42 -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-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-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-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>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane" id="tabs-plays">
|
<div role="tabpanel" class="tab-pane" id="tabs-plays">
|
||||||
|
@ -225,6 +226,68 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -246,6 +309,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var selected_user_id = null;
|
var selected_user_id = null;
|
||||||
|
var _Charts = [];
|
||||||
|
var _enableChartSync = false;
|
||||||
|
|
||||||
// Modal popup dialog
|
// Modal popup dialog
|
||||||
function selectHandler(selectedDate, selectedSeries) {
|
function selectHandler(selectedDate, selectedSeries) {
|
||||||
|
@ -304,15 +369,154 @@
|
||||||
return data_series.map(function(s) {
|
return data_series.map(function(s) {
|
||||||
var obj = Object.assign({}, s);
|
var obj = Object.assign({}, s);
|
||||||
obj.visible = (chart_visibility[s.name] !== false);
|
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
|
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) {
|
function setGraphVisibility(chart_name, data_series, series_name) {
|
||||||
var chart_key = 'HighCharts_' + chart_name;
|
var chart_key = 'HighCharts_' + chart_name;
|
||||||
|
|
||||||
var chart_visibility = data_series.map(function(s) {
|
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));
|
setLocalStorage(chart_key, JSON.stringify(chart_visibility));
|
||||||
|
@ -320,6 +524,12 @@
|
||||||
|
|
||||||
function getGraphColors(data_series) {
|
function getGraphColors(data_series) {
|
||||||
var colors = {
|
var colors = {
|
||||||
|
'Artists': '#461d1d',
|
||||||
|
'Albums': '#9b4141',
|
||||||
|
'Tracks': '#f06464',
|
||||||
|
'Shows': '#ff3300',
|
||||||
|
'Seasons': '#ff6600',
|
||||||
|
'Episodes': '#ff9933',
|
||||||
'TV': '#E5A00D',
|
'TV': '#E5A00D',
|
||||||
'Movies': '#FFFFFF',
|
'Movies': '#FFFFFF',
|
||||||
'Music': '#F06464',
|
'Music': '#F06464',
|
||||||
|
@ -336,6 +546,10 @@
|
||||||
return series_colors;
|
return series_colors;
|
||||||
}
|
}
|
||||||
</script>
|
</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_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_dayofweek.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/graphs/plays_by_hourofday.js${cache_param}"></script>
|
<script src="${http_root}js/graphs/plays_by_hourofday.js${cache_param}"></script>
|
||||||
|
@ -368,6 +582,8 @@
|
||||||
case '#tabs-3':
|
case '#tabs-3':
|
||||||
current_tab = '#tabs-total'
|
current_tab = '#tabs-total'
|
||||||
break
|
break
|
||||||
|
case '#tabs-4':
|
||||||
|
current_tab = '#tabs-library-statistics'
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -436,6 +652,8 @@
|
||||||
function loadGraphsTab1(time_range, yaxis) {
|
function loadGraphsTab1(time_range, yaxis) {
|
||||||
$('#days-selection').show();
|
$('#days-selection').show();
|
||||||
$('#months-selection').hide();
|
$('#months-selection').hide();
|
||||||
|
$('#user-selection').show();
|
||||||
|
$('#yaxis-selection').show();
|
||||||
|
|
||||||
setGraphFormat(yaxis);
|
setGraphFormat(yaxis);
|
||||||
|
|
||||||
|
@ -512,7 +730,7 @@
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "get_plays_by_top_10_users",
|
url: "get_plays_by_top_10_users",
|
||||||
type: 'get',
|
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",
|
dataType: "json",
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
if (yaxis === 'duration') { dataSecondsToHours(data); }
|
if (yaxis === 'duration') { dataSecondsToHours(data); }
|
||||||
|
@ -522,13 +740,15 @@
|
||||||
var hc_plays_by_user = new Highcharts.Chart(hc_plays_by_user_options);
|
var hc_plays_by_user = new Highcharts.Chart(hc_plays_by_user_options);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#nav-tabs-plays').tab('show');
|
$('#nav-tabs-plays').tab('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadGraphsTab2(time_range, yaxis) {
|
function loadGraphsTab2(time_range, yaxis) {
|
||||||
$('#days-selection').show();
|
$('#days-selection').show();
|
||||||
$('#months-selection').hide();
|
$('#months-selection').hide();
|
||||||
|
$('#user-selection').show();
|
||||||
|
$('#yaxis-selection').show();
|
||||||
|
|
||||||
setGraphFormat(yaxis);
|
setGraphFormat(yaxis);
|
||||||
|
|
||||||
|
@ -649,6 +869,8 @@
|
||||||
function loadGraphsTab3(time_range, yaxis) {
|
function loadGraphsTab3(time_range, yaxis) {
|
||||||
$('#days-selection').hide();
|
$('#days-selection').hide();
|
||||||
$('#months-selection').show();
|
$('#months-selection').show();
|
||||||
|
$('#user-selection').show();
|
||||||
|
$('#yaxis-selection').show();
|
||||||
|
|
||||||
setGraphFormat(yaxis);
|
setGraphFormat(yaxis);
|
||||||
|
|
||||||
|
@ -670,6 +892,132 @@
|
||||||
$('#nav-tabs-total').tab('show');
|
$('#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
|
// Tab1 opened
|
||||||
$('#nav-tabs-plays').on('shown.bs.tab', function (e) {
|
$('#nav-tabs-plays').on('shown.bs.tab', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -694,6 +1042,14 @@
|
||||||
loadGraphsTab3(current_month_range, yaxis);
|
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
|
// Date range changed
|
||||||
$('#graph-days').tooltip({ container: 'body', placement: 'top', html: true });
|
$('#graph-days').tooltip({ container: 'body', placement: 'top', html: true });
|
||||||
$('#graph-days').on('change', function() {
|
$('#graph-days').on('change', function() {
|
||||||
|
@ -702,6 +1058,7 @@
|
||||||
setLocalStorage('graph_days', current_day_range);
|
setLocalStorage('graph_days', current_day_range);
|
||||||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(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);
|
$('.days').text(current_day_range);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -732,6 +1089,7 @@
|
||||||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(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-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
|
// Y-axis changed
|
||||||
|
@ -741,14 +1099,15 @@
|
||||||
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); }
|
||||||
if (current_tab === '#tabs-stream') { loadGraphsTab2(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-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') {
|
if (type === 'plays') {
|
||||||
yaxis_format = function() { return this.value; };
|
yaxis_format = function() { return this.value; };
|
||||||
tooltip_format = function() {
|
tooltip_format = function() {
|
||||||
if (moment(this.x, 'X').isValid() && (this.x > 946684800)) {
|
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 {
|
} else {
|
||||||
var s = '<b>'+ this.x +'</b>';
|
var s = '<b>'+ this.x +'</b>';
|
||||||
}
|
}
|
||||||
|
@ -800,6 +1159,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
hc_plays_by_day_options.xAxis.plotBands = [];
|
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_plays_by_stream_type_options.xAxis.plotBands = [];
|
||||||
hc_concurrent_streams_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_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;
|
||||||
|
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_user_by_stream_type_options.yAxis.labels.formatter = yaxis_format;
|
||||||
hc_plays_by_month_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_dayofweek_options.tooltip.formatter = tooltip_format;
|
||||||
hc_plays_by_hourofday_options.tooltip.formatter = tooltip_format;
|
hc_plays_by_hourofday_options.tooltip.formatter = tooltip_format;
|
||||||
hc_plays_by_platform_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_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_plays_by_source_resolution_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>
|
<p class="help-block">Refresh the libraries list when Tautulli starts.</p>
|
||||||
</div>
|
</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">
|
<div class="padded-header">
|
||||||
<h3>Plex.tv Authentication</h3>
|
<h3>Plex.tv Authentication</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -485,11 +485,15 @@ def initialize_scheduler():
|
||||||
# Refresh the users list and libraries list
|
# Refresh the users list and libraries list
|
||||||
user_hours = CONFIG.REFRESH_USERS_INTERVAL if 1 <= CONFIG.REFRESH_USERS_INTERVAL <= 24 else 12
|
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_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',
|
schedule_job(users.refresh_users, 'Refresh users list',
|
||||||
hours=user_hours, minutes=0, seconds=0)
|
hours=user_hours, minutes=0, seconds=0)
|
||||||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||||
hours=library_hours, minutes=0, seconds=0)
|
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',
|
schedule_job(activity_pinger.connect_server, 'Check for server response',
|
||||||
hours=0, minutes=0, seconds=0)
|
hours=0, minutes=0, seconds=0)
|
||||||
|
@ -509,6 +513,9 @@ def initialize_scheduler():
|
||||||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||||
hours=0, minutes=0, seconds=0)
|
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 to reconnect server
|
||||||
schedule_job(activity_pinger.connect_server, 'Check for server response',
|
schedule_job(activity_pinger.connect_server, 'Check for server response',
|
||||||
hours=0, minutes=0, seconds=30, args=(False,))
|
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:
|
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARIES_ON_STARTUP:
|
||||||
libraries.refresh_libraries()
|
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):
|
def sig_handler(signum=None, frame=None):
|
||||||
if signum is not None:
|
if signum is not None:
|
||||||
|
@ -821,6 +831,14 @@ def dbcheck():
|
||||||
"media_info TEXT)"
|
"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
|
# mobile_devices table :: This table keeps record of devices linked with the mobile app
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS mobile_devices (id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
"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)"
|
"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
|
# Create lookup table indices
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_tvmaze_lookup "
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_tvmaze_lookup "
|
||||||
|
|
|
@ -188,6 +188,8 @@ _CONFIG_DEFINITIONS = {
|
||||||
'PLEXPY_AUTO_UPDATE': (int, 'General', 0),
|
'PLEXPY_AUTO_UPDATE': (int, 'General', 0),
|
||||||
'REFRESH_LIBRARIES_INTERVAL': (int, 'Monitoring', 12),
|
'REFRESH_LIBRARIES_INTERVAL': (int, 'Monitoring', 12),
|
||||||
'REFRESH_LIBRARIES_ON_STARTUP': (int, 'Monitoring', 1),
|
'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_INTERVAL': (int, 'Monitoring', 12),
|
||||||
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
|
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
|
||||||
'SESSION_DB_WRITE_ATTEMPTS': (int, 'Advanced', 5),
|
'SESSION_DB_WRITE_ATTEMPTS': (int, 'Advanced', 5),
|
||||||
|
@ -299,6 +301,7 @@ SETTINGS = [
|
||||||
'PMS_VERSION',
|
'PMS_VERSION',
|
||||||
'PMS_WEB_URL',
|
'PMS_WEB_URL',
|
||||||
'REFRESH_LIBRARIES_INTERVAL',
|
'REFRESH_LIBRARIES_INTERVAL',
|
||||||
|
'REFRESH_LIBRARY_STATS_DATA_INTERVAL',
|
||||||
'REFRESH_USERS_INTERVAL',
|
'REFRESH_USERS_INTERVAL',
|
||||||
'SHOW_ADVANCED_SETTINGS',
|
'SHOW_ADVANCED_SETTINGS',
|
||||||
'TIME_FORMAT',
|
'TIME_FORMAT',
|
||||||
|
@ -338,6 +341,7 @@ CHECKED_SETTINGS = [
|
||||||
'PLEXPY_AUTO_UPDATE',
|
'PLEXPY_AUTO_UPDATE',
|
||||||
'PMS_URL_MANUAL',
|
'PMS_URL_MANUAL',
|
||||||
'REFRESH_LIBRARIES_ON_STARTUP',
|
'REFRESH_LIBRARIES_ON_STARTUP',
|
||||||
|
'REFRESH_LIBRARY_STATS_DATA_ON_STARTUP',
|
||||||
'REFRESH_USERS_ON_STARTUP',
|
'REFRESH_USERS_ON_STARTUP',
|
||||||
'SYS_TRAY_ICON',
|
'SYS_TRAY_ICON',
|
||||||
'THEMOVIEDB_LOOKUP',
|
'THEMOVIEDB_LOOKUP',
|
||||||
|
@ -709,3 +713,8 @@ class Config(object):
|
||||||
self.ANON_REDIRECT_DYNAMIC = 1
|
self.ANON_REDIRECT_DYNAMIC = 1
|
||||||
|
|
||||||
self.CONFIG_VERSION = 22
|
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 False
|
||||||
|
|
||||||
return True
|
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}
|
'series': series_output}
|
||||||
return 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):
|
def get_total_plays_per_month(self, time_range='12', y_axis='plays', user_id=None, grouping=None):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ if plexpy.PYTHON2:
|
||||||
import pmsconnect
|
import pmsconnect
|
||||||
import session
|
import session
|
||||||
import users
|
import users
|
||||||
|
import datafactory
|
||||||
from plex import Plex
|
from plex import Plex
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from plexpy import common
|
||||||
|
@ -47,6 +48,7 @@ else:
|
||||||
from plexpy import session
|
from plexpy import session
|
||||||
from plexpy import users
|
from plexpy import users
|
||||||
from plexpy.plex import Plex
|
from plexpy.plex import Plex
|
||||||
|
from plexpy import datafactory
|
||||||
|
|
||||||
|
|
||||||
def refresh_libraries():
|
def refresh_libraries():
|
||||||
|
@ -109,6 +111,59 @@ def refresh_libraries():
|
||||||
logger.warn("Tautulli Libraries :: Unable to refresh libraries list.")
|
logger.warn("Tautulli Libraries :: Unable to refresh libraries list.")
|
||||||
return False
|
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):
|
def add_live_tv_library(refresh=False):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
|
@ -2766,6 +2766,7 @@ class PmsConnect(object):
|
||||||
libraries_output = {'section_id': helpers.get_xml_attr(result, 'key'),
|
libraries_output = {'section_id': helpers.get_xml_attr(result, 'key'),
|
||||||
'section_type': helpers.get_xml_attr(result, 'type'),
|
'section_type': helpers.get_xml_attr(result, 'type'),
|
||||||
'section_name': helpers.get_xml_attr(result, 'title'),
|
'section_name': helpers.get_xml_attr(result, 'title'),
|
||||||
|
'created_at': helpers.get_xml_attr(result, 'createdAt'),
|
||||||
'agent': helpers.get_xml_attr(result, 'agent'),
|
'agent': helpers.get_xml_attr(result, 'agent'),
|
||||||
'thumb': helpers.get_xml_attr(result, 'thumb'),
|
'thumb': helpers.get_xml_attr(result, 'thumb'),
|
||||||
'art': helpers.get_xml_attr(result, 'art')
|
'art': helpers.get_xml_attr(result, 'art')
|
||||||
|
@ -2944,6 +2945,7 @@ class PmsConnect(object):
|
||||||
'agent': library['agent'],
|
'agent': library['agent'],
|
||||||
'thumb': library['thumb'],
|
'thumb': library['thumb'],
|
||||||
'art': library['art'],
|
'art': library['art'],
|
||||||
|
'created_at': library['created_at'],
|
||||||
'count': children_list['library_count'],
|
'count': children_list['library_count'],
|
||||||
'is_active': 1
|
'is_active': 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -2505,6 +2505,48 @@ class WebInterface(object):
|
||||||
else:
|
else:
|
||||||
logger.warn("Unable to retrieve data for get_plays_by_top_10_users.")
|
logger.warn("Unable to retrieve data for get_plays_by_top_10_users.")
|
||||||
return result
|
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.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
|
@ -3248,14 +3290,28 @@ class WebInterface(object):
|
||||||
first_run = True
|
first_run = True
|
||||||
server_changed = True
|
server_changed = True
|
||||||
|
|
||||||
if not first_run:
|
checked_configs = [
|
||||||
for checked_config in config.CHECKED_SETTINGS:
|
"launch_browser", "launch_startup", "enable_https", "https_create_cert",
|
||||||
checked_config = checked_config.lower()
|
"api_enabled", "freeze_db", "check_github",
|
||||||
if checked_config not in kwargs:
|
"group_history_tables",
|
||||||
# checked items should be zero or one. if they were not sent then the item was not checked
|
"pms_url_manual", "week_start_monday",
|
||||||
kwargs[checked_config] = 0
|
"refresh_libraries_on_startup", "refresh_users_on_startup",
|
||||||
else:
|
"notify_consecutive", "notify_recently_added_upgrade",
|
||||||
kwargs[checked_config] = 1
|
"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 http password exists in config, do not overwrite when blank value received
|
||||||
if kwargs.get('http_password') == ' ':
|
if kwargs.get('http_password') == ' ':
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue