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
  • @@ -225,6 +226,68 @@
    +
    +
    +
    +
    +

    Daily addition by media type Last 30 days

    +

    + The total addition count of shows, seasons, episodes and movies per day. +

    +
    +
    +
    Loading chart...
    +
    +
    +
    +
    +
    +
    +
    +

    Library Growth Last 30 days

    +

    + The overall library growth by the library stats per day. +

    +
    +
    +
    Loading chart...
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Addition count by media type Last 30 days

    +

    + The combined total of tv, movies, and music added to the server. +

    +
    +
    +
    + Loading chart... +
    +
    +
    +
    +
    +
    +

    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.

    +
    + +
    +
    + +
    + +
    +

    The interval (in hours) Tautulli will request an update of all media items from your Plex Media Server.

    +

    Minimum 6, maximum 24, default 12.

    +

    This process, depending on your library sizes, can take multiple minutes up to half an hour.

    +
    +
    + +

    Refresh the library statistics data when Tautulli starts.

    +
    +

    Plex.tv Authentication

    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') == ' ':