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:
{"child_count": null,
"count": 887,
"deleted_section": 0,
"do_notify": 1,
"do_notify_created": 1,
"keep_history": 1,

View File

@@ -1,5 +1,28 @@
# 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:

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

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

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

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

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

View File

@@ -818,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', '')
@@ -828,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.")
@@ -840,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
@@ -1111,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)
@@ -1510,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
@@ -1524,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'):
@@ -2008,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 = "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()
@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