Compare commits

..

60 Commits

Author SHA1 Message Date
JonnyWong16
bc2f73d686 v2.1.43 2020-02-03 18:27:41 -08:00
JonnyWong16
645c2ecef6 Use Authorization header for GitHub API 2020-02-03 18:11:56 -08:00
JonnyWong16
4d4a8ca3b2 Trim MaxMind License Key when saving 2020-01-28 10:38:21 -08:00
JonnyWong16
a98bc45c10 Fix HDR for direct stream video 2020-01-24 20:14:48 -08:00
JonnyWong16
b9d5e49a71 Make verbose logging an advanced config option (Fixes Tautulli/Tautulli-Issues#205)
* Verbose logging is enabled by default
* Make toggleVerbose stick
2020-01-20 20:49:37 -08:00
JonnyWong16
e90390be67 Fix plural episodes and albums in newsletter templates (Fixes Tautulli/Tautulli-Issues#203) 2020-01-20 20:08:45 -08:00
JonnyWong16
8ef671c9cb Fix Telegram notification character limit (Closes #1361) 2020-01-20 19:56:38 -08:00
JonnyWong16
8829516cae Fix season 0 number not showing in tables 2020-01-20 19:23:16 -08:00
JonnyWong16
d8e8dfbd45 Revert "Match session user using user_id instead of username"
This reverts commit 6380de3e6c.
2020-01-19 16:43:34 -08:00
JonnyWong16
0b6d9a4890 Add email address log filter 2020-01-19 16:03:10 -08:00
JonnyWong16
55ffd68023 Refactor log filter 2020-01-19 16:02:56 -08:00
JonnyWong16
6380de3e6c Match session user using user_id instead of username 2020-01-19 16:00:36 -08:00
JonnyWong16
d6220a921a Log dynamic range to history and show in stream info modal 2020-01-18 11:46:43 -08:00
JonnyWong16
10e421b9d4 Add color and dynamic range to notification parameters 2020-01-15 12:19:43 -08:00
JonnyWong16
90056bcce2 Indicate HDR content on activity cards 2020-01-15 11:38:50 -08:00
JonnyWong16
4740d0fbf3 Change webhook request data if not Content-Type header is not application/json 2020-01-14 17:36:25 -08:00
JonnyWong16
13374c9ded Fix Windows and macOS platform capitalization 2020-01-14 17:35:35 -08:00
JonnyWong16
2a03be1905 Fix typo in CHANGELOG.md 2020-01-04 17:49:27 -08:00
JonnyWong16
c8f132a750 v2.1.42 2020-01-04 17:46:03 -08:00
JonnyWong16
4d0c4bf4f4 Update GeoLite2 database help text to leave blank for default 2020-01-04 12:03:44 -08:00
JonnyWong16
e321c5b197 Fix GeoLite2 update log message to correct number of days from setting 2020-01-04 11:50:48 -08:00
JonnyWong16
badbfdc4c1 Disable uninstall GeoLite2 database button when not installed 2020-01-04 10:52:11 -08:00
JonnyWong16
7d71086a41 Verify MaxMind license key and GeoLite2 database path before allowing install 2020-01-02 21:28:11 -08:00
JonnyWong16
0e1764755a Change save setting callbacks 2020-01-02 21:25:15 -08:00
JonnyWong16
c31d3ffd6c Download from MaxMind using urllib3 using certifi 2020-01-01 10:10:52 -08:00
JonnyWong16
0cba3988aa Update certifi to 2019.11.28 2020-01-01 10:10:05 -08:00
JonnyWong16
a7a9ed8628 v2.1.41 2019-12-30 14:33:10 -08:00
JonnyWong16
6af96332fa Use tar.extractall instead of tar.extract 2019-12-30 14:29:19 -08:00
JonnyWong16
e334a0fc8b Change GeoLite2 tar extraction 2019-12-30 14:19:12 -08:00
JonnyWong16
54bbbb36a6 Fix Geolite2 db extraction on Windows 2019-12-30 13:58:56 -08:00
JonnyWong16
b8ef56574a v2.1.40 2019-12-30 10:24:46 -08:00
JonnyWong16
bc491628d4 Add links to 3rd Party APIs guide 2019-12-29 15:03:22 -08:00
JonnyWong16
629800c239 Change metadata to links for lookup help text 2019-12-29 11:05:21 -08:00
JonnyWong16
8bf876f88c Add note to save the MaxMind license key 2019-12-28 11:09:52 -08:00
JonnyWong16
a81ad6d73e Set maximum GeoLite2 database update interval to 30 days 2019-12-28 11:06:04 -08:00
JonnyWong16
02220209e3 Add default activity refresh interval to setting help text 2019-12-27 09:48:38 -08:00
JonnyWong16
9450a1434d Remove line break from Python version in startup logs 2019-12-25 14:10:35 -08:00
JonnyWong16
c358693fb2 Fix GeoLite2 update scheduler 2019-12-25 14:10:16 -08:00
JonnyWong16
e7b3d768ce Prevent installing GeoLite2 database without license key 2019-12-25 14:07:59 -08:00
JonnyWong16
ee91da2ff1 Force reinstall of GeoLite2 database 2019-12-25 13:43:12 -08:00
JonnyWong16
0428df8e3f Fix GeoLite2 update button for previous installs 2019-12-25 13:31:46 -08:00
JonnyWong16
d4fee1d701 Fix GeoLite2 last updated time for previous installs 2019-12-25 13:23:33 -08:00
JonnyWong16
3b44a3afd2 Add setting for GeoLite2 database update interval 2019-12-25 12:51:59 -08:00
JonnyWong16
7ee1c51810 Go to correct setting for installing GeoLite2 database 2019-12-25 12:40:58 -08:00
JonnyWong16
f958de2de6 Move 3rd Party API settings to new tab 2019-12-25 11:41:53 -08:00
JonnyWong16
b83eb2e763 Add auto-updating of GeoLite2 database 2019-12-25 11:17:45 -08:00
JonnyWong16
41c9369b43 Change GeoIP database install using license key 2019-12-23 23:58:36 -08:00
JonnyWong16
554be92c39 Update go to setting links 2019-12-23 22:30:49 -08:00
JonnyWong16
7486a50c33 Calculate months for graphs using datetime 2019-12-12 09:12:29 -08:00
JonnyWong16
1e777b1a1b Add logging for notify_action (trigger) 2019-12-11 11:35:30 -08:00
JonnyWong16
b7bb159630 Fix regex IP match in hostname resolution 2019-12-10 10:07:34 -08:00
JonnyWong16
6e0a0d51b5 v2.1.39 2019-12-08 12:12:24 -08:00
JonnyWong16
55aad4e6ee Fix integrity check before database backup 2019-11-29 23:14:33 -08:00
JonnyWong16
d1e401cb0c Remove default subject and body for notify API command 2019-11-24 22:35:58 -08:00
JonnyWong16
018479fae9 Fix favicons in setup wizard 2019-11-24 15:03:36 -08:00
JonnyWong16
1c18e72539 Enable allow Plex admin by default 2019-11-20 21:51:52 -08:00
JonnyWong16
779e710045 Fix setup wizard height 2019-11-20 21:51:36 -08:00
JonnyWong16
089a981f6e Check for database corruption when making backup 2019-11-20 19:03:44 -08:00
JonnyWong16
3b24bbee5f Add web server HTTP username and password to setup wizard 2019-11-20 18:50:17 -08:00
JonnyWong16
f9a597bed9 Use bit.ly link for PayPal in FUNDING file 2019-11-17 21:54:47 -08:00
37 changed files with 808 additions and 421 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,3 @@
github: JonnyWong16
patreon: Tautulli
custom: ["https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=6XPPKTDSX9QFL&lc=US&item_name=Tautulli&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted"]
custom: ["https://bit.ly/2InPp15"]

View File

@@ -1,5 +1,60 @@
# Changelog
## v2.1.43 (2020-02-03)
* Monitoring:
* New: Added HDR indicator on activity card.
* New: Added dynamic range to history steam info modal.
* Notifications:
* Fix: Webhook notification body sent as incorrect data type when Content-Type header is overridden.
* Fix: Telegram notification character limit incorrect for unicode characters.
* New: Added color and dynamic range notification parameters.
* Newsletters:
* Fix: Episodes and Albums plural spelling on recently added newsletter section headers.
* UI:
* Fix: Windows and macOS platform capitalization.
* Fix: Season number 0 not shown for episodes on history tables.
* Other:
* Change: Mask email addresses in logs.
* Change: Update deprecated GitHub access token URL parameter to Authorization header.
## 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)
* UI:
* New: Added creating admin username and password to setup wizard.
* API:
* Change: Remove default notification subject and body for notify API command.
* Other:
* Change: Check for database corruption when making backup.
## v2.1.38 (2019-11-17)
* Notifications:

View File

@@ -106,8 +106,8 @@ def main():
plexpy.QUIET = True
# Do an intial setup of the logger.
logger.initLogger(console=not plexpy.QUIET, log_dir=False,
verbose=plexpy.VERBOSE)
# Require verbose for pre-initilization to see critical errors
logger.initLogger(console=not plexpy.QUIET, log_dir=False, verbose=True)
try:
plexpy.SYS_TIMEZONE = tzlocal.get_localzone()
@@ -234,31 +234,19 @@ def main():
plexpy.CONFIG.ENABLE_HTTPS = False
# Try to start the server. Will exit here is address is already in use.
web_config = {
'http_port': plexpy.HTTP_PORT,
'http_host': plexpy.CONFIG.HTTP_HOST,
'http_root': plexpy.CONFIG.HTTP_ROOT,
'http_environment': plexpy.CONFIG.HTTP_ENVIRONMENT,
'http_proxy': plexpy.CONFIG.HTTP_PROXY,
'enable_https': plexpy.CONFIG.ENABLE_HTTPS,
'https_cert': plexpy.CONFIG.HTTPS_CERT,
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN,
'https_key': plexpy.CONFIG.HTTPS_KEY,
'http_username': plexpy.CONFIG.HTTP_USERNAME,
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
}
webstart.initialize(web_config)
webstart.start()
# Windows system tray icon
if os.name == 'nt' and plexpy.CONFIG.WIN_SYS_TRAY:
plexpy.win_system_tray()
logger.info("Tautulli is ready!")
# Open webbrowser
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, plexpy.HTTP_PORT,
plexpy.HTTP_ROOT)
# Windows system tray icon
if os.name == 'nt' and plexpy.CONFIG.WIN_SYS_TRAY:
plexpy.win_system_tray()
# Wait endlessy for a signal to happen
while True:
if not plexpy.SIGNAL:

View File

@@ -53,14 +53,6 @@ DOCUMENTATION :: END
<td>Newsletter Directory:</td>
<td>${plexpy.CONFIG.NEWSLETTER_DIR}</td>
</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:
<tr>
<td>Arguments:</td>
@@ -102,22 +94,6 @@ DOCUMENTATION :: END
<script>
$(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) {
e.preventDefault();
$('#guidelines-type').text($(this).data('id'))

View File

@@ -226,17 +226,24 @@ DOCUMENTATION :: END
<li class="dashboard-activity-info-item">
<div class="sub-heading">Video</div>
<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':
<%
hw_d = ' (HW)' if data['transcode_hw_decoding'] 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':
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:
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
% elif data['media_type'] == 'photo':
Direct Play (${data['width']}x${data['height']})
@@ -248,12 +255,14 @@ DOCUMENTATION :: END
<li class="dashboard-activity-info-item">
<div class="sub-heading">Audio</div>
<div class="sub-value" id="audio_decision-${sk}">
% 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()})
% elif data['stream_audio_decision'] == 'copy':
Direct Stream (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% else:
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% if data['stream_audio_decision']:
% 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()})
% elif data['stream_audio_decision'] == 'copy':
Direct Stream (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% else:
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
</div>
</li>

View File

@@ -437,6 +437,8 @@
var 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= '';
switch (s.video_resolution.toLowerCase()) {
case 'sd':
@@ -462,11 +464,11 @@
if (s.stream_video_decision === 'transcode') {
var hw_d = (s.transcode_hw_decoding === 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') {
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 {
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') {
video_decision = 'Direct Play (' + s.width + 'x' + s.height + ')';

View File

@@ -166,7 +166,7 @@ history_table_options = {
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
$(td).html('<div class="history-title"><a href="info?' + source + 'rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
$(td).html('<div class="history-title"><a href="info?' + source + 'rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');

View File

@@ -107,7 +107,7 @@ history_table_modal_options = {
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');

View File

@@ -153,7 +153,7 @@ libraries_list_table_options = {
} else if (rowData['media_type'] === 'episode') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
if (rowData['rating_key']) {
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else {

View File

@@ -91,7 +91,7 @@ user_ip_table_options = {
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');

View File

@@ -166,7 +166,7 @@ users_list_table_options = {
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');

View File

@@ -271,7 +271,7 @@
</div>
<p class="help-block">
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>
</div>
<div id="newsletter-email-config">

View File

@@ -485,7 +485,7 @@
'<div class="form-group">' +
'<label>Warning</label>' +
'<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>'
);
$('#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-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-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-android_app" aria-controls="tabs-android_app" role="tab" data-toggle="tab">Tautulli Remote Android App <sup><small>beta</small></sup></a></li>
</ul>
@@ -284,7 +285,7 @@
</div>
<div id="home_refresh_interval_error" class="alert alert-danger settings-alert" role="alert"></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 class="padded-header">
@@ -1008,7 +1009,7 @@
<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.
</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 class="form-group">
<label for="newsletter_auth">Newsletter Authentication</label>
@@ -1022,7 +1023,7 @@
</div>
</div>
<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 class="form-group" id="newsletter_password_option">
<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>
</div>
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-notification_agents">
<div class="padded-header">
<h3>3rd Party APIs</h3>
<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">
<label for="notify_upload_posters">Image Hosting</label>
<label for="notify_upload_posters">Image Host</label>
<div class="row">
<div class="col-md-6">
<div class="${'input-group' if config['notify_upload_posters'] in (1, 3) else ''}">
@@ -1090,8 +1139,8 @@
</div>
<div id="imgur_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 1 else 'block'}">
<div class="form-group">
<p class="help-block" id="imgur_upload_message">
You can register a new Imgur application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>.<br>
<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 Imgur.<br>
Warning: Imgur uploads are rate-limited and newsletters may exceed the limit. Please use Cloudinary for newsletters instead.
</p>
</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 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 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 id="cloudinary_upload_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 3 else 'block'}">
<div class="form-group">
<p class="help-block" id="imgur_upload_message">
You can sign up for Cloudinary <a href="${anon_url('https://cloudinary.com')}" target="_blank">here</a>.<br>
<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 Cloudinary.
</p>
</div>
<div class="form-group">
@@ -1151,6 +1200,13 @@
</p>
</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">
<label>
<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>
</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>
</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 class="padded-header">
@@ -1316,7 +1380,7 @@
<div class="form-group">
<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 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 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>
@@ -1796,11 +1860,6 @@ Rating: {rating}/10 --> Rating: /10
async: true,
complete: function(xhr, status) {
$("#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) {
if (serverChanged) {
verifyServer();
}
verifyPMSWebURL();
if (_callback) {
if (serverChanged) {
verifyServer(_callback);
} else if (typeof _callback === "function") {
_callback();
}
}
@@ -1951,12 +2008,13 @@ $(document).ready(function() {
settingsChanged = true;
});
function saveSettings() {
function saveSettings(showMsg, _callback) {
if (configForm.parsley().validate()) {
doAjaxCall('configUpdate', $(this), 'tabs', true, true, postSaveChecks);
return false;
doAjaxCall('configUpdate', $(this), 'tabs', true, showMsg, _callback);
return true;
} 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() {
preSaveChecks(function () { saveSettings() });
preSaveChecks(function () { saveSettings(true, postSaveChecks) });
});
initConfigCheckbox('#api_enabled');
@@ -2279,6 +2337,7 @@ $(document).ready(function() {
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
$("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
showMsg('Verifying Plex server...', true, true, 10000, false);
$.ajax({
url: 'get_server_id',
data: {
@@ -2316,10 +2375,11 @@ $(document).ready(function() {
} else {
$("#pms_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
$("#pms_ip_group").removeClass("has-error");
showMsg('<i class="fa fa-check"></i> Server verified.', false, true, 5000);
serverChanged = false;
}
if (_callback) {
if (typeof _callback === "function") {
_callback();
}
} else {
@@ -2773,25 +2833,81 @@ $(document).ready(function() {
$('#allow_guest_access').click(function () {
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 () {
var tab = $(this).data('tab-destination');
$("a[href=#" + tab + "]").click();
var scroll_destination = $(this).data('target');
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});
}
var setting = $(this).data('target');
gotoSetting(tab, setting)
});
$('#resources-xml').on('tripleclick', function () {
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 () {
var maxmind_license_key = $("#maxmind_license_key");
maxmind_license_key.val($.trim(maxmind_license_key.val()));
if (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>
</%def>

View File

@@ -178,6 +178,11 @@ DOCUMENTATION :: END
<td>${data['stream_video_framerate']}</td>
<td>${data['video_framerate']}</td>
</tr>
<tr>
<td>Dynamic Range</td>
<td>${data['stream_video_dynamic_range']}</td>
<td>${data['video_dynamic_range']}</td>
</tr>
<tr>
<td>Aspect Ratio</td>
<td>-</td>

View File

@@ -21,28 +21,21 @@
<link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet">
<!-- Favicons -->
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.0">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.0">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.0">
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<!-- ICONS -->
<!-- Android >M39 icon -->
<link rel="icon" type="image/png" sizes="192x192" href="${http_root}images/favicon/android-chrome-192x192.png?v=2.0.0">
<link rel="manifest" href="${http_root}json/Android-manifest.json?v=2.0.0">
<meta name="theme-color" content="#1f1f1f">
<!-- Android -->
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5">
<meta name="theme-color" content="#282a2d">
<!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.0">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.0" color="#1f1f1f">
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
<meta name="apple-mobile-web-app-title" content="Tautulli">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="viewport" content="initial-scale=1">
<meta name="format-detection" content="telephone=no">
<!-- IE10 icon -->
<!-- Microsoft -->
<meta name="application-name" content="Tautulli">
<meta name="msapplication-TileColor" content="#1f1f1f">
<meta name="msapplication-TileImage" content="${http_root}images/favicon/mstile-144x144.png?v=2.0.0">
<meta name="msapplication-config" content="${http_root}xml/IEconfig.xml?v=2.0.0" />
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
</head>
<body>
@@ -67,8 +60,38 @@
</p>
</div>
</div>
<div class="wizard-card" data-cardname="card2">
<h3>Plex Authentication</h3>
<h3>Authentication</h3>
<div class="wizard-input-section">
<p class="help-block">
Please setup an admin username and password for Tautulli.
</p>
</div>
<div class="wizard-input-section">
<label for="http_username">HTTP Username</label>
<div class="row">
<div class="col-xs-8">
<input type="text" class="form-control auth-settings" id="http_username" name="http_username" value="" size="30">
</div>
</div>
</div>
<div class="wizard-input-section">
<label for="http_password">HTTP Password</label>
<div class="row">
<div class="col-xs-8">
<input type="password" class="form-control auth-settings" id="http_password" name="http_password" value="" size="30" autocomplete="new-password">
</div>
</div>
</div>
<input type="hidden" class="form-control" name="http_hash_password" id="http_hash_password" value="1">
<input type="hidden" class="form-control" name="http_plex_admin" id="http_plex_admin" value="1">
<input type="hidden" id="authentication_valid" data-validate="validateAuthentication" value="">
<span style="display: none;" id="authentication-status"></span>
</div>
<div class="wizard-card" data-cardname="card3">
<h3>Plex Account</h3>
<div class="wizard-input-section">
<p class="help-block">
Tautulli requires a Plex.tv account. Click the button below to sign in on Plex.tv. You may need to allow popups in your browser.
@@ -78,7 +101,8 @@
<a class="btn btn-dark" id="sign-in-plex" href="#" role="button">Sign In with Plex</a>
<span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
</div>
<div class="wizard-card" data-cardname="card3">
<div class="wizard-card" data-cardname="card4">
<h3>Plex Media Server</h3>
<div class="wizard-input-section">
<p class="help-block">
@@ -137,7 +161,7 @@
<span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
</div>
<div class="wizard-card" data-cardname="card4">
<div class="wizard-card" data-cardname="card5">
<h3>Activity Logging</h3>
<div class="wizard-input-section">
<p class="help-block">
@@ -162,7 +186,7 @@
</div>
</div>
<div class="wizard-card" data-cardname="card4">
<div class="wizard-card" data-cardname="card6">
<h3>Notifications</h3>
<div class="wizard-input-section">
<p class="help-block">
@@ -175,7 +199,7 @@
</div>
</div>
<div class="wizard-card" data-cardname="card5">
<div class="wizard-card" data-cardname="card7">
<h3>Database Import</h3>
<div class="wizard-input-section">
<p class="help-block">
@@ -227,11 +251,29 @@
<script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/bootstrap-wizard.min.js"></script>
<script>
function validateAuthentication(el) {
var http_username = $("#http_username").val();
var http_password = $("#http_password").val();
var valid_authentication = el.val();
var retValue = {};
if (http_username === "" || http_password === "") {
retValue.status = false;
retValue.msg = "Please enter a username and password.";
$("#authentication-status").html('<i class="fa fa-exclamation-circle"></i> Please enter a username and password.');
$('#authentication-status').fadeIn('fast').delay(2000).fadeOut('fast');
} else {
retValue.status = true;
}
return retValue;
}
function validatePMSip(el) {
var valid_pms_ip = el.val();
var retValue = {};
if (valid_pms_ip == "") {
if (valid_pms_ip === "") {
retValue.status = false;
retValue.msg = "Please verify your server.";
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> Please verify your server.');
@@ -247,7 +289,7 @@
var valid_pms_token = el.val();
var retValue = {};
if (valid_pms_token == "") {
if (valid_pms_token === "") {
retValue.status = false;
retValue.msg = "Please authenticate.";
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> Please authenticate.');
@@ -284,7 +326,7 @@ $(document).ready(function() {
$.fn.wizard.logging = false;
var options = {
keyboard : false,
contentHeight : 400,
contentHeight : 450,
contentWidth : 700,
backdrop: 'static',
buttons: {submitText: 'Finish'},

View File

@@ -694,7 +694,7 @@
<div class="sub-header-count" style="margin-left: auto;margin-right: auto;font-size: 30px;text-align: center;">
<span class="count" style="color: #E5A00D;">${len(recently_added['show'])}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">show${'s' if len(recently_added['show']) > 1 else ''}</span>&nbsp;/&nbsp;
<% total_episodes = sum(season['episode_count'] for show in recently_added['show'] for season in show['season']) %>
<span class="count" style="color: #E5A00D;">${total_episodes}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">episode${'s' if total > 1 else ''}</span>
<span class="count" style="color: #E5A00D;">${total_episodes}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">episode${'s' if total_episodes > 1 else ''}</span>
</div>
</td>
</tr>
@@ -847,7 +847,7 @@
<div class="sub-header-count" style="margin-left: auto;margin-right: auto;font-size: 30px;text-align: center;">
<span class="count" style="color: #E5A00D;">${len(recently_added['artist'])}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">artist${'s' if len(recently_added['artist']) > 1 else ''}</span> /
<% total_albums = sum(artist['album_count'] for artist in recently_added['artist']) %>
<span class="count" style="color: #E5A00D;">${total_albums}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">album${'s' if total > 1 else ''}</span>
<span class="count" style="color: #E5A00D;">${total_albums}</span> <span class="count-units" style="color: #aaaaaa;font-size: 20px;text-transform: uppercase;">album${'s' if total_albums > 1 else ''}</span>
</div>
</td>
</tr>

View File

@@ -694,7 +694,7 @@
<div class="sub-header-count">
<span class="count">${len(recently_added['show'])}</span> <span class="count-units">show${'s' if len(recently_added['show']) > 1 else ''}</span>&nbsp;/&nbsp;
<% total_episodes = sum(season['episode_count'] for show in recently_added['show'] for season in show['season']) %>
<span class="count">${total_episodes}</span> <span class="count-units">episode${'s' if total > 1 else ''}</span>
<span class="count">${total_episodes}</span> <span class="count-units">episode${'s' if total_episodes > 1 else ''}</span>
</div>
</td>
</tr>
@@ -847,7 +847,7 @@
<div class="sub-header-count">
<span class="count">${len(recently_added['artist'])}</span> <span class="count-units">artist${'s' if len(recently_added['artist']) > 1 else ''}</span> /
<% total_albums = sum(artist['album_count'] for artist in recently_added['artist']) %>
<span class="count">${total_albums}</span> <span class="count-units">album${'s' if total > 1 else ''}</span>
<span class="count">${total_albums}</span> <span class="count-units">album${'s' if total_albums > 1 else ''}</span>
</div>
</td>
</tr>

View File

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

View File

@@ -771,36 +771,6 @@ vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
+OkuE6N36B9K
-----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.
# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co.
# Label: "DST Root CA X3"
@@ -1219,36 +1189,6 @@ t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
-----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
# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc
# Label: "Cybertrust Global Root"
@@ -3453,46 +3393,6 @@ AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ
5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
-----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
# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
# Label: "OISTE WISeKey Global Root GB CA"
@@ -4656,3 +4556,47 @@ L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa
LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG
mpv0
-----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

@@ -29,7 +29,6 @@ try:
except ImportError:
no_browser = True
import cherrypy
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
from UniversalAnalytics import Tracker
@@ -40,6 +39,7 @@ import activity_pinger
import common
import database
import datafactory
import helpers
import libraries
import logger
import mobile_app
@@ -51,6 +51,7 @@ import plextv
import users
import versioncheck
import web_socket
import webstart
import plexpy.config
PROG_DIR = None
@@ -64,7 +65,7 @@ SYS_LANGUAGE = None
SYS_ENCODING = None
QUIET = False
VERBOSE = True
VERBOSE = False
DAEMON = False
CREATEPID = False
PIDFILE = None
@@ -121,6 +122,7 @@ def initialize(config_file):
global CONFIG
global CONFIG_FILE
global VERBOSE
global _INITIALIZED
global CURRENT_VERSION
global LATEST_VERSION
@@ -151,6 +153,8 @@ def initialize(config_file):
if not log_writable and not QUIET:
sys.stderr.write("Unable to create the log directory. Logging to screen only.\n")
VERBOSE = VERBOSE or bool(CONFIG.VERBOSE_LOGS)
# Start the logger, disable console if needed
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR if log_writable else None,
verbose=VERBOSE)
@@ -166,7 +170,7 @@ def initialize(config_file):
plexpy.SYS_TIMEZONE.zone, plexpy.SYS_UTC_OFFSET
))
logger.info(u"Python {}".format(
sys.version
sys.version.replace('\n', '')
))
logger.info(u"Program Dir: {}".format(
PROG_DIR
@@ -444,6 +448,8 @@ def initialize_scheduler():
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
schedule_job(config.make_backup, 'Backup Tautulli config',
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:
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
@@ -582,12 +588,14 @@ def dbcheck():
'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, '
'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_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, '
'stream_bitrate INTEGER, stream_video_resolution TEXT, quality_profile 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_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, '
'subtitles INTEGER, stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, '
'transcode_protocol TEXT, transcode_container TEXT, '
@@ -617,7 +625,7 @@ def dbcheck():
'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, '
'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, '
'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, '
'transcode_audio_channels INTEGER, transcode_width INTEGER, transcode_height INTEGER, '
@@ -627,7 +635,7 @@ def dbcheck():
'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_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_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, '
@@ -1200,6 +1208,18 @@ def dbcheck():
'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
try:
c_db.execute('SELECT reference_id FROM session_history')
@@ -1538,6 +1558,17 @@ def dbcheck():
'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
try:
c_db.execute('SELECT do_notify FROM users')
@@ -1972,8 +2003,7 @@ def upgrade():
def shutdown(restart=False, update=False, checkout=False):
logger.info(u"Stopping Tautulli web server...")
cherrypy.engine.exit()
webstart.stop()
# Shutdown the websocket connection
if WEBSOCKET:

View File

@@ -78,6 +78,7 @@ class ActivityProcessor(object):
'video_framerate': session.get('video_framerate', ''),
'video_scan_type': session.get('video_scan_type', ''),
'video_full_resolution': session.get('video_full_resolution', ''),
'video_dynamic_range': session.get('video_dynamic_range', ''),
'aspect_ratio': session.get('aspect_ratio', ''),
'audio_codec': session.get('audio_codec', ''),
'audio_bitrate': session.get('audio_bitrate', ''),
@@ -110,6 +111,7 @@ class ActivityProcessor(object):
'stream_video_framerate': session.get('stream_video_framerate', ''),
'stream_video_scan_type': session.get('stream_video_scan_type', ''),
'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_codec': session.get('stream_audio_codec', ''),
'stream_audio_bitrate': session.get('stream_audio_bitrate', ''),
@@ -353,6 +355,7 @@ class ActivityProcessor(object):
'video_framerate': session['video_framerate'],
'video_scan_type': session['video_scan_type'],
'video_full_resolution': session['video_full_resolution'],
'video_dynamic_range': session['video_dynamic_range'],
'aspect_ratio': session['aspect_ratio'],
'audio_codec': session['audio_codec'],
'audio_bitrate': session['audio_bitrate'],
@@ -387,6 +390,7 @@ class ActivityProcessor(object):
'stream_video_framerate': session['stream_video_framerate'],
'stream_video_scan_type': session['stream_video_scan_type'],
'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_codec': session['stream_audio_codec'],
'stream_audio_bitrate': session['stream_audio_bitrate'],

View File

@@ -411,7 +411,7 @@ class API2:
return
def notify(self, notifier_id='', subject='Tautulli', body='Test notification', **kwargs):
def notify(self, notifier_id='', subject='', body='', **kwargs):
""" Send a notification using Tautulli.
```

View File

@@ -54,7 +54,9 @@ PLATFORM_NAME_OVERRIDES = {
'Mystery 3': 'Playstation 3',
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360',
'WebMAF': 'Playstation 4'
'WebMAF': 'Playstation 4',
'windows': 'Windows',
'osx': 'macOS'
}
PMS_PLATFORM_NAME_OVERRIDES = {
@@ -197,7 +199,8 @@ SCHEDULER_LIST = [
'Refresh libraries list',
'Refresh Plex server URLs',
'Backup Tautulli database',
'Backup Tautulli config'
'Backup Tautulli config',
'Update GeoLite2 database'
]
DATE_TIME_FORMATS = [
@@ -360,6 +363,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 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 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 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.'},
@@ -467,6 +476,12 @@ NOTIFICATION_PARAMETERS = [
{'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 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 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.'},

View File

@@ -175,6 +175,8 @@ _CONFIG_DEFINITIONS = {
'FIRST_RUN_COMPLETE': (int, 'General', 0),
'FREEZE_DB': (int, 'General', 0),
'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_HOLD': (dict, 'General', {'section_ids': [], 'rating_keys': []}),
'GIT_BRANCH': (str, 'General', 'master'),
@@ -289,6 +291,7 @@ _CONFIG_DEFINITIONS = {
'LOG_BLACKLIST': (int, 'General', 1),
'LOG_DIR': (str, 'General', ''),
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
'MAXMIND_LICENSE_KEY': (str, 'General', ''),
'METADATA_CACHE_SECONDS': (int, 'Advanced', 1800),
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
@@ -600,6 +603,7 @@ _CONFIG_DEFINITIONS = {
'UPDATE_LABELS': (int, 'General', 1),
'UPDATE_LIBRARIES_DB_NOTIFY': (int, 'General', 1),
'UPDATE_NOTIFIERS_DB': (int, 'General', 1),
'VERBOSE_LOGS': (int, 'Advanced', 1),
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
'VIDEO_LOGGING_ENABLE': (int, 'Monitoring', 1),
'WEBSOCKET_MONITOR_PING_PONG': (int, 'Advanced', 0),
@@ -923,3 +927,9 @@ class Config(object):
self.BUFFER_THRESHOLD = max(self.BUFFER_THRESHOLD, 10)
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

@@ -69,10 +69,17 @@ def db_filename(filename=FILENAME):
def make_backup(cleanup=False, scheduler=False):
""" Makes a backup of db, removes all but the last 5 backups """
# Check the integrity of the database first
integrity = (integrity_check()['integrity_check'] == 'ok')
corrupt = ''
if not integrity:
corrupt = '.corrupt'
if scheduler:
backup_file = 'tautulli.backup-%s.sched.db' % arrow.now().format('YYYYMMDDHHmmss')
backup_file = 'tautulli.backup-{}{}.sched.db'.format(arrow.now().format('YYYYMMDDHHmmss'), corrupt)
else:
backup_file = 'tautulli.backup-%s.db' % arrow.now().format('YYYYMMDDHHmmss')
backup_file = 'tautulli.backup-{}{}.db'.format(arrow.now().format('YYYYMMDDHHmmss'), corrupt)
backup_folder = plexpy.CONFIG.BACKUP_DIR
backup_file_fp = os.path.join(backup_folder, backup_file)
@@ -85,7 +92,8 @@ def make_backup(cleanup=False, scheduler=False):
shutil.copyfile(db_filename(), backup_file_fp)
db.connection.rollback()
if cleanup:
# Only cleanup if the database integrity is okay
if cleanup and integrity:
now = time.time()
# Delete all scheduled backup older than BACKUP_DAYS.
for root, dirs, files in os.walk(backup_folder):

View File

@@ -878,11 +878,12 @@ class DataFactory(object):
query = 'SELECT bitrate, video_full_resolution, ' \
'optimized_version, optimized_version_profile, optimized_version_title, ' \
'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, ' \
'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_framerate, ' \
'stream_video_framerate, stream_video_dynamic_range, ' \
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \
@@ -899,11 +900,12 @@ class DataFactory(object):
query = 'SELECT bitrate, video_full_resolution, ' \
'optimized_version, optimized_version_profile, optimized_version_title, ' \
'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, ' \
'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_framerate, ' \
'stream_video_framerate, stream_video_dynamic_range, ' \
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \
@@ -950,6 +952,7 @@ class DataFactory(object):
'video_width': item['video_width'],
'video_height': item['video_height'],
'video_framerate': item['video_framerate'],
'video_dynamic_range': item['video_dynamic_range'],
'aspect_ratio': item['aspect_ratio'],
'audio_codec': item['audio_codec'],
'audio_bitrate': item['audio_bitrate'],
@@ -966,6 +969,7 @@ class DataFactory(object):
'stream_video_width': item['stream_video_width'],
'stream_video_height': item['stream_video_height'],
'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_codec': item['stream_audio_codec'],
'stream_audio_bitrate': item['stream_audio_bitrate'],

View File

@@ -369,19 +369,23 @@ class Graphs(object):
# create our date range as some months may not have any data
# but we still want to display them
base = time.localtime()
month_range = [time.localtime(
time.mktime((base.tm_year, base.tm_mon - n, 1, 0, 0, 0, 0, 0, 0))) for n in range(int(time_range))]
dt_today = datetime.date.today()
dt = dt_today
month_range = [dt]
for n in range(int(time_range)-1):
if not ((dt_today.month-n) % 12)-1:
dt = datetime.date(dt.year-1, 12, 1)
else:
dt = datetime.date(dt.year, dt.month-1, 1)
month_range.append(dt)
categories = []
series_1 = []
series_2 = []
series_3 = []
for month_item in sorted(month_range):
dt = datetime.datetime(*month_item[:6])
for dt in sorted(month_range):
date_string = dt.strftime('%Y-%m')
categories.append(dt.strftime('%b %Y').decode(plexpy.SYS_ENCODING, 'replace'))
series_1_value = 0
series_2_value = 0

View File

@@ -14,6 +14,7 @@
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
import base64
import certifi
import cloudinary
from cloudinary.api import delete_resources_by_tag
from cloudinary.uploader import upload
@@ -21,7 +22,6 @@ from cloudinary.utils import cloudinary_url
import datetime
from functools import wraps
import geoip2.database, geoip2.errors
import gzip
import hashlib
import imghdr
from itertools import izip_longest
@@ -34,11 +34,13 @@ from operator import itemgetter
import os
import re
import shlex
import shutil
import socket
import sys
import tarfile
import time
import unicodedata
import urllib, urllib2
import urllib3
from xml.dom import minidom
import xmltodict
@@ -565,7 +567,7 @@ def get_ip(host):
ip_address = ''
if is_valid_ip(host):
return host
elif not re.fullmatch(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', host):
elif not re.match(r'^[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})$', host):
try:
ip_address = socket.getaddrinfo(host, None)[0][4][0]
logger.debug(u"IP Checker :: Resolved %s to %s." % (host, ip_address))
@@ -583,83 +585,127 @@ def is_valid_ip(address):
return False
def install_geoip_db():
maxmind_url = 'http://geolite.maxmind.com/download/geoip/database/'
geolite2_gz = 'GeoLite2-City.mmdb.gz'
geolite2_md5 = 'GeoLite2-City.md5'
geolite2_db = geolite2_gz[:-3]
md5_checksum = ''
def update_geoip_db():
if plexpy.CONFIG.GEOIP_DB_INSTALLED:
logger.info(u"Tautulli Helpers :: Checking for GeoLite2 database updates.")
now = int(time.time())
if now - plexpy.CONFIG.GEOIP_DB_INSTALLED >= plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS * 24 * 60 * 60:
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)
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
logger.debug(u"Tautulli Helpers :: Downloading GeoLite2 gzip file from MaxMind...")
try:
maxmind = urllib.URLopener()
maxmind.retrieve(maxmind_url + geolite2_gz, temp_gz)
md5_checksum = urllib2.urlopen(maxmind_url + geolite2_md5).read()
maxmind = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where())
with maxmind.request('GET', geolite2_db_url, preload_content=False) as r_db, open(temp_gz, 'wb') as f_db:
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:
logger.error(u"Tautulli Helpers :: Failed to download GeoLite2 gzip file from MaxMind: %s" % e)
return False
# Extract the GeoLite2 database file
logger.debug(u"Tautulli Helpers :: Extracting GeoLite2 database...")
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(u"Tautulli Helpers :: Failed to extract the GeoLite2 database: %s" % e)
return False
# Check MD5 hash for GeoLite2 database file
logger.debug(u"Tautulli Helpers :: Checking MD5 checksum for GeoLite2 database...")
# Check MD5 hash for GeoLite2 tar.gz file
logger.debug(u"Tautulli Helpers :: Checking MD5 checksum for GeoLite2 gzip file...")
try:
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""):
hash_md5.update(chunk)
md5_hash = hash_md5.hexdigest()
with open(temp_md5, 'r') as f:
md5_checksum = f.read()
if md5_hash != md5_checksum:
logger.error(u"Tautulli Helpers :: MD5 checksum doesn't match for GeoLite2 database. "
"Checksum: %s, file hash: %s" % (md5_checksum, md5_hash))
return False
except Exception as e:
logger.error(u"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
# Delete temportary GeoLite2 gzip file
logger.debug(u"Tautulli Helpers :: Deleting temporary GeoLite2 gzip file...")
try:
os.remove(temp_gz)
os.remove(temp_md5)
except Exception as e:
logger.warn(u"Tautulli Helpers :: Failed to remove temporary GeoLite2 gzip file: %s" % e)
logger.debug(u"Tautulli Helpers :: GeoLite2 database installed successfully.")
plexpy.CONFIG.__setattr__('GEOIP_DB', geolite2_db)
plexpy.CONFIG.__setattr__('GEOIP_DB', geolite2_db_path)
plexpy.CONFIG.__setattr__('GEOIP_DB_INSTALLED', int(time.time()))
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():
logger.debug(u"Tautulli Helpers :: Uninstalling the GeoLite2 database...")
try:
os.remove(plexpy.CONFIG.GEOIP_DB)
plexpy.CONFIG.__setattr__('GEOIP_DB', '')
plexpy.CONFIG.write()
except Exception as e:
logger.error(u"Tautulli Helpers :: Failed to uninstall the GeoLite2 database: %s" % e)
return False
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
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 ' \
'<a href="settings?install_geoip=true">Settings</a> page.'
@@ -677,7 +723,7 @@ def geoip_lookup(ip_address):
'<a href="settings?install_geoip=true">Settings</a> page.'
except maxminddb.InvalidDatabaseError as e:
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:
return '%s' % e
except Exception as e:

View File

@@ -80,7 +80,7 @@ class BlacklistFilter(logging.Filter):
Log filter for blacklisted tokens and passwords
"""
def __init__(self):
pass
super(BlacklistFilter, self).__init__()
def filter(self, record):
if not plexpy.CONFIG.LOG_BLACKLIST:
@@ -98,30 +98,29 @@ class BlacklistFilter(logging.Filter):
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):
pass
super(RegexFilter, self).__init__()
self.regex = re.compile(r'')
def filter(self, record):
if not plexpy.CONFIG.LOG_BLACKLIST:
return True
try:
# Currently only checking for ipv4 addresses
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', record.msg)
for ip in ipv4:
if helpers.is_public_ip(ip):
record.msg = record.msg.replace(ip, ip.partition('.')[0] + '.***.***.***')
matches = self.regex.findall(record.msg)
for match in matches:
record.msg = self.replace(record.msg, match)
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 []
for ip in ipv4:
if helpers.is_public_ip(ip):
arg = arg.replace(ip, ip.partition('.')[0] + '.***.***.***')
matches = self.regex.findall(arg) if isinstance(arg, basestring) else []
for match in matches:
arg = self.replace(arg, match)
args.append(arg)
record.args = tuple(args)
except:
@@ -129,31 +128,53 @@ class PublicIPFilter(logging.Filter):
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 helpers.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
"""
def __init__(self):
pass
super(PlexTokenFilter, self).__init__()
def filter(self, record):
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:])
self.regex = re.compile(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)')
args = []
for arg in record.args:
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
def replace(self, text, token):
return text.replace(token, 8 * '*' + token[-2:])
@contextlib.contextmanager
@@ -294,6 +315,7 @@ def initLogger(console=False, log_dir=False, verbose=False):
for handler in logger.handlers + logger_api.handlers + logger_plex_websocket.handlers:
handler.addFilter(BlacklistFilter())
handler.addFilter(PublicIPFilter())
handler.addFilter(EmailFilter())
handler.addFilter(PlexTokenFilter())
# Install exception hooks

View File

@@ -90,6 +90,8 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
notifiers_enabled = notifiers.get_notifiers(notify_action=notify_action)
if notifiers_enabled and not manual_trigger:
logger.debug(u"Tautulli NotificationHandler :: Notifiers enabled for notify_action '%s'." % notify_action)
# Check if notification conditions are satisfied
conditions = notify_conditions(notify_action=notify_action,
stream_data=stream_data,
@@ -98,6 +100,9 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
conditions = True
if notifiers_enabled and (manual_trigger or conditions):
if manual_trigger:
logger.debug(u"Tautulli NotificationHandler :: Notifiers enabled for notify_action '%s' (manual trigger)." % notify_action)
if stream_data or timeline_data:
# Build the notification parameters
parameters = build_media_notify_params(notify_action=notify_action,
@@ -138,6 +143,7 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
# Activity notifications
if stream_data:
logger.debug(u"Tautulli NotificationHandler :: Checking global notification conditions.")
# Check if notifications enabled for user and library
# user_data = users.Users()
@@ -163,36 +169,37 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
user_sessions = [s for s in result['sessions'] if s['user_id'] == stream_data['user_id']]
if plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP:
return len(Counter(s['ip_address'] for s in user_sessions)) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
evaluated = len(Counter(s['ip_address'] for s in user_sessions)) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
else:
return len(user_sessions) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
evaluated = len(user_sessions) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
elif notify_action == 'on_newdevice':
data_factory = datafactory.DataFactory()
user_devices = data_factory.get_user_devices(user_id=stream_data['user_id'])
return stream_data['machine_id'] not in user_devices
evaluated = stream_data['machine_id'] not in user_devices
elif stream_data['media_type'] in ('movie', 'episode', 'clip'):
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
if notify_action == 'on_stop':
return (plexpy.CONFIG.NOTIFY_CONSECUTIVE or
evaluated = (plexpy.CONFIG.NOTIFY_CONSECUTIVE or
(stream_data['media_type'] == 'movie' and progress_percent < plexpy.CONFIG.MOVIE_WATCHED_PERCENT) or
(stream_data['media_type'] == 'episode' and progress_percent < plexpy.CONFIG.TV_WATCHED_PERCENT))
elif notify_action == 'on_resume':
return plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99
evaluated = plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99
# All other activity notify actions
else:
return True
evaluated = True
elif stream_data['media_type'] == 'track':
return True
evaluated = True
else:
return False
evaluated = False
logger.debug(u"Tautulli NotificationHandler :: Global notification conditions evaluated to '{}'.".format(evaluated))
# Recently Added notifications
elif timeline_data:
@@ -204,11 +211,13 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
# # logger.debug(u"Tautulli NotificationHandler :: Notifications for library '%s' is disabled." % library_details['section_name'])
# return False
return True
evaluated = True
# Server notifications
else:
return True
evaluated = True
return evaluated
def notify_custom_conditions(notifier_id=None, parameters=None):
@@ -821,6 +830,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_bitrate': notify_params['stream_video_bitrate'],
'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_full_resolution': notify_params['stream_video_full_resolution'],
'stream_video_ref_frames': notify_params['stream_video_ref_frames'],
@@ -931,6 +946,12 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'video_codec_level': notify_params['video_codec_level'],
'video_bitrate': notify_params['video_bitrate'],
'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_full_resolution': notify_params['video_full_resolution'],
'video_ref_frames': notify_params['video_ref_frames'],

View File

@@ -999,8 +999,8 @@ class ANDROIDAPP(Notifier):
config_option.append({
'label': 'Device',
'description': 'No devices registered. '
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" '
'data-target="#top">Get the Android App</a> and register a device.',
'<a data-tab-destination="android_app" data-toggle="tab" data-dismiss="modal">'
'Get the Android App</a> and register a device.',
'input_type': 'help'
})
else:
@@ -1009,8 +1009,8 @@ class ANDROIDAPP(Notifier):
'value': self.config['device_id'],
'name': 'androidapp_device_id',
'description': 'Set your Android app device or '
'<a data-tab-destination="tabs-android_app" data-toggle="tab" data-dismiss="modal" '
'data-target="#top">register a new device</a> with Tautulli.',
'<a data-tab-destination="android_app" data-toggle="tab" data-dismiss="modal">'
'register a new device</a> with Tautulli.',
'input_type': 'select',
'select_options': devices
})
@@ -1264,8 +1264,8 @@ class DISCORD(Notifier):
'value': self.config['incl_card'],
'name': 'discord_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
'data-target="#notify_upload_posters">Image Hosting</a> '
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
'data-target="notify_upload_posters">Image Hosting</a> '
'must be enabled under the notifications settings tab.',
'input_type': 'checkbox'
},
@@ -1639,8 +1639,8 @@ class FACEBOOK(Notifier):
'value': self.config['incl_card'],
'name': 'facebook_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
'data-target="#notify_upload_posters">Image Hosting</a> '
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
'data-target="notify_upload_posters">Image Hosting</a> '
'must be enabled under the notifications settings tab.',
'input_type': 'checkbox'
},
@@ -1962,8 +1962,8 @@ class HIPCHAT(Notifier):
'value': self.config['incl_card'],
'name': 'hipchat_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
'data-target="#notify_upload_posters">Image Hosting</a> '
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
'data-target="notify_upload_posters">Image Hosting</a> '
'must be enabled under the notifications settings tab.<br>'
'Note: This will change the notification type to HTML and emoticons will no longer work.',
'input_type': 'checkbox'
@@ -2184,8 +2184,8 @@ class JOIN(Notifier):
'value': self.config['incl_poster'],
'name': 'join_incl_poster',
'description': 'Include a poster with the notifications.<br>'
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
'data-target="#notify_upload_posters">Image Hosting</a> '
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
'data-target="notify_upload_posters">Image Hosting</a> '
'must be enabled under the notifications settings tab.',
'input_type': 'checkbox'
},
@@ -3348,8 +3348,8 @@ class SLACK(Notifier):
'value': self.config['incl_card'],
'name': 'slack_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.<br>'
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
'data-target="#notify_upload_posters">Image Hosting</a> '
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
'data-target="notify_upload_posters">Image Hosting</a> '
'must be enabled under the notifications settings tab.',
'input_type': 'checkbox'
},
@@ -3416,9 +3416,9 @@ class TELEGRAM(Notifier):
data = {'chat_id': self.config['chat_id']}
if self.config['incl_subject']:
text = subject.encode('utf-8') + '\r\n' + body.encode('utf-8')
text = subject + '\r\n' + body
else:
text = body.encode('utf-8')
text = body
if self.config['html_support']:
data['parse_mode'] = 'HTML'
@@ -3442,7 +3442,7 @@ class TELEGRAM(Notifier):
if len(text) > 1024:
data['disable_notification'] = True
else:
data['caption'] = text
data['caption'] = text.encode('utf-8')
r = self.make_request('https://api.telegram.org/bot{}/sendPhoto'.format(self.config['bot_token']),
data=data, files=files)
@@ -3450,7 +3450,7 @@ class TELEGRAM(Notifier):
if not data.pop('disable_notification', None):
return r
data['text'] = text
data['text'] = (text[:4093] + (text[4093:] and '...')).encode('utf-8')
if self.config['disable_web_preview']:
data['disable_web_page_preview'] = True
@@ -3593,8 +3593,8 @@ class TWITTER(Notifier):
'value': self.config['incl_poster'],
'name': 'twitter_incl_poster',
'description': 'Include a poster with the notifications.<br>'
'Note: <a data-tab-destination="tabs-notifications" data-dismiss="modal" '
'data-target="#notify_upload_posters">Image Hosting</a> '
'Note: <a data-tab-destination="3rd_party_apis" data-dismiss="modal" '
'data-target="notify_upload_posters">Image Hosting</a> '
'must be enabled under the notifications settings tab.',
'input_type': 'checkbox'
}
@@ -3636,7 +3636,12 @@ class WEBHOOK(Notifier):
if 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):
config_option = [{'label': 'Webhook URL',

View File

@@ -1262,6 +1262,11 @@ class PmsConnect(object):
'video_codec_level': helpers.get_xml_attr(stream, 'level'),
'video_bitrate': helpers.get_xml_attr(stream, 'bitrate'),
'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_ref_frames': helpers.get_xml_attr(stream, 'refFrames'),
'video_height': helpers.get_xml_attr(stream, 'height'),
@@ -1698,6 +1703,11 @@ class PmsConnect(object):
video_id = helpers.get_xml_attr(video_stream_info, 'id')
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_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_ref_frames': helpers.get_xml_attr(video_stream_info, 'refFrames'),
'stream_video_language': helpers.get_xml_attr(video_stream_info, 'language'),
@@ -1708,6 +1718,11 @@ class PmsConnect(object):
else:
video_details = {'stream_video_bitrate': '',
'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_ref_frames': '',
'stream_video_language': '',
@@ -1886,6 +1901,11 @@ class PmsConnect(object):
'video_codec_level': '',
'video_bitrate': '',
'video_bit_depth': '',
'video_chroma_subsampling': '',
'video_color_primaries': '',
'video_color_range': '',
'video_color_space': '',
'video_color_trc': '',
'video_frame_rate': '',
'video_ref_frames': '',
'video_height': '',
@@ -1969,6 +1989,22 @@ class PmsConnect(object):
stream_details['stream_video_resolution'],
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 video_details['stream_video_decision'] != 'transcode' \
or 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
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
if sync_id:

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "master"
PLEXPY_RELEASE_VERSION = "v2.1.38"
PLEXPY_RELEASE_VERSION = "v2.1.43"

View File

@@ -158,13 +158,17 @@ def check_update(auto_update=False, notify=False):
def check_github(auto_update=False, notify=False):
plexpy.COMMITS_BEHIND = 0
if plexpy.CONFIG.GIT_TOKEN:
headers = {'Authorization': 'token {}'.format(plexpy.CONFIG.GIT_TOKEN)}
else:
headers = {}
# Get the latest version available from github
logger.info('Retrieving latest version information from GitHub')
url = 'https://api.github.com/repos/%s/%s/commits/%s' % (plexpy.CONFIG.GIT_USER,
plexpy.CONFIG.GIT_REPO,
plexpy.CONFIG.GIT_BRANCH)
if plexpy.CONFIG.GIT_TOKEN: url = url + '?access_token=%s' % plexpy.CONFIG.GIT_TOKEN
version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict)
version = request.request_json(url, headers=headers, timeout=20, validator=lambda x: type(x) == dict)
if version is None:
logger.warn('Could not get the latest version from GitHub. Are you running a local development version?')
@@ -187,8 +191,8 @@ def check_github(auto_update=False, notify=False):
plexpy.CONFIG.GIT_REPO,
plexpy.LATEST_VERSION,
plexpy.CURRENT_VERSION)
if plexpy.CONFIG.GIT_TOKEN: url = url + '?access_token=%s' % plexpy.CONFIG.GIT_TOKEN
commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict)
commits = request.request_json(url, headers=headers, timeout=20, whitelist_status_code=404,
validator=lambda x: type(x) == dict)
if commits is None:
logger.warn('Could not get commits behind from GitHub.')

View File

@@ -53,6 +53,7 @@ import pmsconnect
import users
import versioncheck
import web_socket
import webstart
from plexpy.api2 import API2
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out
from plexpy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library
@@ -2666,8 +2667,11 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def toggleVerbose(self, **kwargs):
plexpy.VERBOSE = not plexpy.VERBOSE
logger.initLogger(console=not plexpy.QUIET,
log_dir=plexpy.CONFIG.LOG_DIR, verbose=plexpy.VERBOSE)
plexpy.CONFIG.__setattr__('VERBOSE_LOGS', plexpy.VERBOSE)
plexpy.CONFIG.write()
logger.initLogger(console=not plexpy.QUIET, log_dir=plexpy.CONFIG.LOG_DIR, verbose=plexpy.VERBOSE)
logger.info(u"Verbose toggled, set to %s", plexpy.VERBOSE)
logger.debug(u"If you read this message, debug logging is available")
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "logs")
@@ -2818,7 +2822,11 @@ class WebInterface(object):
"newsletter_password": plexpy.CONFIG.NEWSLETTER_PASSWORD,
"newsletter_inline_styles": checked(plexpy.CONFIG.NEWSLETTER_INLINE_STYLES),
"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)
@@ -2829,6 +2837,18 @@ class WebInterface(object):
def configUpdate(self, **kwargs):
# Handle the variable config options. Note - keys with False values aren't getting passed
# Check if we should refresh our data
first_run = False
server_changed = False
reschedule = False
https_changed = False
refresh_libraries = False
refresh_users = False
# First run from the setup wizard
if kwargs.pop('first_run', None):
first_run = True
checked_configs = [
"launch_browser", "enable_https", "https_create_cert", "api_enabled", "freeze_db", "check_github",
"grouping_global_history", "grouping_user_history", "grouping_charts", "group_history_tables",
@@ -2863,13 +2883,13 @@ class WebInterface(object):
kwargs['http_hashed_password'] = 1
# Flag to refresh JWT uuid to log out clients
kwargs['jwt_update_secret'] = True
kwargs['jwt_update_secret'] = True and not first_run
elif not kwargs.get('http_hash_password'):
kwargs['http_hashed_password'] = 0
# Flag to refresh JWT uuid to log out clients
kwargs['jwt_update_secret'] = True
kwargs['jwt_update_secret'] = True and not first_run
else:
kwargs['http_hashed_password'] = 0
@@ -2879,18 +2899,6 @@ class WebInterface(object):
kwargs[plain_config] = kwargs[use_config]
del kwargs[use_config]
# Check if we should refresh our data
first_run = False
server_changed = False
reschedule = False
https_changed = False
refresh_libraries = False
refresh_users = False
# First run from the setup wizard
if kwargs.pop('first_run', None):
first_run = True
# If we change any monitoring settings, make sure we reschedule tasks.
if kwargs.get('check_github') != plexpy.CONFIG.CHECK_GITHUB or \
kwargs.get('refresh_libraries_interval') != str(plexpy.CONFIG.REFRESH_LIBRARIES_INTERVAL) or \
@@ -2965,8 +2973,9 @@ class WebInterface(object):
# If first run, start websocket
if first_run:
webstart.restart()
activity_pinger.connect_server(log=True, startup=True)
# Reconfigure scheduler if intervals changed
if reschedule:
plexpy.initialize_scheduler()
@@ -3053,15 +3062,17 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def install_geoip_db(self, **kwargs):
def install_geoip_db(self, update=False, **kwargs):
""" 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:
return {'result': 'success', 'message': 'GeoLite2 database installed successful.'}
return {'result': 'success', 'message': 'GeoLite2 database installed successful.', 'updated': result}
else:
return {'result': 'error', 'message': 'GeoLite2 database install failed.'}
return {'result': 'error', 'message': 'GeoLite2 database install failed.', 'updated': 0}
@cherrypy.expose
@cherrypy.tools.json_out()

View File

@@ -25,6 +25,36 @@ from plexpy.helpers import create_https_certificates
from plexpy.webserve import WebInterface
def start():
logger.info(u"Tautulli WebStart :: Initializing Tautulli web server...")
web_config = {
'http_port': plexpy.HTTP_PORT,
'http_host': plexpy.CONFIG.HTTP_HOST,
'http_root': plexpy.CONFIG.HTTP_ROOT,
'http_environment': plexpy.CONFIG.HTTP_ENVIRONMENT,
'http_proxy': plexpy.CONFIG.HTTP_PROXY,
'enable_https': plexpy.CONFIG.ENABLE_HTTPS,
'https_cert': plexpy.CONFIG.HTTPS_CERT,
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN,
'https_key': plexpy.CONFIG.HTTPS_KEY,
'http_username': plexpy.CONFIG.HTTP_USERNAME,
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
}
initialize(web_config)
def stop():
logger.info(u"Tautulli WebStart :: Stopping Tautulli web server...")
cherrypy.engine.exit()
def restart():
logger.info(u"Tautulli WebStart :: Restarting Tautulli web server...")
stop()
start()
def initialize(options):
# HTTPS stuff stolen from sickbeard