Compare commits

..

26 Commits

Author SHA1 Message Date
JonnyWong16
61fac10079 v2.1.27-beta 2019-03-03 15:30:42 -08:00
JonnyWong16
536e8add17 Encode email exception to unicode 2019-03-03 14:10:05 -08:00
JonnyWong16
cb81bcac57 Return default blank content type (Fixes Tautulli/Tautulli-Issues#165) 2019-03-03 13:39:45 -08:00
JonnyWong16
5dd7806c0e Combine recently added manually due to change in Plex recently added API 2019-03-02 18:42:43 -08:00
JonnyWong16
2a707fc512 Fix typo in f6f5df3 2019-02-24 14:39:07 -08:00
JonnyWong16
469e54a22c Add current release/version to update_check API 2019-02-24 14:36:21 -08:00
JonnyWong16
f6f5df3d1e Add ability to dismiss browser warning 2019-02-24 14:27:34 -08:00
JonnyWong16
ae0960d2e2 Merge pull request #1341 from Arcanemagus/systemd-restart-note
Add Systemd auto restart policy
2019-02-24 14:00:30 -08:00
Landon Abney
a646cc36a1 Enable by default
With the protections from an infinite restart loop in place this should 
be safe to enable by default.
2019-02-24 13:58:19 -08:00
Landon Abney
b243ac5f5c Add burst failure protection
If the process fails 3 times within 90 seconds of a start attempt 
consider it permanently failed and stop all further attempts to restart 
it automatically.
2019-02-24 13:55:33 -08:00
JonnyWong16
bca7744bc5 Remove unicode from Email notification failed error message 2019-02-24 12:51:27 -08:00
JonnyWong16
2fc826c88f Fix some local variable references (Also Fixes Tautulli/Tautulli-Issues#155) 2019-02-24 12:34:55 -08:00
JonnyWong16
6397b1e5a7 Show tags in notification text preview modal 2019-02-24 11:18:18 -08:00
Landon Abney
85b9a47a0d Add note on how to restart automatically
If Tautulli ever crashes due to a failure of some sort the policies 
mentioned here will automatically restart it, with the caveat that they 
will _always_ restart it, even if it is going to crash right away again!
2019-02-22 23:09:34 -08:00
JonnyWong16
5749ab7c92 Remove execute permision from systemd init script (Fixes Tautulli/Tautulli-Issues#149) 2019-02-22 19:50:06 -08:00
JonnyWong16
dcb56cfd20 Add message to complete setup wizard 2019-02-20 21:38:13 -08:00
JonnyWong16
90849f9196 Remove crypto donation 2019-02-20 20:56:50 -08:00
JonnyWong16
5b77cab575 Update Patreon URL 2019-02-20 20:55:59 -08:00
JonnyWong16
6a21d7690a Improve data sanitation (Fixes Tautulli/Tautulli-Issues#161) 2019-02-20 18:35:04 -08:00
JonnyWong16
037e983350 Change PMS beta update check URL 2019-02-19 21:20:34 -08:00
JonnyWong16
aa023f0166 v2.1.26 2018-12-01 15:50:04 -08:00
JonnyWong16
571b5461c0 Fix stream info graph modal history table (Fixes Tautulli/Tautulli-Issues#142) 2018-12-01 15:30:16 -08:00
JonnyWong16
a749b71f7f Fix activity resume after buffering 2018-12-01 15:17:33 -08:00
JonnyWong16
ac259214f7 Merge pull request #1333 from samwiseg00/add/user_email
Add user_email parameter for notifications
2018-11-04 11:49:42 -08:00
JonnyWong16
e11803685c Fix API error when missing cmd 2018-11-04 11:49:13 -08:00
samwiseg00
e4c3601312 Add user_email parameter for notifications 2018-11-04 14:18:34 -05:00
21 changed files with 179 additions and 96 deletions

View File

@@ -1,5 +1,39 @@
# Changelog # Changelog
## v2.1.27-beta (2019-03-03)
* Monitoring:
* Fix: Error when playing synced optimized versions.
* Change: Show message to complete the setup wizard instead of error communicating with server message.
* Change: URL changed on Plex.tv for Plex Media Server beta updates.
* Notifications:
* New: Show the media type exclusion tags in the text preview modal.
* Fix: Unicode error in the Email notification failed response message.
* Fix: Error when a notification agent response is missing the "Content-Type" header.
* UI:
* Fix: Usernames were not being sanitized in dropdown selectors.
* Change: Different display of "All" recently added items on the homepage due to change in the Plex Media Server v1.15+ API.
* API:
* New: Added current Tautulli version to update_check API response.
* Change: API no longer returns sanitized HTML response data.
* Other:
* New: Added auto-restart to systemd init script.
* Fix: Patreon donation URL.
* Remove: Crypto donation options.
## v2.1.26 (2018-12-01)
* Monitoring:
* Fix: Resume event not being triggered after buffering.
* Notifications:
* New: Added user email as a notification parameter.
* Graphs:
* Fix: History model showing no results for stream info graph.
* API:
* Fix: API returning error when missing a cmd.
## v2.1.25 (2018-11-03) ## v2.1.25 (2018-11-03)
* Monitoring: * Monitoring:

View File

@@ -209,7 +209,7 @@ ${next.modalIncludes()}
</div> </div>
</div> </div>
% else: % else:
<div id="donate-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="crypto-donate-modal"> <div id="donate-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="donate-modal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -230,17 +230,13 @@ ${next.modalIncludes()}
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;"> <ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li> <li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li> <li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoin" data-name="Bitcoin" data-address="3FdfJAyNWU15Sf11U9FTgPHuP1hPz32eEN">Bitcoin</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoincash" data-name="Bitcoin Cash" data-address="1H2atabxAQGaFAWYQEiLkXKSnK9CZZvt2n">Bitcoin Cash</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="ethereum" data-name="Ethereum" data-address="0x77ae4c2b8de1a1ccfa93553db39971da58c873d3">Ethereum</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="litecoin" data-name="Litecoin" data-address="LWpPmUqQYHBhMV83XSCsHzPmKLhJt6r57J">Litecoin</a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="patreon-donation" style="text-align: center"> <div role="tabpanel" class="tab-pane active" id="patreon-donation" style="text-align: center">
<p> <p>
Click the button below to continue to Patreon. Click the button below to continue to Patreon.
</p> </p>
<a href="${anon_url('https://www.patreon.com/bePatron?u=10078609')}" target="_blank"> <a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank">
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40"> <img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
</a> </a>
</div> </div>
@@ -252,12 +248,6 @@ ${next.modalIncludes()}
<img src="images/gold-rect-paypal-34px.png" alt="PayPal"> <img src="images/gold-rect-paypal-34px.png" alt="PayPal">
</a> </a>
</div> </div>
<div role="tabpanel" class="tab-pane" id="crypto-donation">
<label>QR Code</label>
<pre id="crypto_qr_code" style="text-align: center"></pre>
<label><span id="crypto_type_label"></span> Address</label>
<pre id="crypto_address" style="text-align: center"></pre>
</div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -293,7 +283,6 @@ ${next.modalIncludes()}
<script src="${http_root}js/pnotify.custom.min.js"></script> <script src="${http_root}js/pnotify.custom.min.js"></script>
<script src="${http_root}js/platform.min.js"></script> <script src="${http_root}js/platform.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script> <script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/jquery.qrcode.min.js"></script>
<script src="${http_root}js/jquery.tripleclick.min.js"></script> <script src="${http_root}js/jquery.tripleclick.min.js"></script>
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS: % if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
<script src="${http_root}js/ajaxNotifications.js"></script> <script src="${http_root}js/ajaxNotifications.js"></script>
@@ -365,16 +354,6 @@ ${next.modalIncludes()}
checkUpdate(function () { $('#nav-update').html('<i class="fa fa-fw fa-arrow-alt-circle-up"></i> Check for Updates'); }); checkUpdate(function () { $('#nav-update').html('<i class="fa fa-fw fa-arrow-alt-circle-up"></i> Check for Updates'); });
}); });
$('#donation_type a.crypto-donation').on('shown.bs.tab', function () {
var crypto_coin = $(this).data('coin');
var crypto_name = $(this).data('name');
var crypto_address = $(this).data('address')
$('#crypto_qr_code').empty().qrcode({
text: crypto_coin + ":" + crypto_address
});
$('#crypto_type_label').html(crypto_name);
$('#crypto_address').html(crypto_address);
});
% endif % endif
$('.dropdown-toggle').click(function (e) { $('.dropdown-toggle').click(function (e) {

View File

@@ -4204,7 +4204,7 @@ a[data-tab-destination] {
background: #cc7b19; background: #cc7b19;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
padding-top: 2px; padding: 2px 10px;
position: absolute; position: absolute;
top: 0; top: 0;
z-index: 9999; z-index: 9999;

View File

@@ -54,7 +54,7 @@
json_data: JSON.stringify(d), json_data: JSON.stringify(d),
user_id: "${data['user_id']}", user_id: "${data['user_id']}",
start_date: "${data['start_date']}", start_date: "${data['start_date']}",
media_type: "${data.get('media_type')}", media_type: "${data.get('media_type') or 'all'}",
transcode_decision: "${data.get('transcode_decision')}" transcode_decision: "${data.get('transcode_decision')}"
}; };
} }

View File

@@ -27,6 +27,8 @@
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div> <div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
% elif config['pms_is_cloud']: % elif config['pms_is_cloud']:
<div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div> <div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div>
% elif not config['first_run_complete']:
<div id="dashboard-no-activity" class="text-muted">The Tautulli setup wizard has not been completed. Please click <a href="welcome">here</a> to go to the setup wizard.</div>
% else: % else:
<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server. <div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':

View File

@@ -10,10 +10,24 @@ if (typeof platform !== 'undefined') {
} }
if (['IE', 'Microsoft Edge', 'IE Mobile'].indexOf(p.name) > -1) { if (['IE', 'Microsoft Edge', 'IE Mobile'].indexOf(p.name) > -1) {
$('body').prepend('<div id="browser-warning"><i class="fa fa-exclamation-circle"></i>&nbsp;' + if (!getCookie('browserDismiss')) {
var $browser_warning = $('<div id="browser-warning">' +
'<i class="fa fa-exclamation-circle"></i>&nbsp;' +
'Tautulli does not support Internet Explorer or Microsoft Edge! ' + 'Tautulli does not support Internet Explorer or Microsoft Edge! ' +
'Please use a different browser such as Chrome or Firefox.</div>'); 'Please use a different browser such as Chrome or Firefox.' +
var offset = $('#browser-warning').height(); '<button type="button" class="close"><i class="fa fa-remove"></i></button>' +
'</div>');
$('body').prepend($browser_warning);
var offset = $browser_warning.height();
warningOffset(offset);
$browser_warning.one('click', 'button.close', function () {
$browser_warning.remove();
warningOffset(-offset);
setCookie('browserDismiss', 'true', 7);
});
function warningOffset(offset) {
var navbar = $('.navbar-fixed-top'); var navbar = $('.navbar-fixed-top');
if (navbar.length) { if (navbar.length) {
navbar.offset({top: navbar.offset().top + offset}); navbar.offset({top: navbar.offset().top + offset});
@@ -23,6 +37,8 @@ if (['IE', 'Microsoft Edge', 'IE Mobile'].indexOf(p.name) > -1) {
container.offset({top: container.offset().top + offset}); container.offset({top: container.offset().top + offset});
} }
} }
}
}
function initConfigCheckbox(elem, toggleElem, reverse) { function initConfigCheckbox(elem, toggleElem, reverse) {
toggleElem = (toggleElem === undefined) ? null : toggleElem; toggleElem = (toggleElem === undefined) ? null : toggleElem;

View File

@@ -8,7 +8,12 @@
% if text: % if text:
% for item in text: % for item in text:
<div style="padding-bottom: 10px;"> <div style="padding-bottom: 10px;">
<h4>${item['media_type'].capitalize()}</h4> <h4>
${item['media_type'].capitalize()}
% if item['media_type'] != 'server':
<span class="inline-pre">&lt;${item['media_type']}&gt;&lt;/${item['media_type']}&gt;</span> tags
% endif
</h4>
% if agent != 'webhook': % if agent != 'webhook':
<pre>${item['subject']}</pre> <pre>${item['subject']}</pre>
% endif % endif

View File

@@ -1532,7 +1532,7 @@
<div style="padding-bottom: 10px;"> <div style="padding-bottom: 10px;">
<p class="help-block"> <p class="help-block">
All text inside <span class="inline-pre">&lt;show&gt;&lt;/show&gt;</span>/<span class="inline-pre">&lt;season&gt;&lt;/season&gt;</span>/<span class="inline-pre">&lt;episode&gt;&lt;/episode&gt;</span> All text inside <span class="inline-pre">&lt;show&gt;&lt;/show&gt;</span>/<span class="inline-pre">&lt;season&gt;&lt;/season&gt;</span>/<span class="inline-pre">&lt;episode&gt;&lt;/episode&gt;</span>
tags will only be sent when the media type is show/season/episode. tags will only be sent when the media type is show, season, or episode, respectively.
</p> </p>
<p><strong style="color: #fff;">Example:</strong></p> <p><strong style="color: #fff;">Example:</strong></p>
<pre>{show_name}&lt;season&gt; - Season {season_num}&lt;/season&gt;&lt;episode&gt; - S{season_num}E{episode_num} - {episode_name}&lt;/episode&gt; was recently added to Plex.</pre> <pre>{show_name}&lt;season&gt; - Season {season_num}&lt;/season&gt;&lt;episode&gt; - S{season_num}E{episode_num} - {episode_name}&lt;/episode&gt; was recently added to Plex.</pre>
@@ -1543,7 +1543,7 @@
<div> <div>
<p class="help-block"> <p class="help-block">
All text inside <span class="inline-pre">&lt;artist&gt;&lt;/artist&gt;</span>/<span class="inline-pre">&lt;album&gt;&lt;/album&gt;</span>/<span class="inline-pre">&lt;track&gt;&lt;/track&gt;</span> All text inside <span class="inline-pre">&lt;artist&gt;&lt;/artist&gt;</span>/<span class="inline-pre">&lt;album&gt;&lt;/album&gt;</span>/<span class="inline-pre">&lt;track&gt;&lt;/track&gt;</span>
tags will only be sent when the media type is artist/album/track. tags will only be sent when the media type is artist, album, or track, respectively.
</p> </p>
<p><strong style="color: #fff;">Example:</strong></p> <p><strong style="color: #fff;">Example:</strong></p>
<pre>{artist_name}&lt;album&gt; - {album_name}&lt;/album&gt;&lt;track&gt; - {album_name} - {track_name}&lt;/track&gt; was recently added to Plex.</pre> <pre>{artist_name}&lt;album&gt; - {album_name}&lt;/album&gt;&lt;track&gt; - {album_name} - {track_name}&lt;/track&gt; was recently added to Plex.</pre>
@@ -2530,8 +2530,10 @@ $(document).ready(function() {
.prop('selected', selected)); .prop('selected', selected));
} }
var download_url = 'https://plex.tv/api/downloads/' + (plex_update_channel === 'plexpass' ? '5' : '1') + '.json?channel=' + plex_update_channel;
$.ajax({ $.ajax({
url: 'https://plex.tv/api/downloads/1.json?channel=' + plex_update_channel, url: download_url,
type: 'GET', type: 'GET',
dataType: 'json', dataType: 'json',
beforeSend: function (xhr) { beforeSend: function (xhr) {

4
init-scripts/init.systemd Executable file → Normal file
View File

@@ -53,6 +53,10 @@ GuessMainPID=no
Type=forking Type=forking
User=tautulli User=tautulli
Group=tautulli Group=tautulli
Restart=on-abnormal
RestartSec=5
StartLimitInterval=90
StartLimitBurst=3
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -266,6 +266,7 @@ class ActivityHandler(object):
last_rating_key = str(db_session['rating_key']) last_rating_key = str(db_session['rating_key'])
last_live_uuid = db_session['live_uuid'] last_live_uuid = db_session['live_uuid']
last_transcode_key = db_session['transcode_key'].split('/')[-1] last_transcode_key = db_session['transcode_key'].split('/')[-1]
last_paused = db_session['last_paused']
# Make sure the same item is being played # Make sure the same item is being played
if this_rating_key == last_rating_key or this_live_uuid == last_live_uuid: if this_rating_key == last_rating_key or this_live_uuid == last_live_uuid:
@@ -280,7 +281,7 @@ class ActivityHandler(object):
if this_state != last_state: if this_state != last_state:
if this_state == 'paused': if this_state == 'paused':
self.on_pause() self.on_pause()
elif last_state == 'paused' and this_state == 'playing': elif last_paused and this_state == 'playing':
self.on_resume() self.on_resume()
elif this_state == 'stopped': elif this_state == 'stopped':
self.on_stop() self.on_stop()

View File

@@ -598,7 +598,7 @@ General optional parameters:
if self._api_cmd == 'docs_md': if self._api_cmd == 'docs_md':
return out['response']['data'] return out['response']['data']
elif self._api_cmd.startswith('download_'): elif self._api_cmd and self._api_cmd.startswith('download_'):
return return
elif self._api_cmd == 'pms_image_proxy': elif self._api_cmd == 'pms_image_proxy':

View File

@@ -331,6 +331,7 @@ NOTIFICATION_PARAMETERS = [
{'name': 'User Streams', 'type': 'int', 'value': 'user_streams', 'description': 'The number of concurrent streams by the person streaming.'}, {'name': 'User Streams', 'type': 'int', 'value': 'user_streams', 'description': 'The number of concurrent streams by the person streaming.'},
{'name': 'User', 'type': 'str', 'value': 'user', 'description': 'The friendly name of the person streaming.'}, {'name': 'User', 'type': 'str', 'value': 'user', 'description': 'The friendly name of the person streaming.'},
{'name': 'Username', 'type': 'str', 'value': 'username', 'description': 'The username of the person streaming.'}, {'name': 'Username', 'type': 'str', 'value': 'username', 'description': 'The username of the person streaming.'},
{'name': 'User Email', 'type': 'str', 'value': 'user_email', 'description': 'The email address of the person streaming.'},
{'name': 'Device', 'type': 'str', 'value': 'device', 'description': 'The type of client device being used for playback.'}, {'name': 'Device', 'type': 'str', 'value': 'device', 'description': 'The type of client device being used for playback.'},
{'name': 'Platform', 'type': 'str', 'value': 'platform', 'description': 'The type of client platform being used for playback.'}, {'name': 'Platform', 'type': 'str', 'value': 'platform', 'description': 'The type of client platform being used for playback.'},
{'name': 'Product', 'type': 'str', 'value': 'product', 'description': 'The type of client product being used for playback.'}, {'name': 'Product', 'type': 'str', 'value': 'product', 'description': 'The type of client product being used for playback.'},

View File

@@ -101,10 +101,6 @@ class DataTables(object):
# Paginate results # Paginate results
result = filtered[parameters['start']:(parameters['start'] + parameters['length'])] result = filtered[parameters['start']:(parameters['start'] + parameters['length'])]
# Sanitize on the way out
result = [{k: helpers.sanitize(v) if isinstance(v, basestring) else v for k, v in row.iteritems()}
for row in result]
output = {'result': result, output = {'result': result,
'draw': draw_counter, 'draw': draw_counter,
'filteredCount': len(filtered), 'filteredCount': len(filtered),

View File

@@ -522,11 +522,28 @@ def process_json_kwargs(json_kwargs):
return params return params
def sanitize(string): def sanitize_out(*dargs, **dkwargs):
if string: """ Helper decorator that sanitized the output
return unicode(string).replace('<','&lt;').replace('>','&gt;') """
def rd(function):
@wraps(function)
def wrapper(*args, **kwargs):
return sanitize(function(*args, **kwargs))
return wrapper
return rd
def sanitize(obj):
if isinstance(obj, basestring):
return unicode(obj).replace('<', '&lt;').replace('>', '&gt;')
elif isinstance(obj, list):
return [sanitize(o) for o in obj]
elif isinstance(obj, dict):
return {k: sanitize(v) for k, v in obj.iteritems()}
elif isinstance(obj, tuple):
return tuple(sanitize(list(obj)))
else: else:
return '' return obj
def is_public_ip(host): def is_public_ip(host):

View File

@@ -749,6 +749,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'user_streams': user_stream_count, 'user_streams': user_stream_count,
'user': notify_params['friendly_name'], 'user': notify_params['friendly_name'],
'username': notify_params['user'], 'username': notify_params['user'],
'user_email': notify_params['email'],
'device': notify_params['device'], 'device': notify_params['device'],
'platform': notify_params['platform'], 'platform': notify_params['platform'],
'product': notify_params['product'], 'product': notify_params['product'],

View File

@@ -746,14 +746,14 @@ class PrettyMetadata(object):
title = '%s - %s' % (self.parameters['artist_name'], self.parameters['album_name']) title = '%s - %s' % (self.parameters['artist_name'], self.parameters['album_name'])
elif self.media_type == 'track': elif self.media_type == 'track':
title = '%s - %s' % (self.parameters['track_name'], self.parameters['track_artist']) title = '%s - %s' % (self.parameters['track_name'], self.parameters['track_artist'])
return title.encode("utf-8") return title.encode('utf-8')
def get_description(self): def get_description(self):
if self.media_type == 'track': if self.media_type == 'track':
description = self.parameters['album_name'] description = self.parameters['album_name']
else: else:
description = self.parameters['summary'] description = self.parameters['summary']
return description.encode("utf-8") return description.encode('utf-8')
def get_plex_url(self): def get_plex_url(self):
return self.parameters['plex_url'] return self.parameters['plex_url']
@@ -863,9 +863,9 @@ class ANDROIDAPP(Notifier):
pretty_metadata = PrettyMetadata(kwargs.get('parameters')) pretty_metadata = PrettyMetadata(kwargs.get('parameters'))
plaintext_data = {'notification_id': notification_id, plaintext_data = {'notification_id': notification_id,
'subject': subject.encode('UTF-8'), 'subject': subject.encode('utf-8'),
'body': body.encode('UTF-8'), 'body': body.encode('utf-8'),
'action': action.encode('UTF-8'), 'action': action.encode('utf-8'),
'priority': self.config['priority'], 'priority': self.config['priority'],
'session_key': pretty_metadata.parameters.get('session_key',''), 'session_key': pretty_metadata.parameters.get('session_key',''),
'session_id': pretty_metadata.parameters.get('session_id',''), 'session_id': pretty_metadata.parameters.get('session_id',''),
@@ -1130,9 +1130,9 @@ class DISCORD(Notifier):
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
if self.config['incl_subject']: if self.config['incl_subject']:
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8") text = subject.encode('utf-8') + '\r\n' + body.encode('utf-8')
else: else:
text = body.encode("utf-8") text = body.encode('utf-8')
data = {'content': text} data = {'content': text}
if self.config['username']: if self.config['username']:
@@ -1363,7 +1363,8 @@ class EMAIL(Notifier):
success = True success = True
except Exception as e: except Exception as e:
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e)) logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(
name=self.NAME, e=str(e).decode('utf-8')))
finally: finally:
if mailserver: if mailserver:
@@ -1545,9 +1546,9 @@ class FACEBOOK(Notifier):
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
if self.config['incl_subject']: if self.config['incl_subject']:
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8") text = subject.encode('utf-8') + '\r\n' + body.encode('utf-8')
else: else:
text = body.encode("utf-8") text = body.encode('utf-8')
data = {'message': text} data = {'message': text}
@@ -1999,8 +2000,8 @@ class IFTTT(Notifier):
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
event = unicode(self.config['event']).format(action=action) event = unicode(self.config['event']).format(action=action)
data = {'value1': subject.encode("utf-8"), data = {'value1': subject.encode('utf-8'),
'value2': body.encode("utf-8")} 'value2': body.encode('utf-8')}
if self.config['value3']: if self.config['value3']:
pretty_metadata = PrettyMetadata(kwargs['parameters']) pretty_metadata = PrettyMetadata(kwargs['parameters'])
@@ -2060,10 +2061,10 @@ class JOIN(Notifier):
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
data = {'apikey': self.config['api_key'], data = {'apikey': self.config['api_key'],
'deviceNames': ','.join(self.config['device_names']), 'deviceNames': ','.join(self.config['device_names']),
'text': body.encode("utf-8")} 'text': body.encode('utf-8')}
if self.config['incl_subject']: if self.config['incl_subject']:
data['title'] = subject.encode("utf-8") data['title'] = subject.encode('utf-8')
if kwargs.get('parameters', {}).get('media_type'): if kwargs.get('parameters', {}).get('media_type'):
# Grab formatted metadata # Grab formatted metadata
@@ -2216,9 +2217,9 @@ class MQTT(Notifier):
logger.error(u"Tautulli Notifiers :: MQTT topic not specified.") logger.error(u"Tautulli Notifiers :: MQTT topic not specified.")
return return
data = {'subject': subject.encode("utf-8"), data = {'subject': subject.encode('utf-8'),
'body': body.encode("utf-8"), 'body': body.encode('utf-8'),
'topic': self.config['topic'].encode("utf-8")} 'topic': self.config['topic'].encode('utf-8')}
auth = {} auth = {}
if self.config['username']: if self.config['username']:
@@ -2577,8 +2578,8 @@ class PROWL(Notifier):
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
data = {'apikey': self.config['key'], data = {'apikey': self.config['key'],
'application': 'Tautulli', 'application': 'Tautulli',
'event': subject.encode("utf-8"), 'event': subject.encode('utf-8'),
'description': body.encode("utf-8"), 'description': body.encode('utf-8'),
'priority': self.config['priority']} 'priority': self.config['priority']}
headers = {'Content-type': 'application/x-www-form-urlencoded'} headers = {'Content-type': 'application/x-www-form-urlencoded'}
@@ -2615,7 +2616,7 @@ class PUSHALOT(Notifier):
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
data = {'AuthorizationToken': self.config['api_key'], data = {'AuthorizationToken': self.config['api_key'],
'Title': subject.encode('utf-8'), 'Title': subject.encode('utf-8'),
'Body': body.encode("utf-8")} 'Body': body.encode('utf-8')}
headers = {'Content-type': 'application/x-www-form-urlencoded'} headers = {'Content-type': 'application/x-www-form-urlencoded'}
@@ -2647,14 +2648,14 @@ class PUSHBULLET(Notifier):
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
data = {'type': 'note', data = {'type': 'note',
'body': body.encode("utf-8")} 'body': body.encode('utf-8')}
headers = {'Content-type': 'application/json', headers = {'Content-type': 'application/json',
'Access-Token': self.config['api_key'] 'Access-Token': self.config['api_key']
} }
if self.config['incl_subject']: if self.config['incl_subject']:
data['title'] = subject.encode("utf-8") data['title'] = subject.encode('utf-8')
# Can only send to a device or channel, not both. # Can only send to a device or channel, not both.
if self.config['device_id']: if self.config['device_id']:
@@ -2783,14 +2784,14 @@ class PUSHOVER(Notifier):
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
data = {'token': self.config['api_token'], data = {'token': self.config['api_token'],
'user': self.config['key'], 'user': self.config['key'],
'message': body.encode("utf-8"), 'message': body.encode('utf-8'),
'sound': self.config['sound'], 'sound': self.config['sound'],
'html': self.config['html_support'], 'html': self.config['html_support'],
'priority': self.config['priority'], 'priority': self.config['priority'],
'timestamp': int(time.time())} 'timestamp': int(time.time())}
if self.config['incl_subject']: if self.config['incl_subject']:
data['title'] = subject.encode("utf-8") data['title'] = subject.encode('utf-8')
if self.config['priority'] == 2: if self.config['priority'] == 2:
data['retry'] = max(30, self.config['retry']) data['retry'] = max(30, self.config['retry'])
@@ -3207,9 +3208,9 @@ class SLACK(Notifier):
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
if self.config['incl_subject']: if self.config['incl_subject']:
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8") text = subject.encode('utf-8') + '\r\n' + body.encode('utf-8')
else: else:
text = body.encode("utf-8") text = body.encode('utf-8')
data = {'text': text} data = {'text': text}
if self.config['channel'] and self.config['channel'].startswith('#'): if self.config['channel'] and self.config['channel'].startswith('#'):
@@ -3753,9 +3754,9 @@ class ZAPIER(Notifier):
return self.agent_notify(_test_data=_test_data) return self.agent_notify(_test_data=_test_data)
def agent_notify(self, subject='', body='', action='', **kwargs): def agent_notify(self, subject='', body='', action='', **kwargs):
data = {'subject': subject.encode("utf-8"), data = {'subject': subject.encode('utf-8'),
'body': body.encode("utf-8"), 'body': body.encode('utf-8'),
'action': action.encode("utf-8")} 'action': action.encode('utf-8')}
if kwargs.get('parameters', {}).get('media_type'): if kwargs.get('parameters', {}).get('media_type'):
# Grab formatted metadata # Grab formatted metadata

View File

@@ -336,7 +336,7 @@ class PlexTV(object):
def get_plextv_downloads(self, plexpass=False, output_format=''): def get_plextv_downloads(self, plexpass=False, output_format=''):
if plexpass: if plexpass:
uri = '/api/downloads/1.json?channel=plexpass' uri = '/api/downloads/5.json?channel=plexpass'
else: else:
uri = '/api/downloads/1.json' uri = '/api/downloads/1.json'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
@@ -566,13 +566,13 @@ class PlexTV(object):
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality') settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution') settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
sync_details = {"device_name": helpers.sanitize(device_name), sync_details = {"device_name": device_name,
"platform": helpers.sanitize(device_platform), "platform": device_platform,
"user_id": device_user_id, "user_id": device_user_id,
"user": helpers.sanitize(device_friendly_name), "user": device_friendly_name,
"username": helpers.sanitize(device_username), "username": device_username,
"root_title": helpers.sanitize(sync_root_title), "root_title": sync_root_title,
"sync_title": helpers.sanitize(sync_title), "sync_title": sync_title,
"metadata_type": sync_metadata_type, "metadata_type": sync_metadata_type,
"content_type": sync_content_type, "content_type": sync_content_type,
"rating_key": rating_key, "rating_key": rating_key,

View File

@@ -446,7 +446,10 @@ class PmsConnect(object):
Output: array Output: array
""" """
if media_type in ('movie', 'show', 'artist', 'other_video'): media_types = ('movie', 'show', 'artist', 'other_video')
recents_list = []
if media_type in media_types:
other_video = False other_video = False
if media_type == 'movie': if media_type == 'movie':
media_type = '1' media_type = '1'
@@ -461,7 +464,12 @@ class PmsConnect(object):
elif section_id: elif section_id:
recent = self.get_library_recently_added(section_id, start, count, output_format='xml') recent = self.get_library_recently_added(section_id, start, count, output_format='xml')
else: else:
recent = self.get_recently_added(start, count, output_format='xml') for media_type in media_types:
recents = self.get_recently_added_details(start, count, media_type)
recents_list += recents['recently_added']
output = {'recently_added': sorted(recents_list, key=lambda k: k['added_at'], reverse=True)[:int(count)]}
return output
try: try:
xml_head = recent.getElementsByTagName('MediaContainer') xml_head = recent.getElementsByTagName('MediaContainer')
@@ -469,8 +477,6 @@ class PmsConnect(object):
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_recently_added: %s." % e) logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_recently_added: %s." % e)
return [] return []
recents_list = []
for a in xml_head: for a in xml_head:
if a.getAttribute('size'): if a.getAttribute('size'):
if a.getAttribute('size') == '0': if a.getAttribute('size') == '0':
@@ -1587,7 +1593,7 @@ class PmsConnect(object):
transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS) transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS)
# Determine if a synced version is being played # Determine if a synced version is being played
sync_id = None sync_id = synced_session_data = synced_item_details = None
if media_type not in ('photo', 'clip') \ if media_type not in ('photo', 'clip') \
and not session.getElementsByTagName('Session') \ and not session.getElementsByTagName('Session') \
and not session.getElementsByTagName('TranscodeSession') \ and not session.getElementsByTagName('TranscodeSession') \
@@ -1604,6 +1610,8 @@ class PmsConnect(object):
sync_id = synced_item_details['sync_id'] sync_id = synced_item_details['sync_id']
synced_xml = self.get_sync_item(sync_id=sync_id, output_format='xml') synced_xml = self.get_sync_item(sync_id=sync_id, output_format='xml')
synced_xml_head = synced_xml.getElementsByTagName('MediaContainer') synced_xml_head = synced_xml.getElementsByTagName('MediaContainer')
synced_xml_items = []
if synced_xml_head[0].getElementsByTagName('Track'): if synced_xml_head[0].getElementsByTagName('Track'):
synced_xml_items = synced_xml_head[0].getElementsByTagName('Track') synced_xml_items = synced_xml_head[0].getElementsByTagName('Track')
elif synced_xml_head[0].getElementsByTagName('Video'): elif synced_xml_head[0].getElementsByTagName('Video'):
@@ -1614,7 +1622,7 @@ class PmsConnect(object):
break break
# Figure out which version is being played # Figure out which version is being played
if sync_id: if sync_id and synced_session_data:
media_info_all = synced_session_data.getElementsByTagName('Media') media_info_all = synced_session_data.getElementsByTagName('Media')
else: else:
media_info_all = session.getElementsByTagName('Media') media_info_all = session.getElementsByTagName('Media')
@@ -1688,6 +1696,7 @@ class PmsConnect(object):
'stream_subtitle_decision': helpers.get_xml_attr(subtitle_stream_info, 'decision') 'stream_subtitle_decision': helpers.get_xml_attr(subtitle_stream_info, 'decision')
} }
else: else:
subtitle_selected = None
subtitle_details = {'stream_subtitle_codec': '', subtitle_details = {'stream_subtitle_codec': '',
'stream_subtitle_container': '', 'stream_subtitle_container': '',
'stream_subtitle_format': '', 'stream_subtitle_format': '',
@@ -1924,6 +1933,7 @@ class PmsConnect(object):
quality_profile = 'Original' quality_profile = 'Original'
if stream_details['optimized_version']: if stream_details['optimized_version']:
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1), optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1),
plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'], plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'],
source_media_details['video_resolution'])) source_media_details['video_resolution']))

View File

@@ -289,7 +289,7 @@ def server_message(response, return_msg=False):
message = None message = None
# First attempt is to 'read' the response as HTML # First attempt is to 'read' the response as HTML
if "text/html" in response.headers.get("content-type"): if "text/html" in response.headers.get("content-type", ""):
try: try:
soup = BeautifulSoup(response.content, "html5lib") soup = BeautifulSoup(response.content, "html5lib")
except Exception: except Exception:

View File

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

View File

@@ -55,7 +55,7 @@ import users
import versioncheck import versioncheck
import web_socket import web_socket
from plexpy.api2 import API2 from plexpy.api2 import API2
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out
from plexpy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library from plexpy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library
from plexpy.webauth import AuthController, requireAuth, member_of from plexpy.webauth import AuthController, requireAuth, member_of
@@ -176,7 +176,8 @@ class WebInterface(object):
"home_refresh_interval": plexpy.CONFIG.HOME_REFRESH_INTERVAL, "home_refresh_interval": plexpy.CONFIG.HOME_REFRESH_INTERVAL,
"pms_name": plexpy.CONFIG.PMS_NAME, "pms_name": plexpy.CONFIG.PMS_NAME,
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD, "pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
"update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG "update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG,
"first_run_complete": plexpy.CONFIG.FIRST_RUN_COMPLETE
} }
return serve_template(templatename="index.html", title="Home", config=config) return serve_template(templatename="index.html", title="Home", config=config)
@@ -349,6 +350,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@sanitize_out()
@addtoapi("get_libraries_table") @addtoapi("get_libraries_table")
def get_library_list(self, **kwargs): def get_library_list(self, **kwargs):
""" Get the data on the Tautulli libraries table. """ Get the data on the Tautulli libraries table.
@@ -427,6 +429,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@sanitize_out()
@addtoapi("get_library_names") @addtoapi("get_library_names")
def get_library_sections(self, **kwargs): def get_library_sections(self, **kwargs):
""" Get a list of library sections and ids on the PMS. """ Get a list of library sections and ids on the PMS.
@@ -1014,6 +1017,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@sanitize_out()
@addtoapi("get_users_table") @addtoapi("get_users_table")
def get_user_list(self, **kwargs): def get_user_list(self, **kwargs):
""" Get the data on Tautulli users table. """ Get the data on Tautulli users table.
@@ -1228,6 +1232,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@sanitize_out()
@addtoapi() @addtoapi()
def get_user_ips(self, user_id=None, **kwargs): def get_user_ips(self, user_id=None, **kwargs):
""" Get the data on Tautulli users IP table. """ Get the data on Tautulli users IP table.
@@ -1294,6 +1299,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@sanitize_out()
@addtoapi() @addtoapi()
def get_user_logins(self, user_id=None, **kwargs): def get_user_logins(self, user_id=None, **kwargs):
""" Get the data on Tautulli user login table. """ Get the data on Tautulli user login table.
@@ -1575,6 +1581,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@sanitize_out()
@addtoapi() @addtoapi()
def get_history(self, user=None, user_id=None, grouping=None, **kwargs): def get_history(self, user=None, user_id=None, grouping=None, **kwargs):
""" Get the Tautulli history. """ Get the Tautulli history.
@@ -1821,6 +1828,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth() @requireAuth()
@sanitize_out()
@addtoapi() @addtoapi()
def get_user_names(self, **kwargs): def get_user_names(self, **kwargs):
""" Get a list of all user and user ids. """ Get a list of all user and user ids.
@@ -2293,6 +2301,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@sanitize_out()
@requireAuth() @requireAuth()
def get_sync(self, machine_id=None, user_id=None, **kwargs): def get_sync(self, machine_id=None, user_id=None, **kwargs):
if user_id == 'null': if user_id == 'null':
@@ -2434,6 +2443,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@sanitize_out()
@addtoapi() @addtoapi()
def get_notification_log(self, **kwargs): def get_notification_log(self, **kwargs):
""" Get the data on the Tautulli notification logs table. """ Get the data on the Tautulli notification logs table.
@@ -2495,6 +2505,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@sanitize_out()
@addtoapi() @addtoapi()
def get_newsletter_log(self, **kwargs): def get_newsletter_log(self, **kwargs):
""" Get the data on the Tautulli newsletter logs table. """ Get the data on the Tautulli newsletter logs table.
@@ -3773,6 +3784,7 @@ class WebInterface(object):
'update': True, 'update': True,
'release': True, 'release': True,
'message': 'A new release (%s) of Tautulli is available.' % plexpy.LATEST_RELEASE, 'message': 'A new release (%s) of Tautulli is available.' % plexpy.LATEST_RELEASE,
'current_release': plexpy.common.RELEASE,
'latest_release': plexpy.LATEST_RELEASE, 'latest_release': plexpy.LATEST_RELEASE,
'release_url': helpers.anon_url( 'release_url': helpers.anon_url(
'https://github.com/%s/%s/releases/tag/%s' 'https://github.com/%s/%s/releases/tag/%s'
@@ -3786,6 +3798,7 @@ class WebInterface(object):
'update': True, 'update': True,
'release': False, 'release': False,
'message': 'A newer version of Tautulli is available.', 'message': 'A newer version of Tautulli is available.',
'current_version': plexpy.CURRENT_VERSION,
'latest_version': plexpy.LATEST_VERSION, 'latest_version': plexpy.LATEST_VERSION,
'commits_behind': plexpy.COMMITS_BEHIND, 'commits_behind': plexpy.COMMITS_BEHIND,
'compare_url': helpers.anon_url( 'compare_url': helpers.anon_url(
@@ -5228,6 +5241,7 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@sanitize_out()
@addtoapi() @addtoapi()
def get_synced_items(self, machine_id='', user_id='', **kwargs): def get_synced_items(self, machine_id='', user_id='', **kwargs):
""" Get a list of synced items on the PMS. """ Get a list of synced items on the PMS.