Compare commits

...

29 Commits

Author SHA1 Message Date
JonnyWong16
98b5cb67ca v2.1.16-beta 2018-07-06 19:06:02 -07:00
JonnyWong16
26db7f1984 Update API docs with rating_image and audience_rating_image 2018-07-06 19:04:06 -07:00
JonnyWong16
e1afbd4eff Remove white text on accordion hover 2018-07-06 18:52:19 -07:00
JonnyWong16
f6090bcdf0 Fix accordion icon colours 2018-07-06 18:44:20 -07:00
JonnyWong16
0950ff7ecf Fix incorrect stream_duration parameter for playback start notifications 2018-07-06 18:43:57 -07:00
JonnyWong16
e766cb6093 Add critic_rating to notification parameters 2018-07-06 18:34:50 -07:00
JonnyWong16
8982ae83ac Percent helper round before trunc 2018-07-06 18:15:18 -07:00
JonnyWong16
2b395a7ad9 Fix API get_logs (Fixes Tautulli/Tautulli-Issues#100) 2018-07-05 18:50:04 -07:00
JonnyWong16
a9b5c91f84 Fix OAuth popup loader 2018-07-05 18:23:10 -07:00
JonnyWong16
c0b960bccf Refactor Plex OAuth code 2018-07-05 09:06:03 -07:00
JonnyWong16
8514cf1975 Reset pong count 2018-07-04 12:25:32 -07:00
JonnyWong16
c5e37badd8 Fix infinite websocket connection loop 2018-07-03 20:06:37 -07:00
JonnyWong16
c0f1079b4e Remove connection attempts from inital websocket connection 2018-07-03 19:58:58 -07:00
JonnyWong16
1da4b8ecb4 Add ping/pong to websocket 2018-07-03 18:10:52 -07:00
JonnyWong16
c2ba2b4e98 Add popup OAuth window to wizard and settings 2018-07-03 09:49:11 -07:00
JonnyWong16
d9ea781462 Center Discord chat popout window 2018-07-03 09:46:37 -07:00
JonnyWong16
5ee5ca7dbf Popup window for Plex OAuth 2018-07-03 09:46:12 -07:00
JonnyWong16
176392d837 Fix typo in newlsetter with single reminaing season 2018-07-02 16:16:05 -07:00
JonnyWong16
b65e6a39a0 Accordion for login methods on login page 2018-07-02 15:56:24 -07:00
JonnyWong16
5c7a3a12e9 Add OAuth to token refresh in settings 2018-07-02 12:28:34 -07:00
JonnyWong16
e7072edbd1 Add OAuth to setup wizard 2018-07-02 12:04:12 -07:00
JonnyWong16
2711597ffb Pass OAuth client headers to server 2018-07-02 11:20:25 -07:00
JonnyWong16
24458bd23c Log login method 2018-07-02 09:55:41 -07:00
JonnyWong16
3fd0708d21 Use client headers for OAuth 2018-07-02 09:55:28 -07:00
JonnyWong16
434cb89ba8 Correct client ID 2018-07-02 09:00:25 -07:00
JonnyWong16
745d398527 Improve Plex OAuth 2018-07-02 08:51:51 -07:00
JonnyWong16
16bfcade8c Log failed OAuth attempts 2018-07-02 00:45:47 -07:00
JonnyWong16
f69f5a79d9 Merge branch 'beta' into nightly 2018-07-01 23:40:03 -07:00
JonnyWong16
3bd1b03faf Add Plex OAuth to login page 2018-07-01 22:55:13 -07:00
24 changed files with 753 additions and 339 deletions

4
API.md
View File

@@ -373,6 +373,7 @@ Returns:
"art": "/library/metadata/1219/art/1503306930",
"aspect_ratio": "1.78",
"audience_rating": "",
"audience_rating_image": "rottentomatoes://image.rating.upright",
"audio_bitrate": "384",
"audio_bitrate_mode": "",
"audio_channel_layout": "5.1(side)",
@@ -449,6 +450,7 @@ Returns:
"progress_percent": "0",
"quality_profile": "Original",
"rating": "7.8",
"rating_image": "rottentomatoes://image.rating.ripe",
"rating_key": "153037",
"relay": 0,
"section_id": "2",
@@ -1084,6 +1086,7 @@ Returns:
"added_at": "1461572396",
"art": "/library/metadata/1219/art/1462175063",
"audience_rating": "8",
"audience_rating_image": "rottentomatoes://image.rating.upright",
"banner": "/library/metadata/1219/banner/1462175063",
"collections": [],
"content_rating": "TV-MA",
@@ -1181,6 +1184,7 @@ Returns:
"parent_thumb": "/library/metadata/153036/thumb/1462175062",
"parent_title": "",
"rating": "7.8",
"rating_image": "rottentomatoes://image.rating.ripe",
"rating_key": "153037",
"section_id": "2",
"sort_title": "Game of Thrones",

View File

@@ -1,5 +1,22 @@
# Changelog
## v2.1.16-beta (2018-07-06)
* Monitoring:
* Fix: Plex server not detected as down during sudden network loss.
* Notifications:
* Fix: Incorrect rounding of percentages in some cases.
* Fix: Incorrect stream duration value for playback start notifications.
* New: Added critic rating parameter for Rotten Tomatoes ratings.
* Newsletters:
* Fix: Typo in "seasons" when there is only one additional season.
* UI:
* New: Added ability to use Plex OAuth to login to Tautulli.
* API:
* Fix: Unicode characters causing get_logs command to return bad data.
* New: Added rating_image and audience_rating_image to get_activity and get_metadata commands.
## v2.1.15-beta (2018-07-01)
* Monitoring:

View File

@@ -291,6 +291,7 @@ ${next.modalIncludes()}
<script src="${http_root}js/bootstrap.min.js"></script>
<script src="${http_root}js/bootstrap-hover-dropdown.min.js"></script>
<script src="${http_root}js/pnotify.custom.min.js"></script>
<script src="${http_root}js/platform.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/jquery.qrcode.min.js"></script>
<script src="${http_root}js/jquery.tripleclick.min.js"></script>

View File

@@ -101,6 +101,9 @@ select.form-control {
overflow: hidden;
text-overflow: ellipsis;
}
.wizard-input-section p.welcome-message {
margin: 20px 0;
}
.wizard-input-section .selectize-control.form-control.selectize-pms-ip .selectize-input > div {
max-width: 360px;
overflow: hidden;
@@ -2970,10 +2973,13 @@ a .home-platforms-list-cover-face:hover
-o-transition: all 0.3s ease;
transition: all 0.3s ease;
}
.accordion li .link:hover {
color: #fff;
.accordion li .link:hover,
.accordion li .link:hover i.fa {
background: #2f2f2f;
}
.accordion li .link i.fa {
color: #999;
}
.accordion li .link span.toggle-right {
float: right;
padding-left: 10px;
@@ -2987,7 +2993,8 @@ a .home-platforms-list-cover-face:hover
-o-transition: all 0.3s ease;
transition: all 0.3s ease;
}
.accordion li.open .link {
.accordion li.open .link,
.accordion li.open .link i.fa {
color: #f9be03;
}
.accordion li.open .fa-chevron-down {
@@ -3470,6 +3477,9 @@ a.no-highlight:hover {
max-width: 1170px;
}
}
.login-body-container {
margin: 50px 0;
}
.login-container {
margin-right: auto;
margin-left: auto;
@@ -3483,6 +3493,11 @@ a.no-highlight:hover {
margin: 0 auto 50px auto;
text-align: center;
}
.login-container .login-method-header {
text-align: center;
font-weight: 600;
text-transform: uppercase;
}
.login-container .form-group {
margin-bottom: 20px;
}
@@ -3503,8 +3518,9 @@ a.no-highlight:hover {
text-shadow: 0 -1px 1px rgba(0,0,0,.4),0 0 15px rgba(0,0,0,.2);
}
.login-container .remember-group {
float: left;
color: #999;
display: inline-block;
margin-top: 7.5px;
}
.login-container .remember-group .control-label {
display: inline;
@@ -3512,6 +3528,33 @@ a.no-highlight:hover {
font-weight: 400;
cursor: pointer;
}
.login-divider {
text-align: center;
border-bottom: 1px solid #555;
line-height: 0.1em;
margin: 50px auto;
max-width: 400px;
text-transform: uppercase;
}
.login-divider span {
background: #1f1f1f;
padding: 0 15px;
color: #999;
}
.login-button-plex {
text-align: center;
}
.login-button-plex .remember-group {
margin-top: 20px;
}
.login-button-plex button#sign-in-plex {
float: none;
}
.login-alert {
text-align: center;
padding: 8px;
display: none;
}
#admin-login-modal .form-group label {
font-weight: 400;
color: #999;

File diff suppressed because one or more lines are too long

View File

@@ -351,21 +351,26 @@ function getCookie(cname) {
}
return "";
}
var Accordion = function (el, multiple) {
var Accordion = function (el, multiple, close) {
this.el = el || {};
this.multiple = multiple || false;
this.close = (close === undefined) ? true : close;
// Variables privadas
var links = this.el.find('.link');
// Evento
links.on('click', {
el: this.el,
multiple: this.multiple
multiple: this.multiple,
close: this.close
}, this.dropdown);
};
Accordion.prototype.dropdown = function (e) {
var $el = e.data.el;
$this = $(this);
$next = $this.next();
if (!e.data.close && $this.parent().hasClass('open')) {
return
}
$next.slideToggle();
$this.parent().toggleClass('open');
if (!e.data.multiple) {
@@ -465,3 +470,168 @@ function openPlexXML(endpoint, plextv, params) {
window.open(xml_url, '_blank');
});
}
function PopupCenter(url, title, w, h) {
// Fixes dual-screen position Most browsers Firefox
var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : window.screenX;
var dualScreenTop = window.screenTop != undefined ? window.screenTop : window.screenY;
var width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
var height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
var left = ((width / 2) - (w / 2)) + dualScreenLeft;
var top = ((height / 2) - (h / 2)) + dualScreenTop;
var newWindow = window.open(url, title, 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);
// Puts focus on the newWindow
if (window.focus) {
newWindow.focus();
}
return newWindow;
}
if (!localStorage.getItem('Tautulli_ClientId')) {
localStorage.setItem('Tautulli_ClientId', uuidv4());
}
function uuidv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
)
}
var x_plex_headers = {
'Accept': 'application/json',
'X-Plex-Product': 'Tautulli',
'X-Plex-Version': 'Plex OAuth',
'X-Plex-Client-Identifier': localStorage.getItem('Tautulli_ClientId'),
'X-Plex-Platform': platform.name,
'X-Plex-Platform-Version': platform.version,
'X-Plex-Device': platform.os.toString(),
'X-Plex-Device-Name': platform.name
};
var plex_oauth_window = null;
const plex_oauth_loader = '<style>' +
'.login-loader-container {' +
'font-family: "Open Sans", Arial, sans-serif;' +
'position: absolute;' +
'top: 0;' +
'right: 0;' +
'bottom: 0;' +
'left: 0;' +
'}' +
'.login-loader-message {' +
'color: #282A2D;' +
'text-align: center;' +
'position: absolute;' +
'left: 50%;' +
'top: 25%;' +
'transform: translate(-50%, -50%);' +
'}' +
'.login-loader {' +
'border: 5px solid #ccc;' +
'-webkit-animation: spin 1s linear infinite;' +
'animation: spin 1s linear infinite;' +
'border-top: 5px solid #282A2D;' +
'border-radius: 50%;' +
'width: 50px;' +
'height: 50px;' +
'position: relative;' +
'left: calc(50% - 25px);' +
'}' +
'@keyframes spin {' +
'0% { transform: rotate(0deg); }' +
'100% { transform: rotate(360deg); }' +
'}' +
'</style>' +
'<div class="login-loader-container">' +
'<div class="login-loader-message">' +
'<div class="login-loader"></div>' +
'<br>' +
'Redirecting to the Plex login page...' +
'</div>' +
'</div>';
function closePlexOAuthWindow() {
if (plex_oauth_window) {
plex_oauth_window.close();
}
}
getPlexOAuthPin = function () {
var deferred = $.Deferred();
$.ajax({
url: 'https://plex.tv/api/v2/pins?strong=true',
type: 'POST',
headers: x_plex_headers,
success: function(data) {
plex_oauth_window.location = 'https://app.plex.tv/auth/#!?clientID=' + x_plex_headers['X-Plex-Client-Identifier'] + '&code=' + data.code;
deferred.resolve({pin: data.id, code: data.code});
},
error: function() {
closePlexOAuthWindow();
deferred.reject();
}
});
return deferred;
};
var polling = null;
function PlexOAuth(success, error, pre) {
if (typeof pre === "function") {
pre()
}
clearTimeout(polling);
closePlexOAuthWindow();
plex_oauth_window = PopupCenter('', 'Plex-OAuth', 600, 700);
$(plex_oauth_window.document.body).html(plex_oauth_loader);
getPlexOAuthPin().then(function (data) {
const pin = data.pin;
const code = data.code;
var keep_polling = true;
(function poll() {
polling = setTimeout(function () {
$.ajax({
url: 'https://plex.tv/api/v2/pins/' + pin,
type: 'GET',
headers: x_plex_headers,
success: function (data) {
if (data.authToken){
keep_polling = false;
closePlexOAuthWindow();
if (typeof success === "function") {
success(data.authToken)
}
}
},
error: function () {
keep_polling = false;
closePlexOAuthWindow();
if (typeof error === "function") {
error()
}
},
complete: function () {
if (keep_polling){
poll();
} else {
clearTimeout(polling);
}
},
timeout: 1000
});
}, 1000);
})();
}, function () {
closePlexOAuthWindow();
if (typeof error === "function") {
error()
}
});
}

View File

@@ -1,4 +1,8 @@
<!doctype html>
<%
import plexpy
plex_login = plexpy.CONFIG.HTTP_PLEX_ADMIN or plexpy.CONFIG.ALLOW_GUEST_ACCESS
%>
<!doctype html>
<html lang="en">
<head>
@@ -32,8 +36,8 @@
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
</head>
<body>
<div class="body-container">
<body style="margin: 0; overflow: auto;">
<div class="login-body-container">
<div class="container-fluid">
<div class="row">
<div class="login-container">
@@ -42,10 +46,38 @@
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<form id="login-form">
<div id="incorrect-login" class="alert alert-danger" style="text-align: center; padding: 8px; display: none;">
Incorrect username or password.
<div id="sign-in-alert" class="alert alert-danger login-alert"></div>
</div>
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<ul id="login-methods" class="accordion list-unstyled">
% if plex_login:
<li class="open">
<div class="link login-method-header">
Sign In with Plex
</div>
<ul class="submenu login-button-plex" style="display: block;">
<li>
<div>
<button id="sign-in-plex" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i>&nbsp; Sign In</button>
</div>
<div class="remember-group">
<label class="control-label">
<input type="checkbox" id="remember_me_plex" name="remember_me_plex" title="for 30 days" value="1" checked="checked" /> Remember me
</label>
</div>
</li>
</ul>
</li>
% endif
<li class="${'open' if not plex_login else ''}">
<div class="link login-method-header">
Sign In with Tautulli
</div>
<ul class="submenu" style="${'display: block;' if not plex_login else ''}">
<li>
<form id="login-form">
<div class="form-group">
<label for="username" class="control-label">
Username
@@ -58,15 +90,19 @@
</label>
<input type="password" id="password" name="password" class="form-control">
</div>
<div class="form-footer">
<div class="remember-group">
<div class="form-group">
<span class="remember-group">
<label class="control-label">
<input type="checkbox" id="remember_me" name="remember_me" title="for 30 days" value="1" checked="checked" /> Remember me
</label>
</div>
</span>
<button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i>&nbsp; Sign In</button>
</div>
</form>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
@@ -75,29 +111,76 @@
</div>
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
<script src="${http_root}js/platform.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script>
<script>
var login_accordion = new Accordion($('#login-methods'), false, false);
function OAuthSuccessCallback(authToken) {
signIn(true, authToken);
}
function OAuthErrorCallback() {
$('#sign-in-alert').text('Error communicating with Plex.tv.').show();
}
$('#sign-in-plex').click(function() {
PlexOAuth(OAuthSuccessCallback, OAuthErrorCallback);
});
$('#login-form').submit(function(event) {
event.preventDefault();
$('#sign-in').prop('disabled', true).html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Sign In');
signIn(false);
});
function signIn(plex, token) {
$('.login-container button').prop('disabled', true);
if (plex) {
$('#sign-in-plex').html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Sign In');
} else {
$('#sign-in').html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Sign In');
}
const username = plex ? null : $('#username').val();
const password = plex ? null : $('#password').val();
const remember_me = plex ? ($('#remember_me_plex').is(':checked') ? '1' : '0')
: ($('#remember_me').is(':checked') ? '1' : '0');
var data = {
username: username,
password: password,
token: token,
remember_me: remember_me
};
data = $.extend(data, x_plex_headers);
$.ajax({
url: '${http_root}auth/signin',
type: 'POST',
data: $(this).serialize(),
data: data,
dataType: 'json',
statusCode: {
200: function() {
window.location = "${redirect_uri or http_root}";
},
401: function() {
$('#incorrect-login').show();
if (plex) {
$('#sign-in-alert').text('Invalid Plex Login.').show();
} else {
$('#sign-in-alert').text('Incorrect username or password.').show();
$('#username').focus();
}
}
},
complete: function() {
$('#sign-in').prop('disabled', false).html('<i class="fa fa-sign-in"></i>&nbsp; Sign In');
$('.login-container button').prop('disabled', false);
if (plex) {
$('#sign-in-plex').html('<i class="fa fa-sign-in"></i>&nbsp; Sign In');
} else {
$('#sign-in').html('<i class="fa fa-sign-in"></i>&nbsp; Sign In');
}
}
});
});
}
</script>
</body>
</html>

