Compare commits

..

20 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
24 changed files with 342 additions and 129 deletions

1
API.md
View File

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

View File

@@ -1,9 +1,24 @@
# 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+
* 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.

View File

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

View File

@@ -324,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:

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.
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'))) {

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.
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'))) {

View File

@@ -15,9 +15,9 @@
<h3><span id="sessions-xml">Activity</span> &nbsp;&nbsp;
<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>

View File

@@ -560,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>' +
@@ -620,6 +622,7 @@ function closePlexOAuthWindow() {
}
getPlexOAuthPin = function () {
var x_plex_headers = getPlexHeaders();
var deferred = $.Deferred();
$.ajax({
@@ -648,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;

View File

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

View File

@@ -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">

View File

@@ -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&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 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');

View File

@@ -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"]}'
},

View File

@@ -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>

View File

@@ -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
@@ -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))

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 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.'},

View File

@@ -1117,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 = ''

View File

@@ -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)

View File

@@ -1456,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:
@@ -1478,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)

View File

@@ -3062,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()

View File

@@ -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):

View File

@@ -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)
@@ -2018,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):
"""

View File

@@ -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)

View File

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

View File

@@ -246,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.
```
@@ -264,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.'}
@@ -759,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,
@@ -947,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()
@@ -1557,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 #####
@@ -4354,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