Compare commits

...

28 Commits

Author SHA1 Message Date
JonnyWong16
7d11e1de2d v2.1.29-beta 2019-04-14 13:43:58 -07:00
JonnyWong16
36aa4a6be3 Update API docs 2019-04-13 23:14:41 -07:00
JonnyWong16
24ed63e07c Add undelete button to edit library/user modal 2019-04-13 22:56:32 -07:00
JonnyWong16
f41ed9953a Improve API response codes 2019-04-13 22:53:56 -07:00
JonnyWong16
6970231687 Log script return status code 2019-04-13 22:01:03 -07:00
JonnyWong16
ea036aa354 Fix typo in notifier config triggers 2019-04-13 18:05:40 -07:00
JonnyWong16
d0a7c2f92c Fix typo in notification parameters list 2019-04-10 18:32:54 -07:00
JonnyWong16
f07acd839b Missing closing div tag in c1fd798 2019-04-07 14:22:49 -07:00
JonnyWong16
c1fd798fe9 Add prefix and suffix text modifiers to notifications 2019-04-04 19:21:26 -07:00
JonnyWong16
2f8d2f23fe Add user IP table column IDs 2019-03-30 13:22:01 -07:00
JonnyWong16
b65a30263e Fix user IP address last seen 2019-03-30 12:10:30 -07:00
JonnyWong16
766e33df0e Add getPlexHeaders function 2019-03-26 09:10:09 -07:00
JonnyWong16
68df0f07c8 Return API result error when unauthenticaed 2019-03-21 10:22:34 -07:00
JonnyWong16
819829554b Use global port and root when getting self URL 2019-03-21 08:50:28 -07:00
JonnyWong16
18a38b16b1 Fix a9169d2 2019-03-20 08:58:30 -07:00
JonnyWong16
a9169d2b53 Fix terminate stream when both session_key and session_id are provided 2019-03-20 08:49:15 -07:00
JonnyWong16
76b9b3e474 Improve terminate stream error messages 2019-03-18 11:57:08 -07:00
JonnyWong16
00405f0b18 Change wording in activity header 2019-03-18 10:42:02 -07:00
JonnyWong16
9dfeccdaed Fix update metadata serach only returning 3 results 2019-03-17 18:19:11 -07:00
JonnyWong16
b6d044fe8f Fix API search not using the limit parameter 2019-03-17 17:59:18 -07:00
JonnyWong16
e949b1486e v2.1.28 2019-03-10 14:47:26 -07:00
JonnyWong16
8e1b6efc51 Reword unable to delete from Cloudinary log message 2019-03-10 14:32:45 -07:00
JonnyWong16
00012ffe09 Merge pull request #1344 from Jabcob/patch-1
Update init.systemd
2019-03-10 14:28:01 -07:00
Jabcob
bcf6b4de77 Update init.systemd
Edited user creation and directory ownership instructions in the configuration notes for clarity. Current version may give novice users the impression that the ownership command is only executed on CentOS/Fedora.
2019-03-09 15:39:26 -06:00
JonnyWong16
b1516e9963 Improve mass delete from Cloudinary 2019-03-08 17:49:28 -08:00
JonnyWong16
231de3a7a5 Merge pull request #1343 from samwiseg0/fix/relayed_int
Fix relayed to be an integer vs string
2019-03-06 18:33:05 -08:00
samwiseg0
b611ea659e Fix relayed to be an integer vs string 2019-03-06 17:02:12 -08:00
JonnyWong16
6e9f299c19 Add secure/insecure icon to activity card 2019-03-05 21:55:13 -08:00
26 changed files with 390 additions and 141 deletions

1
API.md
View File

