Merge branch 'nightly' into python3

# Conflicts:
#	plexpy/__init__.py
#	plexpy/helpers.py
#	plexpy/logger.py
#	plexpy/version.py
This commit is contained in:
JonnyWong16 2020-01-19 16:40:19 -08:00
commit 485609fbb9
22 changed files with 578 additions and 322 deletions

View file

@ -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:

View file

@ -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'))

View file

@ -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

View file

@ -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 + ')';

View file

@ -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">

View file

@ -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');

View file

@ -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>

View file

@ -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>

View file

@ -1,3 +1,3 @@
from .core import where from .core import where
__version__ = "2019.03.09" __version__ = "2019.11.28"

View file

@ -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-----

View file

@ -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')

View file

@ -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'],

View file

@ -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.'},

View file

@ -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

View file

@ -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'],

View file

@ -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:

View file

@ -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

View file

@ -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'],

View file

@ -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',

View file

@ -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:

View file

@ -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"

View file

@ -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()