Compare commits
32 Commits
v2.0.18-be
...
v2.0.19-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
066a95d209 | ||
![]() |
c7cc476623 | ||
![]() |
bd44eb7fe4 | ||
![]() |
6ec4f51077 | ||
![]() |
b4a4f60b04 | ||
![]() |
dc4e6edc9a | ||
![]() |
60b362b19e | ||
![]() |
7e81ce8c06 | ||
![]() |
c7f9e2f721 | ||
![]() |
cab8b1c041 | ||
![]() |
16f270691d | ||
![]() |
d94a1efe75 | ||
![]() |
12755970b7 | ||
![]() |
93e4853ea2 | ||
![]() |
5e0c0365fb | ||
![]() |
c2713c53dd | ||
![]() |
90443b4028 | ||
![]() |
e0109ed179 | ||
![]() |
a53afe05a2 | ||
![]() |
a5d2467bfe | ||
![]() |
8447663e27 | ||
![]() |
64d67d8209 | ||
![]() |
78034b82a9 | ||
![]() |
f77bd6c17b | ||
![]() |
2621da7d36 | ||
![]() |
e1dca1509a | ||
![]() |
df016243dd | ||
![]() |
be72693fec | ||
![]() |
33a1ebdb1a | ||
![]() |
030f9d334b | ||
![]() |
dc743ac378 | ||
![]() |
0010cbe21f |
52
API.md
52
API.md
@@ -93,21 +93,6 @@ Returns:
|
||||
Delete and recreate the cache directory.
|
||||
|
||||
|
||||
### delete_datatable_media_info_cache
|
||||
Delete the media info table cache for a specific library.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
section_id (str): The id of the Plex library section
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
|
||||
|
||||
### delete_image_cache
|
||||
Delete and recreate the image cache directory.
|
||||
|
||||
@@ -176,6 +161,21 @@ Returns:
|
||||
```
|
||||
|
||||
|
||||
### delete_media_info_cache
|
||||
Delete the media info table cache for a specific library.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
section_id (str): The id of the Plex library section
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
|
||||
|
||||
### delete_mobile_device
|
||||
Remove a mobile device from the database.
|
||||
|
||||
@@ -863,6 +863,7 @@ Optional parameters:
|
||||
start (int): Row to start from, 0
|
||||
length (int): Number of items to return, 25
|
||||
search (str): A string to search for, "Thrones"
|
||||
refresh (str): "true" to refresh the media info table
|
||||
|
||||
Returns:
|
||||
json:
|
||||
@@ -2408,7 +2409,26 @@ Uninstalls the GeoLite2 database
|
||||
|
||||
|
||||
### update
|
||||
Check for Tautulli updates on Github.
|
||||
Update Tautulli.
|
||||
|
||||
|
||||
### update_check
|
||||
Check for Tautulli updates.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
json
|
||||
{"result": "success",
|
||||
"update": true,
|
||||
"message": "An update for Tautulli is available."
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### update_metadata_details
|
||||
|
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## v2.0.19-beta (2018-02-16)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Connect to Plex Cloud server without keeping it awake.
|
||||
* Fix: Reconnect to Plex Cloud server after the server wakes up from sleeping.
|
||||
* Notifications:
|
||||
* Fix: Don't send Plex Server Up/Down notifications when Tautulli starts up.
|
||||
* Change: Better handling of Watched notifications.
|
||||
* UI:
|
||||
* New: Added Plex server selection dropdown in the settings.
|
||||
* Fix: Libraries and Users tables not refreshing properly.
|
||||
* Change: Updated the masked info shown to guests.
|
||||
* Change: Check for updates without refreshing to the homepage.
|
||||
* API:
|
||||
* New: Added update_check to the API.
|
||||
* Fix: delete_media_info_cache not deleting the cache.
|
||||
* Change: Document "refresh" parameter for get_library_media_info.
|
||||
* Other:
|
||||
* Fix: Show the full changelog since v2 on a fresh install.
|
||||
|
||||
|
||||
## v2.0.18-beta (2018-02-12)
|
||||
|
||||
* Notifications:
|
||||
|
@@ -33,7 +33,7 @@ import signal
|
||||
import time
|
||||
|
||||
import plexpy
|
||||
from plexpy import config, database, logger, web_socket, webstart
|
||||
from plexpy import config, database, logger, webstart
|
||||
|
||||
|
||||
# Register signals, such as CTRL + C
|
||||
@@ -194,13 +194,6 @@ def main():
|
||||
# Start the background threads
|
||||
plexpy.start()
|
||||
|
||||
# Open connection for websocket
|
||||
try:
|
||||
web_socket.start_thread()
|
||||
except:
|
||||
logger.warn(u"Websocket :: Unable to open connection.")
|
||||
plexpy.initialize_scheduler()
|
||||
|
||||
# Force the http port if neccessary
|
||||
if args.port:
|
||||
http_port = args.port
|
||||
|
@@ -44,16 +44,18 @@
|
||||
% if _session['user_group'] == 'admin':
|
||||
% if plexpy.CONFIG.CHECK_GITHUB and not plexpy.CURRENT_VERSION:
|
||||
<div id="updatebar" style="display: none;">
|
||||
You're running an unknown version of Tautulli.<br />
|
||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Close</a>
|
||||
You are running an unknown version of Tautulli.<br />
|
||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
</div>
|
||||
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win':
|
||||
<div id="updatebar" style="display: none;">
|
||||
A <a href="${anon_url('https://github.com/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank">
|
||||
newer version</a> is available.<br />
|
||||
You're ${plexpy.COMMITS_BEHIND} commits behind.<br />
|
||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Close</a>
|
||||
newer version</a> is available!<br />
|
||||
You are ${plexpy.COMMITS_BEHIND} commits behind.<br />
|
||||
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
</div>
|
||||
% else:
|
||||
<div id="updatebar" style="display: none;"></div>
|
||||
% endif
|
||||
% endif
|
||||
<nav class="navbar navbar-fixed-top">
|
||||
@@ -289,14 +291,44 @@ ${next.modalIncludes()}
|
||||
% endif
|
||||
<script>
|
||||
% if _session['user_group'] == 'admin':
|
||||
$('#updateDismiss').click(function() {
|
||||
$('#updatebar').slideUp('slow');
|
||||
$('body').on('click', '#updateDismiss', function() {
|
||||
$('#updatebar').fadeOut();
|
||||
// Set cookie to remember dismiss decision for 1 hour.
|
||||
setCookie('updateDismiss', 'true', 1/24);
|
||||
});
|
||||
|
||||
if (!getCookie('updateDismiss')) {
|
||||
$('#updatebar').show();
|
||||
if ($('#updatebar').html().length > 0) {
|
||||
$('#updatebar').show();
|
||||
}
|
||||
}
|
||||
|
||||
function checkUpdate(_callback) {
|
||||
// Allow the update bar to show again if previously dismissed.
|
||||
setCookie('updateDismiss', 'true', 0);
|
||||
$.ajax({
|
||||
url: 'update_check',
|
||||
complete: function (xhr, status) {
|
||||
var result = $.parseJSON(xhr.responseText);
|
||||
var msg = '';
|
||||
if (result.update === true) {
|
||||
msg = 'A <a href="' + result.compare_url + '" target="_blank">newer version</a> is available!<br />' +
|
||||
'You are '+ result.commits_behind + ' commits behind.<br />' +
|
||||
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
$('#updatebar').html(msg).fadeIn();
|
||||
} else if (result.update === false) {
|
||||
showMsg('<i class="fa fa-check"></i> ' + result.message, false, true, 2000);
|
||||
} else if (result.update === null) {
|
||||
msg = 'You are running an unknown version of Tautulli.<br />' +
|
||||
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
$('#updatebar').html(msg).fadeIn();
|
||||
}
|
||||
|
||||
if (_callback) {
|
||||
_callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#nav-shutdown").click(function() {
|
||||
@@ -315,11 +347,9 @@ ${next.modalIncludes()}
|
||||
});
|
||||
});
|
||||
|
||||
$("#nav-update").first().one("click", function () {
|
||||
// Allow the update bar to show again if previously dismissed.
|
||||
setCookie('updateDismiss', 'true', 0);
|
||||
$(this).html('<i class="fa fa-spin fa-refresh"></i> Checking');
|
||||
window.location.href = "checkGithub";
|
||||
$('#nav-update').click(function () {
|
||||
$(this).html('<i class="fa fa-fw fa-spin fa-refresh"></i> Checking');
|
||||
checkUpdate(function () { $('#nav-update').html('<i class="fa fa-fw fa-arrow-circle-up"></i> Check for Updates'); });
|
||||
});
|
||||
|
||||
$('#donation_type a.crypto-donation').on('shown.bs.tab', function () {
|
||||
|
@@ -60,7 +60,8 @@ select[multiple] option {
|
||||
-moz-border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
select.form-control {
|
||||
select.form-control,
|
||||
div.form-control .selectize-input {
|
||||
margin: 5px 0 5px 0;
|
||||
color: #fff;
|
||||
border: 0px solid #444;
|
||||
@@ -80,12 +81,37 @@ select.form-control {
|
||||
transition: background-color .3s;
|
||||
}
|
||||
.selectize-control.form-control .selectize-input {
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 4px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.selectize-control.form-control.selectize-pms-ip .selectize-input {
|
||||
padding-left: 12px !important;
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
min-height: 32px !important;
|
||||
}
|
||||
.input-group .selectize-control.form-control.selectize-pms-ip .selectize-input > div {
|
||||
max-width: 450px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.wizard-input-section .selectize-control.form-control.selectize-pms-ip .selectize-input > div {
|
||||
max-width: 360px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#selectize-pms-ip-container .selectize-dropdown.form-control.selectize-pms-ip {
|
||||
margin-left: 15px;
|
||||
}
|
||||
.wizard-input-section .selectize-control.form-control.selectize-pms-ip .selectize-dropdown .selectize-dropdown-content {
|
||||
max-height: 150px;
|
||||
}
|
||||
.wizard-input-section .selectize-dropdown.form-control.selectize-pms-ip {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
|
||||
color: #fff !important;
|
||||
}
|
||||
@@ -134,33 +160,40 @@ 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 .item-value {
|
||||
.selectize-input > div .item-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.selectize-input > div .item-value {
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.selectize-control .selectize-input > div .item-text + .item-value {
|
||||
.selectize-input > div .item-text + .item-value {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.selectize-control .selectize-input > div .item-value:before {
|
||||
.selectize-input > div .item-value:before {
|
||||
content: '<';
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.selectize-control .selectize-input > div .item-value:after {
|
||||
.selectize-input > div .item-value:after {
|
||||
content: '>';
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.selectize-control .selectize-dropdown .caption {
|
||||
.selectize-dropdown .caption {
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
color: #a0a0a0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.selectize-control .selectize-dropdown .select-all,
|
||||
.selectize-control .selectize-dropdown .remove-all {
|
||||
.selectize-dropdown .select-all,
|
||||
.selectize-dropdown .remove-all {
|
||||
font-weight: bold;
|
||||
}
|
||||
.selectize-control .selectize-dropdown .border-all {
|
||||
.selectize-dropdown .border-all {
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
height: 1px;
|
||||
@@ -169,7 +202,7 @@ select.form-control:focus,
|
||||
overflow: hidden;
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
.selectize-control .selectize-dropdown .border-all:last-child {
|
||||
.selectize-dropdown .border-all:last-child {
|
||||
display: none;
|
||||
}
|
||||
.selectize-dropdown .optgroup-header {
|
||||
@@ -619,18 +652,8 @@ textarea.form-control:focus {
|
||||
color: #fff;
|
||||
}
|
||||
.form-control-feedback {
|
||||
position: absolute;
|
||||
color: #F9AA03;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 5px 10px 5px 0;
|
||||
z-index: 2;
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
margin: 5px 40px 5px 0;
|
||||
}
|
||||
.form-control[readonly] {
|
||||
background-color: #555;
|
||||
@@ -3212,16 +3235,16 @@ div.dataTables_info {
|
||||
}
|
||||
#updatebar {
|
||||
background-color: #444;
|
||||
opacity: 0.95;
|
||||
color: #999999;
|
||||
display: none;
|
||||
font-size: 14px;
|
||||
right: 10px;
|
||||
padding: 7px 10px;
|
||||
padding: 10px 10px;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
bottom: 10px;
|
||||
min-height: 22px;
|
||||
width: 250px;
|
||||
width: 400px;
|
||||
z-index: 9999;
|
||||
display: block;
|
||||
}
|
||||
|
@@ -67,8 +67,15 @@ DOCUMENTATION :: END
|
||||
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
|
||||
import plexpy
|
||||
%>
|
||||
<% data = defaultdict(lambda: 'Unknown', **session) %>
|
||||
<% sk = data['session_key'] %>
|
||||
<%
|
||||
data = defaultdict(lambda: 'Unknown', **session)
|
||||
sk = data['session_key']
|
||||
|
||||
href = 'info?rating_key={}'.format(data['rating_key']) if data['rating_key'] else '#'
|
||||
parent_href = 'info?rating_key={}'.format(data['parent_rating_key']) if data['parent_rating_key'] else '#'
|
||||
grandparent_href = 'info?rating_key={}'.format(data['grandparent_rating_key']) if data['grandparent_rating_key'] else '#'
|
||||
user_href = 'user?user_id={}'.format(data['user_id']) if data['user_id'] else '#'
|
||||
%>
|
||||
<div class="dashboard-activity-instance" id="activity-instance-${sk}" data-key="${sk}" data-id="${data['session_id']}"
|
||||
data-rating_key="${data['rating_key']}" data-parent_rating_key="${data['parent_rating_key']}" data-grandparent_rating_key="${data['grandparent_rating_key']}">
|
||||
<div class="dashboard-activity-container">
|
||||
@@ -89,15 +96,15 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
% if data['channel_stream'] == 0:
|
||||
% if data['media_type'] == 'movie':
|
||||
<a id="poster-url-${sk}" href="info?rating_key=${data['rating_key']}" title="${data['title']}">
|
||||
<a id="poster-url-${sk}" href="${href}" title="${data['title']}">
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||
</a>
|
||||
% elif data['media_type'] == 'episode':
|
||||
<a id="poster-url-${sk}" href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">
|
||||
<a id="poster-url-${sk}" href="${grandparent_href}" title="${data['grandparent_title']}">
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['grandparent_thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||
</a>
|
||||
% elif data['media_type'] == 'track':
|
||||
<a id="poster-url-${sk}" href="info?rating_key=${data['parent_rating_key']}" title="${data['parent_title']}">
|
||||
<a id="poster-url-${sk}" href="${parent_href}" title="${data['parent_title']}">
|
||||
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&fallback=cover&refresh=true);"></div>
|
||||
</a>
|
||||
% elif data['media_type'] in ('photo', 'clip'):
|
||||
@@ -269,8 +276,9 @@ DOCUMENTATION :: END
|
||||
<li class="dashboard-activity-info-item">
|
||||
<div class="sub-heading">Location</div>
|
||||
<div class="sub-value time-right">
|
||||
<span id="location-${sk}">${data['location'].upper()}</span>:
|
||||
% if data['ip_address'] != 'N/A':
|
||||
<span id="location-${sk}">${data['location'].upper()}</span>: <span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
||||
<span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
||||
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
|
||||
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
|
||||
</a>
|
||||
@@ -352,13 +360,9 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-activity-metadata-wrapper">
|
||||
% if data['user_id']:
|
||||
<a href="user?user_id=${data['user_id']}" title="${data['friendly_name']}">
|
||||
<a href="${user_href}" title="${data['friendly_name']}">
|
||||
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
|
||||
</a>
|
||||
% else:
|
||||
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
|
||||
% endif
|
||||
<div class="dashboard-activity-metadata-title-container">
|
||||
<div id="play-state-${sk}" class="dashboard-activity-metadata-play_state-icon" title="${data['state'].capitalize()}">
|
||||
% if data['state'] == 'playing':
|
||||
@@ -371,21 +375,21 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
<div class="dashboard-activity-metadata-title">
|
||||
% if data['channel_stream'] == 0:
|
||||
% if data['media_type'] == 'movie':
|
||||
<a href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
|
||||
% elif data['media_type'] == 'episode':
|
||||
<a href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||
- <a href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
|
||||
% elif data['media_type'] == 'track':
|
||||
<a id="metadata-grandparent_title-${sk}" href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||
- <a id="metadata-title-${sk}" href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
|
||||
% elif data['media_type'] == 'photo':
|
||||
<span title="${data['parent_title']}">${data['parent_title']}</span>
|
||||
% elif data['media_type'] == 'clip':
|
||||
<span title="${data['title']}">${data['title']}</span>
|
||||
% else:
|
||||
<span title="${data['title']}">${data['title']}</span>
|
||||
% endif
|
||||
% if data['media_type'] == 'movie':
|
||||
<a href="${href}" title="${data['title']}">${data['title']}</a>
|
||||
% elif data['media_type'] == 'episode':
|
||||
<a href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||
- <a href="${href}" title="${data['title']}">${data['title']}</a>
|
||||
% elif data['media_type'] == 'track':
|
||||
<a id="metadata-grandparent_title-${sk}" href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
|
||||
- <a id="metadata-title-${sk}" href="${href}" title="${data['title']}">${data['title']}</a>
|
||||
% elif data['media_type'] == 'photo':
|
||||
<span title="${data['parent_title']}">${data['parent_title']}</span>
|
||||
% elif data['media_type'] == 'clip':
|
||||
<span title="${data['title']}">${data['title']}</span>
|
||||
% else:
|
||||
<span title="${data['title']}">${data['title']}</span>
|
||||
% endif
|
||||
% elif data['media_type'] == 'episode' and data['grandparent_title']:
|
||||
<span title="${data['grandparent_title']}">${data['grandparent_title']}</span>
|
||||
- <span title="${data['title']}">${data['title']}</span>
|
||||
@@ -425,10 +429,10 @@ DOCUMENTATION :: END
|
||||
% if data['media_type'] == 'movie':
|
||||
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
|
||||
% elif data['media_type'] == 'episode':
|
||||
<a href="info?rating_key=${data['parent_rating_key']}" title="Season ${data['parent_media_index']}" class="sub-heading">S${data['parent_media_index']}</a>
|
||||
· <a href="info?rating_key=${data['rating_key']}" title="Episode ${data['media_index']}" class="sub-heading">E${data['media_index']}</a>
|
||||
<a href="${parent_href}" title="Season ${data['parent_media_index']}" class="sub-heading">S${data['parent_media_index']}</a>
|
||||
· <a href="${href}" title="Episode ${data['media_index']}" class="sub-heading">E${data['media_index']}</a>
|
||||
% elif data['media_type'] == 'track':
|
||||
<a id="metadata-parent_title-${sk}" href="info?rating_key=${data['parent_rating_key']}" title="${data['parent_title']}" class="sub-heading">${data['parent_title']}</a>
|
||||
<a id="metadata-parent_title-${sk}" href="${parent_href}" title="${data['parent_title']}" class="sub-heading">${data['parent_title']}</a>
|
||||
% elif data['media_type'] == 'photo':
|
||||
<span title="${data['title']}" class="sub-heading">${data['title']}</span>
|
||||
% else:
|
||||
@@ -453,11 +457,7 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
</div>
|
||||
<div class="dashboard-activity-metadata-user">
|
||||
% if data['user_id']:
|
||||
<a href="user?user_id=${data['user_id']}" title="${data['friendly_name']}">${data['friendly_name']}</a>
|
||||
% else:
|
||||
${data['friendly_name']}
|
||||
% endif
|
||||
<a href="${user_href}" title="${data['friendly_name']}">${data['friendly_name']}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -88,8 +88,9 @@ DOCUMENTATION :: END
|
||||
% if stat_id in ('top_music', 'popular_music'):
|
||||
<div id="stats-thumb-${stat_id}-bg" class="dashboard-stats-poster-blur" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
% endif
|
||||
<a id="stats-thumb-url-${stat_id}" href="info?rating_key=${row0['rating_key']}" title="${row0['title']}">
|
||||
<% type = 'cover' if stat_id in ('top_music', 'popular_music') else 'poster' %>
|
||||
<% type = 'cover' if stat_id in ('top_music', 'popular_music') else 'poster' %>
|
||||
<% href = 'info?rating_key={}'.format(row0['rating_key']) if row0['rating_key'] else '#' %>
|
||||
<a id="stats-thumb-url-${stat_id}" href="${href}" title="${row0['title']}">
|
||||
% if row0['thumb']:
|
||||
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&fallback=${type});"></div>
|
||||
% else:
|
||||
@@ -98,7 +99,8 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
</div>
|
||||
% elif stat_id == 'top_users':
|
||||
<a id="stats-thumb-url-${stat_id}" href="user?user_id=${row0['user_id']}" title="${row0['friendly_name']}" class="hidden-xs">
|
||||
<% user_href = 'user?user_id={}'.format(row0['user_id']) if row0['user_id'] else '#' %>
|
||||
<a id="stats-thumb-url-${stat_id}" href="${user_href}" title="${row0['friendly_name']}" class="hidden-xs">
|
||||
<div id="stats-thumb-${stat_id}" class="dashboard-stats-circle" style="background-image: url(${row0['user_thumb'] or 'images/gravatar-default.png'})"></div>
|
||||
</a>
|
||||
% elif stat_id == 'top_platforms':
|
||||
@@ -127,26 +129,20 @@ DOCUMENTATION :: END
|
||||
% for row in top_stat['rows']:
|
||||
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${stat_id}" data-rating_key="${row.get('rating_key')}" data-title="${row.get('title')}"
|
||||
data-art="${row.get('art')}" data-thumb="${row.get('thumb')}" data-platform="${row.get('platform_name')}"
|
||||
data-user_id="${row.get('user_id')}" data-friendly_name="${row.get('friendly_name')}"
|
||||
data-user_id="${row.get('user_id')}" data-friendly_name="${row.get('friendly_name')}" data-user_thumb="${row.get('user_thumb')}"
|
||||
data-last_watch="${row.get('last_watch')}" data-started="${row.get('started')}">
|
||||
<div class="sub-list">${loop.index + 1}</div>
|
||||
<div class="sub-value">
|
||||
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
|
||||
% if top_stat['rows'][loop.index]['rating_key']:
|
||||
<a href="info?rating_key=${row['rating_key']}" title="${row['title']}">
|
||||
<% href = 'info?rating_key={}'.format(row['rating_key']) if row['rating_key'] else '#' %>
|
||||
<a href="${href}" title="${row['title']}">
|
||||
${row['title']}
|
||||
</a>
|
||||
% else:
|
||||
${row['title']}
|
||||
% endif
|
||||
% elif stat_id == 'top_users':
|
||||
% if top_stat['rows'][loop.index]['user_id']:
|
||||
<a href="user?user_id=${row['user_id']}" title="${row['friendly_name']}">
|
||||
<% user_href = 'user?user_id={}'.format(row['user_id']) if row['user_id'] else '#' %>
|
||||
<a href="${user_href}" title="${row['friendly_name']}">
|
||||
${row['friendly_name']}
|
||||
</a>
|
||||
% else:
|
||||
${row['friendly_name']}
|
||||
% endif
|
||||
% elif stat_id == 'top_platforms':
|
||||
${row['platform']}
|
||||
% elif stat_id == 'most_concurrent':
|
||||
@@ -182,13 +178,22 @@ DOCUMENTATION :: END
|
||||
var stat_id = $(elem).data('stat_id');
|
||||
var art = $(elem).data('art');
|
||||
var thumb = $(elem).data('thumb');
|
||||
var user_id = $(elem).data('user_id');
|
||||
var user_thumb = $(elem).data('user_thumb');
|
||||
var rating_key = $(elem).data('rating_key');
|
||||
var [height, fallback] = ($.inArray(stat_id, ['top_music', 'popular_music']) > -1) ? [300, 'cover'] : [450, 'poster'];
|
||||
var href;
|
||||
|
||||
if (stat_id == 'most_concurrent') {
|
||||
return
|
||||
} else if (stat_id == 'top_users') {
|
||||
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + (thumb || 'images/gravatar-default.png') + ')');
|
||||
$('#stats-thumb-url-' + stat_id).attr('href', 'user?user_id=' + $(elem).data('user_id')).prop('title', $(elem).data('friendly_name'));
|
||||
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + (user_thumb || 'images/gravatar-default.png') + ')');
|
||||
if (user_id) {
|
||||
href = 'user?user_id=' + user_id;
|
||||
} else {
|
||||
href = '#';
|
||||
}
|
||||
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('friendly_name'));
|
||||
} else if (stat_id == 'top_platforms') {
|
||||
$('#stats-thumb-' + stat_id).removeClass(function (index, className) {
|
||||
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
||||
@@ -197,7 +202,12 @@ DOCUMENTATION :: END
|
||||
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
|
||||
}).addClass('platform-' + $(elem).data('platform'));
|
||||
} else {
|
||||
$('#stats-thumb-url-' + stat_id).attr('href', 'info?rating_key=' + $(elem).data('rating_key')).prop('title', $(elem).data('title'));
|
||||
if (rating_key) {
|
||||
href = 'info?rating_key=' + rating_key;
|
||||
} else {
|
||||
href = '#';
|
||||
}
|
||||
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('title'));
|
||||
if (art) {
|
||||
$('#stats-background-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + art + '&width=500&height=280&fallback=art)');
|
||||
} else {
|
||||
|
@@ -26,7 +26,7 @@ function refreshTab() {
|
||||
|
||||
function showMsg(msg, loader, timeout, ms, error) {
|
||||
var feedback = $("#ajaxMsg");
|
||||
update = $("#updatebar");
|
||||
var update = $("#updatebar");
|
||||
if (update.is(":visible")) {
|
||||
var height = update.height() + 35;
|
||||
feedback.css("bottom", height + "px");
|
||||
@@ -35,7 +35,7 @@ function showMsg(msg, loader, timeout, ms, error) {
|
||||
}
|
||||
var message = $("<div class='msg'>" + msg + "</div>");
|
||||
if (loader) {
|
||||
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||
message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||
feedback.css("padding", "14px 10px")
|
||||
}
|
||||
if (error) {
|
||||
|
@@ -180,18 +180,20 @@
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
$("#refresh-libraries-list").click(function () {
|
||||
showMsg('Refreshing libraries list...', true, false);
|
||||
$.ajax({
|
||||
url: 'refresh_libraries_list',
|
||||
cache: false,
|
||||
async: true,
|
||||
success: function (data) {
|
||||
showMsg('<i class="fa fa-refresh"></i> Libraries list refresh started...', false, true, 2000, false);
|
||||
},
|
||||
complete: function (data) {
|
||||
showMsg('<i class="fa fa-check"></i> Libraries list refreshed.', false, true, 2000, false);
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
showMsg('<i class="fa fa-exclamation-circle"></i> Unable to refresh libraries list.', false, true, 2000, true);
|
||||
complete: function (xhr, status) {
|
||||
var result = $.parseJSON(xhr.responseText);
|
||||
var msg = result.message;
|
||||
if (result.result == 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
|
||||
libraries_list_table.draw();
|
||||
} else {
|
||||
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 2000, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -518,7 +518,6 @@
|
||||
'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)';
|
||||
var $email_selectors = $('#email_to, #email_cc, #email_bcc').selectize({
|
||||
plugins: ['remove_button'],
|
||||
persist: false,
|
||||
maxItems: null,
|
||||
render: {
|
||||
item: function(item, escape) {
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
import plexpy
|
||||
from plexpy import common, notifiers
|
||||
from plexpy.helpers import anon_url
|
||||
from plexpy.helpers import anon_url, checked
|
||||
|
||||
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['label'])
|
||||
%>
|
||||
@@ -623,9 +623,11 @@
|
||||
<div class="form-group has-feedback" id="pms_ip_group">
|
||||
<label for="pms_ip">Plex IP or Hostname</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-9" id="selectize-pms-ip-container">
|
||||
<div class="input-group">
|
||||
<input type="text" class="pms-settings form-control" id="pms_ip" name="pms_ip" value="${config['pms_ip']}" size="30" data-parsley-trigger="change" aria-describedby="server-verified" data-parsley-errors-container="#pms_ip_error" required>
|
||||
<select class="form-control pms-settings selectize-pms-ip" id="pms_ip" name="pms_ip" data-parsley-trigger="change" aria-describedby="server-verified" data-parsley-errors-container="#pms_ip_error" required>
|
||||
<option value="${config['pms_ip']}" selected>${config['pms_ip']}</option>
|
||||
</select>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-form" type="button" id="verify_server_button">Verify Server</button>
|
||||
</span>
|
||||
@@ -634,7 +636,7 @@
|
||||
</div>
|
||||
<div id="pms_ip_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||
</div>
|
||||
<p class="help-block">IP Address or hostname for Plex Media Server.</p>
|
||||
<p class="help-block">Select your Plex Media Server from the dropdown menu or enter an IP address or hostname.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pms_port">Plex Port</label>
|
||||
@@ -648,27 +650,23 @@
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="pms_is_remote" name="pms_is_remote" value="1" ${config['pms_is_remote']}> Remote Server
|
||||
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle" data-id="pms_is_remote" value="1" ${checked(config['pms_is_remote'])}> Remote Server
|
||||
<input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}">
|
||||
</label>
|
||||
<p class="help-block">Check this if your Plex Server is not on the same local network as Tautulli.</p>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1" ${config['pms_ssl']}> Use SSL
|
||||
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle" data-id="pms_ssl" value="1" ${checked(config['pms_ssl'])}> Use SSL
|
||||
<input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}">
|
||||
</label>
|
||||
<p class="help-block">If you have secure connections enabled on your Plex Server, communicate with it securely.</p>
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
% if config['pms_is_cloud']:
|
||||
<label>
|
||||
<input type="checkbox" id="pms_url_manual" name="pms_url_manual" value="1" disabled> Manual Connection
|
||||
</label>
|
||||
<span style="color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
||||
% else:
|
||||
<label>
|
||||
<input type="checkbox" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection
|
||||
</label>
|
||||
% endif
|
||||
<span id="cloudManualConnection" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
||||
<p class="help-block">Use the user defined connection details. Do not retrieve the server connection URL automatically.</p>
|
||||
</div>
|
||||
<div class="form-group advanced-setting">
|
||||
@@ -689,6 +687,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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']}">
|
||||
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;">
|
||||
|
||||
@@ -721,16 +720,10 @@
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
% if config['pms_is_cloud']:
|
||||
<label>
|
||||
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" disabled> Monitor Plex Updates
|
||||
</label>
|
||||
<span style="color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
||||
% else:
|
||||
<label>
|
||||
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" ${config['monitor_pms_updates']}> Monitor Plex Updates
|
||||
</label>
|
||||
% endif
|
||||
<span id="cloudMonitorUpdates" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
||||
<p class="help-block">Enable to have Tautulli check if updates are available for the Plex Media Server.</p>
|
||||
</div>
|
||||
<div id="pms_update_options">
|
||||
@@ -753,17 +746,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
% if config['pms_is_cloud']:
|
||||
<label>
|
||||
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" disabled> Monitor Plex Remote Access
|
||||
</label>
|
||||
<span style="color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
||||
% else:
|
||||
<label>
|
||||
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
|
||||
</label>
|
||||
<span id="cloudMonitorRemoteAccess" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
|
||||
<span id="remoteAccessCheck" style="color: #eb8600; padding-left: 10px;"></span>
|
||||
% endif
|
||||
<p class="help-block">Enable to have Tautulli check if remote access to the Plex Media Server goes down.</p>
|
||||
</div>
|
||||
|
||||
@@ -1601,7 +1588,7 @@ $(document).ready(function() {
|
||||
if (serverChanged || authChanged || httpChanged || directoryChanged) {
|
||||
$('#restart-modal').modal('show');
|
||||
}
|
||||
$("#http_hashed_password").val($("#http_hash_password").is(":checked") ? 1 : 0)
|
||||
$("#http_hashed_password").val($("#http_hash_password").is(":checked") ? 1 : 0);
|
||||
getConfigurationTable();
|
||||
getSchedulerTable();
|
||||
getNotifiersTable();
|
||||
@@ -1661,11 +1648,11 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
$('#menu_link_update_check').click(function() {
|
||||
// Allow the update bar to show again if previously dismissed.
|
||||
setCookie('updateDismiss', 'true', 0);
|
||||
$(this).html('<i class="fa fa-spin fa-refresh"></i> Checking');
|
||||
$(this).prop('disabled', true);
|
||||
window.location.href = 'checkGithub';
|
||||
$(this).html('<i class="fa fa-spin fa-refresh"></i> Checking').prop('disabled', true);
|
||||
checkUpdate(function () {
|
||||
$('#menu_link_update_check').html('<i class="fa fa-arrow-circle-up"></i> Check for Updates')
|
||||
.prop('disabled', false);
|
||||
});
|
||||
});
|
||||
|
||||
$('#modal_link_restart').click(function() {
|
||||
@@ -1787,12 +1774,138 @@ $(document).ready(function() {
|
||||
verifyServer();
|
||||
});
|
||||
|
||||
$('.checkbox-toggle').click(function () {
|
||||
var configToggle = $(this).data('id');
|
||||
if ($(this).is(':checked')) {
|
||||
$('#'+configToggle).val(1);
|
||||
} else {
|
||||
$('#'+configToggle).val(0);
|
||||
}
|
||||
});
|
||||
|
||||
var $select_pms = $('#pms_ip').selectize({
|
||||
createOnBlur: true,
|
||||
openOnFocus: true,
|
||||
maxItems: 1,
|
||||
closeAfterSelect: true,
|
||||
sortField: 'label',
|
||||
searchField: ['label', 'value'],
|
||||
inputClass: 'form-control selectize-input',
|
||||
dropdownParent: '#selectize-pms-ip-container',
|
||||
render: {
|
||||
item: function (item, escape) {
|
||||
var label = item.label || item.value;
|
||||
var caption = item.label ? item.value : null;
|
||||
return '<div data-ssl="' + item.httpsRequired +
|
||||
'" data-local="' + item.local +
|
||||
'" data-identifier="' + item.clientIdentifier +
|
||||
'" data-ip="' + item.ip +
|
||||
'" data-port="' + item.port +
|
||||
'" data-is_cloud="' + item.is_cloud +
|
||||
'" data-label="' + item.label + '">' +
|
||||
'<span class="item-text">' + escape(label) + '</span>' +
|
||||
(caption ? '<span class="item-value">' + escape(caption) + '</span>' : '') +
|
||||
'</div>';
|
||||
},
|
||||
option: function (item, escape) {
|
||||
var label = item.label || item.value;
|
||||
var caption = item.label ? item.value : null;
|
||||
return '<div data-ssl="' + item.httpsRequired +
|
||||
'" data-local="' + item.local +
|
||||
'" data-identifier="' + item.clientIdentifier +
|
||||
'" data-ip="' + item.ip +
|
||||
'" data-port="' + item.port +
|
||||
'" data-is_cloud="' + item.is_cloud +
|
||||
'" data-label="' + item.label + '">' +
|
||||
escape(label) +
|
||||
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
},
|
||||
create: function(input) {
|
||||
return {label: '', value: input};
|
||||
},
|
||||
onChange: function (item) {
|
||||
var pms_ip_selected = this.getItem(item)[0];
|
||||
var identifier = $(pms_ip_selected).data('identifier');
|
||||
var port = $(pms_ip_selected).data('port');
|
||||
var local = $(pms_ip_selected).data('local');
|
||||
var ssl = $(pms_ip_selected).data('ssl');
|
||||
var is_cloud = $(pms_ip_selected).data('is_cloud');
|
||||
|
||||
$("#pms_identifier").val(identifier !== 'undefined' ? identifier : '');
|
||||
$('#pms_port').val(port !== 'undefined' ? port : 32400);
|
||||
$('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0));
|
||||
$('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0);
|
||||
$('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1));
|
||||
$('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0);
|
||||
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
|
||||
$('#pms_url_manual').prop('checked', false);
|
||||
PMSCloudCheck();
|
||||
}
|
||||
});
|
||||
var select_pms = $select_pms[0].selectize;
|
||||
|
||||
function getServerOptions(token) {
|
||||
$.ajax({
|
||||
url: 'discover',
|
||||
data: {
|
||||
token: token
|
||||
},
|
||||
success: function (result) {
|
||||
if (result) {
|
||||
var existing_value = $('#pms_ip').val();
|
||||
result.forEach(function (item) {
|
||||
if (item.value === existing_value) {
|
||||
select_pms.updateOption(item.value, item);
|
||||
} else {
|
||||
select_pms.addOption(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
getServerOptions();
|
||||
|
||||
function PMSCloudCheck() {
|
||||
if ($('#pms_is_cloud').val() === "1") {
|
||||
$('#pms_port').val(443).prop('readonly', true);
|
||||
$('#pms_is_remote_checkbox').prop('checked', true).prop('disabled', true);
|
||||
$('#pms_is_remote').val(1);
|
||||
$('#pms_ssl_checkbox').prop('checked', true).prop('disabled', true);
|
||||
$('#pms_ssl').val(1);
|
||||
$('#pms_url_manual').prop('checked', false).prop('disabled', true);
|
||||
$('#monitor_pms_updates').prop('checked', false).prop('disabled', true);
|
||||
$('#pms_update_options').hide();
|
||||
$('#monitor_remote_access').prop('checked', false).prop('disabled', true);
|
||||
$('#cloudManualConnection').show();
|
||||
$('#cloudMonitorUpdates').show();
|
||||
$('#cloudMonitorRemoteAccess').show();
|
||||
$('#remoteAccessCheck').hide();
|
||||
} else {
|
||||
$('#pms_port').prop('readonly', false);
|
||||
$('#pms_is_remote_checkbox').prop('disabled', false);
|
||||
$('#pms_is_remote').val($('#pms_is_remote_checkbox').is(':checked') ? 1 : 0);
|
||||
$('#pms_ssl_checkbox').prop('disabled', false);
|
||||
$('#pms_ssl').val($('#pms_ssl_checkbox').is(':checked') ? 1 : 0);
|
||||
$('#pms_url_manual').prop('disabled', false);
|
||||
$('#monitor_pms_updates').prop('disabled', false);
|
||||
$('#monitor_remote_access').prop('disabled', false);
|
||||
$('#cloudManualConnection').hide();
|
||||
$('#cloudMonitorUpdates').hide();
|
||||
$('#cloudMonitorRemoteAccess').hide();
|
||||
remoteAccessEnabledCheck()
|
||||
}
|
||||
}
|
||||
PMSCloudCheck();
|
||||
|
||||
function verifyServer(_callback) {
|
||||
var pms_ip = $("#pms_ip").val();
|
||||
var pms_port = $("#pms_port").val();
|
||||
var pms_identifier = $("#pms_identifier").val();
|
||||
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
|
||||
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
|
||||
var pms_ssl = $("#pms_ssl").val();
|
||||
var pms_is_remote = $("#pms_is_remote").val();
|
||||
|
||||
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
|
||||
$("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
|
||||
@@ -1875,6 +1988,7 @@ $(document).ready(function() {
|
||||
$("#pms-token-status").html('<i class="fa fa-check"></i> ' + msg);
|
||||
$("#pms_token").val(authToken);
|
||||
$('#pms-auth-modal').modal('hide');
|
||||
getServerOptions(authToken);
|
||||
} else {
|
||||
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> ' + msg);
|
||||
}
|
||||
@@ -1903,18 +2017,20 @@ $(document).ready(function() {
|
||||
pms_logs_debug = false;
|
||||
pms_logs = false;
|
||||
|
||||
$.ajax({
|
||||
url: 'get_server_pref',
|
||||
data: { pref: 'PublishServerOnPlexOnlineKey' },
|
||||
async: true,
|
||||
success: function(data) {
|
||||
if (data !== 'true') {
|
||||
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
|
||||
$("#monitor_remote_access").attr("disabled", true);
|
||||
$("#monitor_remote_access").attr("checked", false);
|
||||
function remoteAccessEnabledCheck() {
|
||||
$.ajax({
|
||||
url: 'get_server_pref',
|
||||
data: { pref: 'PublishServerOnPlexOnlineKey' },
|
||||
async: true,
|
||||
success: function(data) {
|
||||
if (data !== 'true') {
|
||||
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
|
||||
$("#monitor_remote_access").attr("checked", false).attr("disabled", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
remoteAccessEnabledCheck();
|
||||
|
||||
// Sortable home_sections
|
||||
function set_home_sections() {
|
||||
@@ -1924,11 +2040,11 @@ $(document).ready(function() {
|
||||
home_sections.push(sec.value);
|
||||
});
|
||||
$('#home_sections').val(home_sections);
|
||||
};
|
||||
}
|
||||
|
||||
var sec_cards = ${config['home_sections'] | n};
|
||||
sec_cards.reverse().forEach(function (item) {
|
||||
$('#hsec-' + item).prop('checked', !$(this).prop('checked'))
|
||||
$('#hsec-' + item).prop('checked', !$(this).prop('checked'));
|
||||
$('#hsec-' + item).closest('li.card').prependTo('#sortable_home_sections');
|
||||
});
|
||||
|
||||
@@ -1940,7 +2056,7 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
$('[id^=hsec-]').change(function() { set_home_sections(); });
|
||||
set_home_sections()
|
||||
set_home_sections();
|
||||
|
||||
// Sortable home_stats_cards
|
||||
function set_home_stats_cards() {
|
||||
@@ -1950,11 +2066,11 @@ $(document).ready(function() {
|
||||
home_stats_cards.push(card.value);
|
||||
});
|
||||
$('#home_stats_cards').val(home_stats_cards);
|
||||
};
|
||||
}
|
||||
|
||||
var config_cards = ${config['home_stats_cards'] | n};
|
||||
config_cards.reverse().forEach(function (item) {
|
||||
$('#hscard-' + item).prop('checked', !$(this).prop('checked'))
|
||||
$('#hscard-' + item).prop('checked', !$(this).prop('checked'));
|
||||
$('#hscard-' + item).closest('li.card').prependTo('#sortable_home_stats_cards');
|
||||
});
|
||||
|
||||
@@ -1966,7 +2082,7 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
$('[id^=hscard-]').change(function() { set_home_stats_cards(); });
|
||||
set_home_stats_cards()
|
||||
set_home_stats_cards();
|
||||
|
||||
// Sortable home_library_cards
|
||||
function set_home_library_cards() {
|
||||
@@ -1976,7 +2092,7 @@ $(document).ready(function() {
|
||||
home_library_cards.push(card.value);
|
||||
});
|
||||
$('#home_library_cards').val(home_library_cards);
|
||||
};
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: 'get_library_sections',
|
||||
@@ -2015,12 +2131,10 @@ $(document).ready(function() {
|
||||
|
||||
function allowPlexAdminCheck () {
|
||||
if ($("#http_basic_auth").is(":checked")) {
|
||||
$("#http_plex_admin").attr("disabled", true);
|
||||
$("#http_plex_admin").attr("checked", false);
|
||||
$("#http_plex_admin").attr("checked", false).attr("disabled", true);
|
||||
$("#allowPlexCheck").html("Plex admin login cannot be enabled with basic authentication.");
|
||||
} else if ($('#http_username').val() == '' || $('#http_password').val() == '') {
|
||||
$("#http_plex_admin").attr("disabled", true);
|
||||
$("#http_plex_admin").attr("checked", false);
|
||||
$("#http_plex_admin").attr("checked", false).attr("disabled", true);
|
||||
$("#allowPlexCheck").html("You must set an admin username and password above to allow Plex admin login.");
|
||||
} else {
|
||||
$("#http_plex_admin").attr("disabled", false);
|
||||
@@ -2035,12 +2149,10 @@ $(document).ready(function() {
|
||||
|
||||
function allowGuestAccessCheck () {
|
||||
if ($("#http_basic_auth").is(":checked")) {
|
||||
$("#allow_guest_access").attr("disabled", true);
|
||||
$("#allow_guest_access").attr("checked", false);
|
||||
$("#allow_guest_access").attr("checked", false).attr("disabled", true);
|
||||
$("#allowGuestCheck").html("Guest access cannot be enabled with basic authentication.");
|
||||
} else if ($('#http_username').val() == '' || $('#http_password').val() == '') {
|
||||
$("#allow_guest_access").attr("disabled", true);
|
||||
$("#allow_guest_access").attr("checked", false);
|
||||
$("#allow_guest_access").attr("checked", false).attr("disabled", true);
|
||||
$("#allowGuestCheck").html("You must set an admin username and password above to allow guest access.");
|
||||
} else {
|
||||
$("#allow_guest_access").attr("disabled", false);
|
||||
@@ -2055,8 +2167,7 @@ $(document).ready(function() {
|
||||
|
||||
function hashPasswordCheck () {
|
||||
if ($("#http_basic_auth").is(":checked")) {
|
||||
$("#http_hash_password").attr("checked", false);
|
||||
$("#http_hash_password").attr("disabled", true);
|
||||
$("#http_hash_password").attr("checked", false).attr("disabled", true);
|
||||
$("#hashPasswordCheck").html("Password cannot be hashed with basic authentication.");
|
||||
} else {
|
||||
$("#http_hash_password").attr("disabled", false);
|
||||
|
@@ -184,18 +184,20 @@
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
$("#refresh-users-list").click(function() {
|
||||
showMsg('Refreshing users list...', true, false);
|
||||
$.ajax({
|
||||
url: 'refresh_users_list',
|
||||
cache: false,
|
||||
async: true,
|
||||
success: function(data) {
|
||||
showMsg('<i class="fa fa-check"></i> Users list refresh started...', false, true, 2000, false);
|
||||
},
|
||||
complete: function (data) {
|
||||
showMsg('<i class="fa fa-check"></i> Users list refreshed.', false, true, 2000, false);
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
showMsg('<i class="fa fa-exclamation-circle"></i> Unable to refresh users list.', false, true, 2000, true);
|
||||
complete: function (xhr, status) {
|
||||
var result = $.parseJSON(xhr.responseText);
|
||||
var msg = result.message;
|
||||
if (result.result == 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
|
||||
users_list_table.draw();
|
||||
} else {
|
||||
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 2000, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<%
|
||||
import plexpy
|
||||
from plexpy import common
|
||||
from plexpy import common, helpers
|
||||
%>
|
||||
|
||||
<!doctype html>
|
||||
@@ -47,7 +47,7 @@
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="wizard" id="some-wizard" data-title="Tautulli Setup Wizard">
|
||||
<div class="wizard" id="setup-wizard" data-title="Tautulli Setup Wizard">
|
||||
<form>
|
||||
<div class="wizard-card" data-cardname="card1">
|
||||
<div style="float: right;">
|
||||
@@ -82,22 +82,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" class="form-control pms-auth" name="pms_token" id="pms_token" value="${config['pms_token']}" data-validate="validatePMStoken">
|
||||
<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>
|
||||
</div>
|
||||
<div class="wizard-card" data-cardname="card3">
|
||||
<h3>Plex Media Server</h3>
|
||||
<p class="help-block">Enter your Plex Server details and then click the Verify button to make sure Tautulli can reach the server.</p>
|
||||
<p class="help-block">
|
||||
Select your Plex Media Server from the dropdown menu or enter an IP address or hostname.
|
||||
</p>
|
||||
<div class="wizard-input-section">
|
||||
<label for="pms_ip">Plex IP or Hostname</label>
|
||||
<div class="row">
|
||||
<div class="col-xs-8">
|
||||
<select id="pms_ip" name="pms_ip"></select>
|
||||
<div class="col-xs-12">
|
||||
<select class="form-control selectize-pms-ip" id="pms_ip" name="pms_ip">
|
||||
<option value="${config['pms_ip']}" selected>${config['pms_ip']}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-input-section">
|
||||
<label for="pms_port">Port Number</label>
|
||||
<label for="pms_port">Plex Port</label>
|
||||
<div class="row">
|
||||
<div class="col-xs-3">
|
||||
<input type="text" class="form-control pms_settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
|
||||
@@ -105,20 +109,23 @@
|
||||
<div class="col-xs-4">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1"> Use SSL
|
||||
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle" data-id="pms_ssl" value="1" ${helpers.checked(config['pms_ssl'])}> Use SSL
|
||||
<input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-4">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="pms_is_remote" name="pms_is_remote" value="1"> Remote Server
|
||||
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle" data-id="pms_is_remote" value="1" ${helpers.checked(config['pms_is_remote'])}> Remote Server
|
||||
<input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" class="form-control pms-settings" 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" class="form-control pms-settings" 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>
|
||||
</div>
|
||||
@@ -200,106 +207,6 @@
|
||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||
<script src="${http_root}js/bootstrap-wizard.min.js"></script>
|
||||
<script>
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
$.fn.wizard.logging = false;
|
||||
var options = {
|
||||
keyboard : false,
|
||||
contentHeight : 400,
|
||||
contentWidth : 700,
|
||||
backdrop: 'static',
|
||||
buttons: {submitText: 'Finish'},
|
||||
submitUrl: "configUpdate"
|
||||
};
|
||||
var wizard = $("#some-wizard").wizard(options);
|
||||
wizard.show();
|
||||
|
||||
wizard.on("submit", function(wizard) {
|
||||
// Probably should not success before we know, but hopefully validation is good enough.
|
||||
wizard.submitSuccess();
|
||||
$.ajax({
|
||||
url: "configUpdate",
|
||||
type: "POST",
|
||||
url: wizard.args.submitUrl,
|
||||
data: wizard.serialize(),
|
||||
dataType: "json",
|
||||
complete: function (data) {
|
||||
$(".countdown").countdown(function () { location.reload(); }, 5, "");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$select_pms = $('#pms_ip').selectize({
|
||||
create: true,
|
||||
createOnBlur: true,
|
||||
openOnFocus: true,
|
||||
maxItems: 1,
|
||||
closeAfterSelect: true,
|
||||
onInitialize: function () {
|
||||
var s = this;
|
||||
this.revertSettings.$children.each(function () {
|
||||
$.extend(s.options[this.value], $(this).data());
|
||||
});
|
||||
|
||||
},
|
||||
render: {
|
||||
option: function (item, escape) {
|
||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '" data-label="' + item.label + '">' + item.value + ' (' + item.label + ')</div>';
|
||||
},
|
||||
item: function (item, escape) {
|
||||
// first item is rendered before initialization bug?
|
||||
if (!item.ci) {
|
||||
$.extend(item,
|
||||
$(this.revertSettings.$children)
|
||||
.filter('[value="' + item.value + '"]').data());
|
||||
|
||||
}
|
||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '" data-label="' + item.label + '">' + item.value + ' (' + item.label + ')</div>';
|
||||
}
|
||||
},
|
||||
onChange: function (item) {
|
||||
var ci = $('.selectize-input').find('div').attr('data-ci');
|
||||
var port = $('.selectize-input').find('div').attr('data-port')
|
||||
var local = $('.selectize-input').find('div').attr('data-local')
|
||||
var ssl = $('.selectize-input').find('div').attr('data-use_ssl')
|
||||
|
||||
$("#pms-verify-status").html("");
|
||||
// If a option was added by a user its
|
||||
// data-xxx="undefined"
|
||||
if (ci != "undefined") {
|
||||
// To allow next step in the guide.
|
||||
// servers with clientIdentifier is verified
|
||||
$("#pms_identifier").val(ci);
|
||||
$("#pms_valid").val("valid");
|
||||
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!').show();
|
||||
} else {
|
||||
// Self made options must be verified
|
||||
$("#pms_valid").val("");
|
||||
$("#pms-verify-status").html("").hide();
|
||||
}
|
||||
// If the server is verified set the correct port
|
||||
if (port != "undefined") {
|
||||
$('#pms_port').val(port);
|
||||
} else {
|
||||
// set default port
|
||||
$('#pms_port').val("32400");
|
||||
}
|
||||
if (local != "undefined" && local == '0') {
|
||||
$('#pms_is_remote').prop('checked', true);
|
||||
} else {
|
||||
$('#pms_is_remote').prop('checked', false);
|
||||
}
|
||||
if (ssl != "undefined" && ssl == "1") {
|
||||
$('#pms_ssl').prop('checked', true);
|
||||
} else {
|
||||
$('#pms_ssl').prop('checked', false);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function validatePMSip(el) {
|
||||
var valid_pms_ip = el.val();
|
||||
var retValue = {};
|
||||
@@ -352,6 +259,146 @@
|
||||
return $.isNumeric(n) && (Math.floor(n) == n) && (n >= 0)
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
$.fn.wizard.logging = false;
|
||||
var options = {
|
||||
keyboard : false,
|
||||
contentHeight : 400,
|
||||
contentWidth : 700,
|
||||
backdrop: 'static',
|
||||
buttons: {submitText: 'Finish'},
|
||||
submitUrl: "configUpdate"
|
||||
};
|
||||
var wizard = $("#setup-wizard").wizard(options);
|
||||
wizard.show();
|
||||
|
||||
// Change button classes
|
||||
wizard.find('.wizard-back').addClass('btn-dark');
|
||||
wizard.on('incrementCard', function(wizard) {
|
||||
wizard.find('.wizard-next.btn-success').removeClass('btn-success').addClass('btn-bright');
|
||||
});
|
||||
wizard.on('decrementCard', function(wizard) {
|
||||
wizard.find('.wizard-next').removeClass('btn-bright').text('Next');
|
||||
});
|
||||
|
||||
wizard.on("submit", function(wizard) {
|
||||
// Probably should not success before we know, but hopefully validation is good enough.
|
||||
wizard.submitSuccess();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: wizard.args.submitUrl,
|
||||
data: wizard.serialize(),
|
||||
dataType: "json",
|
||||
complete: function (data) {
|
||||
$(".countdown").countdown(function () { location.reload(); }, 5, "");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$('.checkbox-toggle').click(function () {
|
||||
var configToggle = $(this).data('id');
|
||||
if ($(this).is(':checked')) {
|
||||
$('#'+configToggle).val(1);
|
||||
} else {
|
||||
$('#'+configToggle).val(0);
|
||||
}
|
||||
});
|
||||
|
||||
var $select_pms = $('#pms_ip').selectize({
|
||||
createOnBlur: true,
|
||||
openOnFocus: true,
|
||||
maxItems: 1,
|
||||
closeAfterSelect: true,
|
||||
sortField: 'label',
|
||||
searchField: ['label', 'value'],
|
||||
inputClass: 'form-control selectize-input',
|
||||
render: {
|
||||
item: function (item, escape) {
|
||||
var label = item.label || item.value;
|
||||
var caption = item.label ? item.value : null;
|
||||
return '<div data-ssl="' + item.httpsRequired +
|
||||
'" data-local="' + item.local +
|
||||
'" data-identifier="' + item.clientIdentifier +
|
||||
'" data-ip="' + item.ip +
|
||||
'" data-port="' + item.port +
|
||||
'" data-is_cloud="' + item.is_cloud +
|
||||
'" data-label="' + item.label + '">' +
|
||||
'<span class="item-text">' + escape(label) + '</span>' +
|
||||
(caption ? '<span class="item-value">' + escape(caption) + '</span>' : '') +
|
||||
'</div>';
|
||||
},
|
||||
option: function (item, escape) {
|
||||
var label = item.label || item.value;
|
||||
var caption = item.label ? item.value : null;
|
||||
return '<div data-ssl="' + item.httpsRequired +
|
||||
'" data-local="' + item.local +
|
||||
'" data-identifier="' + item.clientIdentifier +
|
||||
'" data-ip="' + item.ip +
|
||||
'" data-port="' + item.port +
|
||||
'" data-is_cloud="' + item.is_cloud +
|
||||
'" data-label="' + item.label + '">' +
|
||||
escape(label) +
|
||||
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
},
|
||||
create: function(input) {
|
||||
return {label: '', value: input};
|
||||
},
|
||||
onChange: function (item) {
|
||||
var pms_ip_selected = this.getItem(item)[0];
|
||||
var identifier = $(pms_ip_selected).data('identifier');
|
||||
var port = $(pms_ip_selected).data('port');
|
||||
var local = $(pms_ip_selected).data('local');
|
||||
var ssl = $(pms_ip_selected).data('ssl');
|
||||
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_identifier").val(identifier !== 'undefined' ? identifier : '');
|
||||
$('#pms_port').val(port !== 'undefined' ? port : 32400);
|
||||
$('#pms_is_remote_checkbox').prop('checked', (local !== 'undefined' && local === 0));
|
||||
$('#pms_is_remote').val(local !== 'undefined' && local === 0 ? 1 : 0);
|
||||
$('#pms_ssl_checkbox').prop('checked', (ssl !== 'undefined' && ssl === 1));
|
||||
$('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0);
|
||||
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
|
||||
|
||||
if (is_cloud === true) {
|
||||
$('#pms_port').prop('readonly', true);
|
||||
$('#pms_is_remote_checkbox').prop('disabled', true);
|
||||
$('#pms_ssl_checkbox').prop('disabled', true);
|
||||
} else {
|
||||
$('#pms_port').prop('readonly', false);
|
||||
$('#pms_is_remote_checkbox').prop('disabled', false);
|
||||
$('#pms_ssl_checkbox').prop('disabled', false);
|
||||
}
|
||||
}
|
||||
});
|
||||
var select_pms = $select_pms[0].selectize;
|
||||
|
||||
function getServerOptions(token) {
|
||||
/* Set token and returns server options */
|
||||
$.ajax({
|
||||
url: 'discover',
|
||||
data: {
|
||||
token: token
|
||||
},
|
||||
success: function (result) {
|
||||
if (result) {
|
||||
var existing_value = $('#pms_ip').val();
|
||||
result.forEach(function (item) {
|
||||
if (item.value === existing_value) {
|
||||
select_pms.updateOption(item.value, item);
|
||||
} else {
|
||||
select_pms.addOption(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
var pms_verified = false;
|
||||
var authenticated = false;
|
||||
|
||||
@@ -360,14 +407,19 @@
|
||||
var pms_ip = $("#pms_ip").val().trim();
|
||||
var pms_port = $("#pms_port").val().trim();
|
||||
var pms_identifier = $("#pms_identifier").val();
|
||||
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
|
||||
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
|
||||
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').fadeIn('fast');
|
||||
$.ajax({
|
||||
url: 'get_server_id',
|
||||
data: { hostname: pms_ip, port: pms_port, identifier: pms_identifier, ssl: pms_ssl, remote: pms_is_remote },
|
||||
data: {
|
||||
hostname: pms_ip,
|
||||
port: pms_port,
|
||||
identifier: pms_identifier,
|
||||
ssl: pms_ssl,
|
||||
remote: pms_is_remote },
|
||||
cache: true,
|
||||
async: true,
|
||||
timeout: 5000,
|
||||
@@ -444,39 +496,7 @@
|
||||
$('#pms-token-status').fadeIn('fast');
|
||||
}
|
||||
});
|
||||
|
||||
// Send database path to import script
|
||||
//$("#plexwatch-import").click(function() {
|
||||
// var database_path = $("#db_location").val();
|
||||
// var table_name = 'processed';
|
||||
// var import_ignore_interval = 0;
|
||||
// $.ajax({
|
||||
// url: 'get_plexwatch_export_data',
|
||||
// data: {database_path: database_path, table_name:table_name, import_ignore_interval:import_ignore_interval},
|
||||
// cache: false,
|
||||
// async: true,
|
||||
// success: function(data) {
|
||||
// if (data === 'Import has started. Check the Tautulli logs to monitor any problems.') {
|
||||
// $("#plexwatch-import-status").html('Started');
|
||||
// } else {
|
||||
// $("#plexwatch-import-status").html(data);
|
||||
// }
|
||||
// $("#db_location").val('')
|
||||
// }
|
||||
// });
|
||||
//});
|
||||
|
||||
function getServerOptions(token) {
|
||||
/* Set token and returns server options */
|
||||
$.ajax({
|
||||
url: "discover/" + token,
|
||||
success: function (result) {
|
||||
$('#pms_ip').html("");
|
||||
// Add all servers to the "combobox"
|
||||
$select_pms[0].selectize.addOption(result);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
@@ -22,6 +22,7 @@ import subprocess
|
||||
import threading
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
# Some cut down versions of Python may not include this module and it's not critical for us
|
||||
try:
|
||||
import webbrowser
|
||||
@@ -92,7 +93,7 @@ HTTP_ROOT = None
|
||||
DEV = False
|
||||
|
||||
WS_CONNECTED = False
|
||||
PLEX_SERVER_UP = True
|
||||
PLEX_SERVER_UP = None
|
||||
|
||||
|
||||
def initialize(config_file):
|
||||
@@ -157,16 +158,6 @@ def initialize(config_file):
|
||||
except OSError as e:
|
||||
logger.error(u"Could not create cache dir '%s': %s" % (CONFIG.CACHE_DIR, e))
|
||||
|
||||
if CONFIG.CACHE_DIR:
|
||||
session_metadata_folder = os.path.join(CONFIG.CACHE_DIR, 'session_metadata')
|
||||
try:
|
||||
shutil.rmtree(session_metadata_folder, ignore_errors=True)
|
||||
except OSError as e:
|
||||
pass
|
||||
|
||||
if not os.path.exists(session_metadata_folder):
|
||||
os.mkdir(session_metadata_folder)
|
||||
|
||||
# Initialize the database
|
||||
logger.info(u"Checking if the database upgrades are required...")
|
||||
try:
|
||||
@@ -213,6 +204,8 @@ def initialize(config_file):
|
||||
except IOError as e:
|
||||
logger.error(u"Unable to read previous version from file '%s': %s" %
|
||||
(version_lock_file, e))
|
||||
else:
|
||||
prev_version = 'cfd30996264b7e9fe4ef87f02d1cc52d1ae8bfca'
|
||||
|
||||
# Get the currently installed version. Returns None, 'win32' or the git
|
||||
# hash.
|
||||
@@ -284,6 +277,7 @@ def initialize(config_file):
|
||||
_INITIALIZED = True
|
||||
return True
|
||||
|
||||
|
||||
def daemonize():
|
||||
if threading.activeCount() != 1:
|
||||
logger.warn(
|
||||
@@ -393,7 +387,7 @@ def initialize_scheduler():
|
||||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||
hours=library_hours, minutes=0, seconds=0)
|
||||
|
||||
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
||||
schedule_job(activity_pinger.connect_server, 'Check for server response',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
else:
|
||||
@@ -411,12 +405,9 @@ def initialize_scheduler():
|
||||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
# Schedule job to reconnect websocket
|
||||
response_seconds = CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS * CONFIG.WEBSOCKET_CONNECTION_TIMEOUT
|
||||
response_seconds = 60 if response_seconds < 60 else response_seconds
|
||||
|
||||
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
||||
hours=0, minutes=0, seconds=response_seconds)
|
||||
# Schedule job to reconnect server
|
||||
schedule_job(activity_pinger.connect_server, 'Check for server response',
|
||||
hours=0, minutes=0, seconds=60, args=(False,))
|
||||
|
||||
# Start scheduler
|
||||
if start_jobs and len(SCHED.get_jobs()):
|
||||
@@ -460,6 +451,9 @@ def start():
|
||||
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
||||
notifiers.check_browser_enabled()
|
||||
|
||||
if CONFIG.FIRST_RUN_COMPLETE:
|
||||
activity_pinger.connect_server(log=True, startup=True)
|
||||
|
||||
_STARTED = True
|
||||
|
||||
|
||||
@@ -1080,9 +1074,9 @@ def dbcheck():
|
||||
)
|
||||
c_db.execute(
|
||||
'UPDATE session_history_media_info SET transcode_decision = (CASE '
|
||||
'WHEN video_decision = "transcode" OR audio_decision = "transcode" THEN "transcode" '
|
||||
'WHEN video_decision = "copy" OR audio_decision = "copy" THEN "copy" '
|
||||
'WHEN video_decision = "direct play" OR audio_decision = "direct play" THEN "direct play" END)'
|
||||
'WHEN video_decision = "transcode" OR audio_decision = "transcode" THEN "transcode" '
|
||||
'WHEN video_decision = "copy" OR audio_decision = "copy" THEN "copy" '
|
||||
'WHEN video_decision = "direct play" OR audio_decision = "direct play" THEN "direct play" END)'
|
||||
)
|
||||
|
||||
# Upgrade session_history_media_info table from earlier versions
|
||||
@@ -1241,7 +1235,6 @@ def dbcheck():
|
||||
'UPDATE session_history_media_info SET subtitle_codec = "" WHERE subtitle_codec IS NULL '
|
||||
)
|
||||
|
||||
|
||||
# Upgrade session_history_media_info table from earlier versions
|
||||
try:
|
||||
result = c_db.execute('SELECT stream_container FROM session_history_media_info '
|
||||
@@ -1586,24 +1579,26 @@ def dbcheck():
|
||||
if not result.fetchone():
|
||||
logger.debug(u"User 'Local' does not exist. Adding user.")
|
||||
c_db.execute('INSERT INTO users (user_id, username) VALUES (0, "Local")')
|
||||
|
||||
|
||||
# Create table indices
|
||||
c_db.execute(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS idx_tvmaze_lookup ON tvmaze_lookup (rating_key)'
|
||||
)
|
||||
c_db.execute(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS idx_themoviedb_lookup ON themoviedb_lookup (rating_key)'
|
||||
)
|
||||
)
|
||||
|
||||
conn_db.commit()
|
||||
c_db.close()
|
||||
|
||||
|
||||
def upgrade():
|
||||
if CONFIG.UPDATE_NOTIFIERS_DB:
|
||||
notifiers.upgrade_config_to_db()
|
||||
if CONFIG.UPDATE_LIBRARIES_DB_NOTIFY:
|
||||
libraries.update_libraries_db_notify()
|
||||
|
||||
|
||||
def shutdown(restart=False, update=False, checkout=False):
|
||||
cherrypy.engine.exit()
|
||||
SCHED.shutdown(wait=False)
|
||||
|
@@ -273,14 +273,25 @@ class ActivityHandler(object):
|
||||
# Monitor if the stream has reached the watch percentage for notifications
|
||||
# The only purpose of this is for notifications
|
||||
if this_state != 'buffering':
|
||||
progress_percent = helpers.get_percent(db_session['view_offset'], db_session['duration'])
|
||||
notify_states = notification_handler.get_notify_state(session=db_session)
|
||||
if (db_session['media_type'] == 'movie' and progress_percent >= plexpy.CONFIG.MOVIE_WATCHED_PERCENT or
|
||||
db_session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
||||
db_session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||
logger.debug(u"Tautulli ActivityHandler :: Session %s watched." % str(self.get_session_key()))
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_watched'})
|
||||
progress_percent = helpers.get_percent(self.timeline['viewOffset'], db_session['duration'])
|
||||
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||
'clip': plexpy.CONFIG.TV_WATCHED_PERCENT
|
||||
}
|
||||
|
||||
if progress_percent >= watched_percent.get(db_session['media_type'], 101):
|
||||
watched_notifiers = notification_handler.get_notify_state_enabled(
|
||||
session=db_session, notify_action='on_watched', notified=False)
|
||||
|
||||
if watched_notifiers:
|
||||
logger.debug(u"Tautulli ActivityHandler :: Session %s watched."
|
||||
% str(self.get_session_key()))
|
||||
|
||||
for d in watched_notifiers:
|
||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(),
|
||||
'notifier_id': d['notifier_id'],
|
||||
'notify_action': 'on_watched'})
|
||||
|
||||
else:
|
||||
# We don't have this session in our table yet, start a new one.
|
||||
|
@@ -264,12 +264,37 @@ def check_recently_added():
|
||||
plexpy.NOTIFY_QUEUE.put({'timeline_data': item.copy(), 'notify_action': 'on_created'})
|
||||
|
||||
|
||||
def check_server_response():
|
||||
logger.info(u"Tautulli Monitor :: Attempting to reconnect Plex server...")
|
||||
try:
|
||||
web_socket.start_thread()
|
||||
except:
|
||||
logger.warn(u"Websocket :: Unable to open connection.")
|
||||
def connect_server(log=True, startup=False):
|
||||
if plexpy.CONFIG.PMS_IS_CLOUD:
|
||||
if log:
|
||||
logger.info(u"Tautulli Monitor :: Checking for Plex Cloud server status...")
|
||||
|
||||
plex_tv = plextv.PlexTV()
|
||||
status = plex_tv.get_cloud_server_status()
|
||||
|
||||
if status is True:
|
||||
logger.info(u"Tautulli Monitor :: Plex Cloud server is active.")
|
||||
elif status is False:
|
||||
if log:
|
||||
logger.info(u"Tautulli Monitor :: Plex Cloud server is sleeping.")
|
||||
else:
|
||||
if log:
|
||||
logger.error(u"Tautulli Monitor :: Failed to retrieve Plex Cloud server status.")
|
||||
|
||||
if not status and startup:
|
||||
web_socket.on_disconnect()
|
||||
|
||||
else:
|
||||
status = True
|
||||
|
||||
if status:
|
||||
if log and not startup:
|
||||
logger.info(u"Tautulli Monitor :: Attempting to reconnect Plex server...")
|
||||
|
||||
try:
|
||||
web_socket.start_thread()
|
||||
except:
|
||||
logger.error(u"Websocket :: Unable to open connection.")
|
||||
|
||||
|
||||
def check_server_access():
|
||||
@@ -325,4 +350,4 @@ def check_server_updates():
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_pmsupdate', 'pms_download_info': download_info})
|
||||
|
||||
else:
|
||||
logger.info(u"Tautulli Monitor :: No PMS update available.")
|
||||
logger.info(u"Tautulli Monitor :: No PMS update available.")
|
||||
|
@@ -335,14 +335,14 @@ class API2:
|
||||
""" Restart Tautulli."""
|
||||
|
||||
plexpy.SIGNAL = 'restart'
|
||||
self._api_msg = 'Restarting plexpy'
|
||||
self._api_msg = 'Restarting Tautulli'
|
||||
self._api_result_type = 'success'
|
||||
|
||||
def update(self, **kwargs):
|
||||
""" Check for Tautulli updates on Github."""
|
||||
""" Update Tautulli."""
|
||||
|
||||
plexpy.SIGNAL = 'update'
|
||||
self._api_msg = 'Updating plexpy'
|
||||
self._api_msg = 'Updating Tautulli'
|
||||
self._api_result_type = 'success'
|
||||
|
||||
def refresh_libraries_list(self, **kwargs):
|
||||
|
@@ -188,7 +188,7 @@ class DataFactory(object):
|
||||
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||
'photo': 0,
|
||||
'clip': plexpy.CONFIG.MOVIE_WATCHED_PERCENT
|
||||
'clip': plexpy.CONFIG.TV_WATCHED_PERCENT
|
||||
}
|
||||
|
||||
rows = []
|
||||
@@ -612,7 +612,6 @@ class DataFactory(object):
|
||||
'total_plays': item['total_plays'],
|
||||
'total_duration': item['total_duration'],
|
||||
'last_play': item['last_watch'],
|
||||
'thumb': user_thumb,
|
||||
'user_thumb': user_thumb,
|
||||
'grandparent_thumb': '',
|
||||
'art': '',
|
||||
@@ -827,6 +826,9 @@ class DataFactory(object):
|
||||
if session.get_session_shared_libraries():
|
||||
library_cards = session.get_session_shared_libraries()
|
||||
|
||||
if 'first_run_wizard' in library_cards:
|
||||
return None
|
||||
|
||||
library_stats = []
|
||||
|
||||
try:
|
||||
|
@@ -39,11 +39,17 @@ class HTTPHandler(object):
|
||||
else:
|
||||
self.urls = urls
|
||||
|
||||
self.headers = {'X-Plex-Device-Name': 'Tautulli',
|
||||
'X-Plex-Product': 'Tautulli',
|
||||
'X-Plex-Version': plexpy.common.VERSION_NUMBER,
|
||||
'X-Plex-Platform': plexpy.common.PLATFORM,
|
||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_VERSION,
|
||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
||||
}
|
||||
|
||||
self.token = token
|
||||
if self.token:
|
||||
self.headers = {'X-Plex-Token': self.token}
|
||||
else:
|
||||
self.headers = {}
|
||||
self.headers['X-Plex-Token'] = self.token
|
||||
|
||||
self.timeout = timeout
|
||||
self.ssl_verify = ssl_verify
|
||||
@@ -79,9 +85,9 @@ class HTTPHandler(object):
|
||||
if uri:
|
||||
request_urls = [urljoin(url, self.uri) for url in self.urls]
|
||||
|
||||
if no_token and headers:
|
||||
self.headers = headers
|
||||
elif headers:
|
||||
if no_token:
|
||||
self.headers.pop('X-Plex-Token', None)
|
||||
if headers:
|
||||
self.headers.update(headers)
|
||||
|
||||
responses = []
|
||||
|
@@ -1006,13 +1006,13 @@ class Libraries(object):
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Libraries :: Unable to execute database query for undelete: %s." % e)
|
||||
|
||||
def delete_datatable_media_info_cache(self, section_id=None):
|
||||
def delete_media_info_cache(self, section_id=None):
|
||||
import os
|
||||
|
||||
try:
|
||||
if section_id.isdigit():
|
||||
[os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, f)) for f in os.listdir(plexpy.CONFIG.CACHE_DIR)
|
||||
if f.startswith('media_info-%s' % section_id) and f.endswith('.json')]
|
||||
[os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, f)) for f in os.listdir(plexpy.CONFIG.CACHE_DIR)
|
||||
if f.startswith('media_info_%s' % section_id) and f.endswith('.json')]
|
||||
|
||||
logger.debug(u"Tautulli Libraries :: Deleted media info table cache for section_id %s." % section_id)
|
||||
return 'Deleted media info table cache for library with id %s.' % section_id
|
||||
|
@@ -82,11 +82,6 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
|
||||
# Check if any notification agents have notifications enabled for the action
|
||||
notifiers_enabled = notifiers.get_notifiers(notify_action=notify_action)
|
||||
|
||||
# Check if the watched notifications has already been sent
|
||||
if stream_data and notify_action == 'on_watched':
|
||||
watched_notifiers = [d['notifier_id'] for d in get_notify_state(session=stream_data)]
|
||||
notifiers_enabled = [n for n in notifiers_enabled if n['id'] not in watched_notifiers]
|
||||
|
||||
if notifiers_enabled and not manual_trigger:
|
||||
# Check if notification conditions are satisfied
|
||||
conditions = notify_conditions(notify_action=notify_action,
|
||||
@@ -390,6 +385,28 @@ def get_notify_state(session):
|
||||
return notify_states
|
||||
|
||||
|
||||
def get_notify_state_enabled(session, notify_action, notified=True):
|
||||
if notified:
|
||||
timestamp_where = 'AND timestamp IS NOT NULL'
|
||||
else:
|
||||
timestamp_where = 'AND timestamp IS NULL'
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
result = monitor_db.select('SELECT id AS notifier_id, timestamp '
|
||||
'FROM notifiers '
|
||||
'LEFT OUTER JOIN ('
|
||||
'SELECT timestamp, notifier_id '
|
||||
'FROM notify_log '
|
||||
'WHERE session_key = ? '
|
||||
'AND rating_key = ? '
|
||||
'AND user_id = ? '
|
||||
'AND notify_action = ?) AS t ON notifiers.id = t.notifier_id '
|
||||
'WHERE %s = 1 %s' % (notify_action, timestamp_where),
|
||||
args=[session['session_key'], session['rating_key'], session['user_id'], notify_action])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def set_notify_state(notifier, notify_action, subject='', body='', script_args='', session=None):
|
||||
|
||||
if notifier and notify_action:
|
||||
|
@@ -144,14 +144,7 @@ class PlexTV(object):
|
||||
uri = '/users/sign_in.xml'
|
||||
base64string = base64.b64encode(('%s:%s' % (self.username, self.password)).encode('utf-8'))
|
||||
headers = {'Content-Type': 'application/xml; charset=utf-8',
|
||||
'X-Plex-Device-Name': 'Tautulli',
|
||||
'X-Plex-Product': 'Tautulli',
|
||||
'X-Plex-Version': plexpy.common.VERSION_NUMBER,
|
||||
'X-Plex-Platform': plexpy.common.PLATFORM,
|
||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_VERSION,
|
||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
||||
'Authorization': 'Basic %s' % base64string
|
||||
}
|
||||
'Authorization': 'Basic %s' % base64string}
|
||||
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
request_type='POST',
|
||||
@@ -318,6 +311,14 @@ class PlexTV(object):
|
||||
|
||||
return request
|
||||
|
||||
def cloud_server_status(self, output_format=''):
|
||||
uri = '/api/v2/cloud_server'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
return request
|
||||
|
||||
def get_full_users_list(self):
|
||||
friends_list = self.get_plextv_friends(output_format='xml')
|
||||
own_account = self.get_plextv_user_details(output_format='xml')
|
||||
@@ -645,7 +646,8 @@ class PlexTV(object):
|
||||
'ip': helpers.get_xml_attr(c, 'address'),
|
||||
'port': helpers.get_xml_attr(c, 'port'),
|
||||
'local': helpers.get_xml_attr(c, 'local'),
|
||||
'value': helpers.get_xml_attr(c, 'address')
|
||||
'value': helpers.get_xml_attr(c, 'address'),
|
||||
'is_cloud': is_cloud
|
||||
}
|
||||
clean_servers.append(server)
|
||||
|
||||
@@ -753,3 +755,21 @@ class PlexTV(object):
|
||||
devices_list.append(device)
|
||||
|
||||
return devices_list
|
||||
|
||||
def get_cloud_server_status(self):
|
||||
cloud_status = self.cloud_server_status(output_format='xml')
|
||||
|
||||
try:
|
||||
status_info = cloud_status.getElementsByTagName('info')
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_cloud_server_status: %s." % e)
|
||||
return False
|
||||
|
||||
for info in status_info:
|
||||
servers = info.getElementsByTagName('server')
|
||||
for s in servers:
|
||||
if helpers.get_xml_attr(s, 'address') == plexpy.CONFIG.PMS_IP:
|
||||
if helpers.get_xml_attr(info, 'running') == '1':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
@@ -61,7 +61,7 @@ class PmsConnect(object):
|
||||
self.url = plexpy.CONFIG.PMS_URL
|
||||
elif not self.url:
|
||||
self.url = 'http://{hostname}:{port}'.format(hostname=plexpy.CONFIG.PMS_IP,
|
||||
port=plexpy.CONFIG.PMS_PORT)
|
||||
port=plexpy.CONFIG.PMS_PORT)
|
||||
self.timeout = plexpy.CONFIG.PMS_TIMEOUT
|
||||
|
||||
if not self.token:
|
||||
@@ -533,7 +533,12 @@ class PmsConnect(object):
|
||||
metadata = {}
|
||||
|
||||
if cache_key:
|
||||
in_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % cache_key)
|
||||
in_file_folder = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata')
|
||||
in_file_path = os.path.join(in_file_folder, 'metadata-sessionKey-%s.json' % cache_key)
|
||||
|
||||
if not os.path.exists(in_file_folder):
|
||||
os.mkdir(in_file_folder)
|
||||
|
||||
try:
|
||||
with open(in_file_path, 'r') as inFile:
|
||||
metadata = json.load(inFile)
|
||||
@@ -1179,7 +1184,12 @@ class PmsConnect(object):
|
||||
if cache_key:
|
||||
metadata['_cache_time'] = int(time.time())
|
||||
|
||||
out_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % cache_key)
|
||||
out_file_folder = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata')
|
||||
out_file_path = os.path.join(out_file_folder, 'metadata-sessionKey-%s.json' % cache_key)
|
||||
|
||||
if not os.path.exists(out_file_folder):
|
||||
os.mkdir(out_file_folder)
|
||||
|
||||
try:
|
||||
with open(out_file_path, 'w') as outFile:
|
||||
json.dump(metadata, outFile)
|
||||
|
@@ -191,11 +191,7 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
|
||||
'user_thumb': common.DEFAULT_USER_THUMB,
|
||||
'ip_address': 'N/A',
|
||||
'machine_id': '',
|
||||
'platform': 'Platform',
|
||||
'player': 'Player',
|
||||
'quality_profile': 'Unknown',
|
||||
'bandwidth': '',
|
||||
'location': ''
|
||||
'player': 'Player'
|
||||
}
|
||||
|
||||
metadata_to_mask = {'media_index': '0',
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.18-beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.19-beta"
|
||||
|
@@ -33,14 +33,33 @@ ws_reconnect = False
|
||||
|
||||
|
||||
def start_thread():
|
||||
if plexpy.CONFIG.FIRST_RUN_COMPLETE:
|
||||
# Check for any existing sessions on start up
|
||||
activity_pinger.check_active_sessions(ws_request=True)
|
||||
# Start the websocket listener on it's own thread
|
||||
threading.Thread(target=run).start()
|
||||
# Check for any existing sessions on start up
|
||||
activity_pinger.check_active_sessions(ws_request=True)
|
||||
# Start the websocket listener on it's own thread
|
||||
threading.Thread(target=run).start()
|
||||
|
||||
|
||||
def on_connect():
|
||||
if plexpy.PLEX_SERVER_UP is None:
|
||||
plexpy.PLEX_SERVER_UP = True
|
||||
|
||||
if not plexpy.PLEX_SERVER_UP:
|
||||
logger.info(u"Tautulli WebSocket :: The Plex Media Server is back up.")
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intup'})
|
||||
plexpy.PLEX_SERVER_UP = True
|
||||
|
||||
plexpy.initialize_scheduler()
|
||||
|
||||
|
||||
def on_disconnect():
|
||||
if plexpy.PLEX_SERVER_UP is None:
|
||||
plexpy.PLEX_SERVER_UP = False
|
||||
|
||||
if plexpy.PLEX_SERVER_UP:
|
||||
logger.info(u"Tautulli WebSocket :: Unable to get a response from the server, Plex server is down.")
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'})
|
||||
plexpy.PLEX_SERVER_UP = False
|
||||
|
||||
activity_processor.ActivityProcessor().set_temp_stopped()
|
||||
plexpy.initialize_scheduler()
|
||||
|
||||
@@ -55,7 +74,7 @@ def run():
|
||||
|
||||
if plexpy.CONFIG.PMS_SSL and plexpy.CONFIG.PMS_URL[:5] == 'https':
|
||||
uri = plexpy.CONFIG.PMS_URL.replace('https://', 'wss://') + '/:/websockets/notifications'
|
||||
secure = ' secure'
|
||||
secure = 'secure '
|
||||
else:
|
||||
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
||||
plexpy.CONFIG.PMS_IP,
|
||||
@@ -72,34 +91,29 @@ def run():
|
||||
global ws_reconnect
|
||||
ws_reconnect = False
|
||||
reconnects = 0
|
||||
ws_exception = False
|
||||
|
||||
# Try an open the websocket connection
|
||||
while not plexpy.WS_CONNECTED and reconnects <= plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
||||
try:
|
||||
logger.info(u"Tautulli WebSocket :: Opening%s websocket, connection attempt %s." % (secure, str(reconnects + 1)))
|
||||
ws = create_connection(uri, header=header)
|
||||
reconnects = 0
|
||||
logger.info(u"Tautulli WebSocket :: Ready")
|
||||
plexpy.WS_CONNECTED = True
|
||||
while not plexpy.WS_CONNECTED and reconnects < plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
||||
if reconnects == 0:
|
||||
logger.info(u"Tautulli WebSocket :: Opening %swebsocket." % secure)
|
||||
|
||||
if not plexpy.PLEX_SERVER_UP:
|
||||
logger.info(u"Tautulli WebSocket :: The Plex Media Server is back up.")
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intup'})
|
||||
plexpy.PLEX_SERVER_UP = True
|
||||
reconnects += 1
|
||||
|
||||
plexpy.initialize_scheduler()
|
||||
|
||||
except IOError as e:
|
||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||
reconnects += 1
|
||||
# Sleep 5 between connection attempts
|
||||
if reconnects > 1:
|
||||
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
||||
|
||||
except (websocket.WebSocketException, Exception) as e:
|
||||
logger.info(u"Tautulli WebSocket :: Connection attempt %s." % str(reconnects))
|
||||
|
||||
try:
|
||||
ws = create_connection(uri, header=header)
|
||||
logger.info(u"Tautulli WebSocket :: Ready")
|
||||
plexpy.WS_CONNECTED = True
|
||||
except (websocket.WebSocketException, IOError, Exception) as e:
|
||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||
plexpy.WS_CONNECTED = False
|
||||
ws_exception = True
|
||||
break
|
||||
|
||||
if plexpy.WS_CONNECTED:
|
||||
on_connect()
|
||||
|
||||
while plexpy.WS_CONNECTED:
|
||||
try:
|
||||
@@ -109,20 +123,24 @@ def run():
|
||||
reconnects = 0
|
||||
|
||||
except websocket.WebSocketConnectionClosedException:
|
||||
if reconnects <= plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
||||
if reconnects == 0:
|
||||
logger.warn(u"Tautulli WebSocket :: Connection has closed.")
|
||||
|
||||
if not plexpy.CONFIG.PMS_IS_CLOUD and reconnects < plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
||||
reconnects += 1
|
||||
|
||||
# Sleep 5 between connection attempts
|
||||
if reconnects > 1:
|
||||
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
||||
|
||||
logger.warn(u"Tautulli WebSocket :: Connection has closed, reconnection attempt %s." % reconnects)
|
||||
logger.warn(u"Tautulli WebSocket :: Reconnection attempt %s." % str(reconnects))
|
||||
|
||||
try:
|
||||
ws = create_connection(uri, header=header)
|
||||
logger.info(u"Tautulli WebSocket :: Ready")
|
||||
plexpy.WS_CONNECTED = True
|
||||
except IOError as e:
|
||||
logger.info(u"Tautulli WebSocket :: %s." % e)
|
||||
except (websocket.WebSocketException, IOError, Exception) as e:
|
||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||
|
||||
else:
|
||||
ws.shutdown()
|
||||
@@ -131,8 +149,8 @@ def run():
|
||||
|
||||
except (websocket.WebSocketException, Exception) as e:
|
||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||
ws.shutdown()
|
||||
plexpy.WS_CONNECTED = False
|
||||
ws_exception = True
|
||||
break
|
||||
|
||||
# Check if we recieved a restart notification and close websocket connection cleanly
|
||||
@@ -143,13 +161,6 @@ def run():
|
||||
start_thread()
|
||||
|
||||
if not plexpy.WS_CONNECTED and not ws_reconnect:
|
||||
logger.error(u"Tautulli WebSocket :: Connection unavailable.")
|
||||
|
||||
if not ws_exception and plexpy.PLEX_SERVER_UP:
|
||||
logger.info(u"Tautulli WebSocket :: Unable to get an internal response from the server, Plex server is down.")
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'})
|
||||
plexpy.PLEX_SERVER_UP = False
|
||||
|
||||
on_disconnect()
|
||||
|
||||
logger.debug(u"Tautulli WebSocket :: Leaving thread.")
|
||||
|
@@ -28,6 +28,7 @@ from mako.lookup import TemplateLookup
|
||||
from mako import exceptions
|
||||
|
||||
import plexpy
|
||||
import activity_pinger
|
||||
import common
|
||||
import config
|
||||
import database
|
||||
@@ -98,10 +99,11 @@ class WebInterface(object):
|
||||
config = {
|
||||
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
"pms_ip": plexpy.CONFIG.PMS_IP,
|
||||
"pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE),
|
||||
"pms_port": plexpy.CONFIG.PMS_PORT,
|
||||
"pms_is_remote": plexpy.CONFIG.PMS_IS_REMOTE,
|
||||
"pms_ssl": plexpy.CONFIG.PMS_SSL,
|
||||
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
|
||||
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
||||
"pms_ssl": checked(plexpy.CONFIG.PMS_SSL),
|
||||
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
||||
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL
|
||||
}
|
||||
@@ -117,7 +119,7 @@ class WebInterface(object):
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi("get_server_list")
|
||||
def discover(self, token=None, include_cloud=True, all_servers=False, **kwargs):
|
||||
def discover(self, token=None, include_cloud=True, all_servers=True, **kwargs):
|
||||
""" Get all your servers that are published to Plex.tv.
|
||||
|
||||
```
|
||||
@@ -148,7 +150,7 @@ class WebInterface(object):
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
include_cloud = not (include_cloud == 'false')
|
||||
all_servers = (all_servers == 'true')
|
||||
all_servers = not (all_servers == 'false')
|
||||
|
||||
plex_tv = plextv.PlexTV()
|
||||
servers_list = plex_tv.discover(include_cloud=include_cloud,
|
||||
@@ -456,12 +458,17 @@ class WebInterface(object):
|
||||
logger.warn(u"Unable to retrieve data for get_library_sections.")
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
def refresh_libraries_list(self, **kwargs):
|
||||
""" Refresh the libraries list on it's own thread. """
|
||||
threading.Thread(target=libraries.refresh_libraries).start()
|
||||
""" Manually refresh the libraries list. """
|
||||
logger.info(u"Manual libraries list refresh requested.")
|
||||
return True
|
||||
result = libraries.refresh_libraries()
|
||||
|
||||
if result:
|
||||
return {'result': 'success', 'message': 'Libraries list refreshed.'}
|
||||
else:
|
||||
return {'result': 'error', 'message': 'Unable to refresh libraries list.'}
|
||||
|
||||
@cherrypy.expose
|
||||
@requireAuth()
|
||||
@@ -629,6 +636,7 @@ class WebInterface(object):
|
||||
start (int): Row to start from, 0
|
||||
length (int): Number of items to return, 25
|
||||
search (str): A string to search for, "Thrones"
|
||||
refresh (str): "true" to refresh the media info table
|
||||
|
||||
Returns:
|
||||
json:
|
||||
@@ -952,7 +960,7 @@ class WebInterface(object):
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi()
|
||||
def delete_datatable_media_info_cache(self, section_id, **kwargs):
|
||||
def delete_media_info_cache(self, section_id, **kwargs):
|
||||
""" Delete the media info table cache for a specific library.
|
||||
|
||||
```
|
||||
@@ -972,7 +980,7 @@ class WebInterface(object):
|
||||
if section_id not in section_ids:
|
||||
if section_id:
|
||||
library_data = libraries.Libraries()
|
||||
delete_row = library_data.delete_datatable_media_info_cache(section_id=section_id)
|
||||
delete_row = library_data.delete_media_info_cache(section_id=section_id)
|
||||
|
||||
if delete_row:
|
||||
return {'message': delete_row}
|
||||
@@ -1076,12 +1084,17 @@ class WebInterface(object):
|
||||
return user_list
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
def refresh_users_list(self, **kwargs):
|
||||
""" Refresh the users list on it's own thread. """
|
||||
threading.Thread(target=users.refresh_users).start()
|
||||
""" Manually refresh the users list. """
|
||||
logger.info(u"Manual users list refresh requested.")
|
||||
return True
|
||||
result = users.refresh_users()
|
||||
|
||||
if result:
|
||||
return {'result': 'success', 'message': 'Users list refreshed.'}
|
||||
else:
|
||||
return {'result': 'error', 'message': 'Unable to refresh users list.'}
|
||||
|
||||
@cherrypy.expose
|
||||
@requireAuth()
|
||||
@@ -2562,8 +2575,8 @@ class WebInterface(object):
|
||||
"pms_logs_folder": plexpy.CONFIG.PMS_LOGS_FOLDER,
|
||||
"pms_port": plexpy.CONFIG.PMS_PORT,
|
||||
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
||||
"pms_ssl": checked(plexpy.CONFIG.PMS_SSL),
|
||||
"pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE),
|
||||
"pms_ssl": plexpy.CONFIG.PMS_SSL,
|
||||
"pms_is_remote": plexpy.CONFIG.PMS_IS_REMOTE,
|
||||
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
|
||||
"pms_url_manual": checked(plexpy.CONFIG.PMS_URL_MANUAL),
|
||||
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
||||
@@ -2623,7 +2636,7 @@ class WebInterface(object):
|
||||
checked_configs = [
|
||||
"launch_browser", "enable_https", "https_create_cert", "api_enabled", "freeze_db", "check_github",
|
||||
"grouping_global_history", "grouping_user_history", "grouping_charts", "group_history_tables",
|
||||
"pms_ssl", "pms_is_remote", "pms_url_manual", "week_start_monday",
|
||||
"pms_url_manual", "week_start_monday",
|
||||
"refresh_libraries_on_startup", "refresh_users_on_startup",
|
||||
"notify_consecutive", "notify_upload_posters", "notify_recently_added_upgrade",
|
||||
"notify_group_recently_added_grandparent", "notify_group_recently_added_parent",
|
||||
@@ -2746,7 +2759,7 @@ class WebInterface(object):
|
||||
|
||||
# If first run, start websocket
|
||||
if first_run:
|
||||
web_socket.start_thread()
|
||||
activity_pinger.connect_server(log=True, startup=True)
|
||||
|
||||
# Reconfigure scheduler if intervals changed
|
||||
if reschedule:
|
||||
@@ -3505,10 +3518,53 @@ class WebInterface(object):
|
||||
return apikey
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
def checkGithub(self, **kwargs):
|
||||
@addtoapi()
|
||||
def update_check(self, **kwargs):
|
||||
""" Check for Tautulli updates.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
json
|
||||
{"result": "success",
|
||||
"update": true,
|
||||
"message": "An update for Tautulli is available."
|
||||
}
|
||||
```
|
||||
"""
|
||||
versioncheck.checkGithub()
|
||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
|
||||
|
||||
if not plexpy.CURRENT_VERSION:
|
||||
return {'result': 'error',
|
||||
'message': 'You are running an unknown version of Tautulli.',
|
||||
'update': None}
|
||||
|
||||
elif plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and \
|
||||
plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win':
|
||||
return {'result': 'success',
|
||||
'update': True,
|
||||
'message': 'An update for Tautulli is available.',
|
||||
'latest_version': plexpy.LATEST_VERSION,
|
||||
'commits_behind': plexpy.COMMITS_BEHIND,
|
||||
'compare_url': helpers.anon_url(
|
||||
'https://github.com/%s/%s/compare/%s...%s'
|
||||
% (plexpy.CONFIG.GIT_USER,
|
||||
plexpy.CONFIG.GIT_REPO,
|
||||
plexpy.CURRENT_VERSION,
|
||||
plexpy.LATEST_VERSION))
|
||||
}
|
||||
|
||||
else:
|
||||
return {'result': 'success',
|
||||
'update': False,
|
||||
'message': 'Tautulli is up to date.'}
|
||||
|
||||
@cherrypy.expose
|
||||
@requireAuth(member_of("admin"))
|
||||
|
Reference in New Issue
Block a user