Compare commits
64 Commits
v2.1.24-be
...
v2.1.29-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7d11e1de2d | ||
![]() |
36aa4a6be3 | ||
![]() |
24ed63e07c | ||
![]() |
f41ed9953a | ||
![]() |
6970231687 | ||
![]() |
ea036aa354 | ||
![]() |
d0a7c2f92c | ||
![]() |
f07acd839b | ||
![]() |
c1fd798fe9 | ||
![]() |
2f8d2f23fe | ||
![]() |
b65a30263e | ||
![]() |
766e33df0e | ||
![]() |
68df0f07c8 | ||
![]() |
819829554b | ||
![]() |
18a38b16b1 | ||
![]() |
a9169d2b53 | ||
![]() |
76b9b3e474 | ||
![]() |
00405f0b18 | ||
![]() |
9dfeccdaed | ||
![]() |
b6d044fe8f | ||
![]() |
e949b1486e | ||
![]() |
8e1b6efc51 | ||
![]() |
00012ffe09 | ||
![]() |
bcf6b4de77 | ||
![]() |
b1516e9963 | ||
![]() |
231de3a7a5 | ||
![]() |
b611ea659e | ||
![]() |
6e9f299c19 | ||
![]() |
61fac10079 | ||
![]() |
536e8add17 | ||
![]() |
cb81bcac57 | ||
![]() |
5dd7806c0e | ||
![]() |
2a707fc512 | ||
![]() |
469e54a22c | ||
![]() |
f6f5df3d1e | ||
![]() |
ae0960d2e2 | ||
![]() |
a646cc36a1 | ||
![]() |
b243ac5f5c | ||
![]() |
bca7744bc5 | ||
![]() |
2fc826c88f | ||
![]() |
6397b1e5a7 | ||
![]() |
85b9a47a0d | ||
![]() |
5749ab7c92 | ||
![]() |
dcb56cfd20 | ||
![]() |
90849f9196 | ||
![]() |
5b77cab575 | ||
![]() |
6a21d7690a | ||
![]() |
037e983350 | ||
![]() |
aa023f0166 | ||
![]() |
571b5461c0 | ||
![]() |
a749b71f7f | ||
![]() |
ac259214f7 | ||
![]() |
e11803685c | ||
![]() |
e4c3601312 | ||
![]() |
56a91de2c4 | ||
![]() |
e2d217a981 | ||
![]() |
b484f27724 | ||
![]() |
eb04a2e579 | ||
![]() |
c66d8ecd5f | ||
![]() |
79b5f3c36f | ||
![]() |
4a78424b75 | ||
![]() |
4f78d0c98a | ||
![]() |
91b84b4437 | ||
![]() |
8a9b3dc782 |
1
API.md
1
API.md
@@ -893,6 +893,7 @@ Returns:
|
||||
json:
|
||||
{"child_count": null,
|
||||
"count": 887,
|
||||
"deleted_section": 0,
|
||||
"do_notify": 1,
|
||||
"do_notify_created": 1,
|
||||
"keep_history": 1,
|
||||
|
66
CHANGELOG.md
66
CHANGELOG.md
@@ -1,5 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
## v2.1.29-beta (2019-04-14)
|
||||
|
||||
* Monitoring:
|
||||
* Change: "Required Bandwidth" changed to "Reserved Bandwidth" in order to match the Plex dashboard.
|
||||
* Notifications:
|
||||
* New: Added prefix and suffix notification text modifiers. See the "Notification Text Modifiers" help modal for details.
|
||||
* UI:
|
||||
* New: Added "Undelete" button to the edit library and edit user modals.
|
||||
* Fix: User IP address history table showing incorrect "Last Seen" values.
|
||||
* API:
|
||||
* Fix: Search API only returning 3 results.
|
||||
* Fix: Terminate stream API failing when both session_key and session_id were provided.
|
||||
* Change: Improved API response HTTP status codes and error messages.
|
||||
|
||||
|
||||
## v2.1.28 (2019-03-10)
|
||||
|
||||
* Monitoring:
|
||||
* New: Added secure/insecure connection icon on the activity cards. Requires Plex Media Server v1.15+.
|
||||
* Other:
|
||||
* Change: Improved mass deleting of all images from Cloudinary. Requires all previous images on Cloudinary to be manually tagged with "tautulli". New uploads are automatically tagged.
|
||||
|
||||
|
||||
## v2.1.27-beta (2019-03-03)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Error when playing synced optimized versions.
|
||||
* Change: Show message to complete the setup wizard instead of error communicating with server message.
|
||||
* Change: URL changed on Plex.tv for Plex Media Server beta updates.
|
||||
* Notifications:
|
||||
* New: Show the media type exclusion tags in the text preview modal.
|
||||
* Fix: Unicode error in the Email notification failed response message.
|
||||
* Fix: Error when a notification agent response is missing the "Content-Type" header.
|
||||
* UI:
|
||||
* Fix: Usernames were not being sanitized in dropdown selectors.
|
||||
* Change: Different display of "All" recently added items on the homepage due to change in the Plex Media Server v1.15+ API.
|
||||
* API:
|
||||
* New: Added current Tautulli version to update_check API response.
|
||||
* Change: API no longer returns sanitized HTML response data.
|
||||
* Other:
|
||||
* New: Added auto-restart to systemd init script.
|
||||
* Fix: Patreon donation URL.
|
||||
* Remove: Crypto donation options.
|
||||
|
||||
|
||||
## v2.1.26 (2018-12-01)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Resume event not being triggered after buffering.
|
||||
* Notifications:
|
||||
* New: Added user email as a notification parameter.
|
||||
* Graphs:
|
||||
* Fix: History model showing no results for stream info graph.
|
||||
* API:
|
||||
* Fix: API returning error when missing a cmd.
|
||||
|
||||
|
||||
## v2.1.25 (2018-11-03)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Audio and video codec showing up as * on the activity cards.
|
||||
* New: Poster and background image on the activity cards for live TV.
|
||||
* UI:
|
||||
* Fix: Alert message for invalid Tautulli Public Domain setting.
|
||||
|
||||
|
||||
## v2.1.24-beta (2018-10-29)
|
||||
|
||||
* Monitoring:
|
||||
|
@@ -209,7 +209,7 @@ ${next.modalIncludes()}
|
||||
</div>
|
||||
</div>
|
||||
% else:
|
||||
<div id="donate-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="crypto-donate-modal">
|
||||
<div id="donate-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="donate-modal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -230,17 +230,13 @@ ${next.modalIncludes()}
|
||||
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
|
||||
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
|
||||
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
|
||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoin" data-name="Bitcoin" data-address="3FdfJAyNWU15Sf11U9FTgPHuP1hPz32eEN">Bitcoin</a></li>
|
||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoincash" data-name="Bitcoin Cash" data-address="1H2atabxAQGaFAWYQEiLkXKSnK9CZZvt2n">Bitcoin Cash</a></li>
|
||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="ethereum" data-name="Ethereum" data-address="0x77ae4c2b8de1a1ccfa93553db39971da58c873d3">Ethereum</a></li>
|
||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="litecoin" data-name="Litecoin" data-address="LWpPmUqQYHBhMV83XSCsHzPmKLhJt6r57J">Litecoin</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="patreon-donation" style="text-align: center">
|
||||
<p>
|
||||
Click the button below to continue to Patreon.
|
||||
</p>
|
||||
<a href="${anon_url('https://www.patreon.com/bePatron?u=10078609')}" target="_blank">
|
||||
<a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank">
|
||||
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
|
||||
</a>
|
||||
</div>
|
||||
@@ -252,12 +248,6 @@ ${next.modalIncludes()}
|
||||
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
|
||||
</a>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="crypto-donation">
|
||||
<label>QR Code</label>
|
||||
<pre id="crypto_qr_code" style="text-align: center"></pre>
|
||||
<label><span id="crypto_type_label"></span> Address</label>
|
||||
<pre id="crypto_address" style="text-align: center"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -293,7 +283,6 @@ ${next.modalIncludes()}
|
||||
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
||||
<script src="${http_root}js/platform.min.js"></script>
|
||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
||||
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
|
||||
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
||||
<script src="${http_root}js/ajaxNotifications.js"></script>
|
||||
@@ -365,16 +354,6 @@ ${next.modalIncludes()}
|
||||
checkUpdate(function () { $('#nav-update').html('<i class="fa fa-fw fa-arrow-alt-circle-up"></i> Check for Updates'); });
|
||||
});
|
||||
|
||||
$('#donation_type a.crypto-donation').on('shown.bs.tab', function () {
|
||||
var crypto_coin = $(this).data('coin');
|
||||
var crypto_name = $(this).data('name');
|
||||
var crypto_address = $(this).data('address')
|
||||
$('#crypto_qr_code').empty().qrcode({
|
||||
text: crypto_coin + ":" + crypto_address
|
||||
});
|
||||
$('#crypto_type_label').html(crypto_name);
|
||||
$('#crypto_address').html(crypto_address);
|
||||
});
|
||||
% endif
|
||||
|
||||
$('.dropdown-toggle').click(function (e) {
|
||||
|
@@ -4204,8 +4204,13 @@ a[data-tab-destination] {
|
||||
background: #cc7b19;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
padding-top: 2px;
|
||||
padding: 2px 10px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.help-block li {
|
||||
margin-top: 0;
|
||||
color: #737373;
|
||||
}
|
@@ -80,7 +80,9 @@ DOCUMENTATION :: END
|
||||
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">
|
||||
<%
|
||||
if data['channel_stream'] == 0:
|
||||
if data['live'] == 1:
|
||||
background_url = 'images/art-live.png'
|
||||
elif data['channel_stream'] == 0:
|
||||
background_url = 'pms_image_proxy?img=' + data['art'] + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true'
|
||||
else:
|
||||
if (data['art'] and data['art'].startswith('http')) or (data['thumb'] and data['thumb'].startswith('http')):
|
||||
@@ -93,7 +95,9 @@ DOCUMENTATION :: END
|
||||
% if data['media_type'] == 'track':
|
||||
<div id="poster-${sk}-bg" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&opacity=60&background=282828&blur=3&fallback=cover&refresh=true);"></div>
|
||||
% endif
|
||||
% if data['channel_stream'] == 0:
|
||||
% if data['live'] == 1:
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/poster-live.png);"></div>
|
||||
% elif data['channel_stream'] == 0:
|
||||
% if data['media_type'] == 'movie':
|
||||
<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>
|
||||
@@ -113,7 +117,7 @@ DOCUMENTATION :: END
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb'] or data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||
% endif
|
||||
% else:
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/art.png);"></div>
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/poster.png);"></div>
|
||||
% endif
|
||||
% else:
|
||||
% if data['channel_icon'].startswith('http'):
|
||||
@@ -279,10 +283,17 @@ DOCUMENTATION :: END
|
||||
<li class="dashboard-activity-info-item">
|
||||
<div class="sub-heading">Location</div>
|
||||
<div class="sub-value time-right">
|
||||
% if data['secure'] is not None:
|
||||
% if data['secure']:
|
||||
<span data-toggle="tooltip" title="Secure Connection"><i class="fa fa-lock"></i></span>
|
||||
% else:
|
||||
<span data-toggle="tooltip" title="Insecure Connection"><i class="fa fa-unlock"></i></span>
|
||||
% endif
|
||||
% endif
|
||||
<span id="location-${sk}">${data['location'].upper()}</span>:
|
||||
% if data['ip_address'] != 'N/A':
|
||||
<span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
||||
% if data['relay']:
|
||||
% if data['relayed']:
|
||||
<span data-toggle="tooltip" title="Plex Relay"><i class="fa fa-exclamation-circle"></i></span>
|
||||
% else:
|
||||
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
|
||||
@@ -313,7 +324,7 @@ DOCUMENTATION :: END
|
||||
bw = str(bw) + ' kbps'
|
||||
%>
|
||||
<span id="stream-bandwidth-${sk}">${bw}</span>
|
||||
<span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span>
|
||||
<span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate (Reserved Bandwidth)"><i class="fa fa-info-circle"></i></span>
|
||||
% elif data['synced_version'] == 1 or data['channel_stream'] == 1:
|
||||
<span id="stream-bandwidth-${sk}">None</span>
|
||||
% else:
|
||||
|
@@ -21,6 +21,7 @@ parent_count Returns the parent item count for the library.
|
||||
child_count Returns the child item count for the library.
|
||||
do_notify Returns bool value for whether to send notifications for the library.
|
||||
keep_history Returns bool value for whether to keep history for the library.
|
||||
deleted_section Returns bool value for whether the library is marked as deleted.
|
||||
|
||||
DOCUMENTATION :: END
|
||||
</%doc>
|
||||
@@ -59,6 +60,12 @@ DOCUMENTATION :: END
|
||||
<p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this library. This is permanent!</p>
|
||||
</div>
|
||||
% endif
|
||||
% if data['deleted_section']:
|
||||
<div class="form-group">
|
||||
<button class="btn btn-bright" id="undelete-library">Undelete</button>
|
||||
<p class="help-block">Click to re-add the library to the Tautulli libraries list.</p>
|
||||
</div>
|
||||
% endif
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -100,6 +107,12 @@ DOCUMENTATION :: END
|
||||
confirmAjaxCall(url, msg, { section_id: '${data["section_id"]}' }, null, function () { location.reload(); });
|
||||
});
|
||||
|
||||
$('#undelete-library').click(function () {
|
||||
var msg = 'Are you sure you want to undelete this user?';
|
||||
var url = 'undelete_library';
|
||||
confirmAjaxCall(url, msg, { section_id: '${data["section_id"]}' }, null, function () { location.reload(); });
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
// Move #confirm-modal to parent container
|
||||
if (!($('#edit-library-modal').next().is('#confirm-modal-purge'))) {
|
||||
|
@@ -21,6 +21,7 @@ is_restricted Returns bool value for whether the user account is restricte
|
||||
do_notify Returns bool value for whether to send notifications for the user.
|
||||
keep_history Returns bool value for whether to keep history for the user.
|
||||
allow_guest Returns bool value for whether to allow guest access for the user.
|
||||
deleted_user Returns bool value for whether the user is marked as deleted.
|
||||
|
||||
DOCUMENTATION :: END
|
||||
</%doc>
|
||||
@@ -74,6 +75,12 @@ DOCUMENTATION :: END
|
||||
<p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this user. This is permanent!</p>
|
||||
</div>
|
||||
% endif
|
||||
% if data['deleted_user']:
|
||||
<div class="form-group">
|
||||
<button class="btn btn-bright" id="undelete-user">Undelete</button>
|
||||
<p class="help-block">Click to re-add the user to the Tautulli users list.</p>
|
||||
</div>
|
||||
% endif
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -122,6 +129,12 @@ DOCUMENTATION :: END
|
||||
confirmAjaxCall(url, msg, { user_id: '${data["user_id"]}' }, null, function () { location.reload(); });
|
||||
});
|
||||
|
||||
$('#undelete-user').click(function () {
|
||||
var msg = 'Are you sure you want to undelete this user?';
|
||||
var url = 'undelete_user';
|
||||
confirmAjaxCall(url, msg, { user_id: '${data["user_id"]}' }, null, function () { location.reload(); });
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
// Move #confirm-modal-purge to parent container
|
||||
if (!($('#edit-user-modal').next().is('#confirm-modal-purge'))) {
|
||||
|
@@ -54,7 +54,7 @@
|
||||
json_data: JSON.stringify(d),
|
||||
user_id: "${data['user_id']}",
|
||||
start_date: "${data['start_date']}",
|
||||
media_type: "${data.get('media_type')}",
|
||||
media_type: "${data.get('media_type') or 'all'}",
|
||||
transcode_decision: "${data.get('transcode_decision')}"
|
||||
};
|
||||
}
|
||||
|
BIN
data/interfaces/default/images/art-live.png
Normal file
BIN
data/interfaces/default/images/art-live.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
data/interfaces/default/images/poster-live.png
Normal file
BIN
data/interfaces/default/images/poster-live.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
@@ -15,9 +15,9 @@
|
||||
<h3><span id="sessions-xml">Activity</span>
|
||||
<small>
|
||||
<span id="currentActivityHeader" style="display: none;">
|
||||
Streams: <span id="currentActivityHeader-streams"></span> |
|
||||
Sessions: <span id="currentActivityHeader-streams"></span> |
|
||||
Bandwidth: <span id="currentActivityHeader-bandwidth"></span>
|
||||
<span id="currentActivityHeader-bandwidth-tooltip" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span>
|
||||
<span id="currentActivityHeader-bandwidth-tooltip" data-toggle="tooltip" title="Streaming Brain Estimate (Reserved Bandwidth)"><i class="fa fa-info-circle"></i></span>
|
||||
</span>
|
||||
</small>
|
||||
</h3>
|
||||
@@ -27,6 +27,8 @@
|
||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
|
||||
% elif config['pms_is_cloud']:
|
||||
<div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div>
|
||||
% elif not config['first_run_complete']:
|
||||
<div id="dashboard-no-activity" class="text-muted">The Tautulli setup wizard has not been completed. Please click <a href="welcome">here</a> to go to the setup wizard.</div>
|
||||
% else:
|
||||
<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.
|
||||
% if _session['user_group'] == 'admin':
|
||||
|
@@ -10,17 +10,33 @@ if (typeof platform !== 'undefined') {
|
||||
}
|
||||
|
||||
if (['IE', 'Microsoft Edge', 'IE Mobile'].indexOf(p.name) > -1) {
|
||||
$('body').prepend('<div id="browser-warning"><i class="fa fa-exclamation-circle"></i> ' +
|
||||
'Tautulli does not support Internet Explorer or Microsoft Edge! ' +
|
||||
'Please use a different browser such as Chrome or Firefox.</div>');
|
||||
var offset = $('#browser-warning').height();
|
||||
var navbar = $('.navbar-fixed-top');
|
||||
if (navbar.length) {
|
||||
navbar.offset({top: navbar.offset().top + offset});
|
||||
}
|
||||
var container = $('.body-container');
|
||||
if (container.length) {
|
||||
container.offset({top: container.offset().top + offset});
|
||||
if (!getCookie('browserDismiss')) {
|
||||
var $browser_warning = $('<div id="browser-warning">' +
|
||||
'<i class="fa fa-exclamation-circle"></i> ' +
|
||||
'Tautulli does not support Internet Explorer or Microsoft Edge! ' +
|
||||
'Please use a different browser such as Chrome or Firefox.' +
|
||||
'<button type="button" class="close"><i class="fa fa-remove"></i></button>' +
|
||||
'</div>');
|
||||
$('body').prepend($browser_warning);
|
||||
var offset = $browser_warning.height();
|
||||
warningOffset(offset);
|
||||
|
||||
$browser_warning.one('click', 'button.close', function () {
|
||||
$browser_warning.remove();
|
||||
warningOffset(-offset);
|
||||
setCookie('browserDismiss', 'true', 7);
|
||||
});
|
||||
|
||||
function warningOffset(offset) {
|
||||
var navbar = $('.navbar-fixed-top');
|
||||
if (navbar.length) {
|
||||
navbar.offset({top: navbar.offset().top + offset});
|
||||
}
|
||||
var container = $('.body-container');
|
||||
if (container.length) {
|
||||
container.offset({top: container.offset().top + offset});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,16 +560,18 @@ function uuidv4() {
|
||||
});
|
||||
}
|
||||
|
||||
var x_plex_headers = {
|
||||
'Accept': 'application/json',
|
||||
'X-Plex-Product': 'Tautulli',
|
||||
'X-Plex-Version': 'Plex OAuth',
|
||||
'X-Plex-Client-Identifier': getLocalStorage('Tautulli_ClientID', uuidv4(), false),
|
||||
'X-Plex-Platform': p.name,
|
||||
'X-Plex-Platform-Version': p.version,
|
||||
'X-Plex-Device': p.os,
|
||||
'X-Plex-Device-Name': p.name
|
||||
};
|
||||
function getPlexHeaders() {
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'X-Plex-Product': 'Tautulli',
|
||||
'X-Plex-Version': 'Plex OAuth',
|
||||
'X-Plex-Client-Identifier': getLocalStorage('Tautulli_ClientID', uuidv4(), false),
|
||||
'X-Plex-Platform': p.name,
|
||||
'X-Plex-Platform-Version': p.version,
|
||||
'X-Plex-Device': p.os,
|
||||
'X-Plex-Device-Name': p.name
|
||||
};
|
||||
}
|
||||
|
||||
var plex_oauth_window = null;
|
||||
const plex_oauth_loader = '<style>' +
|
||||
@@ -604,6 +622,7 @@ function closePlexOAuthWindow() {
|
||||
}
|
||||
|
||||
getPlexOAuthPin = function () {
|
||||
var x_plex_headers = getPlexHeaders();
|
||||
var deferred = $.Deferred();
|
||||
|
||||
$.ajax({
|
||||
@@ -632,6 +651,7 @@ function PlexOAuth(success, error, pre) {
|
||||
$(plex_oauth_window.document.body).html(plex_oauth_loader);
|
||||
|
||||
getPlexOAuthPin().then(function (data) {
|
||||
var x_plex_headers = getPlexHeaders();
|
||||
const pin = data.pin;
|
||||
const code = data.code;
|
||||
|
||||
|
@@ -150,6 +150,7 @@
|
||||
token: token,
|
||||
remember_me: remember_me
|
||||
};
|
||||
var x_plex_headers = getPlexHeaders();
|
||||
data = $.extend(data, x_plex_headers);
|
||||
|
||||
$.ajax({
|
||||
|
@@ -148,7 +148,7 @@
|
||||
<div class="col-md-12">
|
||||
<label>Notification Triggers</label>
|
||||
<p class="help-block">
|
||||
Select items that will trigger a notification for this ${notifier['agent_label']} notifiation agent.
|
||||
Select items that will trigger a notification for this ${notifier['agent_label']} notification agent.
|
||||
</p>
|
||||
% for action in available_notification_actions:
|
||||
<div class="checkbox">
|
||||
|
@@ -8,7 +8,12 @@
|
||||
% if text:
|
||||
% for item in text:
|
||||
<div style="padding-bottom: 10px;">
|
||||
<h4>${item['media_type'].capitalize()}</h4>
|
||||
<h4>
|
||||
${item['media_type'].capitalize()}
|
||||
% if item['media_type'] != 'server':
|
||||
<span class="inline-pre"><${item['media_type']}></${item['media_type']}></span> tags
|
||||
% endif
|
||||
</h4>
|
||||
% if agent != 'webhook':
|
||||
<pre>${item['subject']}</pre>
|
||||
% endif
|
||||
|
@@ -472,7 +472,7 @@
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" id="http_base_url" name="http_base_url" value="${config['http_base_url']}" placeholder="http://mydomain.com" data-parsley-trigger="change" data-parsley-pattern="^https?:\/\/\S+$" data-parsley-errors-container="#http_base_url_error" data-parsley-error-message="Invalid URL">
|
||||
</div>
|
||||
<div id=http_base_url_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||
<div id="http_base_url_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
Set your public Tautulli domain for self-hosted notification images and newsletters. (e.g. http://mydomain.com)
|
||||
@@ -1532,7 +1532,7 @@
|
||||
<div style="padding-bottom: 10px;">
|
||||
<p class="help-block">
|
||||
All text inside <span class="inline-pre"><show></show></span>/<span class="inline-pre"><season></season></span>/<span class="inline-pre"><episode></episode></span>
|
||||
tags will only be sent when the media type is show/season/episode.
|
||||
tags will only be sent when the media type is show, season, or episode, respectively.
|
||||
</p>
|
||||
<p><strong style="color: #fff;">Example:</strong></p>
|
||||
<pre>{show_name}<season> - Season {season_num}</season><episode> - S{season_num}E{episode_num} - {episode_name}</episode> was recently added to Plex.</pre>
|
||||
@@ -1543,7 +1543,7 @@
|
||||
<div>
|
||||
<p class="help-block">
|
||||
All text inside <span class="inline-pre"><artist></artist></span>/<span class="inline-pre"><album></album></span>/<span class="inline-pre"><track></track></span>
|
||||
tags will only be sent when the media type is artist/album/track.
|
||||
tags will only be sent when the media type is artist, album, or track, respectively.
|
||||
</p>
|
||||
<p><strong style="color: #fff;">Example:</strong></p>
|
||||
<pre>{artist_name}<album> - {album_name}</album><track> - {album_name} - {track_name}</track> was recently added to Plex.</pre>
|
||||
@@ -1588,7 +1588,7 @@
|
||||
<div>
|
||||
<h4>List Slicing</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div style="padding-bottom: 10px;">
|
||||
<p class="help-block">
|
||||
Notification parameters which have a list of items can be sliced with a slice formatter <span class="inline-pre">:[X:Y]</span> to limit the number of items.
|
||||
Note: the first item in the list is numbered <span class="inline-pre">0</span>.
|
||||
@@ -1599,6 +1599,41 @@
|
||||
{actors:[2:]} --> Only the 3rd to last actors (Actors: 2, 3, 4, ...)
|
||||
{actors:[1:5]} --> Only the 2nd to 5th actors (Actors: 1, 2, 3, 4)</pre>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Prefix and Suffix</h4>
|
||||
</div>
|
||||
<div style="padding-bottom: 10px;">
|
||||
<p class="help-block">
|
||||
A prefix or a suffix can be added to the notification parameters using <span class="inline-pre">Prefix<</span> and <span class="inline-pre">>Suffix</span>.
|
||||
If the notification parameter is unavailable, the prefix or suffix will not be displayed.
|
||||
</p>
|
||||
<p><strong style="color: #fff;">Example:</strong></p>
|
||||
<pre>{rating} --> 8.9
|
||||
{Rating: <rating} --> Rating: 8.9
|
||||
{rating>/10} --> 8.9/10
|
||||
{Rating: <rating>/10} --> Rating: 8.9/10</pre>
|
||||
<p><strong style="color: #fff;">Example with unavailable parameter:</strong></p>
|
||||
<pre>{rating} -->
|
||||
Rating: {rating}/10 --> Rating: /10
|
||||
{Rating: <rating>/10} --> </pre>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Combined</h4>
|
||||
</div>
|
||||
<div>
|
||||
<p class="help-block">
|
||||
If combining multiple notification text modifiers, the order of the modifiers must be:
|
||||
</p>
|
||||
<ol class="help-block">
|
||||
<li>Prefix</li>
|
||||
<li>Parameter</li>
|
||||
<li>Case Modifier</li>
|
||||
<li>List Slicing</li>
|
||||
<li>Suffix</li>
|
||||
</ol>
|
||||
<p><strong style="color: #fff;">Example:</strong></p>
|
||||
<pre>{Starring <actors!c:[0]> as the main character.} --> Starring Arnold Schwarzenegger as the main character.</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -1620,7 +1655,7 @@
|
||||
<div class="modal-body">
|
||||
<div>
|
||||
<p class="help-block">
|
||||
If the value for a selected parameter cannot be provided, it will display as blank.
|
||||
If the value for a selected parameter is unavailable, it will display as blank.
|
||||
</p>
|
||||
% for category in common.NEWSLETTER_PARAMETERS:
|
||||
<table class="notification-params">
|
||||
@@ -2301,6 +2336,7 @@ $(document).ready(function() {
|
||||
$("#token_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
|
||||
}
|
||||
function OAuthSuccessCallback(authToken) {
|
||||
var x_plex_headers = getPlexHeaders();
|
||||
$("#pms_token").val(authToken);
|
||||
$("#pms_uuid").val(x_plex_headers['X-Plex-Client-Identifier']);
|
||||
$("#token_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
|
||||
@@ -2530,8 +2566,10 @@ $(document).ready(function() {
|
||||
.prop('selected', selected));
|
||||
}
|
||||
|
||||
var download_url = 'https://plex.tv/api/downloads/' + (plex_update_channel === 'plexpass' ? '5' : '1') + '.json?channel=' + plex_update_channel;
|
||||
|
||||
$.ajax({
|
||||
url: 'https://plex.tv/api/downloads/1.json?channel=' + plex_update_channel,
|
||||
url: download_url,
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
beforeSend: function (xhr) {
|
||||
|
@@ -230,7 +230,7 @@ DOCUMENTATION :: END
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Codec</td>
|
||||
<td>${data['stream_subtitle_codec'].upper()}</td>
|
||||
<td>${data['stream_subtitle_codec'].upper() or '-'}</td>
|
||||
<td>${data['subtitle_codec'].upper()}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@@ -183,6 +183,7 @@ DOCUMENTATION :: END
|
||||
async: true,
|
||||
data: {
|
||||
query: query_string,
|
||||
limit: 30,
|
||||
media_type: '${query["media_type"]}',
|
||||
season_index: '${query["parent_media_index"]}'
|
||||
},
|
||||
|
@@ -274,12 +274,12 @@ DOCUMENTATION :: END
|
||||
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left">Last Seen</th>
|
||||
<th align="left">IP Address</th>
|
||||
<th align="left">Last Platform</th>
|
||||
<th align="left">Last Player</th>
|
||||
<th align="left">Last Played</th>
|
||||
<th align="left">Play Count</th>
|
||||
<th align="left" id="last_seen">Last Seen</th>
|
||||
<th align="left" id="ip_address">IP Address</th>
|
||||
<th align="left" id="platform">Last Platform</th>
|
||||
<th align="left" id="player">Last Player</th>
|
||||
<th align="left" id="last_played">Last Played</th>
|
||||
<th align="left" id="play_count">Play Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
|
12
init-scripts/init.systemd
Executable file → Normal file
12
init-scripts/init.systemd
Executable file → Normal file
@@ -24,9 +24,11 @@
|
||||
# - The example settings in this file assume that Tautulli is installed to: /opt/Tautulli
|
||||
#
|
||||
# - To create this user and give it ownership of the Tautulli directory:
|
||||
# Ubuntu/Debian: sudo addgroup tautulli && sudo adduser --system --no-create-home tautulli --ingroup tautulli
|
||||
# CentOS/Fedora: sudo adduser --system --no-create-home tautulli
|
||||
# sudo chown tautulli:tautulli -R /opt/Tautulli
|
||||
# 1. Create the user:
|
||||
# Ubuntu/Debian: sudo addgroup tautulli && sudo adduser --system --no-create-home tautulli --ingroup tautulli
|
||||
# CentOS/Fedora: sudo adduser --system --no-create-home tautulli
|
||||
# 2. Give the user ownership of the Tautulli directory:
|
||||
# sudo chown tautulli:tautulli -R /opt/Tautulli
|
||||
#
|
||||
# - Adjust ExecStart= to point to:
|
||||
# 1. Your Tautulli executable
|
||||
@@ -53,6 +55,10 @@ GuessMainPID=no
|
||||
Type=forking
|
||||
User=tautulli
|
||||
Group=tautulli
|
||||
Restart=on-abnormal
|
||||
RestartSec=5
|
||||
StartLimitInterval=90
|
||||
StartLimitBurst=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
@@ -266,6 +266,7 @@ class ActivityHandler(object):
|
||||
last_rating_key = str(db_session['rating_key'])
|
||||
last_live_uuid = db_session['live_uuid']
|
||||
last_transcode_key = db_session['transcode_key'].split('/')[-1]
|
||||
last_paused = db_session['last_paused']
|
||||
|
||||
# Make sure the same item is being played
|
||||
if this_rating_key == last_rating_key or this_live_uuid == last_live_uuid:
|
||||
@@ -280,7 +281,7 @@ class ActivityHandler(object):
|
||||
if this_state != last_state:
|
||||
if this_state == 'paused':
|
||||
self.on_pause()
|
||||
elif last_state == 'paused' and this_state == 'playing':
|
||||
elif last_paused and this_state == 'playing':
|
||||
self.on_resume()
|
||||
elif this_state == 'stopped':
|
||||
self.on_stop()
|
||||
|
@@ -54,6 +54,7 @@ class API2:
|
||||
self._api_apikey = None
|
||||
self._api_callback = None # JSONP
|
||||
self._api_result_type = 'error'
|
||||
self._api_response_code = None
|
||||
self._api_profileme = None # For profiling the api call
|
||||
self._api_kwargs = None # Cleaned kwargs
|
||||
self._api_app = False
|
||||
@@ -85,21 +86,27 @@ class API2:
|
||||
|
||||
if not plexpy.CONFIG.API_ENABLED:
|
||||
self._api_msg = 'API not enabled'
|
||||
self._api_response_code = 404
|
||||
|
||||
elif not plexpy.CONFIG.API_KEY:
|
||||
self._api_msg = 'API key not generated'
|
||||
self._api_response_code = 401
|
||||
|
||||
elif len(plexpy.CONFIG.API_KEY) != 32:
|
||||
self._api_msg = 'API key not generated correctly'
|
||||
self._api_response_code = 401
|
||||
|
||||
elif 'apikey' not in kwargs:
|
||||
self._api_msg = 'Parameter apikey is required'
|
||||
self._api_response_code = 401
|
||||
|
||||
elif 'cmd' not in kwargs:
|
||||
self._api_msg = 'Parameter cmd is required. Possible commands are: %s' % ', '.join(self._api_valid_methods)
|
||||
self._api_response_code = 400
|
||||
|
||||
elif 'cmd' in kwargs and kwargs.get('cmd') not in self._api_valid_methods:
|
||||
self._api_msg = 'Unknown command: %s. Possible commands are: %s' % (kwargs.get('cmd', ''), ', '.join(sorted(self._api_valid_methods)))
|
||||
self._api_response_code = 400
|
||||
|
||||
self._api_callback = kwargs.pop('callback', None)
|
||||
self._api_apikey = kwargs.pop('apikey', None)
|
||||
@@ -112,7 +119,7 @@ class API2:
|
||||
if 'app' in kwargs and kwargs.pop('app') == 'true':
|
||||
self._api_app = True
|
||||
|
||||
if plexpy.CONFIG.API_ENABLED and not self._api_msg:
|
||||
if plexpy.CONFIG.API_ENABLED and not self._api_msg or self._api_cmd in ('get_apikey', 'docs', 'docs_md'):
|
||||
if self._api_apikey == plexpy.CONFIG.API_KEY or (self._api_app and self._api_apikey == mobile_app.TEMP_DEVICE_TOKEN):
|
||||
self._api_authenticated = True
|
||||
|
||||
@@ -122,6 +129,7 @@ class API2:
|
||||
|
||||
else:
|
||||
self._api_msg = 'Invalid apikey'
|
||||
self._api_response_code = 401
|
||||
|
||||
if self._api_authenticated and self._api_cmd in self._api_valid_methods:
|
||||
self._api_msg = None
|
||||
@@ -598,7 +606,7 @@ General optional parameters:
|
||||
if self._api_cmd == 'docs_md':
|
||||
return out['response']['data']
|
||||
|
||||
elif self._api_cmd.startswith('download_'):
|
||||
elif self._api_cmd and self._api_cmd.startswith('download_'):
|
||||
return
|
||||
|
||||
elif self._api_cmd == 'pms_image_proxy':
|
||||
@@ -620,7 +628,7 @@ General optional parameters:
|
||||
# if we fail to generate the output fake an error
|
||||
except Exception as e:
|
||||
logger.api_exception(u'Tautulli APIv2 :: ' + traceback.format_exc())
|
||||
cherrypy.response.status = 500
|
||||
self._api_response_code = 500
|
||||
out['message'] = traceback.format_exc()
|
||||
out['result'] = 'error'
|
||||
|
||||
@@ -630,7 +638,7 @@ General optional parameters:
|
||||
out = xmltodict.unparse(out, pretty=True)
|
||||
except Exception as e:
|
||||
logger.api_error(u'Tautulli APIv2 :: Failed to parse xml result')
|
||||
cherrypy.response.status = 500
|
||||
self._api_response_code = 500
|
||||
try:
|
||||
out['message'] = e
|
||||
out['result'] = 'error'
|
||||
@@ -671,12 +679,12 @@ General optional parameters:
|
||||
result = call(**self._api_kwargs)
|
||||
except Exception as e:
|
||||
logger.api_error(u'Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, e))
|
||||
cherrypy.response.status = 400
|
||||
self._api_response_code = 500
|
||||
if self._api_debug:
|
||||
cherrypy.request.show_tracebacks = True
|
||||
# Reraise the exception so the traceback hits the browser
|
||||
raise
|
||||
self._api_msg = 'Check the logs'
|
||||
self._api_msg = 'Check the logs for errors'
|
||||
|
||||
ret = None
|
||||
# The api decorated function can return different result types.
|
||||
@@ -700,12 +708,11 @@ General optional parameters:
|
||||
if ret is None:
|
||||
ret = result
|
||||
|
||||
if ret is not None or self._api_result_type == 'success':
|
||||
if (ret is not None or self._api_result_type == 'success') and self._api_authenticated:
|
||||
# To allow override for restart etc
|
||||
# if the call returns some data we are gonna assume its a success
|
||||
self._api_result_type = 'success'
|
||||
else:
|
||||
self._api_result_type = 'error'
|
||||
self._api_response_code = 200
|
||||
|
||||
# Since some of them methods use a api like response for the ui
|
||||
# {result: error, message: 'Some shit happened'}
|
||||
@@ -716,7 +723,13 @@ General optional parameters:
|
||||
if ret.get('result'):
|
||||
self._api_result_type = ret.pop('result', None)
|
||||
|
||||
if self._api_result_type == 'error':
|
||||
cherrypy.response.status = 500
|
||||
if self._api_result_type == 'success' and not self._api_response_code:
|
||||
self._api_response_code = 200
|
||||
elif self._api_result_type == 'error' and not self._api_response_code:
|
||||
self._api_response_code = 400
|
||||
|
||||
if not self._api_response_code:
|
||||
self._api_response_code = 500
|
||||
|
||||
cherrypy.response.status = self._api_response_code
|
||||
return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))
|
||||
|
@@ -310,14 +310,14 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'},
|
||||
{'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id', 'description': 'The unique identifier for your Plex Server.'},
|
||||
{'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'},
|
||||
{'name': 'Current Year', 'type': 'int', 'value': 'current_year', 'description': 'The year when the notfication is triggered.'},
|
||||
{'name': 'Current Month', 'type': 'int', 'value': 'current_month', 'description': 'The month when the notfication is triggered.', 'example': '1 to 12'},
|
||||
{'name': 'Current Day', 'type': 'int', 'value': 'current_day', 'description': 'The day when the notfication is triggered.', 'example': '1 to 31'},
|
||||
{'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', 'description': 'The hour when the notfication is triggered.', 'example': '0 to 23'},
|
||||
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', 'description': 'The minute when the notfication is triggered.', 'example': '0 to 59'},
|
||||
{'name': 'Current Second', 'type': 'int', 'value': 'current_second', 'description': 'The second when the notfication is triggered.', 'example': '0 to 59'},
|
||||
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', 'description': 'The ISO weekday when the notfication is triggered.', 'example': '1 (Mon) to 7 (Sun)'},
|
||||
{'name': 'Current Week', 'type': 'int', 'value': 'current_week', 'description': 'The ISO week number when the notfication is triggered.', 'example': '1 to 52'},
|
||||
{'name': 'Current Year', 'type': 'int', 'value': 'current_year', 'description': 'The year when the notification is triggered.'},
|
||||
{'name': 'Current Month', 'type': 'int', 'value': 'current_month', 'description': 'The month when the notification is triggered.', 'example': '1 to 12'},
|
||||
{'name': 'Current Day', 'type': 'int', 'value': 'current_day', 'description': 'The day when the notification is triggered.', 'example': '1 to 31'},
|
||||
{'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', 'description': 'The hour when the notification is triggered.', 'example': '0 to 23'},
|
||||
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', 'description': 'The minute when the notification is triggered.', 'example': '0 to 59'},
|
||||
{'name': 'Current Second', 'type': 'int', 'value': 'current_second', 'description': 'The second when the notification is triggered.', 'example': '0 to 59'},
|
||||
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', 'description': 'The ISO weekday when the notification is triggered.', 'example': '1 (Mon) to 7 (Sun)'},
|
||||
{'name': 'Current Week', 'type': 'int', 'value': 'current_week', 'description': 'The ISO week number when the notification is triggered.', 'example': '1 to 52'},
|
||||
{'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification is triggered.'},
|
||||
{'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification is triggered.'},
|
||||
{'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification is triggered.'},
|
||||
@@ -331,6 +331,7 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'User Streams', 'type': 'int', 'value': 'user_streams', 'description': 'The number of concurrent streams by the person streaming.'},
|
||||
{'name': 'User', 'type': 'str', 'value': 'user', 'description': 'The friendly name of the person streaming.'},
|
||||
{'name': 'Username', 'type': 'str', 'value': 'username', 'description': 'The username of the person streaming.'},
|
||||
{'name': 'User Email', 'type': 'str', 'value': 'user_email', 'description': 'The email address of the person streaming.'},
|
||||
{'name': 'Device', 'type': 'str', 'value': 'device', 'description': 'The type of client device being used for playback.'},
|
||||
{'name': 'Platform', 'type': 'str', 'value': 'platform', 'description': 'The type of client platform being used for playback.'},
|
||||
{'name': 'Product', 'type': 'str', 'value': 'product', 'description': 'The type of client product being used for playback.'},
|
||||
|
@@ -1259,8 +1259,11 @@ class DataFactory(object):
|
||||
'GROUP BY rating_key' % where
|
||||
results = monitor_db.select(query, args=args)
|
||||
|
||||
for cloudinary_info in results:
|
||||
helpers.delete_from_cloudinary(rating_key=cloudinary_info['rating_key'])
|
||||
if delete_all:
|
||||
helpers.delete_from_cloudinary(delete_all=delete_all)
|
||||
else:
|
||||
for cloudinary_info in results:
|
||||
helpers.delete_from_cloudinary(rating_key=cloudinary_info['rating_key'])
|
||||
|
||||
logger.info(u"Tautulli DataFactory :: Deleting Cloudinary info%s from the database."
|
||||
% log_msg)
|
||||
|
@@ -101,10 +101,6 @@ class DataTables(object):
|
||||
# Paginate results
|
||||
result = filtered[parameters['start']:(parameters['start'] + parameters['length'])]
|
||||
|
||||
# Sanitize on the way out
|
||||
result = [{k: helpers.sanitize(v) if isinstance(v, basestring) else v for k, v in row.iteritems()}
|
||||
for row in result]
|
||||
|
||||
output = {'result': result,
|
||||
'draw': draw_counter,
|
||||
'filteredCount': len(filtered),
|
||||
|
@@ -522,11 +522,28 @@ def process_json_kwargs(json_kwargs):
|
||||
return params
|
||||
|
||||
|
||||
def sanitize(string):
|
||||
if string:
|
||||
return unicode(string).replace('<','<').replace('>','>')
|
||||
def sanitize_out(*dargs, **dkwargs):
|
||||
""" Helper decorator that sanitized the output
|
||||
"""
|
||||
def rd(function):
|
||||
@wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
return sanitize(function(*args, **kwargs))
|
||||
return wrapper
|
||||
return rd
|
||||
|
||||
|
||||
def sanitize(obj):
|
||||
if isinstance(obj, basestring):
|
||||
return unicode(obj).replace('<', '<').replace('>', '>')
|
||||
elif isinstance(obj, list):
|
||||
return [sanitize(o) for o in obj]
|
||||
elif isinstance(obj, dict):
|
||||
return {k: sanitize(v) for k, v in obj.iteritems()}
|
||||
elif isinstance(obj, tuple):
|
||||
return tuple(sanitize(list(obj)))
|
||||
else:
|
||||
return ''
|
||||
return obj
|
||||
|
||||
|
||||
def is_public_ip(host):
|
||||
@@ -801,7 +818,7 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
|
||||
try:
|
||||
response = upload('data:image/png;base64,{}'.format(base64.b64encode(img_data)),
|
||||
public_id='{}_{}'.format(fallback, rating_key),
|
||||
tags=[fallback, str(rating_key)],
|
||||
tags=['tautulli', fallback, str(rating_key)],
|
||||
context={'title': img_title.encode('utf-8'), 'rating_key': str(rating_key), 'fallback': fallback})
|
||||
logger.debug(u"Tautulli Helpers :: Image '{}' ({}) uploaded to Cloudinary.".format(img_title, fallback))
|
||||
img_url = response.get('url', '')
|
||||
@@ -811,7 +828,7 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
|
||||
return img_url
|
||||
|
||||
|
||||
def delete_from_cloudinary(rating_key):
|
||||
def delete_from_cloudinary(rating_key=None, delete_all=False):
|
||||
""" Deletes an image from Cloudinary """
|
||||
if not plexpy.CONFIG.CLOUDINARY_CLOUD_NAME or not plexpy.CONFIG.CLOUDINARY_API_KEY or not plexpy.CONFIG.CLOUDINARY_API_SECRET:
|
||||
logger.error(u"Tautulli Helpers :: Cannot delete image from Cloudinary. Cloudinary settings not specified in the settings.")
|
||||
@@ -823,9 +840,15 @@ def delete_from_cloudinary(rating_key):
|
||||
api_secret=plexpy.CONFIG.CLOUDINARY_API_SECRET
|
||||
)
|
||||
|
||||
delete_resources_by_tag(str(rating_key))
|
||||
if delete_all:
|
||||
delete_resources_by_tag('tautulli')
|
||||
logger.debug(u"Tautulli Helpers :: Deleted all images from Cloudinary.")
|
||||
elif rating_key:
|
||||
delete_resources_by_tag(str(rating_key))
|
||||
logger.debug(u"Tautulli Helpers :: Deleted images from Cloudinary with rating_key {}.".format(rating_key))
|
||||
else:
|
||||
logger.debug(u"Tautulli Helpers :: Unable to delete images from Cloudinary: No rating_key provided.")
|
||||
|
||||
logger.debug(u"Tautulli Helpers :: Deleted images from Cloudinary with rating_key {}.".format(rating_key))
|
||||
return True
|
||||
|
||||
|
||||
@@ -1094,13 +1117,13 @@ def get_plexpy_url(hostname=None):
|
||||
else:
|
||||
hostname = hostname or plexpy.CONFIG.HTTP_HOST
|
||||
|
||||
if plexpy.CONFIG.HTTP_PORT not in (80, 443):
|
||||
port = ':' + str(plexpy.CONFIG.HTTP_PORT)
|
||||
if plexpy.HTTP_PORT not in (80, 443):
|
||||
port = ':' + str(plexpy.HTTP_PORT)
|
||||
else:
|
||||
port = ''
|
||||
|
||||
if plexpy.CONFIG.HTTP_ROOT.strip('/'):
|
||||
root = '/' + plexpy.CONFIG.HTTP_ROOT.strip('/')
|
||||
if plexpy.HTTP_ROOT.strip('/'):
|
||||
root = '/' + plexpy.HTTP_ROOT.strip('/')
|
||||
else:
|
||||
root = ''
|
||||
|
||||
|
@@ -690,7 +690,8 @@ class Libraries(object):
|
||||
'child_count': 0,
|
||||
'do_notify': 0,
|
||||
'do_notify_created': 0,
|
||||
'keep_history': 1
|
||||
'keep_history': 1,
|
||||
'deleted_section': 0
|
||||
}
|
||||
|
||||
if not section_id:
|
||||
@@ -703,7 +704,7 @@ class Libraries(object):
|
||||
if str(section_id).isdigit():
|
||||
query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \
|
||||
'thumb AS library_thumb, custom_thumb_url AS custom_thumb, art, ' \
|
||||
'do_notify, do_notify_created, keep_history ' \
|
||||
'do_notify, do_notify_created, keep_history, deleted_section ' \
|
||||
'FROM library_sections ' \
|
||||
'WHERE section_id = ? '
|
||||
result = monitor_db.select(query, args=[section_id])
|
||||
@@ -733,7 +734,8 @@ class Libraries(object):
|
||||
'child_count': item['child_count'],
|
||||
'do_notify': item['do_notify'],
|
||||
'do_notify_created': item['do_notify_created'],
|
||||
'keep_history': item['keep_history']
|
||||
'keep_history': item['keep_history'],
|
||||
'deleted_section': item['deleted_section']
|
||||
}
|
||||
return library_details
|
||||
|
||||
@@ -924,7 +926,8 @@ class Libraries(object):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
try:
|
||||
query = 'SELECT section_id, section_name, section_type, agent FROM library_sections WHERE deleted_section = 0'
|
||||
query = 'SELECT section_id, section_name, section_type, agent ' \
|
||||
'FROM library_sections WHERE deleted_section = 0'
|
||||
result = monitor_db.select(query=query)
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Libraries :: Unable to execute database query for get_sections: %s." % e)
|
||||
@@ -1001,23 +1004,31 @@ class Libraries(object):
|
||||
|
||||
try:
|
||||
if section_id and section_id.isdigit():
|
||||
logger.info(u"Tautulli Libraries :: Re-adding library with id %s to database." % section_id)
|
||||
monitor_db.action('UPDATE library_sections SET deleted_section = 0 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET keep_history = 1 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify = 1 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify_created = 1 WHERE section_id = ?', [section_id])
|
||||
query = 'SELECT * FROM library_sections WHERE section_id = ?'
|
||||
result = monitor_db.select(query=query, args=[section_id])
|
||||
if result:
|
||||
logger.info(u"Tautulli Libraries :: Re-adding library with id %s to database." % section_id)
|
||||
monitor_db.action('UPDATE library_sections SET deleted_section = 0 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET keep_history = 1 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify = 1 WHERE section_id = ?', [section_id])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify_created = 1 WHERE section_id = ?', [section_id])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return 'Re-added library with id %s.' % section_id
|
||||
elif section_name:
|
||||
logger.info(u"Tautulli Libraries :: Re-adding library with name %s to database." % section_name)
|
||||
monitor_db.action('UPDATE library_sections SET deleted_section = 0 WHERE section_name = ?', [section_name])
|
||||
monitor_db.action('UPDATE library_sections SET keep_history = 1 WHERE section_name = ?', [section_name])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify = 1 WHERE section_name = ?', [section_name])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify_created = 1 WHERE section_name = ?', [section_name])
|
||||
query = 'SELECT * FROM library_sections WHERE section_name = ?'
|
||||
result = monitor_db.select(query=query, args=[section_name])
|
||||
if result:
|
||||
logger.info(u"Tautulli Libraries :: Re-adding library with name %s to database." % section_name)
|
||||
monitor_db.action('UPDATE library_sections SET deleted_section = 0 WHERE section_name = ?', [section_name])
|
||||
monitor_db.action('UPDATE library_sections SET keep_history = 1 WHERE section_name = ?', [section_name])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify = 1 WHERE section_name = ?', [section_name])
|
||||
monitor_db.action('UPDATE library_sections SET do_notify_created = 1 WHERE section_name = ?', [section_name])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return 'Re-added library with section_name %s.' % section_name
|
||||
else:
|
||||
return 'Unable to re-add library, section_id or section_name not valid.'
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Libraries :: Unable to execute database query for undelete: %s." % e)
|
||||
|
||||
|
@@ -749,6 +749,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'user_streams': user_stream_count,
|
||||
'user': notify_params['friendly_name'],
|
||||
'username': notify_params['user'],
|
||||
'user_email': notify_params['email'],
|
||||
'device': notify_params['device'],
|
||||
'platform': notify_params['platform'],
|
||||
'product': notify_params['product'],
|
||||
@@ -1455,9 +1456,8 @@ def get_themoviedb_info(rating_key=None, media_type=None, themoviedb_id=None):
|
||||
|
||||
|
||||
class CustomFormatter(Formatter):
|
||||
def __init__(self, default='{{{0}}}', default_format_spec='{{{0}:{1}}}'):
|
||||
def __init__(self, default='{{{0}}}'):
|
||||
self.default = default
|
||||
self.default_format_spec = default_format_spec
|
||||
|
||||
def convert_field(self, value, conversion):
|
||||
if conversion is None:
|
||||
@@ -1477,23 +1477,99 @@ class CustomFormatter(Formatter):
|
||||
|
||||
def format_field(self, value, format_spec):
|
||||
if format_spec.startswith('[') and format_spec.endswith(']'):
|
||||
pattern = re.compile(r'\[(-?\d*):?(-?\d*)\]')
|
||||
if re.match(pattern, format_spec): # slice
|
||||
pattern = re.compile(r'\[(?P<start>-?\d*)(?P<slice>:?)(?P<end>-?\d*)\]')
|
||||
match = re.match(pattern, format_spec)
|
||||
if value and match:
|
||||
groups = match.groupdict()
|
||||
items = [x.strip() for x in unicode(value).split(',')]
|
||||
slice_start, slice_end = re.search(pattern, format_spec).groups()
|
||||
slice_start = helpers.cast_to_int(slice_start) or None
|
||||
slice_end = helpers.cast_to_int(slice_end) or None
|
||||
return ', '.join(items[slice(slice_start, slice_end)])
|
||||
else:
|
||||
return value
|
||||
start = groups['start'] or None
|
||||
end = groups['end'] or None
|
||||
if start is not None:
|
||||
start = helpers.cast_to_int(start)
|
||||
if end is not None:
|
||||
end = helpers.cast_to_int(end)
|
||||
if not groups['slice']:
|
||||
end = start + 1
|
||||
value = ', '.join(items[slice(start, end)])
|
||||
return value
|
||||
else:
|
||||
try:
|
||||
return super(CustomFormatter, self).format_field(value, format_spec)
|
||||
except ValueError:
|
||||
return self.default_format_spec.format(value[1:-1], format_spec)
|
||||
return value
|
||||
|
||||
def get_value(self, key, args, kwargs):
|
||||
if isinstance(key, basestring):
|
||||
return kwargs.get(key, self.default.format(key))
|
||||
else:
|
||||
return super(CustomFormatter, self).get_value(key, args, kwargs)
|
||||
|
||||
def parse(self, format_string):
|
||||
parsed = super(CustomFormatter, self).parse(format_string)
|
||||
for literal_text, field_name, format_spec, conversion in parsed:
|
||||
real_format_string = ''
|
||||
if field_name:
|
||||
real_format_string += field_name
|
||||
if conversion:
|
||||
real_format_string += '!' + conversion
|
||||
if format_spec:
|
||||
real_format_string += ':' + format_spec
|
||||
|
||||
prefix = None
|
||||
suffix = None
|
||||
|
||||
if real_format_string != format_string[1:-1]:
|
||||
prefix_split = real_format_string.split('<')
|
||||
if len(prefix_split) == 2:
|
||||
prefix = prefix_split[0].replace('\\n', '\n')
|
||||
real_format_string = prefix_split[1]
|
||||
|
||||
suffix_split = real_format_string.split('>')
|
||||
if len(suffix_split) == 2:
|
||||
suffix = suffix_split[1].replace('\\n', '\n')
|
||||
real_format_string = suffix_split[0]
|
||||
|
||||
if prefix or suffix:
|
||||
real_format_string = '{' + real_format_string + '}'
|
||||
_, field_name, format_spec, conversion, _, _ = self.parse(real_format_string).next()
|
||||
|
||||
yield literal_text, field_name, format_spec, conversion, prefix, suffix
|
||||
|
||||
def _vformat(self, format_string, args, kwargs, used_args, recursion_depth,
|
||||
auto_arg_index=0):
|
||||
if recursion_depth < 0:
|
||||
raise ValueError('Max string recursion exceeded')
|
||||
result = []
|
||||
for literal_text, field_name, format_spec, conversion, prefix, suffix in self.parse(format_string):
|
||||
# output the literal text
|
||||
if literal_text:
|
||||
result.append(literal_text)
|
||||
|
||||
# if there's a field, output it
|
||||
if field_name is not None:
|
||||
# this is some markup, find the object and do
|
||||
# the formatting
|
||||
|
||||
# given the field_name, find the object it references
|
||||
# and the argument it came from
|
||||
obj, arg_used = self.get_field(field_name, args, kwargs)
|
||||
used_args.add(arg_used)
|
||||
|
||||
# do any conversion on the resulting object
|
||||
obj = self.convert_field(obj, conversion)
|
||||
|
||||
# expand the format spec, if needed
|
||||
format_spec = self._vformat(format_spec, args, kwargs,
|
||||
used_args, recursion_depth - 1)
|
||||
|
||||
# format the object and append to the result
|
||||
formatted_field = self.format_field(obj, format_spec)
|
||||
if formatted_field:
|
||||
if prefix:
|
||||
result.append(prefix)
|
||||
result.append(formatted_field)
|
||||
if suffix:
|
||||
result.append(suffix)
|
||||
# result.append(self.format_field(obj, format_spec))
|
||||
|
||||
return ''.join(result)
|
||||
|
@@ -746,14 +746,14 @@ class PrettyMetadata(object):
|
||||
title = '%s - %s' % (self.parameters['artist_name'], self.parameters['album_name'])
|
||||
elif self.media_type == 'track':
|
||||
title = '%s - %s' % (self.parameters['track_name'], self.parameters['track_artist'])
|
||||
return title.encode("utf-8")
|
||||
return title.encode('utf-8')
|
||||
|
||||
def get_description(self):
|
||||
if self.media_type == 'track':
|
||||
description = self.parameters['album_name']
|
||||
else:
|
||||
description = self.parameters['summary']
|
||||
return description.encode("utf-8")
|
||||
return description.encode('utf-8')
|
||||
|
||||
def get_plex_url(self):
|
||||
return self.parameters['plex_url']
|
||||
@@ -863,9 +863,9 @@ class ANDROIDAPP(Notifier):
|
||||
pretty_metadata = PrettyMetadata(kwargs.get('parameters'))
|
||||
|
||||
plaintext_data = {'notification_id': notification_id,
|
||||
'subject': subject.encode('UTF-8'),
|
||||
'body': body.encode('UTF-8'),
|
||||
'action': action.encode('UTF-8'),
|
||||
'subject': subject.encode('utf-8'),
|
||||
'body': body.encode('utf-8'),
|
||||
'action': action.encode('utf-8'),
|
||||
'priority': self.config['priority'],
|
||||
'session_key': pretty_metadata.parameters.get('session_key',''),
|
||||
'session_id': pretty_metadata.parameters.get('session_id',''),
|
||||
@@ -1130,9 +1130,9 @@ class DISCORD(Notifier):
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
if self.config['incl_subject']:
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8")
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode('utf-8')
|
||||
else:
|
||||
text = body.encode("utf-8")
|
||||
text = body.encode('utf-8')
|
||||
|
||||
data = {'content': text}
|
||||
if self.config['username']:
|
||||
@@ -1363,7 +1363,8 @@ class EMAIL(Notifier):
|
||||
success = True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
|
||||
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(
|
||||
name=self.NAME, e=str(e).decode('utf-8')))
|
||||
|
||||
finally:
|
||||
if mailserver:
|
||||
@@ -1545,9 +1546,9 @@ class FACEBOOK(Notifier):
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
if self.config['incl_subject']:
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8")
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode('utf-8')
|
||||
else:
|
||||
text = body.encode("utf-8")
|
||||
text = body.encode('utf-8')
|
||||
|
||||
data = {'message': text}
|
||||
|
||||
@@ -1999,8 +2000,8 @@ class IFTTT(Notifier):
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
event = unicode(self.config['event']).format(action=action)
|
||||
|
||||
data = {'value1': subject.encode("utf-8"),
|
||||
'value2': body.encode("utf-8")}
|
||||
data = {'value1': subject.encode('utf-8'),
|
||||
'value2': body.encode('utf-8')}
|
||||
|
||||
if self.config['value3']:
|
||||
pretty_metadata = PrettyMetadata(kwargs['parameters'])
|
||||
@@ -2060,10 +2061,10 @@ class JOIN(Notifier):
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'apikey': self.config['api_key'],
|
||||
'deviceNames': ','.join(self.config['device_names']),
|
||||
'text': body.encode("utf-8")}
|
||||
'text': body.encode('utf-8')}
|
||||
|
||||
if self.config['incl_subject']:
|
||||
data['title'] = subject.encode("utf-8")
|
||||
data['title'] = subject.encode('utf-8')
|
||||
|
||||
if kwargs.get('parameters', {}).get('media_type'):
|
||||
# Grab formatted metadata
|
||||
@@ -2216,9 +2217,9 @@ class MQTT(Notifier):
|
||||
logger.error(u"Tautulli Notifiers :: MQTT topic not specified.")
|
||||
return
|
||||
|
||||
data = {'subject': subject.encode("utf-8"),
|
||||
'body': body.encode("utf-8"),
|
||||
'topic': self.config['topic'].encode("utf-8")}
|
||||
data = {'subject': subject.encode('utf-8'),
|
||||
'body': body.encode('utf-8'),
|
||||
'topic': self.config['topic'].encode('utf-8')}
|
||||
|
||||
auth = {}
|
||||
if self.config['username']:
|
||||
@@ -2577,8 +2578,8 @@ class PROWL(Notifier):
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'apikey': self.config['key'],
|
||||
'application': 'Tautulli',
|
||||
'event': subject.encode("utf-8"),
|
||||
'description': body.encode("utf-8"),
|
||||
'event': subject.encode('utf-8'),
|
||||
'description': body.encode('utf-8'),
|
||||
'priority': self.config['priority']}
|
||||
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'}
|
||||
@@ -2615,7 +2616,7 @@ class PUSHALOT(Notifier):
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'AuthorizationToken': self.config['api_key'],
|
||||
'Title': subject.encode('utf-8'),
|
||||
'Body': body.encode("utf-8")}
|
||||
'Body': body.encode('utf-8')}
|
||||
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'}
|
||||
|
||||
@@ -2647,14 +2648,14 @@ class PUSHBULLET(Notifier):
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'type': 'note',
|
||||
'body': body.encode("utf-8")}
|
||||
'body': body.encode('utf-8')}
|
||||
|
||||
headers = {'Content-type': 'application/json',
|
||||
'Access-Token': self.config['api_key']
|
||||
}
|
||||
|
||||
if self.config['incl_subject']:
|
||||
data['title'] = subject.encode("utf-8")
|
||||
data['title'] = subject.encode('utf-8')
|
||||
|
||||
# Can only send to a device or channel, not both.
|
||||
if self.config['device_id']:
|
||||
@@ -2783,14 +2784,14 @@ class PUSHOVER(Notifier):
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'token': self.config['api_token'],
|
||||
'user': self.config['key'],
|
||||
'message': body.encode("utf-8"),
|
||||
'message': body.encode('utf-8'),
|
||||
'sound': self.config['sound'],
|
||||
'html': self.config['html_support'],
|
||||
'priority': self.config['priority'],
|
||||
'timestamp': int(time.time())}
|
||||
|
||||
if self.config['incl_subject']:
|
||||
data['title'] = subject.encode("utf-8")
|
||||
data['title'] = subject.encode('utf-8')
|
||||
|
||||
if self.config['priority'] == 2:
|
||||
data['retry'] = max(30, self.config['retry'])
|
||||
@@ -3061,6 +3062,7 @@ class SCRIPTS(Notifier):
|
||||
timer.start()
|
||||
output, error = process.communicate()
|
||||
status = process.returncode
|
||||
logger.debug(u"Tautulli Notifiers :: Subprocess returned with status code %s." % status)
|
||||
finally:
|
||||
if timer:
|
||||
timer.cancel()
|
||||
@@ -3207,9 +3209,9 @@ class SLACK(Notifier):
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
if self.config['incl_subject']:
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8")
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode('utf-8')
|
||||
else:
|
||||
text = body.encode("utf-8")
|
||||
text = body.encode('utf-8')
|
||||
|
||||
data = {'text': text}
|
||||
if self.config['channel'] and self.config['channel'].startswith('#'):
|
||||
@@ -3753,9 +3755,9 @@ class ZAPIER(Notifier):
|
||||
return self.agent_notify(_test_data=_test_data)
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'subject': subject.encode("utf-8"),
|
||||
'body': body.encode("utf-8"),
|
||||
'action': action.encode("utf-8")}
|
||||
data = {'subject': subject.encode('utf-8'),
|
||||
'body': body.encode('utf-8'),
|
||||
'action': action.encode('utf-8')}
|
||||
|
||||
if kwargs.get('parameters', {}).get('media_type'):
|
||||
# Grab formatted metadata
|
||||
|
@@ -336,7 +336,7 @@ class PlexTV(object):
|
||||
|
||||
def get_plextv_downloads(self, plexpass=False, output_format=''):
|
||||
if plexpass:
|
||||
uri = '/api/downloads/1.json?channel=plexpass'
|
||||
uri = '/api/downloads/5.json?channel=plexpass'
|
||||
else:
|
||||
uri = '/api/downloads/1.json'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
@@ -566,13 +566,13 @@ class PlexTV(object):
|
||||
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
|
||||
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
|
||||
|
||||
sync_details = {"device_name": helpers.sanitize(device_name),
|
||||
"platform": helpers.sanitize(device_platform),
|
||||
sync_details = {"device_name": device_name,
|
||||
"platform": device_platform,
|
||||
"user_id": device_user_id,
|
||||
"user": helpers.sanitize(device_friendly_name),
|
||||
"username": helpers.sanitize(device_username),
|
||||
"root_title": helpers.sanitize(sync_root_title),
|
||||
"sync_title": helpers.sanitize(sync_title),
|
||||
"user": device_friendly_name,
|
||||
"username": device_username,
|
||||
"root_title": sync_root_title,
|
||||
"sync_title": sync_title,
|
||||
"metadata_type": sync_metadata_type,
|
||||
"content_type": sync_content_type,
|
||||
"rating_key": rating_key,
|
||||
@@ -842,9 +842,13 @@ class PlexTV(object):
|
||||
return False
|
||||
|
||||
if subscription and helpers.get_xml_attr(subscription[0], 'active') == '1':
|
||||
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 1)
|
||||
plexpy.CONFIG.write()
|
||||
return True
|
||||
else:
|
||||
logger.debug(u"Tautulli PlexTV :: Plex Pass subscription not found.")
|
||||
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 0)
|
||||
plexpy.CONFIG.write()
|
||||
return False
|
||||
|
||||
def get_devices_list(self):
|
||||
|
@@ -101,7 +101,7 @@ class PmsConnect(object):
|
||||
|
||||
Output: array
|
||||
"""
|
||||
uri = '/status/sessions/terminate?sessionId=%s&reason=%s' % (session_id, reason)
|
||||
uri = '/status/sessions/terminate?sessionId=%s&reason=%s' % (session_id, urllib.quote_plus(reason))
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
@@ -446,7 +446,10 @@ class PmsConnect(object):
|
||||
|
||||
Output: array
|
||||
"""
|
||||
if media_type in ('movie', 'show', 'artist', 'other_video'):
|
||||
media_types = ('movie', 'show', 'artist', 'other_video')
|
||||
recents_list = []
|
||||
|
||||
if media_type in media_types:
|
||||
other_video = False
|
||||
if media_type == 'movie':
|
||||
media_type = '1'
|
||||
@@ -461,7 +464,12 @@ class PmsConnect(object):
|
||||
elif section_id:
|
||||
recent = self.get_library_recently_added(section_id, start, count, output_format='xml')
|
||||
else:
|
||||
recent = self.get_recently_added(start, count, output_format='xml')
|
||||
for media_type in media_types:
|
||||
recents = self.get_recently_added_details(start, count, media_type)
|
||||
recents_list += recents['recently_added']
|
||||
|
||||
output = {'recently_added': sorted(recents_list, key=lambda k: k['added_at'], reverse=True)[:int(count)]}
|
||||
return output
|
||||
|
||||
try:
|
||||
xml_head = recent.getElementsByTagName('MediaContainer')
|
||||
@@ -469,8 +477,6 @@ class PmsConnect(object):
|
||||
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_recently_added: %s." % e)
|
||||
return []
|
||||
|
||||
recents_list = []
|
||||
|
||||
for a in xml_head:
|
||||
if a.getAttribute('size'):
|
||||
if a.getAttribute('size') == '0':
|
||||
@@ -1504,7 +1510,9 @@ class PmsConnect(object):
|
||||
'player': helpers.get_xml_attr(player_info, 'title') or helpers.get_xml_attr(player_info, 'product'),
|
||||
'machine_id': helpers.get_xml_attr(player_info, 'machineIdentifier'),
|
||||
'state': helpers.get_xml_attr(player_info, 'state'),
|
||||
'local': helpers.get_xml_attr(player_info, 'local')
|
||||
'local': int(helpers.get_xml_attr(player_info, 'local') == '1'),
|
||||
'relayed': helpers.get_xml_attr(player_info, 'relayed', default_return=None),
|
||||
'secure': helpers.get_xml_attr(player_info, 'secure', default_return=None)
|
||||
}
|
||||
|
||||
# Get the session details
|
||||
@@ -1518,12 +1526,20 @@ class PmsConnect(object):
|
||||
else:
|
||||
session_details = {'session_id': '',
|
||||
'bandwidth': '',
|
||||
'location': 'wan' if player_details['local'] == '0' else 'lan'
|
||||
'location': 'lan' if player_details['local'] else 'wan'
|
||||
}
|
||||
|
||||
# Check if using Plex Relay
|
||||
session_details['relay'] = int(session_details['location'] != 'lan'
|
||||
and player_details['ip_address_public'] == '127.0.0.1')
|
||||
if player_details['relayed'] is None:
|
||||
player_details['relayed'] = int(session_details['location'] != 'lan' and
|
||||
player_details['ip_address_public'] == '127.0.0.1')
|
||||
|
||||
else:
|
||||
player_details['relayed'] = helpers.cast_to_int(player_details['relayed'])
|
||||
|
||||
# Check if secure connection
|
||||
if player_details['secure'] is not None:
|
||||
player_details['secure'] = int(player_details['secure'] == '1')
|
||||
|
||||
# Get the transcode details
|
||||
if session.getElementsByTagName('TranscodeSession'):
|
||||
@@ -1587,7 +1603,7 @@ class PmsConnect(object):
|
||||
transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS)
|
||||
|
||||
# Determine if a synced version is being played
|
||||
sync_id = None
|
||||
sync_id = synced_session_data = synced_item_details = None
|
||||
if media_type not in ('photo', 'clip') \
|
||||
and not session.getElementsByTagName('Session') \
|
||||
and not session.getElementsByTagName('TranscodeSession') \
|
||||
@@ -1604,6 +1620,8 @@ class PmsConnect(object):
|
||||
sync_id = synced_item_details['sync_id']
|
||||
synced_xml = self.get_sync_item(sync_id=sync_id, output_format='xml')
|
||||
synced_xml_head = synced_xml.getElementsByTagName('MediaContainer')
|
||||
|
||||
synced_xml_items = []
|
||||
if synced_xml_head[0].getElementsByTagName('Track'):
|
||||
synced_xml_items = synced_xml_head[0].getElementsByTagName('Track')
|
||||
elif synced_xml_head[0].getElementsByTagName('Video'):
|
||||
@@ -1614,7 +1632,7 @@ class PmsConnect(object):
|
||||
break
|
||||
|
||||
# Figure out which version is being played
|
||||
if sync_id:
|
||||
if sync_id and synced_session_data:
|
||||
media_info_all = synced_session_data.getElementsByTagName('Media')
|
||||
else:
|
||||
media_info_all = session.getElementsByTagName('Media')
|
||||
@@ -1688,6 +1706,7 @@ class PmsConnect(object):
|
||||
'stream_subtitle_decision': helpers.get_xml_attr(subtitle_stream_info, 'decision')
|
||||
}
|
||||
else:
|
||||
subtitle_selected = None
|
||||
subtitle_details = {'stream_subtitle_codec': '',
|
||||
'stream_subtitle_container': '',
|
||||
'stream_subtitle_format': '',
|
||||
@@ -1888,6 +1907,18 @@ class PmsConnect(object):
|
||||
|
||||
stream_details['transcode_decision'] = transcode_decision
|
||||
|
||||
# Override * in audio codecs
|
||||
if stream_details['stream_audio_codec'] == '*':
|
||||
stream_details['stream_audio_codec'] = source_audio_details['audio_codec']
|
||||
if transcode_details['transcode_audio_codec'] == '*':
|
||||
transcode_details['transcode_audio_codec'] = source_audio_details['audio_codec']
|
||||
|
||||
# Override * in video codecs
|
||||
if stream_details['stream_video_codec'] == '*':
|
||||
stream_details['stream_video_codec'] = source_video_details['video_codec']
|
||||
if transcode_details['transcode_video_codec'] == '*':
|
||||
transcode_details['transcode_video_codec'] = source_video_details['video_codec']
|
||||
|
||||
# Get the quality profile
|
||||
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
|
||||
if sync_id:
|
||||
@@ -1912,6 +1943,7 @@ class PmsConnect(object):
|
||||
quality_profile = 'Original'
|
||||
|
||||
if stream_details['optimized_version']:
|
||||
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
|
||||
optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1),
|
||||
plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'],
|
||||
source_media_details['video_resolution']))
|
||||
@@ -1986,25 +2018,42 @@ class PmsConnect(object):
|
||||
|
||||
Output: bool
|
||||
"""
|
||||
plex_tv = plextv.PlexTV()
|
||||
if not plex_tv.get_plexpass_status():
|
||||
msg = 'No Plex Pass subscription'
|
||||
logger.warn(u"Tautulli Pmsconnect :: Failed to terminate session: %s." % msg)
|
||||
return msg
|
||||
|
||||
message = message.encode('utf-8') or 'The server owner has ended the stream.'
|
||||
|
||||
if session_key and not session_id:
|
||||
ap = activity_processor.ActivityProcessor()
|
||||
session = ap.get_session_by_key(session_key=session_key)
|
||||
session_id = session['session_id']
|
||||
ap = activity_processor.ActivityProcessor()
|
||||
|
||||
elif session_id and not session_key:
|
||||
ap = activity_processor.ActivityProcessor()
|
||||
if session_key:
|
||||
session = ap.get_session_by_key(session_key=session_key)
|
||||
if session and not session_id:
|
||||
session_id = session['session_id']
|
||||
|
||||
elif session_id:
|
||||
session = ap.get_session_by_id(session_id=session_id)
|
||||
session_key = session['session_key']
|
||||
if session and not session_key:
|
||||
session_key = session['session_key']
|
||||
|
||||
else:
|
||||
session = session_key = session_id = None
|
||||
|
||||
if not session:
|
||||
msg = 'Invalid session_key (%s) or session_id (%s)' % (session_key, session_id)
|
||||
logger.warn(u"Tautulli Pmsconnect :: Failed to terminate session: %s." % msg)
|
||||
return msg
|
||||
|
||||
if session_id:
|
||||
logger.info(u"Tautulli Pmsconnect :: Terminating session %s (session_id %s)." % (session_key, session_id))
|
||||
result = self.get_sessions_terminate(session_id=session_id, reason=urllib.quote_plus(message))
|
||||
return result
|
||||
result = self.get_sessions_terminate(session_id=session_id, reason=message)
|
||||
return True
|
||||
else:
|
||||
logger.warn(u"Tautulli Pmsconnect :: Failed to terminate session %s. Missing session_id." % session_key)
|
||||
return False
|
||||
msg = 'Missing session_id'
|
||||
logger.warn(u"Tautulli Pmsconnect :: Failed to terminate session: %s." % msg)
|
||||
return msg
|
||||
|
||||
def get_item_children(self, rating_key='', get_grandchildren=False):
|
||||
"""
|
||||
|
@@ -289,7 +289,7 @@ def server_message(response, return_msg=False):
|
||||
message = None
|
||||
|
||||
# First attempt is to 'read' the response as HTML
|
||||
if "text/html" in response.headers.get("content-type"):
|
||||
if "text/html" in response.headers.get("content-type", ""):
|
||||
try:
|
||||
soup = BeautifulSoup(response.content, "html5lib")
|
||||
except Exception:
|
||||
|
@@ -205,7 +205,7 @@ class Users(object):
|
||||
custom_where = ['users.user_id', user_id]
|
||||
|
||||
columns = ['session_history.id',
|
||||
'session_history.started AS last_seen',
|
||||
'MAX(session_history.started) AS last_seen',
|
||||
'session_history.ip_address',
|
||||
'COUNT(session_history.id) AS play_count',
|
||||
'session_history.platform',
|
||||
@@ -668,21 +668,29 @@ class Users(object):
|
||||
|
||||
try:
|
||||
if user_id and str(user_id).isdigit():
|
||||
logger.info(u"Tautulli Users :: Re-adding user with id %s to database." % user_id)
|
||||
monitor_db.action('UPDATE users SET deleted_user = 0 WHERE user_id = ?', [user_id])
|
||||
monitor_db.action('UPDATE users SET keep_history = 1 WHERE user_id = ?', [user_id])
|
||||
monitor_db.action('UPDATE users SET do_notify = 1 WHERE user_id = ?', [user_id])
|
||||
query = 'SELECT * FROM users WHERE user_id = ?'
|
||||
result = monitor_db.select(query=query, args=[user_id])
|
||||
if result:
|
||||
logger.info(u"Tautulli Users :: Re-adding user with id %s to database." % user_id)
|
||||
monitor_db.action('UPDATE users SET deleted_user = 0 WHERE user_id = ?', [user_id])
|
||||
monitor_db.action('UPDATE users SET keep_history = 1 WHERE user_id = ?', [user_id])
|
||||
monitor_db.action('UPDATE users SET do_notify = 1 WHERE user_id = ?', [user_id])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return 'Re-added user with id %s.' % user_id
|
||||
elif username:
|
||||
logger.info(u"Tautulli Users :: Re-adding user with username %s to database." % username)
|
||||
monitor_db.action('UPDATE users SET deleted_user = 0 WHERE username = ?', [username])
|
||||
monitor_db.action('UPDATE users SET keep_history = 1 WHERE username = ?', [username])
|
||||
monitor_db.action('UPDATE users SET do_notify = 1 WHERE username = ?', [username])
|
||||
query = 'SELECT * FROM users WHERE username = ?'
|
||||
result = monitor_db.select(query=query, args=[username])
|
||||
if result:
|
||||
logger.info(u"Tautulli Users :: Re-adding user with username %s to database." % username)
|
||||
monitor_db.action('UPDATE users SET deleted_user = 0 WHERE username = ?', [username])
|
||||
monitor_db.action('UPDATE users SET keep_history = 1 WHERE username = ?', [username])
|
||||
monitor_db.action('UPDATE users SET do_notify = 1 WHERE username = ?', [username])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return 'Re-added user with username %s.' % username
|
||||
else:
|
||||
return 'Unable to re-add user, user_id or username not valid.'
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Users :: Unable to execute database query for undelete: %s." % e)
|
||||
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.24-beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.29-beta"
|
||||
|
@@ -55,7 +55,7 @@ import users
|
||||
import versioncheck
|
||||
import web_socket
|
||||
from plexpy.api2 import API2
|
||||
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json
|
||||
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out
|
||||
from plexpy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library
|
||||
from plexpy.webauth import AuthController, requireAuth, member_of
|
||||
|
||||
@@ -176,7 +176,8 @@ class WebInterface(object):
|
||||
"home_refresh_interval": plexpy.CONFIG.HOME_REFRESH_INTERVAL,
|
||||
"pms_name": plexpy.CONFIG.PMS_NAME,
|
||||
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
|
||||
"update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG
|
||||
"update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG,
|
||||
"first_run_complete": plexpy.CONFIG.FIRST_RUN_COMPLETE
|
||||
}
|
||||
return serve_template(templatename="index.html", title="Home", config=config)
|
||||
|
||||
@@ -245,7 +246,7 @@ class WebInterface(object):
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi()
|
||||
def terminate_session(self, session_key=None, session_id=None, message=None, **kwargs):
|
||||
def terminate_session(self, session_key='', session_id='', message='', **kwargs):
|
||||
""" Stop a streaming session.
|
||||
|
||||
```
|
||||
@@ -263,8 +264,10 @@ class WebInterface(object):
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
result = pms_connect.terminate_session(session_key=session_key, session_id=session_id, message=message)
|
||||
|
||||
if result:
|
||||
if result is True:
|
||||
return {'result': 'success', 'message': 'Session terminated.'}
|
||||
elif result:
|
||||
return {'result': 'error', 'message': 'Failed to terminate session: {}.'.format(result)}
|
||||
else:
|
||||
return {'result': 'error', 'message': 'Failed to terminate session.'}
|
||||
|
||||
@@ -349,6 +352,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi("get_libraries_table")
|
||||
def get_library_list(self, **kwargs):
|
||||
""" Get the data on the Tautulli libraries table.
|
||||
@@ -427,6 +431,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@sanitize_out()
|
||||
@addtoapi("get_library_names")
|
||||
def get_library_sections(self, **kwargs):
|
||||
""" Get a list of library sections and ids on the PMS.
|
||||
@@ -756,6 +761,7 @@ class WebInterface(object):
|
||||
json:
|
||||
{"child_count": null,
|
||||
"count": 887,
|
||||
"deleted_section": 0,
|
||||
"do_notify": 1,
|
||||
"do_notify_created": 1,
|
||||
"keep_history": 1,
|
||||
@@ -944,19 +950,14 @@ class WebInterface(object):
|
||||
```
|
||||
"""
|
||||
library_data = libraries.Libraries()
|
||||
|
||||
if section_id:
|
||||
delete_row = library_data.undelete(section_id=section_id)
|
||||
|
||||
if delete_row:
|
||||
return {'message': delete_row}
|
||||
elif section_name:
|
||||
delete_row = library_data.undelete(section_name=section_name)
|
||||
|
||||
if delete_row:
|
||||
return {'message': delete_row}
|
||||
else:
|
||||
return {'message': 'no data received'}
|
||||
result = library_data.undelete(section_id=section_id, section_name=section_name)
|
||||
if result:
|
||||
if section_id:
|
||||
msg ='section_id %s' % section_id
|
||||
elif section_name:
|
||||
msg = 'section_name %s' % section_name
|
||||
return {'result': 'success', 'message': 'Re-added library with %s.' % msg}
|
||||
return {'result': 'error', 'message': 'Unable to re-add library. Invalid section_id or section_name.'}
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@@ -1014,6 +1015,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi("get_users_table")
|
||||
def get_user_list(self, **kwargs):
|
||||
""" Get the data on Tautulli users table.
|
||||
@@ -1228,6 +1230,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_user_ips(self, user_id=None, **kwargs):
|
||||
""" Get the data on Tautulli users IP table.
|
||||
@@ -1294,6 +1297,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_user_logins(self, user_id=None, **kwargs):
|
||||
""" Get the data on Tautulli user login table.
|
||||
@@ -1551,18 +1555,15 @@ class WebInterface(object):
|
||||
None
|
||||
```
|
||||
"""
|
||||
if user_id:
|
||||
user_data = users.Users()
|
||||
delete_row = user_data.undelete(user_id=user_id)
|
||||
if delete_row:
|
||||
return {'message': delete_row}
|
||||
elif username:
|
||||
user_data = users.Users()
|
||||
delete_row = user_data.undelete(username=username)
|
||||
if delete_row:
|
||||
return {'message': delete_row}
|
||||
else:
|
||||
return {'message': 'no data received'}
|
||||
user_data = users.Users()
|
||||
result = user_data.undelete(user_id=user_id, username=username)
|
||||
if result:
|
||||
if user_id:
|
||||
msg ='user_id %s' % user_id
|
||||
elif username:
|
||||
msg = 'username %s' % username
|
||||
return {'result': 'success', 'message': 'Re-added user with %s.' % msg}
|
||||
return {'result': 'error', 'message': 'Unable to re-add user. Invalid user_id or username.'}
|
||||
|
||||
|
||||
##### History #####
|
||||
@@ -1575,6 +1576,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_history(self, user=None, user_id=None, grouping=None, **kwargs):
|
||||
""" Get the Tautulli history.
|
||||
@@ -1821,6 +1823,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_user_names(self, **kwargs):
|
||||
""" Get a list of all user and user ids.
|
||||
@@ -2293,6 +2296,7 @@ class WebInterface(object):
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@sanitize_out()
|
||||
@requireAuth()
|
||||
def get_sync(self, machine_id=None, user_id=None, **kwargs):
|
||||
if user_id == 'null':
|
||||
@@ -2434,6 +2438,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_notification_log(self, **kwargs):
|
||||
""" Get the data on the Tautulli notification logs table.
|
||||
@@ -2495,6 +2500,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_newsletter_log(self, **kwargs):
|
||||
""" Get the data on the Tautulli newsletter logs table.
|
||||
@@ -3773,6 +3779,7 @@ class WebInterface(object):
|
||||
'update': True,
|
||||
'release': True,
|
||||
'message': 'A new release (%s) of Tautulli is available.' % plexpy.LATEST_RELEASE,
|
||||
'current_release': plexpy.common.RELEASE,
|
||||
'latest_release': plexpy.LATEST_RELEASE,
|
||||
'release_url': helpers.anon_url(
|
||||
'https://github.com/%s/%s/releases/tag/%s'
|
||||
@@ -3786,6 +3793,7 @@ class WebInterface(object):
|
||||
'update': True,
|
||||
'release': False,
|
||||
'message': 'A newer version of Tautulli is available.',
|
||||
'current_version': plexpy.CURRENT_VERSION,
|
||||
'latest_version': plexpy.LATEST_VERSION,
|
||||
'commits_behind': plexpy.COMMITS_BEHIND,
|
||||
'compare_url': helpers.anon_url(
|
||||
@@ -4341,7 +4349,7 @@ class WebInterface(object):
|
||||
```
|
||||
"""
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
result = pms_connect.get_search_results(query)
|
||||
result = pms_connect.get_search_results(query=query, limit=limit)
|
||||
|
||||
if result:
|
||||
return result
|
||||
@@ -5228,6 +5236,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_synced_items(self, machine_id='', user_id='', **kwargs):
|
||||
""" Get a list of synced items on the PMS.
|
||||
|
Reference in New Issue
Block a user