View File

@@ -842,21 +842,23 @@
<h3>Plex.tv Authentication</h3>
</div>
<div class="form-group">
<div class="form-group has-feedback">
<label for="pms_token">Plex.tv Account Token</label>
<div class="row">
<div class="col-md-6">
<div class="input-group">
<input type="text" class="form-control" id="pms_token" name="pms_token" value="${config['pms_token']}" data-parsley-trigger="change" data-parsley-errors-container="#pms_token_error" required>
<span class="input-group-btn">
<button class="btn btn-form" type="button" data-toggle="modal" data-target="#pms-auth-modal">Fetch Token</button>
<button id="sign-in-plex" class="btn btn-form" type="button">Fetch Token</button>
</span>
</div>
<span class="form-control-feedback" id="token_verify" aria-hidden="true" style="right: 80px;"></span>
</div>
<div id="pms_token_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">Token for Plex.tv authentication.</p>
</div>
<input type="hidden" id="pms_uuid" name="pms_uuid" value="${config['pms_uuid']}">
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
@@ -1366,49 +1368,6 @@
</div>
</div>
</div>
<div id="pms-auth-modal" class="modal fade" tabindex="-1" role="dialog"
aria-labelledby="ip-info-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">Fetch Plex.tv Token</h4>
</div>
<div class="modal-body" id="modal-text">
<div>
<p class="help-block">
This will attempt to fetch a new Plex.tv token for you. Tautulli does not store your username and password.
Note: This will not work on Internet Explorer 9 or lower.
</p>
<div class="form-group">
<label for="pms_username">Plex.tv Username</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="pms_username" name="pms_username" size="30">
</div>
</div>
<p class="help-block">Username for Plex.tv authentication.</p>
</div>
<div class="form-group">
<label for="pms_password">Plex.tv Password</label>
<div class="row">
<div class="col-md-6">
<input type="password" class="form-control" id="pms_password" name="pms_password" size="30">
</div>
</div>
<p class="help-block">Password for Plex.tv authentication.</p>
</div>
</div>
</div>
<div class="modal-footer">
<div style="float: left;">
<strong><span id="pms-token-status"></span></strong>
</div>
<input type="button" id="get-pms-auth-token" class="btn btn-bright" value="Fetch Token">
</div>
</div>
</div>
</div>
<div id="app-import-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="app-import-modal"></div>
<div id="add-notifier-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="add-notifier-modal">
<div class="modal-dialog" role="document">
@@ -2296,40 +2255,21 @@ $(document).ready(function() {
window.open(pms_web_url, '_blank');
});
// Plex.tv auth token fetch
$("#get-pms-auth-token").click(function() {
$("#pms-token-status").html('<i class="fa fa-refresh fa-spin"></i> Fetching token...');
var pms_username = $.trim($("#pms_username").val());
var pms_password = $.trim($("#pms_password").val());
if ((pms_username !== '') && (pms_password !== '')) {
$.ajax({
type: 'GET',
url: 'get_plexpy_pms_token',
data: {
username: pms_username,
password: pms_password,
force: true
},
cache: false,
async: true,
complete: function(xhr, status) {
var result = $.parseJSON(xhr.responseText);
var msg = result.message;
if (result.result == 'success') {
var authToken = result.token;
$("#pms-token-status").html('<i class="fa fa-check"></i> ' + msg);
function OAuthPreFunction() {
$("#token_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
}
function OAuthSuccessCallback(authToken) {
$("#pms_token").val(authToken);
$('#pms-auth-modal').modal('hide');
$("#pms_uuid").val(x_plex_headers['X-Plex-Client-Identifier']);
$("#token_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
getServerOptions(authToken);
} else {
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> ' + msg);
}
loadUpdateDistros();
}
});
} else {
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> Username and password required.');
function OAuthErrorCallback() {
$("#token_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
}
$('#sign-in-plex').click(function() {
PlexOAuth(OAuthSuccessCallback, OAuthErrorCallback, OAuthPreFunction);
});
// Load database import modal

View File

@@ -60,7 +60,7 @@
$('#popout-iframe-button').click(function () {
var iframe = $('#support-iframe');
popout_chat = window.open(iframe.data('src'), 'Tautulli-Discord-Support', 'width=1280,height=720');
popout_chat = PopupCenter(iframe.data('src'), 'Tautulli-Discord-Support', 1280, 720);
iframe.attr('src', '').fadeOut();
$('.iframe-overlay').fadeIn();
});

View File

@@ -55,42 +55,36 @@
<img src="${http_root}images/logo-tautulli-45.png" height="45" alt="PlexPy">
</div>
<h3 style="line-height: 50px;">Welcome!</h3>
<br />
<div>
<div class="wizard-input-section">
<p class="welcome-message">
Thanks for taking the time to try out Tautulli. Hope you find it useful.
<br /><br />
</p>
<p class="welcome-message">
Tautulli requires a permanent internet connection to ensure a reliable experience.
<br /><br />
</p>
<p class="welcome-message">
This wizard will help you get set up, to continue press Next.
</p>
</div>
</div>
<div class="wizard-card" data-cardname="card2">
<h3>Plex Authentication</h3>
<p class="help-block">Enter your Plex.tv username and password. Tautulli does not store your username or password.</p>
<div class="wizard-input-section">
<label for="pms_username">Plex.tv Username</label>
<div class="row">
<div class="col-xs-8">
<input type="text" class="form-control pms-auth" id="pms_username" placeholder="" required>
<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.
</p>
</div>
</div>
</div>
<div class="wizard-input-section">
<label for="pms_password">Plex.tv Password</label>
<div class="row">
<div class="col-xs-8">
<input type="password" class="form-control pms-auth" id="pms_password" placeholder="" required>
</div>
</div>
</div>
<input type="hidden" class="form-control pms-auth" name="pms_token" id="pms_token" value="" data-validate="validatePMStoken">
<a class="btn btn-dark" id="pms-authenticate" href="#" role="button">Authenticate</a><span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
<input type="hidden" class="form-control" name="pms_token" id="pms_token" value="" data-validate="validatePMStoken">
<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">
<h3>Plex Media Server</h3>
<div class="wizard-input-section">
<p class="help-block">
Select your Plex Media Server from the dropdown menu or enter an IP address or hostname.
</p>
</div>
<div class="wizard-input-section">
<label for="pms_ip">Plex IP or Hostname</label>
<div class="row">
@@ -128,13 +122,17 @@
<input type="hidden" id="pms_valid" data-validate="validatePMSip" value="">
<input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}">
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a>
<span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
</div>
<div class="wizard-card" data-cardname="card4">
<h3>Activity Logging</h3>
<p class="help-block">Tautulli will keep a history of all streaming activity on your Plex server.</p>
<div class="wizard-input-section">
<p class="help-block">
Tautulli will keep a history of all streaming activity on your Plex server.
</p>
</div>
<div class="wizard-input-section">
<label for="logging_ignore_interval">Ignore Interval</label>
<div class="row">
@@ -145,29 +143,38 @@
</div>
<p class="help-block">The interval (in seconds) an item must be in a playing state before logging it. 0 to disable.</p>
</div>
<div class="wizard-input-section">
<p class="help-block">
Additional options to disable history logging for certain libraries or users can be found by editing them
on the <strong>Libraries</strong> or <strong>Users</strong> pages.
</p>
</div>
</div>
<div class="wizard-card" data-cardname="card4">
<h3>Notifications</h3>
<p class="help-block">Tautulli can send a wide variety of notifications to alert you of activity on your Plex server.</p>
<div class="wizard-input-section">
<p class="help-block">
Tautulli can send a wide variety of notifications to alert you of activity on your Plex server.
</p>
<p class="help-block">
To set up a notification agent, navigate to the <strong>Settings</strong> page
and to the <strong>Notification Agents</strong> tab after you have completed this setup wizard.
</p>
</div>
</div>
<div class="wizard-card" data-cardname="card5">
<h3>Database Import</h3>
<p class="help-block">If you have an existing PlexWatch/Plexivity database, you can import the data into Tautulli.</p>
<div class="wizard-input-section">
<p class="help-block">
If you have an existing PlexWatch/Plexivity database, you can import the data into Tautulli.
</p>
<p class="help-block">
To import a database, navigate to the <strong>Settings</strong> page
and to the <strong>Import & Backups</strong> tab after you have completed this setup wizard.
</p>
</div>
<!-- Required fields but hidden -->
<div style="display: none;">
@@ -205,6 +212,7 @@
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
<script src="${http_root}js/bootstrap.min.js"></script>
<script src="${http_root}js/selectize.min.js"></script>
<script src="${http_root}js/platform.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/bootstrap-wizard.min.js"></script>
<script>
@@ -356,7 +364,7 @@ $(document).ready(function() {
var is_cloud = $(pms_ip_selected).data('is_cloud');
$("#pms_valid").val(identifier !== 'undefined' ? 'valid' : '');
$("#pms-verify-status").html(identifier !== 'undefined' ? '<i class="fa fa-check"></i> Server found!' : '').fadeIn('fast');
$("#pms-verify-status").html(identifier !== 'undefined' ? '<i class="fa fa-check"></i>&nbsp; Server found!' : '').fadeIn('fast');
$("#pms_identifier").val(identifier !== 'undefined' ? identifier : '');
$('#pms_port').val(port !== 'undefined' ? port : 32400);
@@ -414,7 +422,7 @@ $(document).ready(function() {
var pms_ssl = $("#pms_ssl").val();
var pms_is_remote = $("#pms_is_remote").val();
if ((pms_ip !== '') || (pms_port !== '')) {
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i> Validating server...');
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Validating server...');
$('#pms-verify-status').fadeIn('fast');
$.ajax({
url: 'get_server_id',
@@ -429,7 +437,7 @@ $(document).ready(function() {
async: true,
timeout: 5000,
error: function (jqXHR, textStatus, errorThrown) {
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; This is not a Plex Server!');
$('#pms-verify-status').fadeIn('fast');
},
success: function(xhr, status) {
@@ -437,18 +445,18 @@ $(document).ready(function() {
var identifier = result.identifier;
if (identifier) {
$("#pms_identifier").val(identifier);
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
$("#pms-verify-status").html('<i class="fa fa-check"></i>&nbsp; Server found!');
$('#pms-verify-status').fadeIn('fast');
pms_verified = true;
$("#pms_valid").val("valid");
} else {
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; This is not a Plex Server!');
$('#pms-verify-status').fadeIn('fast');
}
}
});
} else {
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> Please enter both fields.');
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; Please enter both fields.');
$('#pms-verify-status').fadeIn('fast');
}
}
@@ -460,47 +468,22 @@ $(document).ready(function() {
$("#pms-verify-status").html("");
});
$( ".pms-auth" ).change(function() {
authenticated = false;
$("#pms_token").val("");
$("#pms-token-status").html("");
});
// Plex.tv auth token fetch
$("#pms-authenticate").click(function() {
$("#pms-token-status").html('<i class="fa fa-refresh fa-spin"></i> Fetching token...');
$('#pms-token-status').fadeIn('fast');
var pms_username = $("#pms_username").val().trim();
var pms_password = $("#pms_password").val().trim();
if ((pms_username !== '') && (pms_password !== '')) {
$.ajax({
type: 'GET',
url: 'get_plexpy_pms_token',
data: {
username: pms_username,
password: pms_password
},
cache: false,
async: true,
complete: function (xhr, status) {
var result = $.parseJSON(xhr.responseText);
var msg = result.message;
if (result.result == 'success') {
var authToken = result.token;
$("#pms-token-status").html('<i class="fa fa-check"></i> ' + msg);
$('#pms-token-status').fadeIn('fast');
function OAuthPreFunction() {
$("#pms_token").val('');
$("#pms-token-status").html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Waiting for authentication...').fadeIn('fast');
}
function OAuthSuccessCallback(authToken) {
$("#pms_token").val(authToken);
$("#pms-token-status").html('<i class="fa fa-check"></i>&nbsp; Authentication successful.').fadeIn('fast');
authenticated = true;
getServerOptions(authToken)
} else {
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> ' + msg);
getServerOptions(authToken);
}
function OAuthErrorCallback() {
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; Error communicating with Plex.tv.').fadeIn('fast');
}
});
} else {
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> Username and password required.');
$('#pms-token-status').fadeIn('fast');
}
$('#sign-in-plex').click(function() {
PlexOAuth(OAuthSuccessCallback, OAuthErrorCallback, OAuthPreFunction);
});
});
</script>

View File

@@ -759,7 +759,8 @@
% if i < min(show['season_count'], 7):
<br>
% elif i == 7 and show['season_count'] > 8:
...plus ${show['season_count'] - 8} more seasons!
<% remaining_seasons = show['season_count'] - 8 %>
...plus ${remaining_seasons} more season${'s' if remaining_seasons > 1 else ''}!
% endif
% endfor
</p>

View File

@@ -760,7 +760,8 @@
% if i < min(show['season_count'], 7):
<br>
% elif i == 7 and show['season_count'] > 8:
...plus ${show['season_count'] - 8} more seasons!
<% remaining_seasons = show['season_count'] - 8 %>
...plus ${remaining_seasons} more season${'s' if remaining_seasons > 1 else ''}!
% endif
% endfor
</p>

View File

@@ -421,6 +421,8 @@ def initialize_scheduler():
schedule_job(activity_pinger.connect_server, 'Check for server response',
hours=0, minutes=0, seconds=0)
schedule_job(web_socket.send_ping, 'Websocket ping',
hours=0, minutes=0, seconds=10)
else:
# Cancel all jobs
@@ -440,6 +442,8 @@ def initialize_scheduler():
# Schedule job to reconnect server
schedule_job(activity_pinger.connect_server, 'Check for server response',
hours=0, minutes=0, seconds=60, args=(False,))
schedule_job(web_socket.send_ping, 'Websocket ping',
hours=0, minutes=0, seconds=0)
# Start scheduler
if start_jobs and len(SCHED.get_jobs()):
@@ -1863,7 +1867,7 @@ def generate_uuid():
def initialize_tracker():
data = {
'dataSource': 'server',
'appName': 'Tautulli',
'appName': common.PRODUCT,
'appVersion': common.RELEASE,
'appId': plexpy.INSTALL_TYPE,
'appInstallerId': plexpy.CONFIG.GIT_BRANCH,

View File

@@ -32,6 +32,7 @@ import xmltodict
import plexpy
import config
import database
import helpers
import libraries
import logger
import mobile_app
@@ -173,10 +174,11 @@ class API2:
end = int(end)
if regex:
logger.api_debug(u'Tautulli APIv2 :: Filtering log using regex %s' % regex)
reg = re.compile('u' + regex, flags=re.I)
logger.api_debug(u"Tautulli APIv2 :: Filtering log using regex '%s'" % regex)
reg = re.compile(regex, flags=re.I)
for line in open(logfile, 'r').readlines():
with open(logfile, 'r') as f:
for line in f.readlines():
temp_loglevel_and_time = None
try:
@@ -191,7 +193,7 @@ class API2:
except IndexError:
# We assume this is a traceback
tl = (len(templog) - 1)
templog[tl]['msg'] += line.replace('\n', '')
templog[tl]['msg'] += helpers.sanitize(unicode(line.replace('\n', ''), 'utf-8'))
continue
if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line:
@@ -199,21 +201,24 @@ class API2:
d = {
'time': temp_loglevel_and_time[0],
'loglevel': loglvl,
'msg': msg.replace('\n', ''),
'msg': helpers.sanitize(unicode(msg.replace('\n', ''), 'utf-8')),
'thread': thread
}
templog.append(d)
if order == 'desc':
templog = templog[::-1]
if end > 0 or start > 0:
logger.api_debug(u'Tautulli APIv2 :: Slicing the log from %s to %s' % (start, end))
logger.api_debug(u"Tautulli APIv2 :: Slicing the log from %s to %s" % (start, end))
templog = templog[start:end]
if sort:
logger.api_debug(u'Tautulli APIv2 :: Sorting log based on %s' % sort)
logger.api_debug(u"Tautulli APIv2 :: Sorting log based on '%s'" % sort)
templog = sorted(templog, key=lambda k: k[sort])
if search:
logger.api_debug(u'Tautulli APIv2 :: Searching log values for %s' % search)
logger.api_debug(u"Tautulli APIv2 :: Searching log values for '%s'" % search)
tt = [d for d in templog for k, v in d.items() if search.lower() in v.lower()]
if len(tt):
@@ -222,16 +227,13 @@ class API2:
if regex:
tt = []
for l in templog:
stringdict = ' '.join('{}{}'.format(k, v) for k, v in l.items())
stringdict = ' '.join(u'{}{}'.format(k, v) for k, v in l.items())
if reg.search(stringdict):
tt.append(l)
if len(tt):
templog = tt
if order == 'desc':
templog = templog[::-1]
return templog
def get_settings(self, key=''):

View File

@@ -19,6 +19,7 @@ from collections import OrderedDict
import version
# Identify Our Application
PRODUCT = 'Tautulli'
PLATFORM = platform.system()
PLATFORM_RELEASE = platform.release()
PLATFORM_VERSION = platform.version()
@@ -27,7 +28,7 @@ PLATFORM_DEVICE_NAME = platform.node()
BRANCH = version.PLEXPY_BRANCH
RELEASE = version.PLEXPY_RELEASE_VERSION
USER_AGENT = 'Tautulli/{} ({} {})'.format(RELEASE, PLATFORM, PLATFORM_RELEASE)
USER_AGENT = '{}/{} ({} {})'.format(PRODUCT, RELEASE, PLATFORM, PLATFORM_RELEASE)
DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
@@ -440,7 +441,8 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Summary', 'type': 'str', 'value': 'summary', 'description': 'A short plot summary for the item.'},
{'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'},
{'name': 'Rating', 'type': 'float', 'value': 'rating', 'description': 'The rating (out of 10) for the item.'},
{'name': 'Audience Rating', 'type': 'float', 'value': 'audience_rating', 'description': 'The audience rating (%) for the item.', 'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'},
{'name': 'Critic Rating', 'type': 'int', 'value': 'critic_rating', 'description': 'The critic rating (%) for the item.', 'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'},
{'name': 'Audience Rating', 'type': 'int', 'value': 'audience_rating', 'description': 'The audience rating (%) for the item.', 'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'},
{'name': 'Duration', 'type': 'int', 'value': 'duration', 'description': 'The duration (in minutes) for the item.'},
{'name': 'Poster URL', 'type': 'str', 'value': 'poster_url', 'description': 'A URL for the movie, TV show, or album poster.'},
{'name': 'Plex URL', 'type': 'str', 'value': 'plex_url', 'description': 'The Plex URL to your server for the item.'},

View File

@@ -466,7 +466,7 @@ def get_percent(value1, value2):
else:
percent = 0
return math.trunc(percent)
return math.trunc(round(percent, 0))
def hex_to_int(hex):

View File

@@ -33,18 +33,22 @@ class HTTPHandler(object):
Retrieve data from Plex Server
"""
def __init__(self, urls, token=None, timeout=10, ssl_verify=True):
def __init__(self, urls, headers=None, token=None, timeout=10, ssl_verify=True):
if isinstance(urls, basestring):
self.urls = urls.split() or urls.split(',')
else:
self.urls = urls
self.headers = {'X-Plex-Product': 'Tautulli',
if headers:
self.headers = headers
else:
self.headers = {'X-Plex-Product': plexpy.common.PRODUCT,
'X-Plex-Version': plexpy.common.RELEASE,
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
'X-Plex-Platform': plexpy.common.PLATFORM,
'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE,
'X-Plex-Device': 'Web',
'X-Plex-Device': '{} {}'.format(plexpy.common.PLATFORM,
plexpy.common.PLATFORM_RELEASE),
'X-Plex-Device-Name': plexpy.common.PLATFORM_DEVICE_NAME
}

View File

@@ -527,7 +527,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
else:
transcode_decision = 'Direct Play'
if notify_action != 'play':
if notify_action != 'on_play':
stream_duration = int((time.time() -
helpers.cast_to_int(session.get('started', 0)) -
helpers.cast_to_int(session.get('paused_counter', 0))) / 60)
@@ -708,6 +708,14 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
child_count = 1
grandchild_count = 1
critic_rating = ''
if notify_params['rating_image'].startswith('rottentomatoes://') and notify_params['rating']:
critic_rating = helpers.get_percent(notify_params['rating'], 10)
audience_rating = ''
if notify_params['audience_rating']:
audience_rating = helpers.get_percent(notify_params['audience_rating'], 10)
now = arrow.now()
now_iso = now.isocalendar()
@@ -856,7 +864,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'summary': notify_params['summary'],
'tagline': notify_params['tagline'],
'rating': notify_params['rating'],
'audience_rating': helpers.get_percent(notify_params['audience_rating'], 10) or '',
'critic_rating': critic_rating,
'audience_rating': audience_rating,
'duration': duration,
'poster_title': notify_params['poster_title'],
'poster_url': notify_params['poster_url'],

View File

@@ -121,7 +121,7 @@ class PlexTV(object):
Plex.tv authentication
"""
def __init__(self, username=None, password=None, token=None):
def __init__(self, username=None, password=None, token=None, headers=None):
self.username = username
self.password = password
self.token = token
@@ -147,7 +147,8 @@ class PlexTV(object):
self.request_handler = http_handler.HTTPHandler(urls=self.urls,
token=self.token,
timeout=self.timeout,
ssl_verify=self.ssl_verify)
ssl_verify=self.ssl_verify,
headers=headers)
def get_plex_auth(self, output_format='raw'):
uri = '/users/sign_in.xml'
@@ -226,6 +227,45 @@ class PlexTV(object):
return server_token
def get_plextv_pin(self, pin='', output_format=''):
if pin:
uri = '/api/v2/pins/' + pin
request = self.request_handler.make_request(uri=uri,
request_type='GET',
output_format=output_format,
no_token=True)
else:
uri = '/api/v2/pins?strong=true'
request = self.request_handler.make_request(uri=uri,
request_type='POST',
output_format=output_format,
no_token=True)
return request
def get_pin(self, pin=''):
plextv_response = self.get_plextv_pin(pin=pin,
output_format='xml')
if plextv_response:
try:
xml_head = plextv_response.getElementsByTagName('pin')
if xml_head:
pin = {'id': xml_head[0].getAttribute('id'),
'code': xml_head[0].getAttribute('code'),
'token': xml_head[0].getAttribute('authToken')
}
return pin
else:
logger.warn(u"Tautulli PlexTV :: Could not get Plex authentication pin.")
return None
except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_pin: %s." % e)
return None
else:
return None
def get_plextv_user_data(self):
plextv_response = self.get_plex_auth(output_format='dict')
@@ -819,3 +859,28 @@ class PlexTV(object):
return True
else:
return False
def get_plex_account_details(self):
account_data = self.get_plextv_user_details(output_format='xml')
try:
xml_head = account_data.getElementsByTagName('user')
except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_plex_account_details: %s." % e)
return None
for a in xml_head:
account_details = {"user_id": helpers.get_xml_attr(a, 'id'),
"username": helpers.get_xml_attr(a, 'username'),
"thumb": helpers.get_xml_attr(a, 'thumb'),
"email": helpers.get_xml_attr(a, 'email'),
"is_home_user": helpers.get_xml_attr(a, 'home'),
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos'),
"user_token": helpers.get_xml_attr(a, 'authToken')
}
return account_details

View File

@@ -527,7 +527,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(m, 'summary'),
'tagline': helpers.get_xml_attr(m, 'tagline'),
'rating': helpers.get_xml_attr(m, 'rating'),
'rating_image': helpers.get_xml_attr(m, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(m, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(m, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(m, 'userRating'),
'duration': helpers.get_xml_attr(m, 'duration'),
'year': helpers.get_xml_attr(m, 'year'),
@@ -680,7 +682,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -728,7 +732,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': duration,
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -773,7 +779,9 @@ class PmsConnect(object):
'summary': show_details['summary'],
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': show_details['duration'],
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -819,7 +827,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -863,7 +873,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -908,7 +920,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(metadata_main, 'summary') or artist_details['summary'],
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -956,7 +970,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': album_details['year'],
@@ -1000,7 +1016,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -1045,7 +1063,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -1090,7 +1110,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -1136,7 +1158,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'rating_image': helpers.get_xml_attr(metadata_main, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(metadata_main, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -1714,7 +1738,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(session, 'summary'),
'tagline': helpers.get_xml_attr(session, 'tagline'),
'rating': helpers.get_xml_attr(session, 'rating'),
'rating_image': helpers.get_xml_attr(session, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(session, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(session, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(session, 'userRating'),
'duration': helpers.get_xml_attr(session, 'duration'),
'year': helpers.get_xml_attr(session, 'year'),
@@ -2037,7 +2063,9 @@ class PmsConnect(object):
'summary': helpers.get_xml_attr(m, 'summary'),
'tagline': helpers.get_xml_attr(m, 'tagline'),
'rating': helpers.get_xml_attr(m, 'rating'),
'rating_image': helpers.get_xml_attr(m, 'ratingImage'),
'audience_rating': helpers.get_xml_attr(m, 'audienceRating'),
'audience_rating_image': helpers.get_xml_attr(m, 'audienceRatingImage'),
'user_rating': helpers.get_xml_attr(m, 'userRating'),
'duration': helpers.get_xml_attr(m, 'duration'),
'year': helpers.get_xml_attr(m, 'year'),

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.1.15-beta"
PLEXPY_RELEASE_VERSION = "v2.1.16-beta"

View File

@@ -31,6 +31,8 @@ import logger
name = 'websocket'
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
ws_shutdown = False
pong_timer = None
pong_count = 0
def start_thread():
@@ -58,6 +60,7 @@ def on_connect():
plexpy.PLEX_SERVER_UP = True
plexpy.initialize_scheduler()
send_ping()
def on_disconnect():
@@ -91,6 +94,37 @@ def close():
plexpy.WS_CONNECTED = False
def send_ping():
if plexpy.WS_CONNECTED:
# logger.debug(u"Tautulli WebSocket :: Sending ping.")
plexpy.WEBSOCKET.ping("Hi?")
global pong_timer
pong_timer = threading.Timer(5.0, wait_pong)
pong_timer.daemon = True
pong_timer.start()
def wait_pong():
global pong_count
pong_count += 1
logger.warning(u"Tautulli WebSocket :: Failed to receive pong from websocket, ping attempt %s." % str(pong_count))
if pong_count >= plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
pong_count = 0
close()
def receive_pong():
# logger.debug(u"Tautulli WebSocket :: Received pong.")
global pong_timer
global pong_count
if pong_timer:
pong_timer = pong_timer.cancel()
pong_count = 0
def run():
from websocket import create_connection
@@ -115,18 +149,7 @@ def run():
reconnects = 0
# Try an open the websocket connection
while not plexpy.WS_CONNECTED and reconnects < plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
if reconnects == 0:
logger.info(u"Tautulli WebSocket :: Opening %swebsocket." % secure)
reconnects += 1
# Sleep 5 between connection attempts
if reconnects > 1:
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
logger.info(u"Tautulli WebSocket :: Connection attempt %s." % str(reconnects))
try:
plexpy.WEBSOCKET = create_connection(uri, header=header)
logger.info(u"Tautulli WebSocket :: Ready")
@@ -196,7 +219,10 @@ def receive(ws):
ws.send_close()
return frame.opcode, None
elif frame.opcode == websocket.ABNF.OPCODE_PING:
# logger.debug(u"Tautulli WebSocket :: Received ping, sending pong.")
ws.pong("Hi!")
elif frame.opcode == websocket.ABNF.OPCODE_PONG:
receive_pong()
return None, None

View File

@@ -19,7 +19,6 @@
# Session tool to be loaded.
from datetime import datetime, timedelta
import re
from urllib import quote, unquote
import cherrypy
@@ -37,17 +36,27 @@ JWT_ALGORITHM = 'HS256'
JWT_COOKIE_NAME = 'tautulli_token_'
def user_login(username=None, password=None):
if not username or not password:
return None
def plex_user_login(username=None, password=None, token=None, headers=None):
user_token = None
user_id = None
# Try to login to Plex.tv to check if the user has a vaild account
plex_tv = PlexTV(username=username, password=password)
if username and password:
plex_tv = PlexTV(username=username, password=password, headers=headers)
plex_user = plex_tv.get_token()
if plex_user:
user_token = plex_user['auth_token']
user_id = plex_user['user_id']
elif token:
plex_tv = PlexTV(token=token, headers=headers)
plex_user = plex_tv.get_plex_account_details()
if plex_user:
user_token = token
user_id = plex_user['user_id']
else:
return None
if user_token and user_id:
# Try to retrieve the user from the database.
# Also make sure guest access is enabled for the user and the user is not deleted.
user_data = Users()
@@ -57,7 +66,7 @@ def user_login(username=None, password=None):
return None
elif plexpy.CONFIG.HTTP_PLEX_ADMIN and user_details['is_admin']:
# Plex admin login
return 'admin'
return user_details, 'admin'
elif not user_details['allow_guest'] or user_details['deleted_user']:
# Guest access is disabled or the user is deleted.
return None
@@ -68,56 +77,65 @@ def user_login(username=None, password=None):
# The user is in the database, and guest access is enabled, so try to retrieve a server token.
# If a server token is returned, then the user is a valid friend of the server.
plex_tv = PlexTV(token=user_token)
plex_tv = PlexTV(token=user_token, headers=headers)
server_token = plex_tv.get_server_token()
if server_token:
# Register the new user / update the access tokens.
monitor_db = MonitorDatabase()
try:
logger.debug(u"Tautulli WebAuth :: Regestering tokens for user '%s' in the database." % username)
result = monitor_db.action('UPDATE users SET user_token = ?, server_token = ? WHERE user_id = ?',
[user_token, server_token, user_id])
logger.debug(u"Tautulli WebAuth :: Registering token for user '%s' in the database."
% user_details['username'])
result = monitor_db.action('UPDATE users SET server_token = ? WHERE user_id = ?',
[server_token, user_details['user_id']])
if result:
# Refresh the users list to make sure we have all the correct permissions.
refresh_users()
# Successful login
return 'guest'
return user_details, 'guest'
else:
logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database." % username)
logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database."
% user_details['username'])
return None
except Exception as e:
logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database: %s." % (username, e))
logger.warn(u"Tautulli WebAuth :: Unable to register user '%s' in database: %s."
% (user_details['username'], e))
return None
else:
logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv server token for user '%s'." % username)
logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv server token for user '%s'."
% user_details['username'])
return None
else:
elif username:
logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv user token for user '%s'." % username)
return None
elif token:
logger.warn(u"Tautulli WebAuth :: Unable to retrieve Plex.tv user token for Plex OAuth.")
return None
def check_credentials(username, password, admin_login='0'):
def check_credentials(username=None, password=None, token=None, admin_login='0', headers=None):
"""Verifies credentials for username and password.
Returns True and the user group on success or False and no user group"""
if username and password:
if plexpy.CONFIG.HTTP_PASSWORD:
user_details = {'user_id': None, 'username': username}
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
return True, 'tautulli admin'
return True, user_details, 'admin'
elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
return True, 'tautulli admin'
return True, user_details, 'admin'
if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS):
plex_login = user_login(username, password)
plex_login = plex_user_login(username=username, password=password, token=token, headers=headers)
if plex_login is not None:
return True, plex_login
return True, plex_login[0], plex_login[1]
return False, None
return False, None, None
def check_jwt_token():
@@ -220,7 +238,7 @@ class AuthController(object):
return
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
def on_login(self, username, user_id=None, user_group=None, success=0):
def on_login(self, username=None, user_id=None, user_group=None, success=False, oauth=False):
"""Called on successful login"""
# Save login to the database
@@ -236,8 +254,10 @@ class AuthController(object):
user_agent=user_agent,
success=success)
if success == 1:
logger.debug(u"Tautulli WebAuth :: %s user '%s' logged into Tautulli." % (user_group.capitalize(), username))
if success:
use_oauth = 'Plex OAuth' if oauth else 'form'
logger.debug(u"Tautulli WebAuth :: %s user '%s' logged into Tautulli using %s login."
% (user_group.capitalize(), username, use_oauth))
def on_logout(self, username, user_group):
"""Called on logout"""
@@ -279,43 +299,37 @@ class AuthController(object):
@cherrypy.expose
@cherrypy.tools.json_out()
def signin(self, username=None, password=None, remember_me='0', admin_login='0', *args, **kwargs):
def signin(self, username=None, password=None, token=None, remember_me='0', admin_login='0', *args, **kwargs):
if cherrypy.request.method != 'POST':
cherrypy.response.status = 405
return {'status': 'error', 'message': 'Sign in using POST.'}
error_message = {'status': 'error', 'message': 'Incorrect username or password.'}
error_message = {'status': 'error', 'message': 'Invalid credentials.'}
valid_login, user_group = check_credentials(username, password, admin_login)
valid_login, user_details, user_group = check_credentials(username=username,
password=password,
token=token,
admin_login=admin_login,
headers=kwargs)
if valid_login:
if user_group == 'tautulli admin':
user_group = 'admin'
user_id = None
else:
if re.match(r"[^@]+@[^@]+\.[^@]+", username):
user_details = Users().get_details(email=username)
else:
user_details = Users().get_details(user=username)
user_id = user_details['user_id']
time_delta = timedelta(days=30) if remember_me == '1' else timedelta(minutes=60)
expiry = datetime.utcnow() + time_delta
payload = {
'user_id': user_id,
'user': username,
'user_id': user_details['user_id'],
'user': user_details['username'],
'user_group': user_group,
'exp': expiry
}
jwt_token = jwt.encode(payload, plexpy.CONFIG.JWT_SECRET, algorithm=JWT_ALGORITHM)
self.on_login(username=username,
user_id=user_id,
self.on_login(username=user_details['username'],
user_id=user_details['user_id'],
user_group=user_group,
success=1)
success=True,
oauth=bool(token))
jwt_cookie = JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID
cherrypy.response.cookie[jwt_cookie] = jwt_token
@@ -326,14 +340,20 @@ class AuthController(object):
cherrypy.response.status = 200
return {'status': 'success', 'token': jwt_token.decode('utf-8'), 'uuid': plexpy.CONFIG.PMS_UUID}
elif admin_login == '1':
elif admin_login == '1' and username:
self.on_login(username=username)
logger.debug(u"Tautulli WebAuth :: Invalid admin login attempt from '%s'." % username)
cherrypy.response.status = 401
return error_message
else:
elif username:
self.on_login(username=username)
logger.debug(u"Tautulli WebAuth :: Invalid login attempt from '%s'." % username)
logger.debug(u"Tautulli WebAuth :: Invalid user login attempt from '%s'." % username)
cherrypy.response.status = 401
return error_message
elif token:
self.on_login(username='Plex OAuth', oauth=True)
logger.debug(u"Tautulli WebAuth :: Invalid Plex OAuth login attempt.")
cherrypy.response.status = 401
return error_message

View File

@@ -4560,6 +4560,7 @@ class WebInterface(object):
"added_at": "1461572396",
"art": "/library/metadata/1219/art/1462175063",
"audience_rating": "8",
"audience_rating_image": "rottentomatoes://image.rating.upright",
"banner": "/library/metadata/1219/banner/1462175063",
"collections": [],
"content_rating": "TV-MA",
@@ -4657,6 +4658,7 @@ class WebInterface(object):
"parent_thumb": "/library/metadata/153036/thumb/1462175062",
"parent_title": "",
"rating": "7.8",
"rating_image": "rottentomatoes://image.rating.ripe",
"rating_key": "153037",
"section_id": "2",
"sort_title": "Game of Thrones",
@@ -4920,6 +4922,7 @@ class WebInterface(object):
"art": "/library/metadata/1219/art/1503306930",
"aspect_ratio": "1.78",
"audience_rating": "",
"audience_rating_image": "rottentomatoes://image.rating.upright",
"audio_bitrate": "384",
"audio_bitrate_mode": "",
"audio_channel_layout": "5.1(side)",
@@ -4996,6 +4999,7 @@ class WebInterface(object):
"progress_percent": "0",
"quality_profile": "Original",
"rating": "7.8",
"rating_image": "rottentomatoes://image.rating.ripe",
"rating_key": "153037",
"relay": 0,
"section_id": "2",