diff --git a/data/interfaces/default/graphs.html b/data/interfaces/default/graphs.html
index 15a96a69..46aae688 100644
--- a/data/interfaces/default/graphs.html
+++ b/data/interfaces/default/graphs.html
@@ -44,6 +44,7 @@
Media Type
Stream Type
Play Totals
+ Library Statistics
+
+
+
+
+
Daily addition by media type Last 30 days
+
+ The total addition count of shows, seasons, episodes and movies per day.
+
+
+
+
+
+
+
Library Growth Last 30 days
+
+ The overall library growth by the library stats per day.
+
+
+
+
+
+
+
+
Addition count by media type Last 30 days
+
+ The combined total of tv, movies, and music added to the server.
+
+
+
+
+
Addition count by source resolution Last 30 days
+
+ The combined total of tv, movies, and music added to the server by source resolution.
+
+
+
+
+ Loading chart...
+
+
+
+
+
+
+
@@ -246,6 +309,8 @@
+
+
+
+
@@ -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 = ''+ moment(this.x).format('ddd MMM D') +'';
+ var s = tooltipFormat ? ''+ moment(this.x).format(tooltipFormat) +'' : ''+ moment(this.x).format('ddd MMM D') +'';
} else {
var s = ''+ this.x +'';
}
@@ -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;
diff --git a/data/interfaces/default/js/graphs/library_additions_by_day.js b/data/interfaces/default/js/graphs/library_additions_by_day.js
new file mode 100644
index 00000000..502cc35f
--- /dev/null
+++ b/data/interfaces/default/js/graphs/library_additions_by_day.js
@@ -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: [{}]
+};
\ No newline at end of file
diff --git a/data/interfaces/default/js/graphs/library_additions_by_media_type.js b/data/interfaces/default/js/graphs/library_additions_by_media_type.js
new file mode 100644
index 00000000..556fce5b
--- /dev/null
+++ b/data/interfaces/default/js/graphs/library_additions_by_media_type.js
@@ -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: [{}]
+};
\ No newline at end of file
diff --git a/data/interfaces/default/js/graphs/library_additions_by_source_resolution.js b/data/interfaces/default/js/graphs/library_additions_by_source_resolution.js
new file mode 100644
index 00000000..dc106dc4
--- /dev/null
+++ b/data/interfaces/default/js/graphs/library_additions_by_source_resolution.js
@@ -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: [{}]
+};
\ No newline at end of file
diff --git a/data/interfaces/default/js/graphs/library_growth_by_day.js b/data/interfaces/default/js/graphs/library_growth_by_day.js
new file mode 100644
index 00000000..b4e7b360
--- /dev/null
+++ b/data/interfaces/default/js/graphs/library_growth_by_day.js
@@ -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: [{}]
+};
\ No newline at end of file
diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html
index 2dd20ff8..5826afe4 100644
--- a/data/interfaces/default/settings.html
+++ b/data/interfaces/default/settings.html
@@ -976,6 +976,25 @@
Refresh the libraries list when Tautulli starts.
+
+
+
+
Refresh the library statistics data when Tautulli starts.
+
+
diff --git a/plexpy/__init__.py b/plexpy/__init__.py
index 10d153d1..9d2c5f3e 100644
--- a/plexpy/__init__.py
+++ b/plexpy/__init__.py
@@ -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 "
diff --git a/plexpy/config.py b/plexpy/config.py
index dbcd294d..f55d54b3 100644
--- a/plexpy/config.py
+++ b/plexpy/config.py
@@ -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
diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py
index d525c9c9..6a7bda63 100644
--- a/plexpy/datafactory.py
+++ b/plexpy/datafactory.py
@@ -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
\ No newline at end of file
diff --git a/plexpy/graphs.py b/plexpy/graphs.py
index 8758f71f..95e8b9d1 100644
--- a/plexpy/graphs.py
+++ b/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()
diff --git a/plexpy/libraries.py b/plexpy/libraries.py
index 33832aba..16eae639 100644
--- a/plexpy/libraries.py
+++ b/plexpy/libraries.py
@@ -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()
diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py
index 2662abf9..66205e19 100644
--- a/plexpy/pmsconnect.py
+++ b/plexpy/pmsconnect.py
@@ -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
}
diff --git a/plexpy/webserve.py b/plexpy/webserve.py
index 7e0c083b..8ed5de28 100644
--- a/plexpy/webserve.py
+++ b/plexpy/webserve.py
@@ -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') == ' ':