@@ -893,6 +893,7 @@ Returns:
json: json:
{"child_count": null, {"child_count": null,
"count": 887, "count": 887,
"deleted_section": 0,
"do_notify": 1, "do_notify": 1,
"do_notify_created": 1, "do_notify_created": 1,
"keep_history": 1, "keep_history": 1,

View File

@@ -1,5 +1,28 @@
# Changelog # 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) ## v2.1.27-beta (2019-03-03)
* Monitoring: * Monitoring:

View File

@@ -4209,3 +4209,8 @@ a[data-tab-destination] {
top: 0; top: 0;
z-index: 9999; z-index: 9999;
} }
.help-block li {
margin-top: 0;
color: #737373;
}

View File

@@ -283,10 +283,17 @@ DOCUMENTATION :: END
<li class="dashboard-activity-info-item"> <li class="dashboard-activity-info-item">
<div class="sub-heading">Location</div> <div class="sub-heading">Location</div>
<div class="sub-value time-right"> <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>: <span id="location-${sk}">${data['location'].upper()}</span>:
% if data['ip_address'] != 'N/A': % if data['ip_address'] != 'N/A':
<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>
% if data['relay']: % if data['relayed']:
<span data-toggle="tooltip" title="Plex Relay"><i class="fa fa-exclamation-circle"></i></span> <span data-toggle="tooltip" title="Plex Relay"><i class="fa fa-exclamation-circle"></i></span>
% else: % else:
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}"> <a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
@@ -317,7 +324,7 @@ DOCUMENTATION :: END
bw = str(bw) + ' kbps' bw = str(bw) + ' kbps'
%> %>
<span id="stream-bandwidth-${sk}">${bw}</span> <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: % elif data['synced_version'] == 1 or data['channel_stream'] == 1:
<span id="stream-bandwidth-${sk}">None</span> <span id="stream-bandwidth-${sk}">None</span>
% else: % else:

View File

