mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-11 07:46:07 -07:00
Merge branch 'nightly' into python3
# Conflicts: # plexpy/__init__.py # plexpy/helpers.py # plexpy/logger.py # plexpy/version.py
This commit is contained in:
commit
485609fbb9
22 changed files with 578 additions and 322 deletions
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -1,5 +1,31 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.1.42 (2020-01-04)
|
||||||
|
|
||||||
|
* Other:
|
||||||
|
* Fix: SSL certificate error when installing GeoLite2 database.
|
||||||
|
* Change: Verify MaxMind license key and GeoLite2 database path before installing.
|
||||||
|
* Change: Disable GeoLite2 database uninstall button when it is not installed.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.1.41 (2019-12-30)
|
||||||
|
|
||||||
|
* Other:
|
||||||
|
* Fix: Failing to extract the GeoLite2 database on Windows.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.1.40 (2019-12-30)
|
||||||
|
|
||||||
|
* UI:
|
||||||
|
* Change: Moved 3rd Party API settings to new tab in the settings.
|
||||||
|
* Graphs:
|
||||||
|
* Change: Improve calculating month ranges for Play Totals graphs.
|
||||||
|
* Other:
|
||||||
|
* Fix: Failing to verify a Plex Media Server using a hostname.
|
||||||
|
* Change: A license key is now required to install the MaxMind GeoLite2 database for IP geolocation. Please follow the guide in the wiki to reinstall the GeoLite2 database.
|
||||||
|
* Change: The GeoLite2 database will now automatically update periodically if installed.
|
||||||
|
|
||||||
|
|
||||||
## v2.1.39 (2019-12-08)
|
## v2.1.39 (2019-12-08)
|
||||||
|
|
||||||
* UI:
|
* UI:
|
||||||
|
|
|
@ -53,14 +53,6 @@ DOCUMENTATION :: END
|
||||||
<td>Newsletter Directory:</td>
|
<td>Newsletter Directory:</td>
|
||||||
<td>${plexpy.CONFIG.NEWSLETTER_DIR}</td>
|
<td>${plexpy.CONFIG.NEWSLETTER_DIR}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>GeoLite2 Database:</td>
|
|
||||||
% if plexpy.CONFIG.GEOIP_DB:
|
|
||||||
<td>${plexpy.CONFIG.GEOIP_DB} | <a class="no-highlight" href="#" id="reinstall_geoip_db">Reinstall / Update</a> | <a class="no-highlight" href="#" id="uninstall_geoip_db">Uninstall</a></td>
|
|
||||||
% else:
|
|
||||||
<td><a class="no-highlight" href="#" id="install_geoip_db">Click here to install the GeoLite2 database.</a></td>
|
|
||||||
% endif
|
|
||||||
</tr>
|
|
||||||
% if plexpy.ARGS:
|
% if plexpy.ARGS:
|
||||||
<tr>
|
<tr>
|
||||||
<td>Arguments:</td>
|
<td>Arguments:</td>
|
||||||
|
@ -102,22 +94,6 @@ DOCUMENTATION :: END
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$("#install_geoip_db, #reinstall_geoip_db").click(function () {
|
|
||||||
var msg = 'Are you sure you want to install the GeoLite2 database?<br /><br />' +
|
|
||||||
'The database is used to lookup IP address geolocation info.<br />' +
|
|
||||||
'The database will be downloaded from <a href="${anon_url("https://dev.maxmind.com/geoip/geoip2/geolite2/")}" target="_blank">MaxMind</a>, <br />' +
|
|
||||||
'and requires <strong>100MB</strong> of free space to install in your Tautulli directory.<br />'
|
|
||||||
var url = 'install_geoip_db';
|
|
||||||
confirmAjaxCall(url, msg, null, 'Installing GeoLite2 database.', getConfigurationTable);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#uninstall_geoip_db").click(function () {
|
|
||||||
var msg = 'Are you sure you want to uninstall the GeoLite2 database?<br /><br />' +
|
|
||||||
'You will not be able to lookup IP address geolocation info.';
|
|
||||||
var url = 'uninstall_geoip_db';
|
|
||||||
confirmAjaxCall(url, msg, null, 'Uninstalling GeoLite2 database.', getConfigurationTable);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.guidelines-modal-link').on('click', function (e) {
|
$('.guidelines-modal-link').on('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$('#guidelines-type').text($(this).data('id'))
|
$('#guidelines-type').text($(this).data('id'))
|
||||||
|
|
|
@ -226,17 +226,24 @@ DOCUMENTATION :: END
|
||||||
<li class="dashboard-activity-info-item">
|
<li class="dashboard-activity-info-item">
|
||||||
<div class="sub-heading">Video</div>
|
<div class="sub-heading">Video</div>
|
||||||
<div class="sub-value" id="video_decision-${sk}">
|
<div class="sub-value" id="video_decision-${sk}">
|
||||||
% if data['media_type'] in ('movie', 'episode', 'clip'):
|
% if data['media_type'] in ('movie', 'episode', 'clip') and data['stream_video_decision']:
|
||||||
|
<%
|
||||||
|
if data['video_dynamic_range'] == 'HDR':
|
||||||
|
video_dynamic_range = ' ' + data['video_dynamic_range']
|
||||||
|
stream_video_dynamic_range = ' ' + data['stream_video_dynamic_range']
|
||||||
|
else:
|
||||||
|
video_dynamic_range = stream_video_dynamic_range = ''
|
||||||
|
%>
|
||||||
% if data['stream_video_decision'] == 'transcode':
|
% if data['stream_video_decision'] == 'transcode':
|
||||||
<%
|
<%
|
||||||
hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
|
hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
|
||||||
hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
|
hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
|
||||||
%>
|
%>
|
||||||
Transcode (${data['video_codec'].upper()}${hw_d} ${data['video_full_resolution']} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${data['stream_video_full_resolution']})
|
Transcode (${data['video_codec'].upper()}${hw_d} ${data['video_full_resolution']}${video_dynamic_range} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${data['stream_video_full_resolution']}${stream_video_dynamic_range})
|
||||||
% elif data['stream_video_decision'] == 'copy':
|
% elif data['stream_video_decision'] == 'copy':
|
||||||
Direct Stream (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']})
|
Direct Stream (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']}${stream_video_dynamic_range})
|
||||||
% else:
|
% else:
|
||||||
Direct Play (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']})
|
Direct Play (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']}${stream_video_dynamic_range})
|
||||||
% endif
|
% endif
|
||||||
% elif data['media_type'] == 'photo':
|
% elif data['media_type'] == 'photo':
|
||||||
Direct Play (${data['width']}x${data['height']})
|
Direct Play (${data['width']}x${data['height']})
|
||||||
|
@ -248,6 +255,7 @@ DOCUMENTATION :: END
|
||||||
<li class="dashboard-activity-info-item">
|
<li class="dashboard-activity-info-item">
|
||||||
<div class="sub-heading">Audio</div>
|
<div class="sub-heading">Audio</div>
|
||||||
<div class="sub-value" id="audio_decision-${sk}">
|
<div class="sub-value" id="audio_decision-${sk}">
|
||||||
|
% if data['stream_audio_decision']:
|
||||||
% if data['stream_audio_decision'] == 'transcode':
|
% if data['stream_audio_decision'] == 'transcode':
|
||||||
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} <i class="fa fa-long-arrow-right"></i> ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} <i class="fa fa-long-arrow-right"></i> ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||||
% elif data['stream_audio_decision'] == 'copy':
|
% elif data['stream_audio_decision'] == 'copy':
|
||||||
|
@ -255,6 +263,7 @@ DOCUMENTATION :: END
|
||||||
% else:
|
% else:
|
||||||
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||||
% endif
|
% endif
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
% endif
|
% endif
|
||||||
|
|
|
@ -437,6 +437,8 @@
|
||||||
|
|
||||||
var video_decision = '';
|
var video_decision = '';
|
||||||
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.stream_video_decision) {
|
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.stream_video_decision) {
|
||||||
|
var v_bd = (s.video_dynamic_range === 'HDR') ? ' ' + s.video_dynamic_range : '';
|
||||||
|
var sv_bd = (s.video_dynamic_range === 'HDR') ? ' ' + s.stream_video_dynamic_range : '';
|
||||||
var v_res= '';
|
var v_res= '';
|
||||||
switch (s.video_resolution.toLowerCase()) {
|
switch (s.video_resolution.toLowerCase()) {
|
||||||
case 'sd':
|
case 'sd':
|
||||||
|
@ -462,11 +464,11 @@
|
||||||
if (s.stream_video_decision === 'transcode') {
|
if (s.stream_video_decision === 'transcode') {
|
||||||
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
|
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
|
||||||
var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : '';
|
var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : '';
|
||||||
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
|
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + v_bd + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + sv_bd + ')';
|
||||||
} else if (s.stream_video_decision === 'copy') {
|
} else if (s.stream_video_decision === 'copy') {
|
||||||
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
|
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + sv_bd + ')';
|
||||||
} else {
|
} else {
|
||||||
video_decision = 'Direct Play (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
|
video_decision = 'Direct Play (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + sv_bd + ')';
|
||||||
}
|
}
|
||||||
} else if (s.media_type === 'photo') {
|
} else if (s.media_type === 'photo') {
|
||||||
video_decision = 'Direct Play (' + s.width + 'x' + s.height + ')';
|
video_decision = 'Direct Play (' + s.width + 'x' + s.height + ')';
|
||||||
|
|
|
@ -271,7 +271,7 @@
|
||||||
</div>
|
</div>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
Select an existing notification agent where the subject and body text will be sent.<br>
|
Select an existing notification agent where the subject and body text will be sent.<br>
|
||||||
Note: Self-hosted newsletters must be enabled under <a data-tab-destination="tabs-notifications" data-dismiss="modal" data-target="#newsletter_self_hosted">Newsletters</a> to include a link to the newsletter.
|
Note: Self-hosted newsletters must be enabled under <a data-tab-destination="notifications" data-dismiss="modal" data-target="newsletter_self_hosted">Newsletters</a> to include a link to the newsletter.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="newsletter-email-config">
|
<div id="newsletter-email-config">
|
||||||
|
|
|
@ -485,7 +485,7 @@
|
||||||
'<div class="form-group">' +
|
'<div class="form-group">' +
|
||||||
'<label>Warning</label>' +
|
'<label>Warning</label>' +
|
||||||
'<p class="help-block" style="color: #eb8600;">Facebook requires HTTPS for authorization. ' +
|
'<p class="help-block" style="color: #eb8600;">Facebook requires HTTPS for authorization. ' +
|
||||||
'Please enable HTTPS for Tautulli under <a data-tab-destination="tabs-web_interface" data-dismiss="modal" data-target="#enable_https">Web Interface</a>.</p>' +
|
'Please enable HTTPS for Tautulli under <a data-tab-destination="web_interface" data-dismiss="modal" data-target="enable_https">Web Interface</a>.</p>' +
|
||||||
'</div>'
|
'</div>'
|
||||||
);
|
);
|
||||||
$('#facebook_redirect_uri').val('HTTPS not enabled');
|
$('#facebook_redirect_uri').val('HTTPS not enabled');
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
<li role="presentation"><a href="#tabs-notifications" aria-controls="tabs-notifications" role="tab" data-toggle="tab">Notifications & Newsletters</a></li>
|
<li role="presentation"><a href="#tabs-notifications" aria-controls="tabs-notifications" role="tab" data-toggle="tab">Notifications & Newsletters</a></li>
|
||||||
<li role="presentation"><a href="#tabs-notification_agents" aria-controls="tabs-notification_agents" role="tab" data-toggle="tab">Notification Agents</a></li>
|
<li role="presentation"><a href="#tabs-notification_agents" aria-controls="tabs-notification_agents" role="tab" data-toggle="tab">Notification Agents</a></li>
|
||||||
<li role="presentation"><a href="#tabs-newsletter_agents" aria-controls="tabs-newsletter_agents" role="tab" data-toggle="tab">Newsletter Agents</a></li>
|
<li role="presentation"><a href="#tabs-newsletter_agents" aria-controls="tabs-newsletter_agents" role="tab" data-toggle="tab">Newsletter Agents</a></li>
|
||||||
|
<li role="presentation"><a href="#tabs-3rd_party_apis" aria-controls="tabs-3rd_party_apis" role="tab" data-toggle="tab">3rd Party APIs</a></li>
|
||||||
<li role="presentation"><a href="#tabs-import_backups" aria-controls="tabs-import_backups" role="tab" data-toggle="tab">Import & Backups</a></li>
|
<li role="presentation"><a href="#tabs-import_backups" aria-controls="tabs-import_backups" role="tab" data-toggle="tab">Import & Backups</a></li>
|
||||||
<li role="presentation"><a href="#tabs-android_app" aria-controls="tabs-android_app" role="tab" data-toggle="tab">Tautulli Remote Android App <sup><small>beta</small></sup></a></li>
|
<li role="presentation"><a href="#tabs-android_app" aria-controls="tabs-android_app" role="tab" data-toggle="tab">Tautulli Remote Android App <sup><small>beta</small></sup></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -284,7 +285,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="home_refresh_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
|
<div id="home_refresh_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help-block">Set the interval (in seconds) to refresh the current activity on the homepage. Minimum 2.</p>
|
<p class="help-block">Set the interval (in seconds) to refresh the current activity on the homepage. Minimum 2, default 10.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
|
@ -1008,7 +1009,7 @@
|
||||||
<p class="help-block" id="self_host_newsletter_message">
|
<p class="help-block" id="self_host_newsletter_message">
|
||||||
Note: The <span class="inline-pre">${http_root}newsletter</span> endpoint on your domain must be publicly accessible from the internet.
|
Note: The <span class="inline-pre">${http_root}newsletter</span> endpoint on your domain must be publicly accessible from the internet.
|
||||||
</p>
|
</p>
|
||||||
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p>
|
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="web_interface" data-target="http_base_url">Web Interface</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="newsletter_auth">Newsletter Authentication</label>
|
<label for="newsletter_auth">Newsletter Authentication</label>
|
||||||
|
@ -1022,7 +1023,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help-block">Select the authentication method to use for self-hosted newsletters.</p>
|
<p class="help-block">Select the authentication method to use for self-hosted newsletters.</p>
|
||||||
<p class="help-block settings-warning newsletter-guest-access-warning">Warning: Guest Access is not enabled under <a data-tab-destination="tabs-web_interface" data-target="#allow_guest_access">Web Interface</a>.</p>
|
<p class="help-block settings-warning newsletter-guest-access-warning">Warning: Guest Access is not enabled under <a data-tab-destination="web_interface" data-target="allow_guest_access">Web Interface</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="newsletter_password_option">
|
<div class="form-group" id="newsletter_password_option">
|
||||||
<label for="newsletter_password">Newsletter Password</label>
|
<label for="newsletter_password">Newsletter Password</label>
|
||||||
|
@ -1063,12 +1064,60 @@
|
||||||
<p class="help-block">Enter the full path to where newsletter files will be saved.</p>
|
<p class="help-block">Enter the full path to where newsletter files will be saved.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="padded-header">
|
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
|
||||||
<h3>3rd Party APIs</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div role="tabpanel" class="tab-pane" id="tabs-notification_agents">
|
||||||
|
|
||||||
|
<div class="padded-header">
|
||||||
|
<h3>Notification Agents</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="help-block">
|
||||||
|
Add a new notification agent, or configure an existing notification agent by clicking the settings icon on the right.
|
||||||
|
</p>
|
||||||
|
<p class="help-block">
|
||||||
|
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/Notification-Agents-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>Notification Agents Guide</a> for instructions on setting up each notification agent.
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<div id="plexpy-notifiers-table">
|
||||||
|
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading notification agents...</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="tabpanel" class="tab-pane" id="tabs-newsletter_agents">
|
||||||
|
|
||||||
|
<div class="padded-header">
|
||||||
|
<h3>Newsletter Agents</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="help-block">
|
||||||
|
Add a new newsletter agent, or configure an existing newsletter agent by clicking the settings icon on the right.
|
||||||
|
</p>
|
||||||
|
<p class="help-block settings-warning" id="newsletter_upload_warning">
|
||||||
|
Warning: The <a data-tab-destination="3rd_party_apis" data-target="notify_upload_posters">Image Hosting</a> setting must be enabled for images to display on the newsletter.</span>
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
<div id="plexpy-newsletters-table">
|
||||||
|
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading newsletter agents...</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="tabpanel" class="tab-pane" id="tabs-3rd_party_apis">
|
||||||
|
|
||||||
|
<div class="padded-header">
|
||||||
|
<h3>Image Hosting</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="help-block">Image hosting is used to provide posters and artwork for some notification agents and newsletters.</p>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="notify_upload_posters">Image Hosting</label>
|
<label for="notify_upload_posters">Image Host</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="${'input-group' if config['notify_upload_posters'] in (1, 3) else ''}">
|
<div class="${'input-group' if config['notify_upload_posters'] in (1, 3) else ''}">
|
||||||
|
@ -1090,8 +1139,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="imgur_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 1 else 'block'}">
|
<div id="imgur_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 1 else 'block'}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p class="help-block" id="imgur_upload_message">
|
<p class="help-block">
|
||||||
You can register a new Imgur application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>.<br>
|
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>3rd Party APIs Guide</a> for instructions on setting up Imgur.<br>
|
||||||
Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead.
|
Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1108,13 +1157,13 @@
|
||||||
<div id="self_host_image_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 2 else 'block'}">
|
<div id="self_host_image_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 2 else 'block'}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p class="help-block" id="self_host_image_message">Note: The <span class="inline-pre">${http_root}image</span> endpoint on your domain must be publicly accessible from the internet.</p>
|
<p class="help-block" id="self_host_image_message">Note: The <span class="inline-pre">${http_root}image</span> endpoint on your domain must be publicly accessible from the internet.</p>
|
||||||
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="tabs-web_interface" data-target="#http_base_url">Web Interface</a>.</p>
|
<p class="help-block settings-warning base-url-warning">Warning: Public Tautulli domain not set under <a data-tab-destination="web_interface" data-target="http_base_url">Web Interface</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="cloudinary_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 3 else 'block'}">
|
<div id="cloudinary_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 3 else 'block'}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p class="help-block" id="imgur_upload_message">
|
<p class="help-block">
|
||||||
You can sign up for Cloudinary <a href="${anon_url('https://cloudinary.com')}" target="_blank">here</a>.<br>
|
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>3rd Party APIs Guide</a> for instructions on setting up Cloudinary.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -1151,6 +1200,13 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="padded-header">
|
||||||
|
<h3>Metadata Lookups</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="help-block">Metadata lookups are used to provide additional links for notifications when available.</p>
|
||||||
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="themoviedb_lookup" id="themoviedb_lookup" value="1" ${config['themoviedb_lookup']}> Lookup TheMovieDB Links
|
<input type="checkbox" name="themoviedb_lookup" id="themoviedb_lookup" value="1" ${config['themoviedb_lookup']}> Lookup TheMovieDB Links
|
||||||
|
@ -1170,50 +1226,58 @@
|
||||||
<p class="help-block">Enable to lookup links to MusicBrainz for music when available.</p>
|
<p class="help-block">Enable to lookup links to MusicBrainz for music when available.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="padded-header">
|
||||||
|
<h3>Geolocation Database</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="help-block">The GeoLite2 database is used to geolocate IP addresses.</p>
|
||||||
|
<p class="help-block">
|
||||||
|
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>3rd Party APIs Guide</a> for instructions on setting up MaxMind.<br>
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="maxmind_license_key">MaxMind License Key</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input type="text" class="form-control" id="maxmind_license_key" name="maxmind_license_key" value="${config['maxmind_license_key']}" data-parsley-trigger="change">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">
|
||||||
|
Enter your MaxMind License Key to install the GeoLite2 database.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="geoip_db">GeoLite2 Database File</label> ${docker_msg | n}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" id="geoip_db" name="geoip_db" value="${config['geoip_db']}" ${docker_setting} data-parsley-trigger="change" data-parsley-pattern=".+\.mmdb$" data-parsley-errors-container="#geoip_db_error" data-parsley-error-message="Must end with '.mmdb'">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="btn btn-form" type="button" id="install_geoip_db">${'Update' if config["geoip_db_installed"] else 'Install'}</button>
|
||||||
|
<button class="btn btn-form" type="button" id="uninstall_geoip_db" ${'disabled' if not config['geoip_db_installed'] else ''}>Uninstall</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="geoip_db_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">
|
||||||
|
Leave blank to install in the default location. GeoLite2 database last updated <strong><span id="geoip_db_updated">never</span></strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group advanced-setting">
|
||||||
|
<label for="geoip_db_update_days">GeoLite2 Database Update Interval</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<input type="text" class="form-control" data-parsley-type="integer" id="geoip_db_update_days" name="geoip_db_update_days" value="${config['geoip_db_update_days']}" size="5" data-parsley-range="[7, 30]" data-parsley-trigger="change" data-parsley-errors-container="#geoip_db_update_days_error" required>
|
||||||
|
</div>
|
||||||
|
<div id="geoip_db_update_days_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">The interval (in days) Tautulli will automatically update the GeoLite2 database. Minimum 7, maximum 30, default 30.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
|
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div role="tabpanel" class="tab-pane" id="tabs-notification_agents">
|
|
||||||
|
|
||||||
<div class="padded-header">
|
|
||||||
<h3>Notification Agents</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="help-block">
|
|
||||||
Add a new notification agent, or configure an existing notification agent by clicking the settings icon on the right.
|
|
||||||
</p>
|
|
||||||
<p class="help-block">
|
|
||||||
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/Notification-Agents-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>Notification Agents Guide</a> for instructions on setting up each notification agent.
|
|
||||||
</p>
|
|
||||||
<br />
|
|
||||||
<div id="plexpy-notifiers-table">
|
|
||||||
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading notification agents...</div>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div role="tabpanel" class="tab-pane" id="tabs-newsletter_agents">
|
|
||||||
|
|
||||||
<div class="padded-header">
|
|
||||||
<h3>Newsletter Agents</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="help-block">
|
|
||||||
Add a new newsletter agent, or configure an existing newsletter agent by clicking the settings icon on the right.
|
|
||||||
</p>
|
|
||||||
<p class="help-block settings-warning" id="newsletter_upload_warning">
|
|
||||||
Warning: The <a data-tab-destination="tabs-notifications" data-target="#notify_upload_posters">Image Hosting</a> setting must be enabled for images to display on the newsletter.</span>
|
|
||||||
</p>
|
|
||||||
<br/>
|
|
||||||
<div id="plexpy-newsletters-table">
|
|
||||||
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading newsletter agents...</div>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div role="tabpanel" class="tab-pane" id="tabs-import_backups">
|
<div role="tabpanel" class="tab-pane" id="tabs-import_backups">
|
||||||
|
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
|
@ -1316,7 +1380,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Registered Devices</label>
|
<label>Registered Devices</label>
|
||||||
<p class="help-block">Register a new device using a QR code, or configure an existing device by clicking the settings icon on the right.</p>
|
<p class="help-block">Register a new device using a QR code, or configure an existing device by clicking the settings icon on the right.</p>
|
||||||
<p id="app_api_msg" style="color: #eb8600;">The API must be enabled under <a data-tab-destination="tabs-web_interface" data-target="#api_enabled">Web Interface</a> to use the app.</p>
|
<p id="app_api_msg" style="color: #eb8600;">Warning: The API must be enabled under <a data-tab-destination="web_interface" data-target="api_enabled">Web Interface</a> to use the app.</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div id="plexpy-mobile-devices-table" class="col-md-12">
|
<div id="plexpy-mobile-devices-table" class="col-md-12">
|
||||||
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading registered devices...</div>
|
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading registered devices...</div>
|
||||||
|
@ -1796,11 +1860,6 @@ Rating: {rating}/10 --> Rating: /10
|
||||||
async: true,
|
async: true,
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
$("#plexpy-configuration-table").html(xhr.responseText);
|
$("#plexpy-configuration-table").html(xhr.responseText);
|
||||||
if ("${kwargs.get('install_geoip')}" == 'true') {
|
|
||||||
$('#install_geoip_db').removeClass('no-highlight').css('color','#e9a049');
|
|
||||||
} else if ("${kwargs.get('reinstall_geoip')}" == 'true') {
|
|
||||||
$('#reinstall_geoip_db').removeClass('no-highlight').css('color','#e9a049');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1921,12 +1980,10 @@ $(document).ready(function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function preSaveChecks(_callback) {
|
function preSaveChecks(_callback) {
|
||||||
if (serverChanged) {
|
|
||||||
verifyServer();
|
|
||||||
}
|
|
||||||
verifyPMSWebURL();
|
verifyPMSWebURL();
|
||||||
|
if (serverChanged) {
|
||||||
if (_callback) {
|
verifyServer(_callback);
|
||||||
|
} else if (typeof _callback === "function") {
|
||||||
_callback();
|
_callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1951,12 +2008,13 @@ $(document).ready(function() {
|
||||||
settingsChanged = true;
|
settingsChanged = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings(showMsg, _callback) {
|
||||||
if (configForm.parsley().validate()) {
|
if (configForm.parsley().validate()) {
|
||||||
doAjaxCall('configUpdate', $(this), 'tabs', true, true, postSaveChecks);
|
doAjaxCall('configUpdate', $(this), 'tabs', true, showMsg, _callback);
|
||||||
return false;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
showMsg('<i class="fa fa-exclamation-circle"></i> Please verify your settings.', false, true, 5000, true)
|
showMsg('<i class="fa fa-exclamation-circle"></i> Please verify your settings.', false, true, 5000, true);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1970,7 +2028,7 @@ $(document).ready(function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
$('.save-button').click(function() {
|
$('.save-button').click(function() {
|
||||||
preSaveChecks(function () { saveSettings() });
|
preSaveChecks(function () { saveSettings(true, postSaveChecks) });
|
||||||
});
|
});
|
||||||
|
|
||||||
initConfigCheckbox('#api_enabled');
|
initConfigCheckbox('#api_enabled');
|
||||||
|
@ -2279,6 +2337,7 @@ $(document).ready(function() {
|
||||||
|
|
||||||
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
|
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
|
||||||
$("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
|
$("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
|
||||||
|
showMsg('Verifying Plex server...', true, true, 10000, false);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_server_id',
|
url: 'get_server_id',
|
||||||
data: {
|
data: {
|
||||||
|
@ -2316,10 +2375,11 @@ $(document).ready(function() {
|
||||||
} else {
|
} else {
|
||||||
$("#pms_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
|
$("#pms_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
|
||||||
$("#pms_ip_group").removeClass("has-error");
|
$("#pms_ip_group").removeClass("has-error");
|
||||||
|
showMsg('<i class="fa fa-check"></i> Server verified.', false, true, 5000);
|
||||||
serverChanged = false;
|
serverChanged = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_callback) {
|
if (typeof _callback === "function") {
|
||||||
_callback();
|
_callback();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -2773,25 +2833,79 @@ $(document).ready(function() {
|
||||||
|
|
||||||
$('#allow_guest_access').click(function () {
|
$('#allow_guest_access').click(function () {
|
||||||
newsletterPasswordEnabled();
|
newsletterPasswordEnabled();
|
||||||
})
|
});
|
||||||
|
|
||||||
|
function gotoSetting(tab, setting){
|
||||||
|
$("a[href=#tabs-" + tab + "]").click();
|
||||||
|
if (setting) {
|
||||||
|
_setting = '#' + setting;
|
||||||
|
if ($(_setting).closest('.advanced-setting').length && !$('#menu_link_show_advanced_settings').hasClass('active')) {
|
||||||
|
$('#menu_link_show_advanced_settings').click()
|
||||||
|
}
|
||||||
|
var body_container = $('.body-container');
|
||||||
|
var scroll_pos = setting ? body_container.scrollTop() + $(_setting).offset().top - 100 : 0;
|
||||||
|
body_container.animate({scrollTop: scroll_pos});
|
||||||
|
$(_setting).closest('.form-group, .checkbox').delay(500).fadeOut().fadeIn('slow').fadeOut().fadeIn('slow');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$('body').on('click', 'a[data-tab-destination]', function () {
|
$('body').on('click', 'a[data-tab-destination]', function () {
|
||||||
var tab = $(this).data('tab-destination');
|
var tab = $(this).data('tab-destination');
|
||||||
$("a[href=#" + tab + "]").click();
|
var setting = $(this).data('target');
|
||||||
var scroll_destination = $(this).data('target');
|
gotoSetting(tab, setting)
|
||||||
if (scroll_destination) {
|
|
||||||
if ($(scroll_destination).closest('.advanced-setting').length && !$('#menu_link_show_advanced_settings').hasClass('active')) {
|
|
||||||
$('#menu_link_show_advanced_settings').click()
|
|
||||||
}
|
|
||||||
var body_container = $('.body-container')
|
|
||||||
var scroll_pos = scroll_destination ? body_container.scrollTop() + $(scroll_destination).offset().top - 100 : 0;
|
|
||||||
body_container.animate({scrollTop: scroll_pos});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#resources-xml').on('tripleclick', function () {
|
$('#resources-xml').on('tripleclick', function () {
|
||||||
openPlexXML('/api/resources', true, {includeHttps: 1});
|
openPlexXML('/api/resources', true, {includeHttps: 1});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ("${kwargs.get('install_geoip')}" === 'true') {
|
||||||
|
gotoSetting('3rd_party_apis', 'geoip_db')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("${config['geoip_db_installed']}" > "0") {
|
||||||
|
$("#geoip_db_updated").text(moment("${config['geoip_db_installed']}", "X").fromNow());
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#install_geoip_db").click(function () {
|
||||||
|
if ($.trim($("#maxmind_license_key").val()) === "") {
|
||||||
|
$("#maxmind_license_key").focus();
|
||||||
|
showMsg('<i class="fa fa-exclamation-circle"></i> Maxmind License Key is required.', false, true, 5000, true);
|
||||||
|
return false;
|
||||||
|
} else if (!(saveSettings())){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var msg = 'Are you sure you want to install the GeoLite2 database?<br /><br />' +
|
||||||
|
'The database is used to lookup IP address geolocation info.<br />' +
|
||||||
|
'The database will be downloaded from <a href="${anon_url("https://dev.maxmind.com/geoip/geoip2/geolite2/")}" target="_blank">MaxMind</a>, <br />' +
|
||||||
|
'and requires <strong>100MB</strong> of free space to install.<br />';
|
||||||
|
var url = 'install_geoip_db';
|
||||||
|
if ($(this).text() === 'Update') {
|
||||||
|
url += '?update=true';
|
||||||
|
}
|
||||||
|
confirmAjaxCall(url, msg, null, 'Installing GeoLite2 database.', function (result) {
|
||||||
|
if (result.result === "success") {
|
||||||
|
$('#install_geoip_db').text('Update');
|
||||||
|
$('#uninstall_geoip_db').prop('disabled', false);
|
||||||
|
$('#geoip_db_updated').text(moment(result.updated, "X").fromNow());
|
||||||
|
}
|
||||||
|
getSchedulerTable();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#uninstall_geoip_db").click(function () {
|
||||||
|
var msg = 'Are you sure you want to uninstall the GeoLite2 database?<br /><br />' +
|
||||||
|
'You will not be able to lookup IP address geolocation info.';
|
||||||
|
var url = 'uninstall_geoip_db';
|
||||||
|
confirmAjaxCall(url, msg, null, 'Uninstalling GeoLite2 database.', function (result) {
|
||||||
|
if (result.result === "success") {
|
||||||
|
$('#install_geoip_db').text('Install');
|
||||||
|
$('#uninstall_geoip_db').prop('disabled', true);
|
||||||
|
$('#geoip_db_updated').text('never');
|
||||||
|
}
|
||||||
|
getSchedulerTable();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
|
@ -178,6 +178,11 @@ DOCUMENTATION :: END
|
||||||
<td>${data['stream_video_framerate']}</td>
|
<td>${data['stream_video_framerate']}</td>
|
||||||
<td>${data['video_framerate']}</td>
|
<td>${data['video_framerate']}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Dynamic Range</td>
|
||||||
|
<td>${data['stream_video_dynamic_range']}</td>
|
||||||
|
<td>${data['video_dynamic_range']}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Aspect Ratio</td>
|
<td>Aspect Ratio</td>
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
from .core import where
|
from .core import where
|
||||||
|
|
||||||
__version__ = "2019.03.09"
|
__version__ = "2019.11.28"
|
||||||
|
|
|
@ -771,36 +771,6 @@ vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
|
||||||
+OkuE6N36B9K
|
+OkuE6N36B9K
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
# Issuer: CN=Class 2 Primary CA O=Certplus
|
|
||||||
# Subject: CN=Class 2 Primary CA O=Certplus
|
|
||||||
# Label: "Certplus Class 2 Primary CA"
|
|
||||||
# Serial: 177770208045934040241468760488327595043
|
|
||||||
# MD5 Fingerprint: 88:2c:8c:52:b8:a2:3c:f3:f7:bb:03:ea:ae:ac:42:0b
|
|
||||||
# SHA1 Fingerprint: 74:20:74:41:72:9c:dd:92:ec:79:31:d8:23:10:8d:c2:81:92:e2:bb
|
|
||||||
# SHA256 Fingerprint: 0f:99:3c:8a:ef:97:ba:af:56:87:14:0e:d5:9a:d1:82:1b:b4:af:ac:f0:aa:9a:58:b5:d5:7a:33:8a:3a:fb:cb
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw
|
|
||||||
PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz
|
|
||||||
cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9
|
|
||||||
MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz
|
|
||||||
IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ
|
|
||||||
ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR
|
|
||||||
VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL
|
|
||||||
kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd
|
|
||||||
EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas
|
|
||||||
H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0
|
|
||||||
HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud
|
|
||||||
DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4
|
|
||||||
QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu
|
|
||||||
Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/
|
|
||||||
AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8
|
|
||||||
yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR
|
|
||||||
FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA
|
|
||||||
ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB
|
|
||||||
kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7
|
|
||||||
l7+ijrRU
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
# Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co.
|
# Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co.
|
||||||
# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co.
|
# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co.
|
||||||
# Label: "DST Root CA X3"
|
# Label: "DST Root CA X3"
|
||||||
|
@ -1219,36 +1189,6 @@ t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
|
||||||
WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
|
WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
# Issuer: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center
|
|
||||||
# Subject: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center
|
|
||||||
# Label: "Deutsche Telekom Root CA 2"
|
|
||||||
# Serial: 38
|
|
||||||
# MD5 Fingerprint: 74:01:4a:91:b1:08:c4:58:ce:47:cd:f0:dd:11:53:08
|
|
||||||
# SHA1 Fingerprint: 85:a4:08:c0:9c:19:3e:5d:51:58:7d:cd:d6:13:30:fd:8c:de:37:bf
|
|
||||||
# SHA256 Fingerprint: b6:19:1a:50:d0:c3:97:7f:7d:a9:9b:cd:aa:c8:6a:22:7d:ae:b9:67:9e:c7:0b:a3:b0:c9:d9:22:71:c1:70:d3
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc
|
|
||||||
MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj
|
|
||||||
IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB
|
|
||||||
IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE
|
|
||||||
RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl
|
|
||||||
U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290
|
|
||||||
IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU
|
|
||||||
ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC
|
|
||||||
QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr
|
|
||||||
rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S
|
|
||||||
NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc
|
|
||||||
QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH
|
|
||||||
txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP
|
|
||||||
BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC
|
|
||||||
AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp
|
|
||||||
tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa
|
|
||||||
IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl
|
|
||||||
6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+
|
|
||||||
xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU
|
|
||||||
Cm26OWMohpLzGITY+9HPBVZkVw==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc
|
# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc
|
||||||
# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc
|
# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc
|
||||||
# Label: "Cybertrust Global Root"
|
# Label: "Cybertrust Global Root"
|
||||||
|
@ -3453,46 +3393,6 @@ AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ
|
||||||
5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
|
5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
# Issuer: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903
|
|
||||||
# Subject: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903
|
|
||||||
# Label: "Certinomis - Root CA"
|
|
||||||
# Serial: 1
|
|
||||||
# MD5 Fingerprint: 14:0a:fd:8d:a8:28:b5:38:69:db:56:7e:61:22:03:3f
|
|
||||||
# SHA1 Fingerprint: 9d:70:bb:01:a5:a4:a0:18:11:2e:f7:1c:01:b9:32:c5:34:e7:88:a8
|
|
||||||
# SHA256 Fingerprint: 2a:99:f5:bc:11:74:b7:3c:bb:1d:62:08:84:e0:1c:34:e5:1c:cb:39:78:da:12:5f:0e:33:26:88:83:bf:41:58
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET
|
|
||||||
MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb
|
|
||||||
BgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz
|
|
||||||
MTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx
|
|
||||||
FzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g
|
|
||||||
Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2
|
|
||||||
fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl
|
|
||||||
LieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV
|
|
||||||
WZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF
|
|
||||||
TKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb
|
|
||||||
5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc
|
|
||||||
CbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri
|
|
||||||
wsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ
|
|
||||||
wx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG
|
|
||||||
m/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4
|
|
||||||
F2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng
|
|
||||||
WVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB
|
|
||||||
BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0
|
|
||||||
2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF
|
|
||||||
AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/
|
|
||||||
0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw
|
|
||||||
F6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS
|
|
||||||
g081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj
|
|
||||||
qh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN
|
|
||||||
h4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/
|
|
||||||
ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V
|
|
||||||
btaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj
|
|
||||||
Y/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ
|
|
||||||
8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW
|
|
||||||
gQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
||||||
# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
|
# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
|
||||||
# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
|
# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
|
||||||
# Label: "OISTE WISeKey Global Root GB CA"
|
# Label: "OISTE WISeKey Global Root GB CA"
|
||||||
|
@ -4656,3 +4556,47 @@ L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa
|
||||||
LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG
|
LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG
|
||||||
mpv0
|
mpv0
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
# Issuer: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only
|
||||||
|
# Subject: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only
|
||||||
|
# Label: "Entrust Root Certification Authority - G4"
|
||||||
|
# Serial: 289383649854506086828220374796556676440
|
||||||
|
# MD5 Fingerprint: 89:53:f1:83:23:b7:7c:8e:05:f1:8c:71:38:4e:1f:88
|
||||||
|
# SHA1 Fingerprint: 14:88:4e:86:26:37:b0:26:af:59:62:5c:40:77:ec:35:29:ba:96:01
|
||||||
|
# SHA256 Fingerprint: db:35:17:d1:f6:73:2a:2d:5a:b9:7c:53:3e:c7:07:79:ee:32:70:a6:2f:b4:ac:42:38:37:24:60:e6:f0:1e:88
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw
|
||||||
|
gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL
|
||||||
|
Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg
|
||||||
|
MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw
|
||||||
|
BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0
|
||||||
|
MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT
|
||||||
|
MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1
|
||||||
|
c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ
|
||||||
|
bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg
|
||||||
|
Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B
|
||||||
|
AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ
|
||||||
|
2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E
|
||||||
|
T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j
|
||||||
|
5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM
|
||||||
|
C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T
|
||||||
|
DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX
|
||||||
|
wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A
|
||||||
|
2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm
|
||||||
|
nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8
|
||||||
|
dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl
|
||||||
|
N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj
|
||||||
|
c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
|
||||||
|
VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS
|
||||||
|
5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS
|
||||||
|
Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr
|
||||||
|
hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/
|
||||||
|
B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI
|
||||||
|
AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw
|
||||||
|
H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+
|
||||||
|
b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk
|
||||||
|
2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol
|
||||||
|
IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk
|
||||||
|
5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY
|
||||||
|
n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
|
@ -172,7 +172,7 @@ def initialize(config_file):
|
||||||
SYS_TIMEZONE.zone, SYS_UTC_OFFSET
|
SYS_TIMEZONE.zone, SYS_UTC_OFFSET
|
||||||
))
|
))
|
||||||
logger.info("Python {}".format(
|
logger.info("Python {}".format(
|
||||||
sys.version
|
sys.version.replace('\n', '')
|
||||||
))
|
))
|
||||||
logger.info("Program Dir: {}".format(
|
logger.info("Program Dir: {}".format(
|
||||||
PROG_DIR
|
PROG_DIR
|
||||||
|
@ -450,6 +450,8 @@ def initialize_scheduler():
|
||||||
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
|
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
|
||||||
schedule_job(config.make_backup, 'Backup Tautulli config',
|
schedule_job(config.make_backup, 'Backup Tautulli config',
|
||||||
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
|
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
|
||||||
|
schedule_job(helpers.update_geoip_db, 'Update GeoLite2 database',
|
||||||
|
hours=12 * bool(CONFIG.GEOIP_DB_INSTALLED), minutes=0, seconds=0)
|
||||||
|
|
||||||
if WS_CONNECTED and CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
|
if WS_CONNECTED and CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
|
||||||
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
|
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
|
||||||
|
@ -588,12 +590,14 @@ def dbcheck():
|
||||||
'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, '
|
'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, '
|
||||||
'transcode_decision TEXT, container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, '
|
'transcode_decision TEXT, container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, '
|
||||||
'video_codec TEXT, video_bitrate INTEGER, video_resolution TEXT, video_width INTEGER, video_height INTEGER, '
|
'video_codec TEXT, video_bitrate INTEGER, video_resolution TEXT, video_width INTEGER, video_height INTEGER, '
|
||||||
'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, aspect_ratio TEXT, '
|
'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, '
|
||||||
|
'video_dynamic_range TEXT, aspect_ratio TEXT, '
|
||||||
'audio_codec TEXT, audio_bitrate INTEGER, audio_channels INTEGER, subtitle_codec TEXT, '
|
'audio_codec TEXT, audio_bitrate INTEGER, audio_channels INTEGER, subtitle_codec TEXT, '
|
||||||
'stream_bitrate INTEGER, stream_video_resolution TEXT, quality_profile TEXT, '
|
'stream_bitrate INTEGER, stream_video_resolution TEXT, quality_profile TEXT, '
|
||||||
'stream_container_decision TEXT, stream_container TEXT, '
|
'stream_container_decision TEXT, stream_container TEXT, '
|
||||||
'stream_video_decision TEXT, stream_video_codec TEXT, stream_video_bitrate INTEGER, stream_video_width INTEGER, '
|
'stream_video_decision TEXT, stream_video_codec TEXT, stream_video_bitrate INTEGER, stream_video_width INTEGER, '
|
||||||
'stream_video_height INTEGER, stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, '
|
'stream_video_height INTEGER, stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, '
|
||||||
|
'stream_video_dynamic_range TEXT, '
|
||||||
'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, '
|
'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, '
|
||||||
'subtitles INTEGER, stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, '
|
'subtitles INTEGER, stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, '
|
||||||
'transcode_protocol TEXT, transcode_container TEXT, '
|
'transcode_protocol TEXT, transcode_container TEXT, '
|
||||||
|
@ -623,7 +627,7 @@ def dbcheck():
|
||||||
'video_decision TEXT, audio_decision TEXT, transcode_decision TEXT, duration INTEGER DEFAULT 0, '
|
'video_decision TEXT, audio_decision TEXT, transcode_decision TEXT, duration INTEGER DEFAULT 0, '
|
||||||
'container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, video_bitrate INTEGER, video_bit_depth INTEGER, '
|
'container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, video_bitrate INTEGER, video_bit_depth INTEGER, '
|
||||||
'video_codec TEXT, video_codec_level TEXT, video_width INTEGER, video_height INTEGER, video_resolution TEXT, '
|
'video_codec TEXT, video_codec_level TEXT, video_width INTEGER, video_height INTEGER, video_resolution TEXT, '
|
||||||
'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, aspect_ratio TEXT, '
|
'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, video_dynamic_range TEXT, aspect_ratio TEXT, '
|
||||||
'audio_bitrate INTEGER, audio_codec TEXT, audio_channels INTEGER, transcode_protocol TEXT, '
|
'audio_bitrate INTEGER, audio_codec TEXT, audio_channels INTEGER, transcode_protocol TEXT, '
|
||||||
'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, '
|
'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, '
|
||||||
'transcode_audio_channels INTEGER, transcode_width INTEGER, transcode_height INTEGER, '
|
'transcode_audio_channels INTEGER, transcode_width INTEGER, transcode_height INTEGER, '
|
||||||
|
@ -633,7 +637,7 @@ def dbcheck():
|
||||||
'stream_container TEXT, stream_container_decision TEXT, stream_bitrate INTEGER, '
|
'stream_container TEXT, stream_container_decision TEXT, stream_bitrate INTEGER, '
|
||||||
'stream_video_decision TEXT, stream_video_bitrate INTEGER, stream_video_codec TEXT, stream_video_codec_level TEXT, '
|
'stream_video_decision TEXT, stream_video_bitrate INTEGER, stream_video_codec TEXT, stream_video_codec_level TEXT, '
|
||||||
'stream_video_bit_depth INTEGER, stream_video_height INTEGER, stream_video_width INTEGER, stream_video_resolution TEXT, '
|
'stream_video_bit_depth INTEGER, stream_video_height INTEGER, stream_video_width INTEGER, stream_video_resolution TEXT, '
|
||||||
'stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, '
|
'stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, stream_video_dynamic_range TEXT, '
|
||||||
'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, '
|
'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, '
|
||||||
'stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, stream_subtitle_container TEXT, stream_subtitle_forced INTEGER, '
|
'stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, stream_subtitle_container TEXT, stream_subtitle_forced INTEGER, '
|
||||||
'subtitles INTEGER, subtitle_codec TEXT, synced_version INTEGER, synced_version_profile TEXT, '
|
'subtitles INTEGER, subtitle_codec TEXT, synced_version INTEGER, synced_version_profile TEXT, '
|
||||||
|
@ -1206,6 +1210,18 @@ def dbcheck():
|
||||||
'ALTER TABLE sessions ADD COLUMN stream_video_full_resolution TEXT'
|
'ALTER TABLE sessions ADD COLUMN stream_video_full_resolution TEXT'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Upgrade sessions table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT video_dynamic_range FROM sessions')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table sessions.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE sessions ADD COLUMN video_dynamic_range TEXT'
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE sessions ADD COLUMN stream_video_dynamic_range TEXT'
|
||||||
|
)
|
||||||
|
|
||||||
# Upgrade session_history table from earlier versions
|
# Upgrade session_history table from earlier versions
|
||||||
try:
|
try:
|
||||||
c_db.execute('SELECT reference_id FROM session_history')
|
c_db.execute('SELECT reference_id FROM session_history')
|
||||||
|
@ -1544,6 +1560,17 @@ def dbcheck():
|
||||||
'ELSE stream_video_resolution || "p" END)'
|
'ELSE stream_video_resolution || "p" END)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Upgrade session_history_media_info table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT video_dynamic_range FROM session_history_media_info')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table session_history_media_info.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE session_history_media_info ADD COLUMN video_dynamic_range TEXT '
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE session_history_media_info ADD COLUMN stream_video_dynamic_range TEXT '
|
||||||
|
)
|
||||||
# Upgrade users table from earlier versions
|
# Upgrade users table from earlier versions
|
||||||
try:
|
try:
|
||||||
c_db.execute('SELECT do_notify FROM users')
|
c_db.execute('SELECT do_notify FROM users')
|
||||||
|
|
|
@ -83,6 +83,7 @@ class ActivityProcessor(object):
|
||||||
'video_framerate': session.get('video_framerate', ''),
|
'video_framerate': session.get('video_framerate', ''),
|
||||||
'video_scan_type': session.get('video_scan_type', ''),
|
'video_scan_type': session.get('video_scan_type', ''),
|
||||||
'video_full_resolution': session.get('video_full_resolution', ''),
|
'video_full_resolution': session.get('video_full_resolution', ''),
|
||||||
|
'video_dynamic_range': session.get('video_dynamic_range', ''),
|
||||||
'aspect_ratio': session.get('aspect_ratio', ''),
|
'aspect_ratio': session.get('aspect_ratio', ''),
|
||||||
'audio_codec': session.get('audio_codec', ''),
|
'audio_codec': session.get('audio_codec', ''),
|
||||||
'audio_bitrate': session.get('audio_bitrate', ''),
|
'audio_bitrate': session.get('audio_bitrate', ''),
|
||||||
|
@ -115,6 +116,7 @@ class ActivityProcessor(object):
|
||||||
'stream_video_framerate': session.get('stream_video_framerate', ''),
|
'stream_video_framerate': session.get('stream_video_framerate', ''),
|
||||||
'stream_video_scan_type': session.get('stream_video_scan_type', ''),
|
'stream_video_scan_type': session.get('stream_video_scan_type', ''),
|
||||||
'stream_video_full_resolution': session.get('stream_video_full_resolution', ''),
|
'stream_video_full_resolution': session.get('stream_video_full_resolution', ''),
|
||||||
|
'stream_video_dynamic_range': session.get('stream_video_dynamic_range', ''),
|
||||||
'stream_audio_decision': session.get('stream_audio_decision', ''),
|
'stream_audio_decision': session.get('stream_audio_decision', ''),
|
||||||
'stream_audio_codec': session.get('stream_audio_codec', ''),
|
'stream_audio_codec': session.get('stream_audio_codec', ''),
|
||||||
'stream_audio_bitrate': session.get('stream_audio_bitrate', ''),
|
'stream_audio_bitrate': session.get('stream_audio_bitrate', ''),
|
||||||
|
@ -358,6 +360,7 @@ class ActivityProcessor(object):
|
||||||
'video_framerate': session['video_framerate'],
|
'video_framerate': session['video_framerate'],
|
||||||
'video_scan_type': session['video_scan_type'],
|
'video_scan_type': session['video_scan_type'],
|
||||||
'video_full_resolution': session['video_full_resolution'],
|
'video_full_resolution': session['video_full_resolution'],
|
||||||
|
'video_dynamic_range': session['video_dynamic_range'],
|
||||||
'aspect_ratio': session['aspect_ratio'],
|
'aspect_ratio': session['aspect_ratio'],
|
||||||
'audio_codec': session['audio_codec'],
|
'audio_codec': session['audio_codec'],
|
||||||
'audio_bitrate': session['audio_bitrate'],
|
'audio_bitrate': session['audio_bitrate'],
|
||||||
|
@ -392,6 +395,7 @@ class ActivityProcessor(object):
|
||||||
'stream_video_framerate': session['stream_video_framerate'],
|
'stream_video_framerate': session['stream_video_framerate'],
|
||||||
'stream_video_scan_type': session['stream_video_scan_type'],
|
'stream_video_scan_type': session['stream_video_scan_type'],
|
||||||
'stream_video_full_resolution': session['stream_video_full_resolution'],
|
'stream_video_full_resolution': session['stream_video_full_resolution'],
|
||||||
|
'stream_video_dynamic_range': session['stream_video_dynamic_range'],
|
||||||
'stream_audio_decision': session['stream_audio_decision'],
|
'stream_audio_decision': session['stream_audio_decision'],
|
||||||
'stream_audio_codec': session['stream_audio_codec'],
|
'stream_audio_codec': session['stream_audio_codec'],
|
||||||
'stream_audio_bitrate': session['stream_audio_bitrate'],
|
'stream_audio_bitrate': session['stream_audio_bitrate'],
|
||||||
|
|
|
@ -61,7 +61,9 @@ PLATFORM_NAME_OVERRIDES = {
|
||||||
'Mystery 3': 'Playstation 3',
|
'Mystery 3': 'Playstation 3',
|
||||||
'Mystery 4': 'Playstation 4',
|
'Mystery 4': 'Playstation 4',
|
||||||
'Mystery 5': 'Xbox 360',
|
'Mystery 5': 'Xbox 360',
|
||||||
'WebMAF': 'Playstation 4'
|
'WebMAF': 'Playstation 4',
|
||||||
|
'windows': 'Windows',
|
||||||
|
'osx': 'macOS'
|
||||||
}
|
}
|
||||||
|
|
||||||
PMS_PLATFORM_NAME_OVERRIDES = {
|
PMS_PLATFORM_NAME_OVERRIDES = {
|
||||||
|
@ -204,7 +206,8 @@ SCHEDULER_LIST = [
|
||||||
'Refresh libraries list',
|
'Refresh libraries list',
|
||||||
'Refresh Plex server URLs',
|
'Refresh Plex server URLs',
|
||||||
'Backup Tautulli database',
|
'Backup Tautulli database',
|
||||||
'Backup Tautulli config'
|
'Backup Tautulli config',
|
||||||
|
'Update GeoLite2 database'
|
||||||
]
|
]
|
||||||
|
|
||||||
DATE_TIME_FORMATS = [
|
DATE_TIME_FORMATS = [
|
||||||
|
@ -367,6 +370,12 @@ NOTIFICATION_PARAMETERS = [
|
||||||
{'name': 'Stream Video Codec Level', 'type': 'int', 'value': 'stream_video_codec_level', 'description': 'The video codec level of the stream.'},
|
{'name': 'Stream Video Codec Level', 'type': 'int', 'value': 'stream_video_codec_level', 'description': 'The video codec level of the stream.'},
|
||||||
{'name': 'Stream Video Bitrate', 'type': 'int', 'value': 'stream_video_bitrate', 'description': 'The video bitrate (in kbps) of the stream.'},
|
{'name': 'Stream Video Bitrate', 'type': 'int', 'value': 'stream_video_bitrate', 'description': 'The video bitrate (in kbps) of the stream.'},
|
||||||
{'name': 'Stream Video Bit Depth', 'type': 'int', 'value': 'stream_video_bit_depth', 'description': 'The video bit depth of the stream.'},
|
{'name': 'Stream Video Bit Depth', 'type': 'int', 'value': 'stream_video_bit_depth', 'description': 'The video bit depth of the stream.'},
|
||||||
|
{'name': 'Stream Video Chroma Subsampling', 'type': 'str', 'value': 'stream_video_chroma_subsampling', 'description': 'The video chroma subsampling of the stream.'},
|
||||||
|
{'name': 'Stream Video Color Primaries', 'type': 'srt', 'value': 'stream_video_color_primaries', 'description': 'The video color primaries of the stream.'},
|
||||||
|
{'name': 'Stream Video Color Range', 'type': 'srt', 'value': 'stream_video_color_range', 'description': 'The video color range of the stream.'},
|
||||||
|
{'name': 'Stream Video Color Space', 'type': 'str', 'value': 'stream_video_color_space', 'description': 'The video color space of the stream.'},
|
||||||
|
{'name': 'Stream Video Color Transfer Function', 'type': 'str', 'value': 'stream_video_color_trc', 'description': 'The video transfer function of the stream.'},
|
||||||
|
{'name': 'Stream Video Dynamic Range', 'type': 'str', 'value': 'stream_video_dynamic_range', 'description': 'The video dynamic range of the stream.', 'example': 'HDR or SDR'},
|
||||||
{'name': 'Stream Video Framerate', 'type': 'str', 'value': 'stream_video_framerate', 'description': 'The video framerate of the stream.'},
|
{'name': 'Stream Video Framerate', 'type': 'str', 'value': 'stream_video_framerate', 'description': 'The video framerate of the stream.'},
|
||||||
{'name': 'Stream Video Full Resolution', 'type': 'str', 'value': 'stream_video_full_resolution', 'description': 'The video resolution of the stream with scan type.'},
|
{'name': 'Stream Video Full Resolution', 'type': 'str', 'value': 'stream_video_full_resolution', 'description': 'The video resolution of the stream with scan type.'},
|
||||||
{'name': 'Stream Video Ref Frames', 'type': 'int', 'value': 'stream_video_ref_frames', 'description': 'The video reference frames of the stream.'},
|
{'name': 'Stream Video Ref Frames', 'type': 'int', 'value': 'stream_video_ref_frames', 'description': 'The video reference frames of the stream.'},
|
||||||
|
@ -474,6 +483,12 @@ NOTIFICATION_PARAMETERS = [
|
||||||
{'name': 'Video Codec Level', 'type': 'int', 'value': 'video_codec_level', 'description': 'The video codec level of the original media.'},
|
{'name': 'Video Codec Level', 'type': 'int', 'value': 'video_codec_level', 'description': 'The video codec level of the original media.'},
|
||||||
{'name': 'Video Bitrate', 'type': 'int', 'value': 'video_bitrate', 'description': 'The video bitrate of the original media.'},
|
{'name': 'Video Bitrate', 'type': 'int', 'value': 'video_bitrate', 'description': 'The video bitrate of the original media.'},
|
||||||
{'name': 'Video Bit Depth', 'type': 'int', 'value': 'video_bit_depth', 'description': 'The video bit depth of the original media.'},
|
{'name': 'Video Bit Depth', 'type': 'int', 'value': 'video_bit_depth', 'description': 'The video bit depth of the original media.'},
|
||||||
|
{'name': 'Video Chroma Subsampling', 'type': 'str', 'value': 'video_chroma_subsampling', 'description': 'The video chroma subsampling of the original media.'},
|
||||||
|
{'name': 'Video Color Primaries', 'type': 'srt', 'value': 'video_color_primaries', 'description': 'The video color primaries of the original media.'},
|
||||||
|
{'name': 'Video Color Range', 'type': 'srt', 'value': 'video_color_range', 'description': 'The video color range of the original media.'},
|
||||||
|
{'name': 'Video Color Space', 'type': 'str', 'value': 'video_color_space', 'description': 'The video color space of the original media.'},
|
||||||
|
{'name': 'Video Color Transfer Function', 'type': 'str', 'value': 'video_color_trc', 'description': 'The video transfer function of the original media.'},
|
||||||
|
{'name': 'Video Dynamic Range', 'type': 'str', 'value': 'video_dynamic_range', 'description': 'The video dynamic range of the original media.', 'example': 'HDR or SDR'},
|
||||||
{'name': 'Video Framerate', 'type': 'str', 'value': 'video_framerate', 'description': 'The video framerate of the original media.'},
|
{'name': 'Video Framerate', 'type': 'str', 'value': 'video_framerate', 'description': 'The video framerate of the original media.'},
|
||||||
{'name': 'Video Full Resolution', 'type': 'str', 'value': 'video_full_resolution', 'description': 'The video resolution of the original media with scan type.'},
|
{'name': 'Video Full Resolution', 'type': 'str', 'value': 'video_full_resolution', 'description': 'The video resolution of the original media with scan type.'},
|
||||||
{'name': 'Video Ref Frames', 'type': 'int', 'value': 'video_ref_frames', 'description': 'The video reference frames of the original media.'},
|
{'name': 'Video Ref Frames', 'type': 'int', 'value': 'video_ref_frames', 'description': 'The video reference frames of the original media.'},
|
||||||
|
|
|
@ -180,6 +180,8 @@ _CONFIG_DEFINITIONS = {
|
||||||
'FIRST_RUN_COMPLETE': (int, 'General', 0),
|
'FIRST_RUN_COMPLETE': (int, 'General', 0),
|
||||||
'FREEZE_DB': (int, 'General', 0),
|
'FREEZE_DB': (int, 'General', 0),
|
||||||
'GEOIP_DB': (str, 'General', ''),
|
'GEOIP_DB': (str, 'General', ''),
|
||||||
|
'GEOIP_DB_INSTALLED': (int, 'General', 0),
|
||||||
|
'GEOIP_DB_UPDATE_DAYS': (int, 'General', 30),
|
||||||
'GET_FILE_SIZES': (int, 'General', 0),
|
'GET_FILE_SIZES': (int, 'General', 0),
|
||||||
'GET_FILE_SIZES_HOLD': (dict, 'General', {'section_ids': [], 'rating_keys': []}),
|
'GET_FILE_SIZES_HOLD': (dict, 'General', {'section_ids': [], 'rating_keys': []}),
|
||||||
'GIT_BRANCH': (str, 'General', 'master'),
|
'GIT_BRANCH': (str, 'General', 'master'),
|
||||||
|
@ -294,6 +296,7 @@ _CONFIG_DEFINITIONS = {
|
||||||
'LOG_BLACKLIST': (int, 'General', 1),
|
'LOG_BLACKLIST': (int, 'General', 1),
|
||||||
'LOG_DIR': (str, 'General', ''),
|
'LOG_DIR': (str, 'General', ''),
|
||||||
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
|
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
|
||||||
|
'MAXMIND_LICENSE_KEY': (str, 'General', ''),
|
||||||
'METADATA_CACHE_SECONDS': (int, 'Advanced', 1800),
|
'METADATA_CACHE_SECONDS': (int, 'Advanced', 1800),
|
||||||
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
||||||
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
||||||
|
@ -934,3 +937,9 @@ class Config(object):
|
||||||
self.BUFFER_THRESHOLD = max(self.BUFFER_THRESHOLD, 10)
|
self.BUFFER_THRESHOLD = max(self.BUFFER_THRESHOLD, 10)
|
||||||
|
|
||||||
self.CONFIG_VERSION = 13
|
self.CONFIG_VERSION = 13
|
||||||
|
|
||||||
|
if self.CONFIG_VERSION == 13:
|
||||||
|
if not self.GEOIP_DB:
|
||||||
|
self.GEOIP_DB = os.path.join(plexpy.DATA_DIR, 'GeoLite2-City.mmdb')
|
||||||
|
|
||||||
|
self.CONFIG_VERSION = 14
|
||||||
|
|
|
@ -888,11 +888,12 @@ class DataFactory(object):
|
||||||
query = 'SELECT bitrate, video_full_resolution, ' \
|
query = 'SELECT bitrate, video_full_resolution, ' \
|
||||||
'optimized_version, optimized_version_profile, optimized_version_title, ' \
|
'optimized_version, optimized_version_profile, optimized_version_title, ' \
|
||||||
'synced_version, synced_version_profile, ' \
|
'synced_version, synced_version_profile, ' \
|
||||||
'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \
|
'container, video_codec, video_bitrate, video_width, video_height, video_framerate, ' \
|
||||||
|
'video_dynamic_range, aspect_ratio, ' \
|
||||||
'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \
|
'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \
|
||||||
'stream_bitrate, stream_video_full_resolution, quality_profile, stream_container_decision, stream_container, ' \
|
'stream_bitrate, stream_video_full_resolution, quality_profile, stream_container_decision, stream_container, ' \
|
||||||
'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \
|
'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \
|
||||||
'stream_video_framerate, ' \
|
'stream_video_framerate, stream_video_dynamic_range, ' \
|
||||||
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
|
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
|
||||||
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
|
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
|
||||||
'transcode_hw_decoding, transcode_hw_encoding, ' \
|
'transcode_hw_decoding, transcode_hw_encoding, ' \
|
||||||
|
@ -909,11 +910,12 @@ class DataFactory(object):
|
||||||
query = 'SELECT bitrate, video_full_resolution, ' \
|
query = 'SELECT bitrate, video_full_resolution, ' \
|
||||||
'optimized_version, optimized_version_profile, optimized_version_title, ' \
|
'optimized_version, optimized_version_profile, optimized_version_title, ' \
|
||||||
'synced_version, synced_version_profile, ' \
|
'synced_version, synced_version_profile, ' \
|
||||||
'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \
|
'container, video_codec, video_bitrate, video_width, video_height, video_framerate, ' \
|
||||||
|
'video_dynamic_range, aspect_ratio, ' \
|
||||||
'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \
|
'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \
|
||||||
'stream_bitrate, stream_video_full_resolution, quality_profile, stream_container_decision, stream_container, ' \
|
'stream_bitrate, stream_video_full_resolution, quality_profile, stream_container_decision, stream_container, ' \
|
||||||
'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \
|
'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \
|
||||||
'stream_video_framerate, ' \
|
'stream_video_framerate, stream_video_dynamic_range, ' \
|
||||||
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
|
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
|
||||||
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
|
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
|
||||||
'transcode_hw_decoding, transcode_hw_encoding, ' \
|
'transcode_hw_decoding, transcode_hw_encoding, ' \
|
||||||
|
@ -960,6 +962,7 @@ class DataFactory(object):
|
||||||
'video_width': item['video_width'],
|
'video_width': item['video_width'],
|
||||||
'video_height': item['video_height'],
|
'video_height': item['video_height'],
|
||||||
'video_framerate': item['video_framerate'],
|
'video_framerate': item['video_framerate'],
|
||||||
|
'video_dynamic_range': item['video_dynamic_range'],
|
||||||
'aspect_ratio': item['aspect_ratio'],
|
'aspect_ratio': item['aspect_ratio'],
|
||||||
'audio_codec': item['audio_codec'],
|
'audio_codec': item['audio_codec'],
|
||||||
'audio_bitrate': item['audio_bitrate'],
|
'audio_bitrate': item['audio_bitrate'],
|
||||||
|
@ -976,6 +979,7 @@ class DataFactory(object):
|
||||||
'stream_video_width': item['stream_video_width'],
|
'stream_video_width': item['stream_video_width'],
|
||||||
'stream_video_height': item['stream_video_height'],
|
'stream_video_height': item['stream_video_height'],
|
||||||
'stream_video_framerate': item['stream_video_framerate'],
|
'stream_video_framerate': item['stream_video_framerate'],
|
||||||
|
'stream_video_dynamic_range': item['stream_video_dynamic_range'],
|
||||||
'stream_audio_decision': item['stream_audio_decision'],
|
'stream_audio_decision': item['stream_audio_decision'],
|
||||||
'stream_audio_codec': item['stream_audio_codec'],
|
'stream_audio_codec': item['stream_audio_codec'],
|
||||||
'stream_audio_bitrate': item['stream_audio_bitrate'],
|
'stream_audio_bitrate': item['stream_audio_bitrate'],
|
||||||
|
|
|
@ -27,6 +27,7 @@ from past.builtins import basestring
|
||||||
from past.utils import old_div
|
from past.utils import old_div
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import certifi
|
||||||
import cloudinary
|
import cloudinary
|
||||||
from cloudinary.api import delete_resources_by_tag
|
from cloudinary.api import delete_resources_by_tag
|
||||||
from cloudinary.uploader import upload
|
from cloudinary.uploader import upload
|
||||||
|
@ -35,7 +36,6 @@ import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import geoip2.database
|
import geoip2.database
|
||||||
import geoip2.errors
|
import geoip2.errors
|
||||||
import gzip
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import imghdr
|
import imghdr
|
||||||
from itertools import zip_longest
|
from itertools import zip_longest
|
||||||
|
@ -50,16 +50,13 @@ from operator import itemgetter
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
import tarfile
|
||||||
import time
|
import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import urllib.request
|
import urllib3
|
||||||
import urllib.parse
|
|
||||||
import urllib.error
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
import xmltodict
|
import xmltodict
|
||||||
|
|
||||||
|
@ -604,83 +601,127 @@ def is_valid_ip(address):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def install_geoip_db():
|
def update_geoip_db():
|
||||||
maxmind_url = 'http://geolite.maxmind.com/download/geoip/database/'
|
if plexpy.CONFIG.GEOIP_DB_INSTALLED:
|
||||||
geolite2_gz = 'GeoLite2-City.mmdb.gz'
|
logger.info(u"Tautulli Helpers :: Checking for GeoLite2 database updates.")
|
||||||
geolite2_md5 = 'GeoLite2-City.md5'
|
now = int(time.time())
|
||||||
geolite2_db = geolite2_gz[:-3]
|
if now - plexpy.CONFIG.GEOIP_DB_INSTALLED >= plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS * 24 * 60 * 60:
|
||||||
md5_checksum = ''
|
return install_geoip_db(update=True)
|
||||||
|
logger.info(u"Tautulli Helpers :: GeoLite2 database already updated within the last %s days."
|
||||||
|
% plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS)
|
||||||
|
|
||||||
|
|
||||||
|
def install_geoip_db(update=False):
|
||||||
|
if not plexpy.CONFIG.MAXMIND_LICENSE_KEY:
|
||||||
|
logger.error(u"Tautulli Helpers :: Failed to download GeoLite2 database file from MaxMind: Missing MaxMindLicense Key")
|
||||||
|
return False
|
||||||
|
|
||||||
|
maxmind_db = 'GeoLite2-City'
|
||||||
|
maxmind_url = 'https://download.maxmind.com/app/geoip_download?edition_id={db}&suffix={{suffix}}&license_key={key}'.format(
|
||||||
|
db=maxmind_db, key=plexpy.CONFIG.MAXMIND_LICENSE_KEY)
|
||||||
|
geolite2_db_url = maxmind_url.format(suffix='tar.gz')
|
||||||
|
geolite2_md5_url = maxmind_url.format(suffix='tar.gz.md5')
|
||||||
|
geolite2_gz = maxmind_db + '.tar.gz'
|
||||||
|
geolite2_md5 = geolite2_gz + '.md5'
|
||||||
|
geolite2_db = maxmind_db + '.mmdb'
|
||||||
|
geolite2_db_path = plexpy.CONFIG.GEOIP_DB or os.path.join(plexpy.DATA_DIR, geolite2_db)
|
||||||
|
|
||||||
|
# Check path ends with .mmdb
|
||||||
|
if os.path.splitext(geolite2_db_path)[1] != os.path.splitext(geolite2_db)[1]:
|
||||||
|
geolite2_db_path = os.path.join(geolite2_db_path, geolite2_db)
|
||||||
|
|
||||||
temp_gz = os.path.join(plexpy.CONFIG.CACHE_DIR, geolite2_gz)
|
temp_gz = os.path.join(plexpy.CONFIG.CACHE_DIR, geolite2_gz)
|
||||||
geolite2_db = plexpy.CONFIG.GEOIP_DB or os.path.join(plexpy.DATA_DIR, geolite2_db)
|
temp_md5 = os.path.join(plexpy.CONFIG.CACHE_DIR, geolite2_md5)
|
||||||
|
|
||||||
# Retrieve the GeoLite2 gzip file
|
# Retrieve the GeoLite2 gzip file
|
||||||
logger.debug("Tautulli Helpers :: Downloading GeoLite2 gzip file from MaxMind...")
|
logger.debug("Tautulli Helpers :: Downloading GeoLite2 gzip file from MaxMind...")
|
||||||
try:
|
try:
|
||||||
maxmind = urllib.request.URLopener()
|
maxmind = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where())
|
||||||
maxmind.retrieve(maxmind_url + geolite2_gz, temp_gz)
|
with maxmind.request('GET', geolite2_db_url, preload_content=False) as r_db, open(temp_gz, 'wb') as f_db:
|
||||||
md5_checksum = urllib.request.urlopen(maxmind_url + geolite2_md5).read()
|
shutil.copyfileobj(r_db, f_db)
|
||||||
|
with maxmind.request('GET', geolite2_md5_url, preload_content=False) as r_md5, open(temp_md5, 'wb') as f_md5:
|
||||||
|
shutil.copyfileobj(r_md5, f_md5)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli Helpers :: Failed to download GeoLite2 gzip file from MaxMind: %s" % e)
|
logger.error("Tautulli Helpers :: Failed to download GeoLite2 gzip file from MaxMind: %s" % e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Extract the GeoLite2 database file
|
# Check MD5 hash for GeoLite2 tar.gz file
|
||||||
logger.debug("Tautulli Helpers :: Extracting GeoLite2 database...")
|
logger.debug(u"Tautulli Helpers :: Checking MD5 checksum for GeoLite2 gzip file...")
|
||||||
try:
|
|
||||||
with gzip.open(temp_gz, 'rb') as gz:
|
|
||||||
with open(geolite2_db, 'wb') as db:
|
|
||||||
db.write(gz.read())
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Tautulli Helpers :: Failed to extract the GeoLite2 database: %s" % e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check MD5 hash for GeoLite2 database file
|
|
||||||
logger.debug("Tautulli Helpers :: Checking MD5 checksum for GeoLite2 database...")
|
|
||||||
try:
|
try:
|
||||||
hash_md5 = hashlib.md5()
|
hash_md5 = hashlib.md5()
|
||||||
with open(geolite2_db, 'rb') as f:
|
with open(temp_gz, 'rb') as f:
|
||||||
for chunk in iter(lambda: f.read(4096), b""):
|
for chunk in iter(lambda: f.read(4096), b""):
|
||||||
hash_md5.update(chunk)
|
hash_md5.update(chunk)
|
||||||
md5_hash = hash_md5.hexdigest()
|
md5_hash = hash_md5.hexdigest()
|
||||||
|
|
||||||
|
with open(temp_md5, 'r') as f:
|
||||||
|
md5_checksum = f.read()
|
||||||
|
|
||||||
if md5_hash != md5_checksum:
|
if md5_hash != md5_checksum:
|
||||||
logger.error("Tautulli Helpers :: MD5 checksum doesn't match for GeoLite2 database. "
|
logger.error("Tautulli Helpers :: MD5 checksum doesn't match for GeoLite2 database. "
|
||||||
"Checksum: %s, file hash: %s" % (md5_checksum, md5_hash))
|
"Checksum: %s, file hash: %s" % (md5_checksum, md5_hash))
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli Helpers :: Failed to generate MD5 checksum for GeoLite2 database: %s" % e)
|
logger.error(u"Tautulli Helpers :: Failed to generate MD5 checksum for GeoLite2 gzip file: %s" % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Extract the GeoLite2 database file
|
||||||
|
logger.debug(u"Tautulli Helpers :: Extracting GeoLite2 database...")
|
||||||
|
try:
|
||||||
|
mmdb = None
|
||||||
|
with tarfile.open(temp_gz, 'r:gz') as tar:
|
||||||
|
for member in tar.getmembers():
|
||||||
|
if geolite2_db in member.name:
|
||||||
|
member.name = os.path.basename(member.name)
|
||||||
|
tar.extractall(path=os.path.dirname(geolite2_db_path), members=[member])
|
||||||
|
mmdb = True
|
||||||
|
break
|
||||||
|
if not mmdb:
|
||||||
|
raise Exception("{} not found in gzip file.".format(geolite2_db))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(u"Tautulli Helpers :: Failed to extract the GeoLite2 database: %s" % e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Delete temportary GeoLite2 gzip file
|
# Delete temportary GeoLite2 gzip file
|
||||||
logger.debug("Tautulli Helpers :: Deleting temporary GeoLite2 gzip file...")
|
logger.debug("Tautulli Helpers :: Deleting temporary GeoLite2 gzip file...")
|
||||||
try:
|
try:
|
||||||
os.remove(temp_gz)
|
os.remove(temp_gz)
|
||||||
|
os.remove(temp_md5)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn("Tautulli Helpers :: Failed to remove temporary GeoLite2 gzip file: %s" % e)
|
logger.warn("Tautulli Helpers :: Failed to remove temporary GeoLite2 gzip file: %s" % e)
|
||||||
|
|
||||||
logger.debug("Tautulli Helpers :: GeoLite2 database installed successfully.")
|
plexpy.CONFIG.__setattr__('GEOIP_DB', geolite2_db_path)
|
||||||
plexpy.CONFIG.__setattr__('GEOIP_DB', geolite2_db)
|
plexpy.CONFIG.__setattr__('GEOIP_DB_INSTALLED', int(time.time()))
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
return True
|
logger.debug(u"Tautulli Helpers :: GeoLite2 database installed successfully.")
|
||||||
|
|
||||||
|
if not update:
|
||||||
|
plexpy.schedule_job(update_geoip_db, 'Update GeoLite2 database', hours=12, minutes=0, seconds=0)
|
||||||
|
|
||||||
|
return plexpy.CONFIG.GEOIP_DB_INSTALLED
|
||||||
|
|
||||||
|
|
||||||
def uninstall_geoip_db():
|
def uninstall_geoip_db():
|
||||||
logger.debug("Tautulli Helpers :: Uninstalling the GeoLite2 database...")
|
logger.debug("Tautulli Helpers :: Uninstalling the GeoLite2 database...")
|
||||||
try:
|
try:
|
||||||
os.remove(plexpy.CONFIG.GEOIP_DB)
|
os.remove(plexpy.CONFIG.GEOIP_DB)
|
||||||
plexpy.CONFIG.__setattr__('GEOIP_DB', '')
|
|
||||||
plexpy.CONFIG.write()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli Helpers :: Failed to uninstall the GeoLite2 database: %s" % e)
|
logger.error("Tautulli Helpers :: Failed to uninstall the GeoLite2 database: %s" % e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.debug("Tautulli Helpers :: GeoLite2 database uninstalled successfully.")
|
plexpy.CONFIG.__setattr__('GEOIP_DB_INSTALLED', 0)
|
||||||
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
|
logger.debug(u"Tautulli Helpers :: GeoLite2 database uninstalled successfully.")
|
||||||
|
|
||||||
|
plexpy.schedule_job(update_geoip_db, 'Update GeoLite2 database', hours=0, minutes=0, seconds=0)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def geoip_lookup(ip_address):
|
def geoip_lookup(ip_address):
|
||||||
if not plexpy.CONFIG.GEOIP_DB:
|
if not plexpy.CONFIG.GEOIP_DB_INSTALLED:
|
||||||
return 'GeoLite2 database not installed. Please install from the ' \
|
return 'GeoLite2 database not installed. Please install from the ' \
|
||||||
'<a href="settings?install_geoip=true">Settings</a> page.'
|
'<a href="settings?install_geoip=true">Settings</a> page.'
|
||||||
|
|
||||||
|
@ -698,7 +739,7 @@ def geoip_lookup(ip_address):
|
||||||
'<a href="settings?install_geoip=true">Settings</a> page.'
|
'<a href="settings?install_geoip=true">Settings</a> page.'
|
||||||
except maxminddb.InvalidDatabaseError as e:
|
except maxminddb.InvalidDatabaseError as e:
|
||||||
return 'Invalid GeoLite2 database. Please reinstall from the ' \
|
return 'Invalid GeoLite2 database. Please reinstall from the ' \
|
||||||
'<a href="settings?reinstall_geoip=true">Settings</a> page.'
|
'<a href="settings?install_geoip=true">Settings</a> page.'
|
||||||
except geoip2.errors.AddressNotFoundError as e:
|
except geoip2.errors.AddressNotFoundError as e:
|
||||||
return '%s' % e
|
return '%s' % e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -88,7 +88,7 @@ class BlacklistFilter(logging.Filter):
|
||||||
Log filter for blacklisted tokens and passwords
|
Log filter for blacklisted tokens and passwords
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
super(BlacklistFilter, self).__init__()
|
||||||
|
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
if not plexpy.CONFIG.LOG_BLACKLIST:
|
if not plexpy.CONFIG.LOG_BLACKLIST:
|
||||||
|
@ -106,30 +106,29 @@ class BlacklistFilter(logging.Filter):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class PublicIPFilter(logging.Filter):
|
class RegexFilter(logging.Filter):
|
||||||
"""
|
"""
|
||||||
Log filter for public IP addresses
|
Base class for regex log filter
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
super(RegexFilter, self).__init__()
|
||||||
|
|
||||||
|
self.regex = re.compile(r'')
|
||||||
|
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
if not plexpy.CONFIG.LOG_BLACKLIST:
|
if not plexpy.CONFIG.LOG_BLACKLIST:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Currently only checking for ipv4 addresses
|
matches = self.regex.findall(record.msg)
|
||||||
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', record.msg)
|
for match in matches:
|
||||||
for ip in ipv4:
|
record.msg = self.replace(record.msg, match)
|
||||||
if is_public_ip(ip):
|
|
||||||
record.msg = record.msg.replace(ip, ip.partition('.')[0] + '.***.***.***')
|
|
||||||
|
|
||||||
args = []
|
args = []
|
||||||
for arg in record.args:
|
for arg in record.args:
|
||||||
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', arg) if isinstance(arg, basestring) else []
|
matches = self.regex.findall(arg) if isinstance(arg, basestring) else []
|
||||||
for ip in ipv4:
|
for match in matches:
|
||||||
if is_public_ip(ip):
|
arg = self.replace(arg, match)
|
||||||
arg = arg.replace(ip, ip.partition('.')[0] + '.***.***.***')
|
|
||||||
args.append(arg)
|
args.append(arg)
|
||||||
record.args = tuple(args)
|
record.args = tuple(args)
|
||||||
except:
|
except:
|
||||||
|
@ -137,31 +136,53 @@ class PublicIPFilter(logging.Filter):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def replace(self, text, match):
|
||||||
|
return text
|
||||||
|
|
||||||
class PlexTokenFilter(logging.Filter):
|
|
||||||
|
class PublicIPFilter(RegexFilter):
|
||||||
|
"""
|
||||||
|
Log filter for public IP addresses
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
super(PublicIPFilter, self).__init__()
|
||||||
|
|
||||||
|
# Currently only checking for ipv4 addresses
|
||||||
|
self.regex = re.compile(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})')
|
||||||
|
|
||||||
|
def replace(self, text, ip):
|
||||||
|
if is_public_ip(ip):
|
||||||
|
return text.replace(ip, ip.partition('.')[0] + '.***.***.***')
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class EmailFilter(RegexFilter):
|
||||||
|
"""
|
||||||
|
Log filter for email addresses
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
super(EmailFilter, self).__init__()
|
||||||
|
|
||||||
|
self.regex = re.compile(r'([a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@'
|
||||||
|
r'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)',
|
||||||
|
re.IGNORECASE)
|
||||||
|
|
||||||
|
def replace(self, text, email):
|
||||||
|
email_parts = email.partition('@')
|
||||||
|
return text.replace(email, email_parts[0][:2] + 8 * '*' + email_parts[1] + 8 * '*')
|
||||||
|
|
||||||
|
|
||||||
|
class PlexTokenFilter(RegexFilter):
|
||||||
"""
|
"""
|
||||||
Log filter for X-Plex-Token
|
Log filter for X-Plex-Token
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
super(PlexTokenFilter, self).__init__()
|
||||||
|
|
||||||
def filter(self, record):
|
self.regex = re.compile(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)')
|
||||||
try:
|
|
||||||
tokens = re.findall(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)', record.msg)
|
|
||||||
for token in tokens:
|
|
||||||
record.msg = record.msg.replace(token, 8 * '*' + token[-2:])
|
|
||||||
|
|
||||||
args = []
|
def replace(self, text, token):
|
||||||
for arg in record.args:
|
return text.replace(token, 8 * '*' + token[-2:])
|
||||||
tokens = re.findall(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)', arg) if isinstance(arg, basestring) else []
|
|
||||||
for token in tokens:
|
|
||||||
arg = arg.replace(token, 8 * '*' + token[-2:])
|
|
||||||
args.append(arg)
|
|
||||||
record.args = tuple(args)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
|
@ -302,6 +323,7 @@ def initLogger(console=False, log_dir=False, verbose=False):
|
||||||
for handler in logger.handlers + logger_api.handlers + logger_plex_websocket.handlers:
|
for handler in logger.handlers + logger_api.handlers + logger_plex_websocket.handlers:
|
||||||
handler.addFilter(BlacklistFilter())
|
handler.addFilter(BlacklistFilter())
|
||||||
handler.addFilter(PublicIPFilter())
|
handler.addFilter(PublicIPFilter())
|
||||||
|
handler.addFilter(EmailFilter())
|
||||||
handler.addFilter(PlexTokenFilter())
|
handler.addFilter(PlexTokenFilter())
|
||||||
|
|
||||||
# Install exception hooks
|
# Install exception hooks
|
||||||
|
|
|
@ -841,6 +841,12 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||||
'stream_video_codec_level': notify_params['stream_video_codec_level'],
|
'stream_video_codec_level': notify_params['stream_video_codec_level'],
|
||||||
'stream_video_bitrate': notify_params['stream_video_bitrate'],
|
'stream_video_bitrate': notify_params['stream_video_bitrate'],
|
||||||
'stream_video_bit_depth': notify_params['stream_video_bit_depth'],
|
'stream_video_bit_depth': notify_params['stream_video_bit_depth'],
|
||||||
|
'stream_video_chroma_subsampling': notify_params['stream_video_chroma_subsampling'],
|
||||||
|
'stream_video_color_primaries': notify_params['stream_video_color_primaries'],
|
||||||
|
'stream_video_color_range': notify_params['stream_video_color_range'],
|
||||||
|
'stream_video_color_space': notify_params['stream_video_color_space'],
|
||||||
|
'stream_video_color_trc': notify_params['stream_video_color_trc'],
|
||||||
|
'stream_video_dynamic_range': notify_params['stream_video_dynamic_range'],
|
||||||
'stream_video_framerate': notify_params['stream_video_framerate'],
|
'stream_video_framerate': notify_params['stream_video_framerate'],
|
||||||
'stream_video_full_resolution': notify_params['stream_video_full_resolution'],
|
'stream_video_full_resolution': notify_params['stream_video_full_resolution'],
|
||||||
'stream_video_ref_frames': notify_params['stream_video_ref_frames'],
|
'stream_video_ref_frames': notify_params['stream_video_ref_frames'],
|
||||||
|
@ -951,6 +957,12 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||||
'video_codec_level': notify_params['video_codec_level'],
|
'video_codec_level': notify_params['video_codec_level'],
|
||||||
'video_bitrate': notify_params['video_bitrate'],
|
'video_bitrate': notify_params['video_bitrate'],
|
||||||
'video_bit_depth': notify_params['video_bit_depth'],
|
'video_bit_depth': notify_params['video_bit_depth'],
|
||||||
|
'video_chroma_subsampling': notify_params['video_chroma_subsampling'],
|
||||||
|
'video_color_primaries': notify_params['video_color_primaries'],
|
||||||
|
'video_color_range': notify_params['video_color_range'],
|
||||||
|
'video_color_space': notify_params['video_color_space'],
|
||||||
|
'video_color_trc': notify_params['video_color_trc'],
|
||||||
|
'video_dynamic_range': notify_params['video_dynamic_range'],
|
||||||
'video_framerate': notify_params['video_framerate'],
|
'video_framerate': notify_params['video_framerate'],
|
||||||
'video_full_resolution': notify_params['video_full_resolution'],
|
'video_full_resolution': notify_params['video_full_resolution'],
|
||||||
'video_ref_frames': notify_params['video_ref_frames'],
|
'video_ref_frames': notify_params['video_ref_frames'],
|
||||||
|
|
|
@ -1000,8 +1000,8 @@ class ANDROIDAPP(Notifier):
|
||||||
config_option.append({
|
config_option.append({
|
||||||
'label': 'Device',
|
'label': 'Device',
|
||||||
'description': 'No devices registered. '
|
'description': 'No devices registered. '
|
||||||
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" '
|
'<a data-tab-destination="android_app" data-toggle="tab" data-dismiss="modal">'
|
||||||
'data-target="#top">Get the Android App</a> and register a device.',
|
'Get the Android App</a> and register a device.',
|
||||||
'input_type': 'help'
|
'input_type': 'help'
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
|
@ -1010,8 +1010,8 @@ class ANDROIDAPP(Notifier):
|
||||||
'value': self.config['device_id'],
|
'value': self.config['device_id'],
|
||||||
'name': 'androidapp_device_id',
|
'name': 'androidapp_device_id',
|
||||||
'description': 'Set your Android app device or '
|
'description': 'Set your Android app device or '
|
||||||
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" '
|
'<a data-tab-destination="android_app" data-toggle="tab" data-dismiss="modal">'
|
||||||
'data-target="#top">register a new device</a> with Tautulli.',
|
'register a new device</a> with Tautulli.',
|
||||||
'input_type': 'select',
|
'input_type': 'select',
|
||||||
'select_options': devices
|
'select_options': devices
|
||||||
})
|
})
|
||||||
|
@ -1265,8 +1265,8 @@ class DISCORD(Notifier):
|
||||||
'value': self.config['incl_card'],
|
'value': self.config['incl_card'],
|
||||||
'name': 'discord_incl_card',
|
'name': 'discord_incl_card',
|
||||||
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
|
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
|
||||||
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
|
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
|
||||||
'data-target="#notify_upload_posters">Image Hosting</a> '
|
'data-target="notify_upload_posters">Image Hosting</a> '
|
||||||
'must be enabled under the notifications settings tab.',
|
'must be enabled under the notifications settings tab.',
|
||||||
'input_type': 'checkbox'
|
'input_type': 'checkbox'
|
||||||
},
|
},
|
||||||
|
@ -1640,8 +1640,8 @@ class FACEBOOK(Notifier):
|
||||||
'value': self.config['incl_card'],
|
'value': self.config['incl_card'],
|
||||||
'name': 'facebook_incl_card',
|
'name': 'facebook_incl_card',
|
||||||
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
|
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
|
||||||
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
|
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
|
||||||
'data-target="#notify_upload_posters">Image Hosting</a> '
|
'data-target="notify_upload_posters">Image Hosting</a> '
|
||||||
'must be enabled under the notifications settings tab.',
|
'must be enabled under the notifications settings tab.',
|
||||||
'input_type': 'checkbox'
|
'input_type': 'checkbox'
|
||||||
},
|
},
|
||||||
|
@ -1963,8 +1963,8 @@ class HIPCHAT(Notifier):
|
||||||
'value': self.config['incl_card'],
|
'value': self.config['incl_card'],
|
||||||
'name': 'hipchat_incl_card',
|
'name': 'hipchat_incl_card',
|
||||||
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
|
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
|
||||||
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
|
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
|
||||||
'data-target="#notify_upload_posters">Image Hosting</a> '
|
'data-target="notify_upload_posters">Image Hosting</a> '
|
||||||
'must be enabled under the notifications settings tab.<br>'
|
'must be enabled under the notifications settings tab.<br>'
|
||||||
'Note: This will change the notification type to HTML and emoticons will no longer work.',
|
'Note: This will change the notification type to HTML and emoticons will no longer work.',
|
||||||
'input_type': 'checkbox'
|
'input_type': 'checkbox'
|
||||||
|
@ -2185,8 +2185,8 @@ class JOIN(Notifier):
|
||||||
'value': self.config['incl_poster'],
|
'value': self.config['incl_poster'],
|
||||||
'name': 'join_incl_poster',
|
'name': 'join_incl_poster',
|
||||||
'description': 'Include a poster with the notifications.<br>'
|
'description': 'Include a poster with the notifications.<br>'
|
||||||
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
|
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
|
||||||
'data-target="#notify_upload_posters">Image Hosting</a> '
|
'data-target="notify_upload_posters">Image Hosting</a> '
|
||||||
'must be enabled under the notifications settings tab.',
|
'must be enabled under the notifications settings tab.',
|
||||||
'input_type': 'checkbox'
|
'input_type': 'checkbox'
|
||||||
},
|
},
|
||||||
|
@ -3272,8 +3272,8 @@ class SLACK(Notifier):
|
||||||
'value': self.config['incl_card'],
|
'value': self.config['incl_card'],
|
||||||
'name': 'slack_incl_card',
|
'name': 'slack_incl_card',
|
||||||
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
|
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
|
||||||
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
|
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
|
||||||
'data-target="#notify_upload_posters">Image Hosting</a> '
|
'data-target="notify_upload_posters">Image Hosting</a> '
|
||||||
'must be enabled under the notifications settings tab.',
|
'must be enabled under the notifications settings tab.',
|
||||||
'input_type': 'checkbox'
|
'input_type': 'checkbox'
|
||||||
},
|
},
|
||||||
|
@ -3517,8 +3517,8 @@ class TWITTER(Notifier):
|
||||||
'value': self.config['incl_poster'],
|
'value': self.config['incl_poster'],
|
||||||
'name': 'twitter_incl_poster',
|
'name': 'twitter_incl_poster',
|
||||||
'description': 'Include a poster with the notifications.<br>'
|
'description': 'Include a poster with the notifications.<br>'
|
||||||
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
|
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
|
||||||
'data-target="#notify_upload_posters">Image Hosting</a> '
|
'data-target="notify_upload_posters">Image Hosting</a> '
|
||||||
'must be enabled under the notifications settings tab.',
|
'must be enabled under the notifications settings tab.',
|
||||||
'input_type': 'checkbox'
|
'input_type': 'checkbox'
|
||||||
}
|
}
|
||||||
|
@ -3560,7 +3560,12 @@ class WEBHOOK(Notifier):
|
||||||
if webhook_headers:
|
if webhook_headers:
|
||||||
headers.update(webhook_headers)
|
headers.update(webhook_headers)
|
||||||
|
|
||||||
return self.make_request(self.config['hook'], method=self.config['method'], headers=headers, json=webhook_body)
|
if headers['Content-Type'] == 'application/json':
|
||||||
|
data = {'json': webhook_body}
|
||||||
|
else:
|
||||||
|
data = {'data': webhook_body}
|
||||||
|
|
||||||
|
return self.make_request(self.config['hook'], method=self.config['method'], headers=headers, **data)
|
||||||
|
|
||||||
def _return_config_options(self):
|
def _return_config_options(self):
|
||||||
config_option = [{'label': 'Webhook URL',
|
config_option = [{'label': 'Webhook URL',
|
||||||
|
|
|
@ -1272,6 +1272,11 @@ class PmsConnect(object):
|
||||||
'video_codec_level': helpers.get_xml_attr(stream, 'level'),
|
'video_codec_level': helpers.get_xml_attr(stream, 'level'),
|
||||||
'video_bitrate': helpers.get_xml_attr(stream, 'bitrate'),
|
'video_bitrate': helpers.get_xml_attr(stream, 'bitrate'),
|
||||||
'video_bit_depth': helpers.get_xml_attr(stream, 'bitDepth'),
|
'video_bit_depth': helpers.get_xml_attr(stream, 'bitDepth'),
|
||||||
|
'video_chroma_subsampling': helpers.get_xml_attr(stream, 'chromaSubsampling'),
|
||||||
|
'video_color_primaries': helpers.get_xml_attr(stream, 'colorPrimaries'),
|
||||||
|
'video_color_range': helpers.get_xml_attr(stream, 'colorRange'),
|
||||||
|
'video_color_space': helpers.get_xml_attr(stream, 'colorSpace'),
|
||||||
|
'video_color_trc': helpers.get_xml_attr(stream, 'colorTrc'),
|
||||||
'video_frame_rate': helpers.get_xml_attr(stream, 'frameRate'),
|
'video_frame_rate': helpers.get_xml_attr(stream, 'frameRate'),
|
||||||
'video_ref_frames': helpers.get_xml_attr(stream, 'refFrames'),
|
'video_ref_frames': helpers.get_xml_attr(stream, 'refFrames'),
|
||||||
'video_height': helpers.get_xml_attr(stream, 'height'),
|
'video_height': helpers.get_xml_attr(stream, 'height'),
|
||||||
|
@ -1533,7 +1538,7 @@ class PmsConnect(object):
|
||||||
|
|
||||||
# Get the user details
|
# Get the user details
|
||||||
user_info = session.getElementsByTagName('User')[0]
|
user_info = session.getElementsByTagName('User')[0]
|
||||||
user_details = users.Users().get_details(user=helpers.get_xml_attr(user_info, 'title'))
|
user_details = users.Users().get_details(user_id=helpers.get_xml_attr(user_info, 'id'))
|
||||||
|
|
||||||
# Get the player details
|
# Get the player details
|
||||||
player_info = session.getElementsByTagName('Player')[0]
|
player_info = session.getElementsByTagName('Player')[0]
|
||||||
|
@ -1708,6 +1713,11 @@ class PmsConnect(object):
|
||||||
video_id = helpers.get_xml_attr(video_stream_info, 'id')
|
video_id = helpers.get_xml_attr(video_stream_info, 'id')
|
||||||
video_details = {'stream_video_bitrate': helpers.get_xml_attr(video_stream_info, 'bitrate'),
|
video_details = {'stream_video_bitrate': helpers.get_xml_attr(video_stream_info, 'bitrate'),
|
||||||
'stream_video_bit_depth': helpers.get_xml_attr(video_stream_info, 'bitDepth'),
|
'stream_video_bit_depth': helpers.get_xml_attr(video_stream_info, 'bitDepth'),
|
||||||
|
'stream_video_chroma_subsampling': helpers.get_xml_attr(video_stream_info, 'chromaSubsampling'),
|
||||||
|
'stream_video_color_primaries': helpers.get_xml_attr(video_stream_info, 'colorPrimaries'),
|
||||||
|
'stream_video_color_range': helpers.get_xml_attr(video_stream_info, 'colorRange'),
|
||||||
|
'stream_video_color_space': helpers.get_xml_attr(video_stream_info, 'colorSpace'),
|
||||||
|
'stream_video_color_trc': helpers.get_xml_attr(video_stream_info, 'colorTrc'),
|
||||||
'stream_video_codec_level': helpers.get_xml_attr(video_stream_info, 'level'),
|
'stream_video_codec_level': helpers.get_xml_attr(video_stream_info, 'level'),
|
||||||
'stream_video_ref_frames': helpers.get_xml_attr(video_stream_info, 'refFrames'),
|
'stream_video_ref_frames': helpers.get_xml_attr(video_stream_info, 'refFrames'),
|
||||||
'stream_video_language': helpers.get_xml_attr(video_stream_info, 'language'),
|
'stream_video_language': helpers.get_xml_attr(video_stream_info, 'language'),
|
||||||
|
@ -1718,6 +1728,11 @@ class PmsConnect(object):
|
||||||
else:
|
else:
|
||||||
video_details = {'stream_video_bitrate': '',
|
video_details = {'stream_video_bitrate': '',
|
||||||
'stream_video_bit_depth': '',
|
'stream_video_bit_depth': '',
|
||||||
|
'stream_video_chroma_subsampling': '',
|
||||||
|
'stream_video_color_primaries': '',
|
||||||
|
'stream_video_color_range': '',
|
||||||
|
'stream_video_color_space': '',
|
||||||
|
'stream_video_color_trc': '',
|
||||||
'stream_video_codec_level': '',
|
'stream_video_codec_level': '',
|
||||||
'stream_video_ref_frames': '',
|
'stream_video_ref_frames': '',
|
||||||
'stream_video_language': '',
|
'stream_video_language': '',
|
||||||
|
@ -1896,6 +1911,11 @@ class PmsConnect(object):
|
||||||
'video_codec_level': '',
|
'video_codec_level': '',
|
||||||
'video_bitrate': '',
|
'video_bitrate': '',
|
||||||
'video_bit_depth': '',
|
'video_bit_depth': '',
|
||||||
|
'video_chroma_subsampling': '',
|
||||||
|
'video_color_primaries': '',
|
||||||
|
'video_color_range': '',
|
||||||
|
'video_color_space': '',
|
||||||
|
'video_color_trc': '',
|
||||||
'video_frame_rate': '',
|
'video_frame_rate': '',
|
||||||
'video_ref_frames': '',
|
'video_ref_frames': '',
|
||||||
'video_height': '',
|
'video_height': '',
|
||||||
|
@ -1979,6 +1999,21 @@ class PmsConnect(object):
|
||||||
stream_details['stream_video_resolution'],
|
stream_details['stream_video_resolution'],
|
||||||
stream_details['stream_video_resolution'] + (video_details['stream_video_scan_type'][:1] or 'p'))
|
stream_details['stream_video_resolution'] + (video_details['stream_video_scan_type'][:1] or 'p'))
|
||||||
|
|
||||||
|
if helpers.cast_to_int(source_video_details['video_bit_depth']) > 8 \
|
||||||
|
and source_video_details['video_color_space'] == 'bt2020nc':
|
||||||
|
stream_details['video_dynamic_range'] = 'HDR'
|
||||||
|
else:
|
||||||
|
stream_details['video_dynamic_range'] = 'SDR'
|
||||||
|
|
||||||
|
if helpers.cast_to_int(video_details['stream_video_bit_depth']) > 8 \
|
||||||
|
and video_details['stream_video_color_space'] == 'bt2020nc':
|
||||||
|
stream_details['stream_video_dynamic_range'] = 'HDR'
|
||||||
|
else:
|
||||||
|
stream_details['stream_video_dynamic_range'] = 'SDR'
|
||||||
|
else:
|
||||||
|
stream_details['video_dynamic_range'] = ''
|
||||||
|
stream_details['stream_video_dynamic_range'] = ''
|
||||||
|
|
||||||
# Get the quality profile
|
# Get the quality profile
|
||||||
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
|
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
|
||||||
if sync_id:
|
if sync_id:
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
PLEXPY_BRANCH = "master"
|
PLEXPY_BRANCH = "master"
|
||||||
PLEXPY_RELEASE_VERSION = "v2.1.39"
|
PLEXPY_RELEASE_VERSION = "v2.1.42"
|
||||||
|
|
|
@ -2830,7 +2830,11 @@ class WebInterface(object):
|
||||||
"newsletter_password": plexpy.CONFIG.NEWSLETTER_PASSWORD,
|
"newsletter_password": plexpy.CONFIG.NEWSLETTER_PASSWORD,
|
||||||
"newsletter_inline_styles": checked(plexpy.CONFIG.NEWSLETTER_INLINE_STYLES),
|
"newsletter_inline_styles": checked(plexpy.CONFIG.NEWSLETTER_INLINE_STYLES),
|
||||||
"newsletter_custom_dir": plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR,
|
"newsletter_custom_dir": plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR,
|
||||||
"win_sys_tray": checked(plexpy.CONFIG.WIN_SYS_TRAY)
|
"win_sys_tray": checked(plexpy.CONFIG.WIN_SYS_TRAY),
|
||||||
|
"maxmind_license_key": plexpy.CONFIG.MAXMIND_LICENSE_KEY,
|
||||||
|
"geoip_db": plexpy.CONFIG.GEOIP_DB,
|
||||||
|
"geoip_db_installed": plexpy.CONFIG.GEOIP_DB_INSTALLED,
|
||||||
|
"geoip_db_update_days": plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS
|
||||||
}
|
}
|
||||||
|
|
||||||
return serve_template(templatename="settings.html", title="Settings", config=config, kwargs=kwargs)
|
return serve_template(templatename="settings.html", title="Settings", config=config, kwargs=kwargs)
|
||||||
|
@ -3066,15 +3070,17 @@ class WebInterface(object):
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi()
|
@addtoapi()
|
||||||
def install_geoip_db(self, **kwargs):
|
def install_geoip_db(self, update=False, **kwargs):
|
||||||
""" Downloads and installs the GeoLite2 database """
|
""" Downloads and installs the GeoLite2 database """
|
||||||
|
|
||||||
result = helpers.install_geoip_db()
|
update = True if update == 'true' else False
|
||||||
|
|
||||||
|
result = helpers.install_geoip_db(update=update)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
return {'result': 'success', 'message': 'GeoLite2 database installed successful.'}
|
return {'result': 'success', 'message': 'GeoLite2 database installed successful.', 'updated': result}
|
||||||
else:
|
else:
|
||||||
return {'result': 'error', 'message': 'GeoLite2 database install failed.'}
|
return {'result': 'error', 'message': 'GeoLite2 database install failed.', 'updated': 0}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue