Compare commits
20 Commits
v2.0.13-be
...
v2.0.14-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
45c2ccdffe | ||
![]() |
fc14c3165f | ||
![]() |
0fad245148 | ||
![]() |
79609c384e | ||
![]() |
09054ddb4b | ||
![]() |
6f912d4aa2 | ||
![]() |
96033a8214 | ||
![]() |
5ca65f4797 | ||
![]() |
d2fccbde68 | ||
![]() |
e6b48d7baf | ||
![]() |
3e51310511 | ||
![]() |
32b43202c2 | ||
![]() |
446170f8de | ||
![]() |
c5a9ecd4ac | ||
![]() |
2af5f817a3 | ||
![]() |
4e55cf3cd4 | ||
![]() |
eeb0478813 | ||
![]() |
33739f1cb2 | ||
![]() |
515e6a8071 | ||
![]() |
2b22f8eb4f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@
|
||||
*.db*
|
||||
*.db-journal
|
||||
*.ini
|
||||
release.lock
|
||||
version.lock
|
||||
logs/*
|
||||
cache/*
|
||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## v2.0.14-beta (2018-01-20)
|
||||
|
||||
* Monitoring:
|
||||
* Change: Added "Cellular" bandwidth to "WAN" in activity header.
|
||||
* Notifications:
|
||||
* Fix: Plex Web URL for tracks now go to the album page.
|
||||
* Fix: Recently added notifications being sent for the entire library when DVR EPG data was refreshed.
|
||||
* Fix: Notifier settings not loading with an apostrophe in the custom condition values.
|
||||
* Fix: Custom email addresses not being saved when closing the notifier settings.
|
||||
* Change: Re-enabled Browser notifications.
|
||||
* Change: Renamed "PlexPy" update notification parameters to "Tautulli".
|
||||
* Change: Emails no longer automatically insert HTML line breaks.
|
||||
* Change: "Date" header added to email notifications.
|
||||
* UI:
|
||||
* Change: Show all changelogs since the previous version when updating.
|
||||
|
||||
|
||||
## v2.0.13-beta (2018-01-13)
|
||||
|
||||
* Notifications:
|
||||
|
@@ -2,6 +2,7 @@
|
||||
import plexpy
|
||||
from plexpy import version
|
||||
from plexpy.helpers import anon_url
|
||||
from plexpy.notifiers import BROWSER_NOTIFIERS
|
||||
%>
|
||||
<!doctype html>
|
||||
|
||||
@@ -283,6 +284,9 @@ ${next.modalIncludes()}
|
||||
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
||||
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
||||
<script src="${http_root}js/ajaxNotifications.js"></script>
|
||||
% endif
|
||||
<script>
|
||||
% if _session['user_group'] == 'admin':
|
||||
$('#updateDismiss').click(function() {
|
||||
|
@@ -131,19 +131,19 @@ select.form-control:focus,
|
||||
.react-selectize.root-node.open .react-selectize-control .react-selectize-toggle-button path {
|
||||
fill: #999 !important;
|
||||
}
|
||||
.selectize-control .selectize-input > div .email {
|
||||
.selectize-control .selectize-input > div .item-value {
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.selectize-control .selectize-input > div .user + .email {
|
||||
.selectize-control .selectize-input > div .item-text + .item-value {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.selectize-control .selectize-input > div .email:before {
|
||||
.selectize-control .selectize-input > div .item-value:before {
|
||||
content: '<';
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.selectize-control .selectize-input > div .email:after {
|
||||
.selectize-control .selectize-input > div .item-value:after {
|
||||
content: '>';
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
@@ -153,6 +153,25 @@ select.form-control:focus,
|
||||
display: block;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
.selectize-control .selectize-dropdown .select-all,
|
||||
.selectize-control .selectize-dropdown .remove-all {
|
||||
font-weight: bold;
|
||||
}
|
||||
.selectize-control .selectize-dropdown .border-all {
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
height: 1px;
|
||||
margin: 9px -12px 9px -12px;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
.selectize-control .selectize-dropdown .border-all:last-child {
|
||||
display: none;
|
||||
}
|
||||
.selectize-dropdown .optgroup-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
select.form-control option {
|
||||
color: #555;
|
||||
background-color: #fff;
|
||||
@@ -201,7 +220,7 @@ object {
|
||||
}
|
||||
.nav .open > a, .nav .open > a:hover, .nav .open > a:focus {
|
||||
background-color: #2f2f2f;
|
||||
border-color: none;
|
||||
border-color: unset;
|
||||
}
|
||||
.dropdown-menu {
|
||||
background-color: #282828;
|
||||
@@ -969,7 +988,6 @@ a .users-poster-face:hover {
|
||||
background-image: -o-linear-gradient(top, #fbb450, #f89406);
|
||||
background-image: linear-gradient(to bottom, #fbb450, #f89406);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
@@ -1724,7 +1742,6 @@ a:hover .dashboard-recent-media-cover {
|
||||
background-image: -moz-linear-gradient(top,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
||||
background-image: linear-gradient(to bottom,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3000000', endColorstr='#e6000000', GradientType=0);
|
||||
webkit-box-shadow: inset 0 0 0 2px #e9a049;
|
||||
-moz-box-shadow: inset 0 0 0 2px #e9a049;
|
||||
box-shadow: inset 0 0 0 2px #e9a049;
|
||||
|
@@ -202,7 +202,7 @@ DOCUMENTATION :: END
|
||||
<div class="sub-heading">Container</div>
|
||||
<div class="sub-value" id="transcode_container-${sk}">
|
||||
% if data.get('stream_container_decision') == 'transcode':
|
||||
Transcode (${data['container'].upper()} → ${data['stream_container'].upper()})
|
||||
Transcode (${data['container'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_container'].upper()})
|
||||
% else:
|
||||
Direct Play (${data['container'].upper()})
|
||||
% endif
|
||||
@@ -218,7 +218,7 @@ DOCUMENTATION :: END
|
||||
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} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} → ${data['stream_video_codec'].upper()}${hw_e} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
|
||||
Transcode (${data['video_codec'].upper()}${hw_d} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
|
||||
% elif data.get('stream_video_decision') == 'copy':
|
||||
Direct Stream (${data['stream_video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
|
||||
% else:
|
||||
@@ -235,7 +235,7 @@ DOCUMENTATION :: END
|
||||
<div class="sub-heading">Audio</div>
|
||||
<div class="sub-value" id="audio_decision-${sk}">
|
||||
% if data.get('stream_audio_decision') == 'transcode':
|
||||
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} → ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} <i class="fa fa-long-arrow-right"></i> ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
|
||||
% elif data.get('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:
|
||||
@@ -250,7 +250,7 @@ DOCUMENTATION :: END
|
||||
<div class="sub-value" id="subtitle_decision-${sk}">
|
||||
% if data['subtitles'] == 1:
|
||||
% if data['stream_subtitle_decision'] == 'transcode':
|
||||
Transcode (${data['subtitle_codec'].upper()} → ${data['stream_subtitle_codec'].upper()})
|
||||
Transcode (${data['subtitle_codec'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_subtitle_codec'].upper()})
|
||||
% elif data['stream_subtitle_decision'] == 'copy':
|
||||
Direct Stream (${data['subtitle_codec'].upper()})
|
||||
% elif data['stream_subtitle_decision'] == 'burn':
|
||||
|
@@ -131,12 +131,13 @@
|
||||
<%def name="modalIncludes()">
|
||||
|
||||
% if _session['user_group'] == 'admin' and config['update_show_changelog']:
|
||||
<% from plexpy.common import VERSION_NUMBER %>
|
||||
<div id="changelog-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||
<h4 class="modal-title">Tautulli Updated</h4>
|
||||
<h4 class="modal-title">Tautulli Updated to <strong>${VERSION_NUMBER}</strong></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
@@ -321,7 +322,7 @@
|
||||
$('#currentActivityHeader').show();
|
||||
|
||||
sessions.forEach(function (session) {
|
||||
var s = new Proxy(session, defaultHandler);
|
||||
var s = (typeof Proxy === "function") ? new Proxy(session, defaultHandler) : session;
|
||||
var key = s.session_key;
|
||||
var session_id = s.session_id;
|
||||
var instance = $('#activity-instance-' + key);
|
||||
@@ -395,7 +396,7 @@
|
||||
|
||||
var transcode_container = '';
|
||||
if (s.stream_container_decision === 'transcode') {
|
||||
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' → ' + s.stream_container.toUpperCase() + ')';
|
||||
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')';
|
||||
} else {
|
||||
transcode_container = 'Direct Play (' + s.container.toUpperCase() + ')';
|
||||
}
|
||||
@@ -428,7 +429,7 @@
|
||||
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 + ' → ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
|
||||
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 + ')';
|
||||
} else if (s.stream_video_decision === 'copy') {
|
||||
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
|
||||
} else {
|
||||
@@ -444,7 +445,7 @@
|
||||
var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase();
|
||||
var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase();
|
||||
if (s.stream_audio_decision === 'transcode') {
|
||||
audio_decision = 'Transcode (' + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ' → ' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
|
||||
audio_decision = 'Transcode (' + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ' <i class="fa fa-long-arrow-right"></i> ' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
|
||||
} else if (s.stream_audio_decision === 'copy') {
|
||||
audio_decision = 'Direct Stream (' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
|
||||
} else {
|
||||
@@ -456,7 +457,7 @@
|
||||
var subtitle_decision = 'None';
|
||||
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.subtitles === 1) {
|
||||
if (s.stream_subtitle_decision === 'transcode') {
|
||||
subtitle_decision = 'Transcode (' + s.subtitle_codec.toUpperCase() + ' → ' + s.stream_subtitle_codec.toUpperCase() + ')';
|
||||
subtitle_decision = 'Transcode (' + s.subtitle_codec.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_subtitle_codec.toUpperCase() + ')';
|
||||
} else if (s.stream_subtitle_decision === 'copy') {
|
||||
subtitle_decision = 'Direct Stream (' + s.subtitle_codec.toUpperCase() + ')';
|
||||
} else if (s.stream_subtitle_decision === 'burn') {
|
||||
@@ -817,7 +818,7 @@
|
||||
$.ajax({
|
||||
url: 'get_changelog',
|
||||
data: {
|
||||
latest_only: true,
|
||||
since_prev_release: true,
|
||||
update_shown: true
|
||||
},
|
||||
cache: false,
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
PNotify.prototype.options.addclass = "stack-bottomright";
|
||||
PNotify.prototype.options.buttons.closer_hover = false;
|
||||
PNotify.prototype.options.desktop = { desktop: true, icon: 'images/logo.png' }
|
||||
PNotify.prototype.options.desktop = { desktop: true, icon: 'images/logo-circle.png' };
|
||||
PNotify.prototype.options.history = false;
|
||||
PNotify.prototype.options.shadow = false;
|
||||
PNotify.prototype.options.stack = { dir1: 'up', dir2: 'left', firstpos1: 25, firstpos2: 25 };
|
||||
@@ -21,7 +21,7 @@ function check_notifications() {
|
||||
$.getJSON('get_browser_notifications', function (data) {
|
||||
if (data) {
|
||||
$.each(data, function (i, notification) {
|
||||
if (notification.delay == 0) {
|
||||
if (notification.delay === 0) {
|
||||
PNotify.prototype.options.hide = false;
|
||||
} else {
|
||||
PNotify.prototype.options.hide = true;
|
||||
@@ -34,7 +34,7 @@ function check_notifications() {
|
||||
setTimeout(function () {
|
||||
"use strict";
|
||||
check_notifications();
|
||||
}, 3000);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
@@ -385,8 +385,9 @@
|
||||
|
||||
$("#clear-logs").click(function () {
|
||||
var logfile = $(".tab-pane.active").data('logfile')
|
||||
var title = $("#log_tabs li.active a").text()
|
||||
|
||||
$("#confirm-message").text("Are you sure you want to clear the Tautulli logs?");
|
||||
$("#confirm-message").text("Are you sure you want to clear the " + title + "?");
|
||||
$('#confirm-modal').modal();
|
||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||
$.ajax({
|
||||
@@ -421,7 +422,7 @@
|
||||
});
|
||||
|
||||
$("#clear-notify-logs").click(function () {
|
||||
$("#confirm-message").text("Are you sure you want to clear the Tautulli notification logs?");
|
||||
$("#confirm-message").text("Are you sure you want to clear the Tautulli Notification Logs?");
|
||||
$('#confirm-modal').modal();
|
||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||
$.ajax({
|
||||
@@ -442,7 +443,7 @@
|
||||
});
|
||||
|
||||
$("#clear-login-logs").click(function () {
|
||||
$("#confirm-message").text("Are you sure you want to clear the Tautulli login logs?");
|
||||
$("#confirm-message").text("Are you sure you want to clear the Tautulli Login Logs?");
|
||||
$('#confirm-modal').modal();
|
||||
$('#confirm-modal').one('click', '#confirm-button', function () {
|
||||
$.ajax({
|
||||
|
@@ -98,6 +98,33 @@
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
% elif item['input_type'] == 'selectize':
|
||||
<div class="form-group">
|
||||
<label for="${item['name']}">${item['label']}</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" id="${item['name']}" name="${item['name']}">
|
||||
<option value="select-all">Select All</option>
|
||||
<option value="remove-all">Remove All</option>
|
||||
% if isinstance(item['select_options'], dict):
|
||||
% for section, options in item['select_options'].iteritems():
|
||||
<optgroup label="${section}">
|
||||
% for option in sorted(options, key=lambda x: x['text'].lower()):
|
||||
<option value="${option['value']}">${option['text']}</option>
|
||||
% endfor
|
||||
</optgroup>
|
||||
% endfor
|
||||
% else:
|
||||
<option value="border-all"></option>
|
||||
% for option in sorted(item['select_options'], key=lambda x: x['text'].lower()):
|
||||
<option value="${option['value']}">${option['text']}</option>
|
||||
% endfor
|
||||
% endif
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">${item['description'] | n}</p>
|
||||
</div>
|
||||
% endif
|
||||
% endfor
|
||||
</div>
|
||||
@@ -306,7 +333,7 @@
|
||||
$('#notifier-config-modal').unbind('hidden.bs.modal');
|
||||
|
||||
// Need this for setting conditions since conditions contain the character "
|
||||
$('#custom_conditions').val('${notifier['custom_conditions'] | n}')
|
||||
$('#custom_conditions').val(${json.dumps(notifier["custom_conditions"]) | n});
|
||||
|
||||
$('#condition-widget').filterer({
|
||||
parameters: ${parameters | n},
|
||||
@@ -314,7 +341,7 @@
|
||||
updateConditions: function(newConditions){
|
||||
$('#custom_conditions').val(JSON.stringify(newConditions));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function reloadModal() {
|
||||
$.ajax({
|
||||
@@ -332,7 +359,7 @@
|
||||
if (jqXHR) {
|
||||
var result = $.parseJSON(jqXHR.responseText);
|
||||
var msg = result.message;
|
||||
if (result.result == 'success') {
|
||||
if (result.result === 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
|
||||
} else {
|
||||
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
|
||||
@@ -392,7 +419,7 @@
|
||||
|
||||
% if notifier['agent_name'] == 'facebook':
|
||||
function disableFacebookRequest() {
|
||||
if ($('#facebook_app_id').val() != '' && $('#facebook_app_secret').val() != '') { $('#facebook_facebookStep1').prop('disabled', false); }
|
||||
if ($('#facebook_app_id').val() !== '' && $('#facebook_app_secret').val() !== '') { $('#facebook_facebookStep1').prop('disabled', false); }
|
||||
else { $('#facebook_facebookStep1').prop('disabled', true); }
|
||||
}
|
||||
disableFacebookRequest();
|
||||
@@ -406,19 +433,20 @@
|
||||
$('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1));
|
||||
}
|
||||
|
||||
var facebook_token;
|
||||
$.ajax({
|
||||
url: 'facebookStep1',
|
||||
data: {
|
||||
app_id: $('#facebook_app_id').val(),
|
||||
app_secret: $('#facebook_app_secret').val(),
|
||||
redirect_uri: $('#facebook_redirect_uri').val(),
|
||||
redirect_uri: $('#facebook_redirect_uri').val()
|
||||
},
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function (xhr, status) {
|
||||
var result = $.parseJSON(xhr.responseText);
|
||||
var msg = result.msg;
|
||||
if (result.result == 'success') {
|
||||
if (result.result === 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
|
||||
window.open(result.url);
|
||||
|
||||
@@ -457,18 +485,18 @@
|
||||
|
||||
$('#notifier-config-modal').on('hidden.bs.modal', function () {
|
||||
facebook_token = false;
|
||||
})
|
||||
});
|
||||
|
||||
% elif notifier['agent_name'] == 'browser':
|
||||
$('#browser_allow_browser').click(function () {
|
||||
PNotify.desktop.permission();
|
||||
})
|
||||
});
|
||||
|
||||
% elif notifier['agent_name'] == 'osx':
|
||||
$('#osxnotifyregister').click(function () {
|
||||
var osx_notify_app = $('#osx_notify_app').val();
|
||||
$.get('osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
|
||||
})
|
||||
});
|
||||
|
||||
% elif notifier['agent_name'] == 'email':
|
||||
var REGEX_EMAIL = '([a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@' +
|
||||
@@ -477,26 +505,37 @@
|
||||
plugins: ['remove_button'],
|
||||
persist: false,
|
||||
maxItems: null,
|
||||
valueField: 'email',
|
||||
labelField: 'user',
|
||||
searchField: ['user', 'email'],
|
||||
options: ${json.dumps(user_emails) | n},
|
||||
render: {
|
||||
item: function(item, escape) {
|
||||
return '<div>' +
|
||||
(item.user ? '<span class="user">' + escape(item.user) + '</span>' : '') +
|
||||
(item.email ? '<span class="email">' + escape(item.email) + '</span>' : '') +
|
||||
(item.text ? '<span class="item-text">' + escape(item.text) + '</span>' : '') +
|
||||
(item.value ? '<span class="item-value">' + escape(item.value) + '</span>' : '') +
|
||||
'</div>';
|
||||
},
|
||||
option: function(item, escape) {
|
||||
var label = item.user || item.email;
|
||||
var caption = item.user ? item.email : null;
|
||||
var label = item.text || item.value;
|
||||
var caption = item.text ? item.value : null;
|
||||
if (item.value.endsWith('-all')) {
|
||||
return '<div class="' + item.value + '">' + escape(label) + '</div>'
|
||||
}
|
||||
return '<div>' +
|
||||
escape(label) +
|
||||
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
},
|
||||
onItemAdd: function(value) {
|
||||
if (value === 'select-all') {
|
||||
var all_keys = $.map(this.options, function(option){
|
||||
return option.value.endsWith('-all') ? null : option.value;
|
||||
});
|
||||
this.setValue(all_keys);
|
||||
} else if (value === 'remove-all') {
|
||||
this.clear();
|
||||
this.refreshOptions();
|
||||
this.positionDropdown();
|
||||
}
|
||||
},
|
||||
createFilter: function(input) {
|
||||
var match, regex;
|
||||
|
||||
@@ -514,16 +553,15 @@
|
||||
},
|
||||
create: function(input) {
|
||||
if ((new RegExp('^' + REGEX_EMAIL + '$', 'i')).test(input)) {
|
||||
return {email: input};
|
||||
return {value: input};
|
||||
}
|
||||
var match = input.match(new RegExp('^([^<]*)\<' + REGEX_EMAIL + '\>$', 'i'));
|
||||
if (match) {
|
||||
return {
|
||||
email : match[2],
|
||||
user : $.trim(match[1])
|
||||
value : match[2],
|
||||
text : $.trim(match[1])
|
||||
};
|
||||
}
|
||||
alert('Invalid email address.');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -541,7 +579,6 @@
|
||||
create: true
|
||||
});
|
||||
var join_device_names = $join_device_names[0].selectize;
|
||||
console.log(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'join_device_names'), [])) | n});
|
||||
join_device_names.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'join_device_names'), [])) | n});
|
||||
% endif
|
||||
|
||||
@@ -673,7 +710,7 @@
|
||||
});
|
||||
|
||||
function sendTestNotification() {
|
||||
if ('${notifier["agent_name"]}' != 'browser') {
|
||||
if ('${notifier["agent_name"]}' !== 'browser') {
|
||||
$.ajax({
|
||||
url: 'send_notification',
|
||||
data: {
|
||||
@@ -697,7 +734,7 @@
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if ($('#browser_auto_hide_delay').val() == "0") {
|
||||
if ($('#browser_auto_hide_delay').val() === "0") {
|
||||
PNotify.prototype.options.hide = false;
|
||||
} else {
|
||||
PNotify.prototype.options.hide = true;
|
||||
|
@@ -34,7 +34,7 @@ from apscheduler.triggers.interval import IntervalTrigger
|
||||
|
||||
import activity_handler
|
||||
import activity_pinger
|
||||
import config
|
||||
import common
|
||||
import database
|
||||
import libraries
|
||||
import logger
|
||||
@@ -42,7 +42,6 @@ import mobile_app
|
||||
import notification_handler
|
||||
import notifiers
|
||||
import plextv
|
||||
import pmsconnect
|
||||
import users
|
||||
import versioncheck
|
||||
import plexpy.config
|
||||
@@ -83,6 +82,7 @@ INSTALL_TYPE = None
|
||||
CURRENT_VERSION = None
|
||||
LATEST_VERSION = None
|
||||
COMMITS_BEHIND = None
|
||||
PREV_RELEASE = None
|
||||
|
||||
UMASK = None
|
||||
|
||||
@@ -102,7 +102,9 @@ def initialize(config_file):
|
||||
global _INITIALIZED
|
||||
global CURRENT_VERSION
|
||||
global LATEST_VERSION
|
||||
global PREV_RELEASE
|
||||
global UMASK
|
||||
|
||||
CONFIG = plexpy.config.Config(config_file)
|
||||
CONFIG_FILE = config_file
|
||||
|
||||
@@ -190,6 +192,17 @@ def initialize(config_file):
|
||||
CONFIG.JWT_SECRET = generate_uuid()
|
||||
CONFIG.write()
|
||||
|
||||
# Get the previous version from the file
|
||||
version_lock_file = os.path.join(DATA_DIR, "version.lock")
|
||||
prev_version = None
|
||||
if os.path.isfile(version_lock_file):
|
||||
try:
|
||||
with open(version_lock_file, "r") as fp:
|
||||
prev_version = fp.read()
|
||||
except IOError as e:
|
||||
logger.error(u"Unable to read previous version from file '%s': %s" %
|
||||
(version_lock_file, e))
|
||||
|
||||
# Get the currently installed version. Returns None, 'win32' or the git
|
||||
# hash.
|
||||
CURRENT_VERSION, CONFIG.GIT_REMOTE, CONFIG.GIT_BRANCH = versioncheck.getVersion()
|
||||
@@ -198,8 +211,6 @@ def initialize(config_file):
|
||||
# This allowes one to restore to that version. The idea is that if we
|
||||
# arrive here, most parts of Tautulli seem to work.
|
||||
if CURRENT_VERSION:
|
||||
version_lock_file = os.path.join(DATA_DIR, "version.lock")
|
||||
|
||||
try:
|
||||
with open(version_lock_file, "w") as fp:
|
||||
fp.write(CURRENT_VERSION)
|
||||
@@ -217,6 +228,32 @@ def initialize(config_file):
|
||||
else:
|
||||
LATEST_VERSION = CURRENT_VERSION
|
||||
|
||||
# Get the previous release from the file
|
||||
release_file = os.path.join(DATA_DIR, "release.lock")
|
||||
PREV_RELEASE = common.VERSION_NUMBER
|
||||
if os.path.isfile(release_file):
|
||||
try:
|
||||
with open(release_file, "r") as fp:
|
||||
PREV_RELEASE = fp.read()
|
||||
except IOError as e:
|
||||
logger.error(u"Unable to read previous release from file '%s': %s" %
|
||||
(release_file, e))
|
||||
elif prev_version == 'cfd30996264b7e9fe4ef87f02d1cc52d1ae8bfca': # Commit hash for v1.4.25
|
||||
PREV_RELEASE = 'v1.4.25'
|
||||
|
||||
# Check if the release was updated
|
||||
if common.VERSION_NUMBER != PREV_RELEASE:
|
||||
CONFIG.UPDATE_SHOW_CHANGELOG = 1
|
||||
CONFIG.write()
|
||||
|
||||
# Write current release version to file for update checking
|
||||
try:
|
||||
with open(release_file, "w") as fp:
|
||||
fp.write(common.VERSION_NUMBER)
|
||||
except IOError as e:
|
||||
logger.error(u"Unable to write current release to file '%s': %s" %
|
||||
(release_file, e))
|
||||
|
||||
# Get the real PMS urls for SSL and remote access
|
||||
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
||||
plextv.get_server_resources()
|
||||
@@ -410,6 +447,7 @@ def start():
|
||||
|
||||
# Start background notification thread
|
||||
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
||||
notifiers.check_browser_enabled()
|
||||
|
||||
_STARTED = True
|
||||
|
||||
@@ -1549,6 +1587,7 @@ def upgrade():
|
||||
def shutdown(restart=False, update=False, checkout=False):
|
||||
cherrypy.engine.exit()
|
||||
SCHED.shutdown(wait=False)
|
||||
activity_handler.ACTIVITY_SCHED.shutdown(wait=False)
|
||||
|
||||
# Stop the notification threads
|
||||
for i in range(CONFIG.NOTIFICATION_THREADS):
|
||||
@@ -1579,23 +1618,35 @@ def shutdown(restart=False, update=False, checkout=False):
|
||||
|
||||
if restart:
|
||||
logger.info(u"Tautulli is restarting...")
|
||||
|
||||
exe = sys.executable
|
||||
args = [exe, FULL_PATH]
|
||||
args += ARGS
|
||||
if '--nolaunch' not in args:
|
||||
args += ['--nolaunch']
|
||||
|
||||
# os.execv fails with spaced names on Windows
|
||||
# https://bugs.python.org/issue19066
|
||||
# Separate out logger so we can shutdown logger after
|
||||
if NOFORK:
|
||||
logger.info('Running as service, not forking. Exiting...')
|
||||
elif os.name == 'nt':
|
||||
logger.info('Restarting Tautulli with %s', args)
|
||||
subprocess.Popen(args, cwd=os.getcwd())
|
||||
else:
|
||||
logger.info('Restarting Tautulli with %s', args)
|
||||
|
||||
logger.shutdown()
|
||||
|
||||
# os.execv fails with spaced names on Windows
|
||||
# https://bugs.python.org/issue19066
|
||||
if NOFORK:
|
||||
pass
|
||||
elif os.name == 'nt':
|
||||
subprocess.Popen(args, cwd=os.getcwd())
|
||||
else:
|
||||
os.execv(exe, args)
|
||||
|
||||
else:
|
||||
logger.shutdown()
|
||||
|
||||
os._exit(0)
|
||||
|
||||
|
||||
|
@@ -121,13 +121,19 @@ class ActivityHandler(object):
|
||||
|
||||
# Write it to the history table
|
||||
monitor_proc = activity_processor.ActivityProcessor()
|
||||
monitor_proc.write_session_history(session=db_session)
|
||||
row_id = monitor_proc.write_session_history(session=db_session)
|
||||
|
||||
# Remove the session from our temp session table
|
||||
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
|
||||
% (str(self.get_session_key()), str(self.get_rating_key())))
|
||||
ap.delete_session(session_key=self.get_session_key())
|
||||
delete_metadata_cache(self.get_session_key())
|
||||
if row_id:
|
||||
schedule_callback('session_key-{}'.format(self.get_session_key()), remove_job=True)
|
||||
|
||||
# Remove the session from our temp session table
|
||||
logger.debug(u"Tautulli ActivityHandler :: Removing sessionKey %s ratingKey %s from session queue"
|
||||
% (str(self.get_session_key()), str(self.get_rating_key())))
|
||||
ap.delete_session(row_id=row_id)
|
||||
delete_metadata_cache(self.get_session_key())
|
||||
else:
|
||||
schedule_callback('session_key-{}'.format(self.get_session_key()), func=force_stop_stream,
|
||||
args=[self.get_session_key()], seconds=30)
|
||||
|
||||
def on_pause(self, still_paused=False):
|
||||
if self.is_valid_session():
|
||||
@@ -245,9 +251,6 @@ class ActivityHandler(object):
|
||||
elif this_state == 'stopped':
|
||||
self.on_stop()
|
||||
|
||||
# Remove the callback if the stream is stopped
|
||||
schedule_callback('session_key-{}'.format(self.get_session_key()), remove_job=True)
|
||||
|
||||
elif this_state == 'buffering':
|
||||
self.on_buffer()
|
||||
|
||||
@@ -324,6 +327,7 @@ class TimelineHandler(object):
|
||||
9: 'album',
|
||||
10: 'track'}
|
||||
|
||||
identifier = self.timeline.get('identifier')
|
||||
state_type = self.timeline.get('state')
|
||||
media_type = media_types.get(self.timeline.get('type'))
|
||||
section_id = self.timeline.get('sectionID', 0)
|
||||
@@ -332,6 +336,10 @@ class TimelineHandler(object):
|
||||
media_state = self.timeline.get('mediaState')
|
||||
queue_size = self.timeline.get('queueSize')
|
||||
|
||||
# Return if it is not a library event (i.e. DVR EPG event)
|
||||
if identifier != 'com.plexapp.plugins.library':
|
||||
return
|
||||
|
||||
# Add a new media item to the recently added queue
|
||||
if media_type and section_id > 0 and \
|
||||
((state_type == 0 and metadata_state == 'created')): # or \
|
||||
@@ -433,13 +441,14 @@ def force_stop_stream(session_key):
|
||||
ap = activity_processor.ActivityProcessor()
|
||||
session = ap.get_session_by_key(session_key=session_key)
|
||||
|
||||
success = ap.write_session_history(session=session)
|
||||
row_id = ap.write_session_history(session=session)
|
||||
|
||||
if success:
|
||||
if row_id:
|
||||
# If session is written to the databaase successfully, remove the session from the session table
|
||||
logger.info(u"Tautulli ActivityHandler :: Removing stale stream with sessionKey %s ratingKey %s from session queue"
|
||||
% (session['session_key'], session['rating_key']))
|
||||
ap.delete_session(session_key=session_key)
|
||||
ap.delete_session(row_id=row_id)
|
||||
delete_metadata_cache(session_key)
|
||||
|
||||
else:
|
||||
session['write_attempts'] += 1
|
||||
@@ -451,7 +460,7 @@ def force_stop_stream(session_key):
|
||||
ap.increment_write_attempts(session_key=session_key)
|
||||
|
||||
# Reschedule for 30 seconds later
|
||||
schedule_callback('session_key={}'.format(session_key), function=force_stop_stream,
|
||||
schedule_callback('session_key-{}'.format(session_key), function=force_stop_stream,
|
||||
args=[session_key], seconds=30)
|
||||
|
||||
else:
|
||||
|
@@ -160,14 +160,13 @@ def check_active_sessions(ws_request=False):
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream, 'notify_action': 'on_stop'})
|
||||
|
||||
# Write the item history on playback stop
|
||||
success = monitor_process.write_session_history(session=stream)
|
||||
|
||||
if success:
|
||||
row_id = monitor_process.write_session_history(session=stream)
|
||||
|
||||
if row_id:
|
||||
# If session is written to the databaase successfully, remove the session from the session table
|
||||
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
||||
% (stream['session_key'], stream['rating_key']))
|
||||
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
monitor_process.delete_session(row_id=row_id)
|
||||
else:
|
||||
stream['write_attempts'] += 1
|
||||
|
||||
@@ -175,18 +174,14 @@ def check_active_sessions(ws_request=False):
|
||||
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||
"Will try again on the next pass. Write attempt %s."
|
||||
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
||||
monitor_db.action('UPDATE sessions SET write_attempts = ? '
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['write_attempts'], stream['session_key'], stream['rating_key']])
|
||||
monitor_process.increment_write_attempts(session_key=stream['session_key'])
|
||||
else:
|
||||
logger.warn(u"Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||
"Removing session from the database. Write attempt %s."
|
||||
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
||||
logger.debug(u"Tautulli Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
||||
% (stream['session_key'], stream['rating_key']))
|
||||
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
|
||||
monitor_process.delete_session(session_key=stream['session_key'])
|
||||
|
||||
# Process the newly received session data
|
||||
for session in media_container:
|
||||
|
@@ -155,7 +155,12 @@ class ActivityProcessor(object):
|
||||
|
||||
# Reload json from raw stream info
|
||||
if session.get('raw_stream_info'):
|
||||
session.update(json.loads(session['raw_stream_info']))
|
||||
raw_stream_info = json.loads(session['raw_stream_info'])
|
||||
# Don't overwrite id, session_key, stopped
|
||||
raw_stream_info.pop('id', None)
|
||||
raw_stream_info.pop('session_key', None)
|
||||
raw_stream_info.pop('stopped', None)
|
||||
session.update(raw_stream_info)
|
||||
|
||||
session = defaultdict(str, session)
|
||||
|
||||
@@ -177,6 +182,7 @@ class ActivityProcessor(object):
|
||||
else:
|
||||
logger.debug(u"Tautulli ActivityProcessor :: ratingKey %s not logged. Does not meet logging criteria. "
|
||||
u"Media type is '%s'" % (session['rating_key'], session['media_type']))
|
||||
return session['id']
|
||||
|
||||
if str(session['paused_counter']).isdigit():
|
||||
real_play_time = stopped - session['started'] - int(session['paused_counter'])
|
||||
@@ -284,7 +290,7 @@ class ActivityProcessor(object):
|
||||
|
||||
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
||||
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
|
||||
if prev_session == new_session == None:
|
||||
if prev_session is None and new_session is None:
|
||||
args = [last_id, last_id]
|
||||
elif prev_session['rating_key'] == new_session['rating_key'] and prev_session['view_offset'] <= new_session['view_offset']:
|
||||
args = [prev_session['reference_id'], new_session['id']]
|
||||
@@ -414,8 +420,8 @@ class ActivityProcessor(object):
|
||||
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_metadata transaction...")
|
||||
self.db.upsert(table_name='session_history_metadata', key_dict=keys, value_dict=values)
|
||||
|
||||
# Return true when the session is successfully written to the database
|
||||
return True
|
||||
# Return the session row id when the session is successfully written to the database
|
||||
return session['id']
|
||||
|
||||
def get_sessions(self, user_id=None, ip_address=None):
|
||||
query = 'SELECT * FROM sessions'
|
||||
@@ -456,9 +462,11 @@ class ActivityProcessor(object):
|
||||
|
||||
return None
|
||||
|
||||
def delete_session(self, session_key=None):
|
||||
def delete_session(self, session_key=None, row_id=None):
|
||||
if str(session_key).isdigit():
|
||||
self.db.action('DELETE FROM sessions WHERE session_key = ?', [session_key])
|
||||
elif str(row_id).isdigit():
|
||||
self.db.action('DELETE FROM sessions WHERE id = ?', [row_id])
|
||||
|
||||
def set_session_last_paused(self, session_key=None, timestamp=None):
|
||||
if str(session_key).isdigit():
|
||||
|
@@ -477,12 +477,12 @@ NOTIFICATION_PARAMETERS = [
|
||||
{
|
||||
'category': 'Tautulli Update Available',
|
||||
'parameters': [
|
||||
{'name': 'Plexpy Update Version', 'type': 'int', 'value': 'plexpy_update_version', 'description': 'The available update version for Tautulli.'},
|
||||
{'name': 'Plexpy Update Tar', 'type': 'int', 'value': 'plexpy_update_tar', 'description': 'The tar download URL for the available update.'},
|
||||
{'name': 'Plexpy Update Zip', 'type': 'int', 'value': 'plexpy_update_zip', 'description': 'The zip download URL for the available update.'},
|
||||
{'name': 'Plexpy Update Commit', 'type': 'int', 'value': 'plexpy_update_commit', 'description': 'The commit hash for the available update.'},
|
||||
{'name': 'Plexpy Update Behind', 'type': 'int', 'value': 'plexpy_update_behind', 'description': 'The number of commits behind for the available update.'},
|
||||
{'name': 'Plexpy Update Changelog', 'type': 'int', 'value': 'plexpy_update_changelog', 'description': 'The changelog for the available update.'},
|
||||
{'name': 'Tautulli Update Version', 'type': 'int', 'value': 'tautulli_update_version', 'description': 'The available update version for Tautulli.'},
|
||||
{'name': 'Tautulli Update Tar', 'type': 'int', 'value': 'tautulli_update_tar', 'description': 'The tar download URL for the available update.'},
|
||||
{'name': 'Tautulli Update Zip', 'type': 'int', 'value': 'tautulli_update_zip', 'description': 'The zip download URL for the available update.'},
|
||||
{'name': 'Tautulli Update Commit', 'type': 'int', 'value': 'tautulli_update_commit', 'description': 'The commit hash for the available update.'},
|
||||
{'name': 'Tautulli Update Behind', 'type': 'int', 'value': 'tautulli_update_behind', 'description': 'The number of commits behind for the available update.'},
|
||||
{'name': 'Tautulli Update Changelog', 'type': 'int', 'value': 'tautulli_update_changelog', 'description': 'The changelog for the available update.'},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
@@ -306,6 +306,11 @@ def initHooks(global_exceptions=True, thread_exceptions=True, pass_original=True
|
||||
# Monkey patch the run() by monkey patching the __init__ method
|
||||
threading.Thread.__init__ = new_init
|
||||
|
||||
|
||||
def shutdown():
|
||||
logging.shutdown()
|
||||
|
||||
|
||||
# Expose logger methods
|
||||
# Main Tautulli logger
|
||||
info = logger.info
|
||||
|
@@ -523,10 +523,15 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
remaining_duration = duration - view_offset
|
||||
|
||||
# Build Plex URL
|
||||
if notify_params['media_type'] == 'track':
|
||||
plex_web_rating_key = notify_params['parent_rating_key']
|
||||
else:
|
||||
plex_web_rating_key = notify_params['rating_key']
|
||||
|
||||
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
|
||||
web_url=plexpy.CONFIG.PMS_WEB_URL,
|
||||
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
rating_key=rating_key)
|
||||
rating_key=plex_web_rating_key)
|
||||
|
||||
# Get media IDs from guid and build URLs
|
||||
if 'imdb://' in notify_params['guid']:
|
||||
|
@@ -64,6 +64,9 @@ import users
|
||||
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
||||
|
||||
|
||||
BROWSER_NOTIFIERS = {}
|
||||
|
||||
|
||||
AGENT_IDS = {'growl': 0,
|
||||
'prowl': 1,
|
||||
'xbmc': 2,
|
||||
@@ -551,6 +554,10 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
|
||||
db.upsert(table_name='notifiers', key_dict=keys, value_dict=values)
|
||||
logger.info(u"Tautulli Notifiers :: Updated notification agent: %s (notifier_id %s)." % (agent['label'], notifier_id))
|
||||
blacklist_logger()
|
||||
|
||||
if agent['name'] == 'browser':
|
||||
check_browser_enabled()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Notifiers :: Unable to update notification agent: %s." % e)
|
||||
@@ -994,40 +1001,15 @@ class BROWSER(Notifier):
|
||||
Browser notifications
|
||||
"""
|
||||
NAME = 'Browser'
|
||||
_DEFAULT_CONFIG = {'enabled': 0,
|
||||
'auto_hide_delay': 5
|
||||
_DEFAULT_CONFIG = {'auto_hide_delay': 5
|
||||
}
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
logger.info(u"Tautulli Notifiers :: {name} notification sent.".format(name=self.NAME))
|
||||
return True
|
||||
|
||||
def get_notifications(self):
|
||||
if not self.config['enabled']:
|
||||
return
|
||||
|
||||
db = database.MonitorDatabase()
|
||||
result = db.select('SELECT subject_text, body_text FROM notify_log '
|
||||
'WHERE agent_id = 17 AND timestamp >= ? ',
|
||||
args=[time.time() - 3])
|
||||
|
||||
notifications = []
|
||||
for item in result:
|
||||
notification = {'subject_text': item['subject_text'],
|
||||
'body_text': item['body_text'],
|
||||
'delay': self.config['auto_hide_delay']}
|
||||
notifications.append(notification)
|
||||
|
||||
return {'notifications': notifications}
|
||||
|
||||
def return_config_options(self):
|
||||
config_option = [{'label': 'Enable Browser Notifications',
|
||||
'value': self.config['enabled'],
|
||||
'name': 'browser_enabled',
|
||||
'description': 'Enable to display desktop notifications from your browser.',
|
||||
'input_type': 'checkbox'
|
||||
},
|
||||
{'label': 'Allow Notifications',
|
||||
config_option = [{'label': 'Allow Notifications',
|
||||
'value': 'Allow Notifications',
|
||||
'name': 'browser_allow_browser',
|
||||
'description': 'Click to allow browser notifications. You must click this button for each browser.',
|
||||
@@ -1258,13 +1240,13 @@ class EMAIL(Notifier):
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
if self.config['html_support']:
|
||||
body = body.replace('\n', '<br />')
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg.attach(MIMEText(bleach.clean(body, strip=True), 'plain', 'utf-8'))
|
||||
msg.attach(MIMEText(body, 'html', 'utf-8'))
|
||||
else:
|
||||
msg = MIMEText(body, 'plain', 'utf-8')
|
||||
|
||||
msg['Date'] = email.utils.formatdate(localtime=True)
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = email.utils.formataddr((self.config['from_name'], self.config['from']))
|
||||
msg['To'] = ','.join(self.config['to'])
|
||||
@@ -1293,8 +1275,25 @@ class EMAIL(Notifier):
|
||||
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
|
||||
return False
|
||||
|
||||
def get_user_emails(self):
|
||||
emails = {u['email']: u['friendly_name'] for u in users.Users().get_users() if u['email']}
|
||||
|
||||
user_emails_to = {v: '' for v in self.config['to']}
|
||||
user_emails_cc = {v: '' for v in self.config['cc']}
|
||||
user_emails_bcc = {v: '' for v in self.config['bcc']}
|
||||
|
||||
user_emails_to.update(emails)
|
||||
user_emails_cc.update(emails)
|
||||
user_emails_bcc.update(emails)
|
||||
|
||||
user_emails_to = [{'value': k, 'text': v} for k, v in user_emails_to.iteritems()]
|
||||
user_emails_cc = [{'value': k, 'text': v} for k, v in user_emails_cc.iteritems()]
|
||||
user_emails_bcc = [{'value': k, 'text': v} for k, v in user_emails_bcc.iteritems()]
|
||||
|
||||
return user_emails_to, user_emails_cc, user_emails_bcc
|
||||
|
||||
def return_config_options(self):
|
||||
user_emails = {} # User selection set with selectize options
|
||||
user_emails_to, user_emails_cc, user_emails_bcc = self.get_user_emails()
|
||||
|
||||
config_option = [{'label': 'From Name',
|
||||
'value': self.config['from_name'],
|
||||
@@ -1312,22 +1311,22 @@ class EMAIL(Notifier):
|
||||
'value': self.config['to'],
|
||||
'name': 'email_to',
|
||||
'description': 'The email address(es) of the recipients.',
|
||||
'input_type': 'select',
|
||||
'select_options': user_emails
|
||||
'input_type': 'selectize',
|
||||
'select_options': user_emails_to
|
||||
},
|
||||
{'label': 'CC',
|
||||
'value': self.config['cc'],
|
||||
'name': 'email_cc',
|
||||
'description': 'The email address(es) to CC.',
|
||||
'input_type': 'select',
|
||||
'select_options': user_emails
|
||||
'input_type': 'selectize',
|
||||
'select_options': user_emails_cc
|
||||
},
|
||||
{'label': 'BCC',
|
||||
'value': self.config['bcc'],
|
||||
'name': 'email_bcc',
|
||||
'description': 'The email address(es) to BCC.',
|
||||
'input_type': 'select',
|
||||
'select_options': user_emails
|
||||
'input_type': 'selectize',
|
||||
'select_options': user_emails_bcc
|
||||
},
|
||||
{'label': 'SMTP Server',
|
||||
'value': self.config['smtp_server'],
|
||||
@@ -1362,8 +1361,7 @@ class EMAIL(Notifier):
|
||||
{'label': 'Enable HTML Support',
|
||||
'value': self.config['html_support'],
|
||||
'name': 'email_html_support',
|
||||
'description': 'Style your messages using HTML tags. '
|
||||
'Line breaks (<br>) will be inserted automatically.',
|
||||
'description': 'Style your messages using HTML tags.',
|
||||
'input_type': 'checkbox'
|
||||
}
|
||||
]
|
||||
@@ -3527,3 +3525,27 @@ def upgrade_config_to_db():
|
||||
notifier_id = add_notifier_config(agent_id=agent_id)
|
||||
set_notifier_config(notifier_id=notifier_id, agent_id=agent_id, **notifier_config)
|
||||
|
||||
|
||||
def check_browser_enabled():
|
||||
global BROWSER_NOTIFIERS
|
||||
BROWSER_NOTIFIERS = {}
|
||||
for n in get_notifiers():
|
||||
if n['agent_id'] == 17 and n['active']:
|
||||
notifier_config = get_notifier_config(n['id'])
|
||||
BROWSER_NOTIFIERS[n['id']] = notifier_config['config']['auto_hide_delay']
|
||||
|
||||
|
||||
def get_browser_notifications():
|
||||
db = database.MonitorDatabase()
|
||||
result = db.select('SELECT notifier_id, subject_text, body_text FROM notify_log '
|
||||
'WHERE agent_id = 17 AND timestamp >= ? ',
|
||||
args=[time.time() - 5])
|
||||
|
||||
notifications = []
|
||||
for item in result:
|
||||
notification = {'subject_text': item['subject_text'],
|
||||
'body_text': item['body_text'],
|
||||
'delay': BROWSER_NOTIFIERS.get(item['notifier_id'], 5)}
|
||||
notifications.append(notification)
|
||||
|
||||
return {'notifications': notifications}
|
||||
|
@@ -537,7 +537,7 @@ class PmsConnect(object):
|
||||
try:
|
||||
with open(in_file_path, 'r') as inFile:
|
||||
metadata = json.load(inFile)
|
||||
except IOError as e:
|
||||
except (IOError, ValueError) as e:
|
||||
pass
|
||||
|
||||
if metadata:
|
||||
@@ -1162,7 +1162,7 @@ class PmsConnect(object):
|
||||
try:
|
||||
with open(out_file_path, 'w') as outFile:
|
||||
json.dump(metadata, outFile)
|
||||
except IOError as e:
|
||||
except (IOError, ValueError) as e:
|
||||
logger.error(u"Tautulli Pmsconnect :: Unable to create cache file for metadata (sessionKey %s): %s"
|
||||
% (cache_key, e))
|
||||
|
||||
|
@@ -597,7 +597,7 @@ class Users(object):
|
||||
for item in result:
|
||||
user = {'user_id': item['user_id'],
|
||||
'username': item['username'],
|
||||
'friendly_name': item['friendly_name'],
|
||||
'friendly_name': item['friendly_name'] or item['username'],
|
||||
'email': item['email']
|
||||
}
|
||||
users.append(user)
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.13-beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.14-beta"
|
||||
|
@@ -298,14 +298,14 @@ def checkout_git_branch():
|
||||
logger.info('Output: ' + str(output))
|
||||
|
||||
|
||||
def read_changelog(latest_only=False):
|
||||
def read_changelog(latest_only=False, since_prev_release=False):
|
||||
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
|
||||
|
||||
if not os.path.isfile(changelog_file):
|
||||
return '<h4>Missing changelog file</h4>'
|
||||
|
||||
try:
|
||||
output = ''
|
||||
output = ['']
|
||||
prev_level = 0
|
||||
|
||||
latest_version_found = False
|
||||
@@ -329,27 +329,34 @@ def read_changelog(latest_only=False):
|
||||
break
|
||||
elif latest_only:
|
||||
latest_version_found = True
|
||||
# Add a space to the end of the release to match tags
|
||||
elif since_prev_release and str(plexpy.PREV_RELEASE) + ' ' in header_text:
|
||||
break
|
||||
|
||||
output += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
|
||||
output[-1] += '<h' + header_level + '>' + header_text + '</h' + header_level + '>'
|
||||
|
||||
elif line_list_match:
|
||||
line_level = len(line_list_match.group(1)) / 2
|
||||
line_text = line_list_match.group(2)
|
||||
|
||||
if line_level > prev_level:
|
||||
output += '<ul>' * (line_level - prev_level) + '<li>' + line_text + '</li>'
|
||||
output[-1] += '<ul>' * (line_level - prev_level) + '<li>' + line_text + '</li>'
|
||||
elif line_level < prev_level:
|
||||
output += '</ul>' * (prev_level - line_level) + '<li>' + line_text + '</li>'
|
||||
output[-1] += '</ul>' * (prev_level - line_level) + '<li>' + line_text + '</li>'
|
||||
else:
|
||||
output += '<li>' + line_text + '</li>'
|
||||
output[-1] += '<li>' + line_text + '</li>'
|
||||
|
||||
prev_level = line_level
|
||||
|
||||
elif line.strip() == '' and prev_level:
|
||||
output += '</ul>' * (prev_level)
|
||||
output[-1] += '</ul>' * (prev_level)
|
||||
output.append('')
|
||||
prev_level = 0
|
||||
|
||||
return output
|
||||
if since_prev_release:
|
||||
output.reverse()
|
||||
|
||||
return ''.join(output)
|
||||
|
||||
except IOError as e:
|
||||
logger.error('Tautulli Version Checker :: Unable to open changelog file. %s' % e)
|
||||
|
@@ -180,7 +180,7 @@ def process(opcode, data):
|
||||
info = json.loads(data)
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli WebSocket :: Error decoding message from websocket: %s" % e)
|
||||
logger.debug(data)
|
||||
logger.websocket_error(data)
|
||||
return False
|
||||
|
||||
info = info.get('NotificationContainer', info)
|
||||
|
@@ -3126,8 +3126,7 @@ class WebInterface(object):
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
def get_browser_notifications(self, **kwargs):
|
||||
browser = notifiers.BROWSER()
|
||||
result = browser.get_notifications()
|
||||
result = notifiers.get_browser_notifications()
|
||||
|
||||
if result:
|
||||
notifications = result['notifications']
|
||||
@@ -3546,13 +3545,20 @@ class WebInterface(object):
|
||||
|
||||
@cherrypy.expose
|
||||
@requireAuth(member_of("admin"))
|
||||
def get_changelog(self, latest_only=False, update_shown=False, **kwargs):
|
||||
latest_only = True if latest_only == 'true' else False
|
||||
def get_changelog(self, latest_only=False, since_prev_release=False, update_shown=False, **kwargs):
|
||||
latest_only = (latest_only == 'true')
|
||||
since_prev_release = (since_prev_release == 'true')
|
||||
|
||||
if since_prev_release and plexpy.PREV_RELEASE == common.VERSION_NUMBER:
|
||||
latest_only = True
|
||||
since_prev_release = False
|
||||
|
||||
# Set update changelog shown status
|
||||
if update_shown == 'true':
|
||||
plexpy.CONFIG.__setattr__('UPDATE_SHOW_CHANGELOG', 0)
|
||||
plexpy.CONFIG.write()
|
||||
return versioncheck.read_changelog(latest_only=latest_only)
|
||||
|
||||
return versioncheck.read_changelog(latest_only=latest_only, since_prev_release=since_prev_release)
|
||||
|
||||
##### Info #####
|
||||
|
||||
@@ -4457,7 +4463,7 @@ class WebInterface(object):
|
||||
counts['total_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||
if s['location'] == 'lan':
|
||||
counts['lan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||
elif s['location'] == 'wan':
|
||||
else:
|
||||
counts['wan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||
|
||||
result.update(counts)
|
||||
|
Reference in New Issue
Block a user