@@ -21,6 +21,7 @@ parent_count Returns the parent item count for the library.
child_count Returns the child 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. 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. 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 DOCUMENTATION :: END
</%doc> </%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> <p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this library. This is permanent!</p>
</div> </div>
% endif % 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> </fieldset>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -100,6 +107,12 @@ DOCUMENTATION :: END
confirmAjaxCall(url, msg, { section_id: '${data["section_id"]}' }, null, function () { location.reload(); }); 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() { $(document).ready(function() {
// Move #confirm-modal to parent container // Move #confirm-modal to parent container
if (!($('#edit-library-modal').next().is('#confirm-modal-purge'))) { if (!($('#edit-library-modal').next().is('#confirm-modal-purge'))) {

View File

@@ -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. 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. 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. 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 DOCUMENTATION :: END
</%doc> </%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> <p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this user. This is permanent!</p>
</div> </div>
% endif % 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> </fieldset>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -122,6 +129,12 @@ DOCUMENTATION :: END
confirmAjaxCall(url, msg, { user_id: '${data["user_id"]}' }, null, function () { location.reload(); }); 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() { $(document).ready(function() {
// Move #confirm-modal-purge to parent container // Move #confirm-modal-purge to parent container
if (!($('#edit-user-modal').next().is('#confirm-modal-purge'))) { if (!($('#edit-user-modal').next().is('#confirm-modal-purge'))) {

View File

@@ -15,9 +15,9 @@
<h3><span id="sessions-xml">Activity</span> &nbsp;&nbsp; <h3><span id="sessions-xml">Activity</span> &nbsp;&nbsp;
<small> <small>
<span id="currentActivityHeader" style="display: none;"> <span id="currentActivityHeader" style="display: none;">
Streams: <span id="currentActivityHeader-streams"></span> | Sessions: <span id="currentActivityHeader-streams"></span> |
Bandwidth: <span id="currentActivityHeader-bandwidth"></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> </span>
</small> </small>
</h3> </h3>

View File

@@ -560,16 +560,18 @@ function uuidv4() {
}); });
} }
var x_plex_headers = { function getPlexHeaders() {
'Accept': 'application/json', return {
'X-Plex-Product': 'Tautulli', 'Accept': 'application/json',
'X-Plex-Version': 'Plex OAuth', 'X-Plex-Product': 'Tautulli',
'X-Plex-Client-Identifier': getLocalStorage('Tautulli_ClientID', uuidv4(), false), 'X-Plex-Version': 'Plex OAuth',
'X-Plex-Platform': p.name, 'X-Plex-Client-Identifier': getLocalStorage('Tautulli_ClientID', uuidv4(), false),
'X-Plex-Platform-Version': p.version, 'X-Plex-Platform': p.name,
'X-Plex-Device': p.os, 'X-Plex-Platform-Version': p.version,
'X-Plex-Device-Name': p.name 'X-Plex-Device': p.os,
}; 'X-Plex-Device-Name': p.name
};
}
var plex_oauth_window = null; var plex_oauth_window = null;
const plex_oauth_loader = '<style>' + const plex_oauth_loader = '<style>' +
@@ -620,6 +622,7 @@ function closePlexOAuthWindow() {
} }
getPlexOAuthPin = function () { getPlexOAuthPin = function () {
var x_plex_headers = getPlexHeaders();
var deferred = $.Deferred(); var deferred = $.Deferred();
$.ajax({ $.ajax({
@@ -648,6 +651,7 @@ function PlexOAuth(success, error, pre) {
$(plex_oauth_window.document.body).html(plex_oauth_loader); $(plex_oauth_window.document.body).html(plex_oauth_loader);
getPlexOAuthPin().then(function (data) { getPlexOAuthPin().then(function (data) {
var x_plex_headers = getPlexHeaders();
const pin = data.pin; const pin = data.pin;
const code = data.code; const code = data.code;

View File

@@ -150,6 +150,7 @@
token: token, token: token,
remember_me: remember_me remember_me: remember_me
}; };
var x_plex_headers = getPlexHeaders();
data = $.extend(data, x_plex_headers); data = $.extend(data, x_plex_headers);
$.ajax({ $.ajax({

View File

@@ -148,7 +148,7 @@
<div class="col-md-12"> <div class="col-md-12">
<label>Notification Triggers</label> <label>Notification Triggers</label>
<p class="help-block"> <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> </p>
% for action in available_notification_actions: % for action in available_notification_actions:
<div class="checkbox"> <div class="checkbox">

View File

@@ -1588,7 +1588,7 @@
<div> <div>
<h4>List Slicing</h4> <h4>List Slicing</h4>
</div> </div>
<div> <div style="padding-bottom: 10px;">
<p class="help-block"> <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. 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>. 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:[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> {actors:[1:5]} --> Only the 2nd to 5th actors (Actors: 1, 2, 3, 4)</pre>
</div> </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&lt;</span> and <span class="inline-pre">&gt;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: &lt;rating} --> Rating: 8.9
{rating&gt;/10} --> 8.9/10
{Rating: &lt;rating&gt;/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: &lt;rating&gt;/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 &lt;actors!c:[0]&gt; as the main character.} --> Starring Arnold Schwarzenegger as the main character.</pre>
</div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -1620,7 +1655,7 @@
<div class="modal-body"> <div class="modal-body">
<div> <div>
<p class="help-block"> <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> </p>
% for category in common.NEWSLETTER_PARAMETERS: % for category in common.NEWSLETTER_PARAMETERS:
<table class="notification-params"> <table class="notification-params">
@@ -2301,6 +2336,7 @@ $(document).ready(function() {
$("#token_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast'); $("#token_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
} }
function OAuthSuccessCallback(authToken) { function OAuthSuccessCallback(authToken) {
var x_plex_headers = getPlexHeaders();
$("#pms_token").val(authToken); $("#pms_token").val(authToken);
$("#pms_uuid").val(x_plex_headers['X-Plex-Client-Identifier']); $("#pms_uuid").val(x_plex_headers['X-Plex-Client-Identifier']);
$("#token_verify").html('<i class="fa fa-check"></i>').fadeIn('fast'); $("#token_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');

View File

@@ -183,6 +183,7 @@ DOCUMENTATION :: END
async: true, async: true,
data: { data: {
query: query_string, query: query_string,
limit: 30,
media_type: '${query["media_type"]}', media_type: '${query["media_type"]}',
season_index: '${query["parent_media_index"]}' season_index: '${query["parent_media_index"]}'
}, },

View File

@@ -274,12 +274,12 @@ DOCUMENTATION :: END
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%"> <table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
<thead> <thead>
<tr> <tr>
<th align="left">Last Seen</th> <th align="left" id="last_seen">Last Seen</th>
<th align="left">IP Address</th> <th align="left" id="ip_address">IP Address</th>
<th align="left">Last Platform</th> <th align="left" id="platform">Last Platform</th>
<th align="left">Last Player</th> <th align="left" id="player">Last Player</th>
<th align="left">Last Played</th> <th align="left" id="last_played">Last Played</th>
<th align="left">Play Count</th> <th align="left" id="play_count">Play Count</th>
</tr> </tr>
</thead> </thead>
</table> </table>

View File

@@ -24,9 +24,11 @@
# - The example settings in this file assume that Tautulli is installed to: /opt/Tautulli # - 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: # - 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 # 1. Create the user:
# CentOS/Fedora: sudo adduser --system --no-create-home tautulli # Ubuntu/Debian: sudo addgroup tautulli && sudo adduser --system --no-create-home tautulli --ingroup tautulli
# sudo chown tautulli:tautulli -R /opt/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: # - Adjust ExecStart= to point to:
# 1. Your Tautulli executable # 1. Your Tautulli executable

View File

@@ -54,6 +54,7 @@ class API2:
self._api_apikey = None self._api_apikey = None
self._api_callback = None # JSONP self._api_callback = None # JSONP
self._api_result_type = 'error' self._api_result_type = 'error'
self._api_response_code = None
self._api_profileme = None # For profiling the api call self._api_profileme = None # For profiling the api call
self._api_kwargs = None # Cleaned kwargs self._api_kwargs = None # Cleaned kwargs
self._api_app = False self._api_app = False
@@ -85,21 +86,27 @@ class API2:
if not plexpy.CONFIG.API_ENABLED: if not plexpy.CONFIG.API_ENABLED:
self._api_msg = 'API not enabled' self._api_msg = 'API not enabled'
self._api_response_code = 404
elif not plexpy.CONFIG.API_KEY: elif not plexpy.CONFIG.API_KEY:
self._api_msg = 'API key not generated' self._api_msg = 'API key not generated'
self._api_response_code = 401
elif len(plexpy.CONFIG.API_KEY) != 32: elif len(plexpy.CONFIG.API_KEY) != 32:
self._api_msg = 'API key not generated correctly' self._api_msg = 'API key not generated correctly'
self._api_response_code = 401
elif 'apikey' not in kwargs: elif 'apikey' not in kwargs:
self._api_msg = 'Parameter apikey is required' self._api_msg = 'Parameter apikey is required'
self._api_response_code = 401
elif 'cmd' not in kwargs: elif 'cmd' not in kwargs:
self._api_msg = 'Parameter cmd is required. Possible commands are: %s' % ', '.join(self._api_valid_methods) 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: 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_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_callback = kwargs.pop('callback', None)
self._api_apikey = kwargs.pop('apikey', None) self._api_apikey = kwargs.pop('apikey', None)
@@ -112,7 +119,7 @@ class API2:
if 'app' in kwargs and kwargs.pop('app') == 'true': if 'app' in kwargs and kwargs.pop('app') == 'true':
self._api_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): 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 self._api_authenticated = True
@@ -122,6 +129,7 @@ class API2:
else: else:
self._api_msg = 'Invalid apikey' self._api_msg = 'Invalid apikey'
self._api_response_code = 401
if self._api_authenticated and self._api_cmd in self._api_valid_methods: if self._api_authenticated and self._api_cmd in self._api_valid_methods:
self._api_msg = None self._api_msg = None
@@ -620,7 +628,7 @@ General optional parameters:
# if we fail to generate the output fake an error # if we fail to generate the output fake an error
except Exception as e: except Exception as e:
logger.api_exception(u'Tautulli APIv2 :: ' + traceback.format_exc()) logger.api_exception(u'Tautulli APIv2 :: ' + traceback.format_exc())
cherrypy.response.status = 500 self._api_response_code = 500
out['message'] = traceback.format_exc() out['message'] = traceback.format_exc()
out['result'] = 'error' out['result'] = 'error'
@@ -630,7 +638,7 @@ General optional parameters:
out = xmltodict.unparse(out, pretty=True) out = xmltodict.unparse(out, pretty=True)
except Exception as e: except Exception as e:
logger.api_error(u'Tautulli APIv2 :: Failed to parse xml result') logger.api_error(u'Tautulli APIv2 :: Failed to parse xml result')
cherrypy.response.status = 500 self._api_response_code = 500
try: try:
out['message'] = e out['message'] = e
out['result'] = 'error' out['result'] = 'error'
@@ -671,12 +679,12 @@ General optional parameters:
result = call(**self._api_kwargs) result = call(**self._api_kwargs)
except Exception as e: except Exception as e:
logger.api_error(u'Tautulli APIv2 :: Failed to run %s with %s: %s' % (self._api_cmd, self._api_kwargs, 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: if self._api_debug:
cherrypy.request.show_tracebacks = True cherrypy.request.show_tracebacks = True
# Reraise the exception so the traceback hits the browser # Reraise the exception so the traceback hits the browser
raise raise
self._api_msg = 'Check the logs' self._api_msg = 'Check the logs for errors'
ret = None ret = None
# The api decorated function can return different result types. # The api decorated function can return different result types.
@@ -700,12 +708,11 @@ General optional parameters:
if ret is None: if ret is None:
ret = result 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 # To allow override for restart etc
# if the call returns some data we are gonna assume its a success # if the call returns some data we are gonna assume its a success
self._api_result_type = 'success' self._api_result_type = 'success'
else: self._api_response_code = 200
self._api_result_type = 'error'
# Since some of them methods use a api like response for the ui # Since some of them methods use a api like response for the ui
# {result: error, message: 'Some shit happened'} # {result: error, message: 'Some shit happened'}
@@ -716,7 +723,13 @@ General optional parameters:
if ret.get('result'): if ret.get('result'):
self._api_result_type = ret.pop('result', None) self._api_result_type = ret.pop('result', None)
if self._api_result_type == 'error': if self._api_result_type == 'success' and not self._api_response_code:
cherrypy.response.status = 500 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)) return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))

View File

@@ -310,14 +310,14 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'}, {'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': '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': '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 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 notfication is triggered.', 'example': '1 to 12'}, {'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 notfication is triggered.', 'example': '1 to 31'}, {'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 notfication is triggered.', 'example': '0 to 23'}, {'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 notfication is triggered.', 'example': '0 to 59'}, {'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 notfication 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 notfication is triggered.', 'example': '1 (Mon) to 7 (Sun)'}, {'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 notfication is triggered.', 'example': '1 to 52'}, {'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': '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': '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.'}, {'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification is triggered.'},

View File

@@ -1259,8 +1259,11 @@ class DataFactory(object):
'GROUP BY rating_key' % where 'GROUP BY rating_key' % where
results = monitor_db.select(query, args=args) results = monitor_db.select(query, args=args)
for cloudinary_info in results: if delete_all:
helpers.delete_from_cloudinary(rating_key=cloudinary_info['rating_key']) 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." logger.info(u"Tautulli DataFactory :: Deleting Cloudinary info%s from the database."
% log_msg) % log_msg)

View File

@@ -818,7 +818,7 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
try: try:
response = upload('data:image/png;base64,{}'.format(base64.b64encode(img_data)), response = upload('data:image/png;base64,{}'.format(base64.b64encode(img_data)),
public_id='{}_{}'.format(fallback, rating_key), 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}) 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)) logger.debug(u"Tautulli Helpers :: Image '{}' ({}) uploaded to Cloudinary.".format(img_title, fallback))
img_url = response.get('url', '') img_url = response.get('url', '')
@@ -828,7 +828,7 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
return img_url return img_url
def delete_from_cloudinary(rating_key): def delete_from_cloudinary(rating_key=None, delete_all=False):
""" Deletes an image from Cloudinary """ """ 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: 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.") logger.error(u"Tautulli Helpers :: Cannot delete image from Cloudinary. Cloudinary settings not specified in the settings.")
@@ -840,9 +840,15 @@ def delete_from_cloudinary(rating_key):
api_secret=plexpy.CONFIG.CLOUDINARY_API_SECRET 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 return True
@@ -1111,13 +1117,13 @@ def get_plexpy_url(hostname=None):
else: else:
hostname = hostname or plexpy.CONFIG.HTTP_HOST hostname = hostname or plexpy.CONFIG.HTTP_HOST
if plexpy.CONFIG.HTTP_PORT not in (80, 443): if plexpy.HTTP_PORT not in (80, 443):
port = ':' + str(plexpy.CONFIG.HTTP_PORT) port = ':' + str(plexpy.HTTP_PORT)
else: else:
port = '' port = ''
if plexpy.CONFIG.HTTP_ROOT.strip('/'): if plexpy.HTTP_ROOT.strip('/'):
root = '/' + plexpy.CONFIG.HTTP_ROOT.strip('/') root = '/' + plexpy.HTTP_ROOT.strip('/')
else: else:
root = '' root = ''

View File

@@ -690,7 +690,8 @@ class Libraries(object):
'child_count': 0, 'child_count': 0,
'do_notify': 0, 'do_notify': 0,
'do_notify_created': 0, 'do_notify_created': 0,
'keep_history': 1 'keep_history': 1,
'deleted_section': 0
} }
if not section_id: if not section_id:
@@ -703,7 +704,7 @@ class Libraries(object):
if str(section_id).isdigit(): if str(section_id).isdigit():
query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \ query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \
'thumb AS library_thumb, custom_thumb_url AS custom_thumb, art, ' \ '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 ' \ 'FROM library_sections ' \
'WHERE section_id = ? ' 'WHERE section_id = ? '
result = monitor_db.select(query, args=[section_id]) result = monitor_db.select(query, args=[section_id])
@@ -733,7 +734,8 @@ class Libraries(object):
'child_count': item['child_count'], 'child_count': item['child_count'],
'do_notify': item['do_notify'], 'do_notify': item['do_notify'],
'do_notify_created': item['do_notify_created'], '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 return library_details
@@ -924,7 +926,8 @@ class Libraries(object):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
try: 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) result = monitor_db.select(query=query)
except Exception as e: except Exception as e:
logger.warn(u"Tautulli Libraries :: Unable to execute database query for get_sections: %s." % e) logger.warn(u"Tautulli Libraries :: Unable to execute database query for get_sections: %s." % e)
@@ -1001,23 +1004,31 @@ class Libraries(object):
try: try:
if section_id and section_id.isdigit(): if section_id and section_id.isdigit():
logger.info(u"Tautulli Libraries :: Re-adding library with id %s to database." % section_id) query = 'SELECT * FROM library_sections WHERE section_id = ?'
monitor_db.action('UPDATE library_sections SET deleted_section = 0 WHERE section_id = ?', [section_id]) result = monitor_db.select(query=query, args=[section_id])
monitor_db.action('UPDATE library_sections SET keep_history = 1 WHERE section_id = ?', [section_id]) if result:
monitor_db.action('UPDATE library_sections SET do_notify = 1 WHERE section_id = ?', [section_id]) logger.info(u"Tautulli Libraries :: Re-adding library with id %s to database." % section_id)
monitor_db.action('UPDATE library_sections SET do_notify_created = 1 WHERE section_id = ?', [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: elif section_name:
logger.info(u"Tautulli Libraries :: Re-adding library with name %s to database." % section_name) query = 'SELECT * FROM library_sections WHERE section_name = ?'
monitor_db.action('UPDATE library_sections SET deleted_section = 0 WHERE section_name = ?', [section_name]) result = monitor_db.select(query=query, args=[section_name])
monitor_db.action('UPDATE library_sections SET keep_history = 1 WHERE section_name = ?', [section_name]) if result:
monitor_db.action('UPDATE library_sections SET do_notify = 1 WHERE section_name = ?', [section_name]) logger.info(u"Tautulli Libraries :: Re-adding library with name %s to database." % section_name)
monitor_db.action('UPDATE library_sections SET do_notify_created = 1 WHERE section_name = ?', [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: except Exception as e:
logger.warn(u"Tautulli Libraries :: Unable to execute database query for undelete: %s." % e) logger.warn(u"Tautulli Libraries :: Unable to execute database query for undelete: %s." % e)

View File

@@ -1456,9 +1456,8 @@ def get_themoviedb_info(rating_key=None, media_type=None, themoviedb_id=None):
class CustomFormatter(Formatter): class CustomFormatter(Formatter):
def __init__(self, default='{{{0}}}', default_format_spec='{{{0}:{1}}}'): def __init__(self, default='{{{0}}}'):
self.default = default self.default = default
self.default_format_spec = default_format_spec
def convert_field(self, value, conversion): def convert_field(self, value, conversion):
if conversion is None: if conversion is None:
@@ -1478,23 +1477,99 @@ class CustomFormatter(Formatter):
def format_field(self, value, format_spec): def format_field(self, value, format_spec):
if format_spec.startswith('[') and format_spec.endswith(']'): if format_spec.startswith('[') and format_spec.endswith(']'):
pattern = re.compile(r'\[(-?\d*):?(-?\d*)\]') pattern = re.compile(r'\[(?P<start>-?\d*)(?P<slice>:?)(?P<end>-?\d*)\]')
if re.match(pattern, format_spec): # slice match = re.match(pattern, format_spec)
if value and match:
groups = match.groupdict()
items = [x.strip() for x in unicode(value).split(',')] items = [x.strip() for x in unicode(value).split(',')]
slice_start, slice_end = re.search(pattern, format_spec).groups() start = groups['start'] or None
slice_start = helpers.cast_to_int(slice_start) or None end = groups['end'] or None
slice_end = helpers.cast_to_int(slice_end) or None if start is not None:
return ', '.join(items[slice(slice_start, slice_end)]) start = helpers.cast_to_int(start)
else: if end is not None:
return value end = helpers.cast_to_int(end)
if not groups['slice']:
end = start + 1
value = ', '.join(items[slice(start, end)])
return value
else: else:
try: try:
return super(CustomFormatter, self).format_field(value, format_spec) return super(CustomFormatter, self).format_field(value, format_spec)
except ValueError: except ValueError:
return self.default_format_spec.format(value[1:-1], format_spec) return value
def get_value(self, key, args, kwargs): def get_value(self, key, args, kwargs):
if isinstance(key, basestring): if isinstance(key, basestring):
return kwargs.get(key, self.default.format(key)) return kwargs.get(key, self.default.format(key))
else: else:
return super(CustomFormatter, self).get_value(key, args, kwargs) 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)

View File

@@ -3062,6 +3062,7 @@ class SCRIPTS(Notifier):
timer.start() timer.start()
output, error = process.communicate() output, error = process.communicate()
status = process.returncode status = process.returncode
logger.debug(u"Tautulli Notifiers :: Subprocess returned with status code %s." % status)
finally: finally:
if timer: if timer:
timer.cancel() timer.cancel()

View File

@@ -842,9 +842,13 @@ class PlexTV(object):
return False return False
if subscription and helpers.get_xml_attr(subscription[0], 'active') == '1': if subscription and helpers.get_xml_attr(subscription[0], 'active') == '1':
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 1)
plexpy.CONFIG.write()
return True return True
else: else:
logger.debug(u"Tautulli PlexTV :: Plex Pass subscription not found.") logger.debug(u"Tautulli PlexTV :: Plex Pass subscription not found.")
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 0)
plexpy.CONFIG.write()
return False return False
def get_devices_list(self): def get_devices_list(self):

View File

@@ -101,7 +101,7 @@ class PmsConnect(object):
Output: array 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 = self.request_handler.make_request(uri=uri,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -1510,7 +1510,9 @@ class PmsConnect(object):
'player': helpers.get_xml_attr(player_info, 'title') or helpers.get_xml_attr(player_info, 'product'), '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'), 'machine_id': helpers.get_xml_attr(player_info, 'machineIdentifier'),
'state': helpers.get_xml_attr(player_info, 'state'), '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 # Get the session details
@@ -1524,12 +1526,20 @@ class PmsConnect(object):
else: else:
session_details = {'session_id': '', session_details = {'session_id': '',
'bandwidth': '', 'bandwidth': '',
'location': 'wan' if player_details['local'] == '0' else 'lan' 'location': 'lan' if player_details['local'] else 'wan'
} }
# Check if using Plex Relay # Check if using Plex Relay
session_details['relay'] = int(session_details['location'] != 'lan' if player_details['relayed'] is None:
and player_details['ip_address_public'] == '127.0.0.1') 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 # Get the transcode details
if session.getElementsByTagName('TranscodeSession'): if session.getElementsByTagName('TranscodeSession'):
@@ -2008,25 +2018,42 @@ class PmsConnect(object):
Output: bool 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.' message = message.encode('utf-8') or 'The server owner has ended the stream.'
if session_key and not session_id: ap = activity_processor.ActivityProcessor()
ap = activity_processor.ActivityProcessor()
session = ap.get_session_by_key(session_key=session_key)
session_id = session['session_id']
elif session_id and not session_key: if session_key:
ap = activity_processor.ActivityProcessor() 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 = 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: if session_id:
logger.info(u"Tautulli Pmsconnect :: Terminating session %s (session_id %s)." % (session_key, 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)) result = self.get_sessions_terminate(session_id=session_id, reason=message)
return result return True
else: else:
logger.warn(u"Tautulli Pmsconnect :: Failed to terminate session %s. Missing session_id." % session_key) msg = 'Missing session_id'
return False logger.warn(u"Tautulli Pmsconnect :: Failed to terminate session: %s." % msg)
return msg
def get_item_children(self, rating_key='', get_grandchildren=False): def get_item_children(self, rating_key='', get_grandchildren=False):
""" """

View File

@@ -205,7 +205,7 @@ class Users(object):
custom_where = ['users.user_id', user_id] custom_where = ['users.user_id', user_id]
columns = ['session_history.id', columns = ['session_history.id',
'session_history.started AS last_seen', 'MAX(session_history.started) AS last_seen',
'session_history.ip_address', 'session_history.ip_address',
'COUNT(session_history.id) AS play_count', 'COUNT(session_history.id) AS play_count',
'session_history.platform', 'session_history.platform',
@@ -668,21 +668,29 @@ class Users(object):
try: try:
if user_id and str(user_id).isdigit(): if user_id and str(user_id).isdigit():
logger.info(u"Tautulli Users :: Re-adding user with id %s to database." % user_id) query = 'SELECT * FROM users WHERE user_id = ?'
monitor_db.action('UPDATE users SET deleted_user = 0 WHERE user_id = ?', [user_id]) result = monitor_db.select(query=query, args=[user_id])
monitor_db.action('UPDATE users SET keep_history = 1 WHERE user_id = ?', [user_id]) if result:
monitor_db.action('UPDATE users SET do_notify = 1 WHERE user_id = ?', [user_id]) 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: elif username:
logger.info(u"Tautulli Users :: Re-adding user with username %s to database." % username) query = 'SELECT * FROM users WHERE username = ?'
monitor_db.action('UPDATE users SET deleted_user = 0 WHERE username = ?', [username]) result = monitor_db.select(query=query, args=[username])
monitor_db.action('UPDATE users SET keep_history = 1 WHERE username = ?', [username]) if result:
monitor_db.action('UPDATE users SET do_notify = 1 WHERE username = ?', [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])
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: except Exception as e:
logger.warn(u"Tautulli Users :: Unable to execute database query for undelete: %s." % e) logger.warn(u"Tautulli Users :: Unable to execute database query for undelete: %s." % e)

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta" PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.1.27-beta" PLEXPY_RELEASE_VERSION = "v2.1.29-beta"

View File

@@ -246,7 +246,7 @@ class WebInterface(object):
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@addtoapi() @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. """ Stop a streaming session.
``` ```
@@ -264,8 +264,10 @@ class WebInterface(object):
pms_connect = pmsconnect.PmsConnect() pms_connect = pmsconnect.PmsConnect()
result = pms_connect.terminate_session(session_key=session_key, session_id=session_id, message=message) 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.'} return {'result': 'success', 'message': 'Session terminated.'}
elif result:
return {'result': 'error', 'message': 'Failed to terminate session: {}.'.format(result)}
else: else:
return {'result': 'error', 'message': 'Failed to terminate session.'} return {'result': 'error', 'message': 'Failed to terminate session.'}
@@ -759,6 +761,7 @@ class WebInterface(object):
json: json:
{"child_count": null, {"child_count": null,
"count": 887, "count": 887,
"deleted_section": 0,
"do_notify": 1, "do_notify": 1,
"do_notify_created": 1, "do_notify_created": 1,
"keep_history": 1, "keep_history": 1,
@@ -947,19 +950,14 @@ class WebInterface(object):
``` ```
""" """
library_data = libraries.Libraries() library_data = libraries.Libraries()
result = library_data.undelete(section_id=section_id, section_name=section_name)
if section_id: if result:
delete_row = library_data.undelete(section_id=section_id) if section_id:
msg ='section_id %s' % section_id
if delete_row: elif section_name:
return {'message': delete_row} msg = 'section_name %s' % section_name
elif section_name: return {'result': 'success', 'message': 'Re-added library with %s.' % msg}
delete_row = library_data.undelete(section_name=section_name) return {'result': 'error', 'message': 'Unable to re-add library. Invalid section_id or section_name.'}
if delete_row:
return {'message': delete_row}
else:
return {'message': 'no data received'}
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@@ -1557,18 +1555,15 @@ class WebInterface(object):
None None
``` ```
""" """
if user_id: user_data = users.Users()
user_data = users.Users() result = user_data.undelete(user_id=user_id, username=username)
delete_row = user_data.undelete(user_id=user_id) if result:
if delete_row: if user_id:
return {'message': delete_row} msg ='user_id %s' % user_id
elif username: elif username:
user_data = users.Users() msg = 'username %s' % username
delete_row = user_data.undelete(username=username) return {'result': 'success', 'message': 'Re-added user with %s.' % msg}
if delete_row: return {'result': 'error', 'message': 'Unable to re-add user. Invalid user_id or username.'}
return {'message': delete_row}
else:
return {'message': 'no data received'}
##### History ##### ##### History #####
@@ -4354,7 +4349,7 @@ class WebInterface(object):
``` ```
""" """
pms_connect = pmsconnect.PmsConnect() pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_search_results(query) result = pms_connect.get_search_results(query=query, limit=limit)
if result: if result:
return result return result