Compare commits
20 Commits
v2.1.26
...
v2.1.27-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
61fac10079 | ||
![]() |
536e8add17 | ||
![]() |
cb81bcac57 | ||
![]() |
5dd7806c0e | ||
![]() |
2a707fc512 | ||
![]() |
469e54a22c | ||
![]() |
f6f5df3d1e | ||
![]() |
ae0960d2e2 | ||
![]() |
a646cc36a1 | ||
![]() |
b243ac5f5c | ||
![]() |
bca7744bc5 | ||
![]() |
2fc826c88f | ||
![]() |
6397b1e5a7 | ||
![]() |
85b9a47a0d | ||
![]() |
5749ab7c92 | ||
![]() |
dcb56cfd20 | ||
![]() |
90849f9196 | ||
![]() |
5b77cab575 | ||
![]() |
6a21d7690a | ||
![]() |
037e983350 |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,5 +1,27 @@
|
||||
# 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:
|
||||
|
@@ -209,7 +209,7 @@ ${next.modalIncludes()}
|
||||
</div>
|
||||
</div>
|
||||
% else:
|
||||
<div id="donate-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="crypto-donate-modal">
|
||||
<div id="donate-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="donate-modal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -230,17 +230,13 @@ ${next.modalIncludes()}
|
||||
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
|
||||
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
|
||||
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
|
||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoin" data-name="Bitcoin" data-address="3FdfJAyNWU15Sf11U9FTgPHuP1hPz32eEN">Bitcoin</a></li>
|
||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoincash" data-name="Bitcoin Cash" data-address="1H2atabxAQGaFAWYQEiLkXKSnK9CZZvt2n">Bitcoin Cash</a></li>
|
||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="ethereum" data-name="Ethereum" data-address="0x77ae4c2b8de1a1ccfa93553db39971da58c873d3">Ethereum</a></li>
|
||||
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="litecoin" data-name="Litecoin" data-address="LWpPmUqQYHBhMV83XSCsHzPmKLhJt6r57J">Litecoin</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="patreon-donation" style="text-align: center">
|
||||
<p>
|
||||
Click the button below to continue to Patreon.
|
||||
</p>
|
||||
<a href="${anon_url('https://www.patreon.com/bePatron?u=10078609')}" target="_blank">
|
||||
<a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank">
|
||||
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
|
||||
</a>
|
||||
</div>
|
||||
@@ -252,12 +248,6 @@ ${next.modalIncludes()}
|
||||
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
|
||||
</a>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="crypto-donation">
|
||||
<label>QR Code</label>
|
||||
<pre id="crypto_qr_code" style="text-align: center"></pre>
|
||||
<label><span id="crypto_type_label"></span> Address</label>
|
||||
<pre id="crypto_address" style="text-align: center"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -293,7 +283,6 @@ ${next.modalIncludes()}
|
||||
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
||||
<script src="${http_root}js/platform.min.js"></script>
|
||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
||||
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
|
||||
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
||||
<script src="${http_root}js/ajaxNotifications.js"></script>
|
||||
@@ -365,16 +354,6 @@ ${next.modalIncludes()}
|
||||
checkUpdate(function () { $('#nav-update').html('<i class="fa fa-fw fa-arrow-alt-circle-up"></i> Check for Updates'); });
|
||||
});
|
||||
|
||||
$('#donation_type a.crypto-donation').on('shown.bs.tab', function () {
|
||||
var crypto_coin = $(this).data('coin');
|
||||
var crypto_name = $(this).data('name');
|
||||
var crypto_address = $(this).data('address')
|
||||
$('#crypto_qr_code').empty().qrcode({
|
||||
text: crypto_coin + ":" + crypto_address
|
||||
});
|
||||
$('#crypto_type_label').html(crypto_name);
|
||||
$('#crypto_address').html(crypto_address);
|
||||
});
|
||||
% endif
|
||||
|
||||
$('.dropdown-toggle').click(function (e) {
|
||||
|
@@ -4204,7 +4204,7 @@ a[data-tab-destination] {
|
||||
background: #cc7b19;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
padding-top: 2px;
|
||||
padding: 2px 10px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 9999;
|
||||
|
@@ -27,6 +27,8 @@
|
||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
|
||||
% elif config['pms_is_cloud']:
|
||||
<div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div>
|
||||
% elif not config['first_run_complete']:
|
||||
<div id="dashboard-no-activity" class="text-muted">The Tautulli setup wizard has not been completed. Please click <a href="welcome">here</a> to go to the setup wizard.</div>
|
||||
% else:
|
||||
<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.
|
||||
% if _session['user_group'] == 'admin':
|
||||
|
@@ -10,17 +10,33 @@ if (typeof platform !== 'undefined') {
|
||||
}
|
||||
|
||||
if (['IE', 'Microsoft Edge', 'IE Mobile'].indexOf(p.name) > -1) {
|
||||
$('body').prepend('<div id="browser-warning"><i class="fa fa-exclamation-circle"></i> ' +
|
||||
'Tautulli does not support Internet Explorer or Microsoft Edge! ' +
|
||||
'Please use a different browser such as Chrome or Firefox.</div>');
|
||||
var offset = $('#browser-warning').height();
|
||||
var navbar = $('.navbar-fixed-top');
|
||||
if (navbar.length) {
|
||||
navbar.offset({top: navbar.offset().top + offset});
|
||||
}
|
||||
var container = $('.body-container');
|
||||
if (container.length) {
|
||||
container.offset({top: container.offset().top + offset});
|
||||
if (!getCookie('browserDismiss')) {
|
||||
var $browser_warning = $('<div id="browser-warning">' +
|
||||
'<i class="fa fa-exclamation-circle"></i> ' +
|
||||
'Tautulli does not support Internet Explorer or Microsoft Edge! ' +
|
||||
'Please use a different browser such as Chrome or Firefox.' +
|
||||
'<button type="button" class="close"><i class="fa fa-remove"></i></button>' +
|
||||
'</div>');
|
||||
$('body').prepend($browser_warning);
|
||||
var offset = $browser_warning.height();
|
||||
warningOffset(offset);
|
||||
|
||||
$browser_warning.one('click', 'button.close', function () {
|
||||
$browser_warning.remove();
|
||||
warningOffset(-offset);
|
||||
setCookie('browserDismiss', 'true', 7);
|
||||
});
|
||||
|
||||
function warningOffset(offset) {
|
||||
var navbar = $('.navbar-fixed-top');
|
||||
if (navbar.length) {
|
||||
navbar.offset({top: navbar.offset().top + offset});
|
||||
}
|
||||
var container = $('.body-container');
|
||||
if (container.length) {
|
||||
container.offset({top: container.offset().top + offset});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,12 @@
|
||||
% if text:
|
||||
% for item in text:
|
||||
<div style="padding-bottom: 10px;">
|
||||
<h4>${item['media_type'].capitalize()}</h4>
|
||||
<h4>
|
||||
${item['media_type'].capitalize()}
|
||||
% if item['media_type'] != 'server':
|
||||
<span class="inline-pre"><${item['media_type']}></${item['media_type']}></span> tags
|
||||
% endif
|
||||
</h4>
|
||||
% if agent != 'webhook':
|
||||
<pre>${item['subject']}</pre>
|
||||
% endif
|
||||
|
@@ -1532,7 +1532,7 @@
|
||||
<div style="padding-bottom: 10px;">
|
||||
<p class="help-block">
|
||||
All text inside <span class="inline-pre"><show></show></span>/<span class="inline-pre"><season></season></span>/<span class="inline-pre"><episode></episode></span>
|
||||
tags will only be sent when the media type is show/season/episode.
|
||||
tags will only be sent when the media type is show, season, or episode, respectively.
|
||||
</p>
|
||||
<p><strong style="color: #fff;">Example:</strong></p>
|
||||
<pre>{show_name}<season> - Season {season_num}</season><episode> - S{season_num}E{episode_num} - {episode_name}</episode> was recently added to Plex.</pre>
|
||||
@@ -1543,7 +1543,7 @@
|
||||
<div>
|
||||
<p class="help-block">
|
||||
All text inside <span class="inline-pre"><artist></artist></span>/<span class="inline-pre"><album></album></span>/<span class="inline-pre"><track></track></span>
|
||||
tags will only be sent when the media type is artist/album/track.
|
||||
tags will only be sent when the media type is artist, album, or track, respectively.
|
||||
</p>
|
||||
<p><strong style="color: #fff;">Example:</strong></p>
|
||||
<pre>{artist_name}<album> - {album_name}</album><track> - {album_name} - {track_name}</track> was recently added to Plex.</pre>
|
||||
@@ -2530,8 +2530,10 @@ $(document).ready(function() {
|
||||
.prop('selected', selected));
|
||||
}
|
||||
|
||||
var download_url = 'https://plex.tv/api/downloads/' + (plex_update_channel === 'plexpass' ? '5' : '1') + '.json?channel=' + plex_update_channel;
|
||||
|
||||
$.ajax({
|
||||
url: 'https://plex.tv/api/downloads/1.json?channel=' + plex_update_channel,
|
||||
url: download_url,
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
beforeSend: function (xhr) {
|
||||
|
4
init-scripts/init.systemd
Executable file → Normal file
4
init-scripts/init.systemd
Executable file → Normal file
@@ -53,6 +53,10 @@ GuessMainPID=no
|
||||
Type=forking
|
||||
User=tautulli
|
||||
Group=tautulli
|
||||
Restart=on-abnormal
|
||||
RestartSec=5
|
||||
StartLimitInterval=90
|
||||
StartLimitBurst=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
@@ -101,10 +101,6 @@ class DataTables(object):
|
||||
# Paginate results
|
||||
result = filtered[parameters['start']:(parameters['start'] + parameters['length'])]
|
||||
|
||||
# Sanitize on the way out
|
||||
result = [{k: helpers.sanitize(v) if isinstance(v, basestring) else v for k, v in row.iteritems()}
|
||||
for row in result]
|
||||
|
||||
output = {'result': result,
|
||||
'draw': draw_counter,
|
||||
'filteredCount': len(filtered),
|
||||
|
@@ -522,11 +522,28 @@ def process_json_kwargs(json_kwargs):
|
||||
return params
|
||||
|
||||
|
||||
def sanitize(string):
|
||||
if string:
|
||||
return unicode(string).replace('<','<').replace('>','>')
|
||||
def sanitize_out(*dargs, **dkwargs):
|
||||
""" Helper decorator that sanitized the output
|
||||
"""
|
||||
def rd(function):
|
||||
@wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
return sanitize(function(*args, **kwargs))
|
||||
return wrapper
|
||||
return rd
|
||||
|
||||
|
||||
def sanitize(obj):
|
||||
if isinstance(obj, basestring):
|
||||
return unicode(obj).replace('<', '<').replace('>', '>')
|
||||
elif isinstance(obj, list):
|
||||
return [sanitize(o) for o in obj]
|
||||
elif isinstance(obj, dict):
|
||||
return {k: sanitize(v) for k, v in obj.iteritems()}
|
||||
elif isinstance(obj, tuple):
|
||||
return tuple(sanitize(list(obj)))
|
||||
else:
|
||||
return ''
|
||||
return obj
|
||||
|
||||
|
||||
def is_public_ip(host):
|
||||
|
@@ -746,14 +746,14 @@ class PrettyMetadata(object):
|
||||
title = '%s - %s' % (self.parameters['artist_name'], self.parameters['album_name'])
|
||||
elif self.media_type == 'track':
|
||||
title = '%s - %s' % (self.parameters['track_name'], self.parameters['track_artist'])
|
||||
return title.encode("utf-8")
|
||||
return title.encode('utf-8')
|
||||
|
||||
def get_description(self):
|
||||
if self.media_type == 'track':
|
||||
description = self.parameters['album_name']
|
||||
else:
|
||||
description = self.parameters['summary']
|
||||
return description.encode("utf-8")
|
||||
return description.encode('utf-8')
|
||||
|
||||
def get_plex_url(self):
|
||||
return self.parameters['plex_url']
|
||||
@@ -863,9 +863,9 @@ class ANDROIDAPP(Notifier):
|
||||
pretty_metadata = PrettyMetadata(kwargs.get('parameters'))
|
||||
|
||||
plaintext_data = {'notification_id': notification_id,
|
||||
'subject': subject.encode('UTF-8'),
|
||||
'body': body.encode('UTF-8'),
|
||||
'action': action.encode('UTF-8'),
|
||||
'subject': subject.encode('utf-8'),
|
||||
'body': body.encode('utf-8'),
|
||||
'action': action.encode('utf-8'),
|
||||
'priority': self.config['priority'],
|
||||
'session_key': pretty_metadata.parameters.get('session_key',''),
|
||||
'session_id': pretty_metadata.parameters.get('session_id',''),
|
||||
@@ -1130,9 +1130,9 @@ class DISCORD(Notifier):
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
if self.config['incl_subject']:
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8")
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode('utf-8')
|
||||
else:
|
||||
text = body.encode("utf-8")
|
||||
text = body.encode('utf-8')
|
||||
|
||||
data = {'content': text}
|
||||
if self.config['username']:
|
||||
@@ -1363,7 +1363,8 @@ class EMAIL(Notifier):
|
||||
success = True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(name=self.NAME, e=e))
|
||||
logger.error(u"Tautulli Notifiers :: {name} notification failed: {e}".format(
|
||||
name=self.NAME, e=str(e).decode('utf-8')))
|
||||
|
||||
finally:
|
||||
if mailserver:
|
||||
@@ -1545,9 +1546,9 @@ class FACEBOOK(Notifier):
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
if self.config['incl_subject']:
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8")
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode('utf-8')
|
||||
else:
|
||||
text = body.encode("utf-8")
|
||||
text = body.encode('utf-8')
|
||||
|
||||
data = {'message': text}
|
||||
|
||||
@@ -1999,8 +2000,8 @@ class IFTTT(Notifier):
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
event = unicode(self.config['event']).format(action=action)
|
||||
|
||||
data = {'value1': subject.encode("utf-8"),
|
||||
'value2': body.encode("utf-8")}
|
||||
data = {'value1': subject.encode('utf-8'),
|
||||
'value2': body.encode('utf-8')}
|
||||
|
||||
if self.config['value3']:
|
||||
pretty_metadata = PrettyMetadata(kwargs['parameters'])
|
||||
@@ -2060,10 +2061,10 @@ class JOIN(Notifier):
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'apikey': self.config['api_key'],
|
||||
'deviceNames': ','.join(self.config['device_names']),
|
||||
'text': body.encode("utf-8")}
|
||||
'text': body.encode('utf-8')}
|
||||
|
||||
if self.config['incl_subject']:
|
||||
data['title'] = subject.encode("utf-8")
|
||||
data['title'] = subject.encode('utf-8')
|
||||
|
||||
if kwargs.get('parameters', {}).get('media_type'):
|
||||
# Grab formatted metadata
|
||||
@@ -2216,9 +2217,9 @@ class MQTT(Notifier):
|
||||
logger.error(u"Tautulli Notifiers :: MQTT topic not specified.")
|
||||
return
|
||||
|
||||
data = {'subject': subject.encode("utf-8"),
|
||||
'body': body.encode("utf-8"),
|
||||
'topic': self.config['topic'].encode("utf-8")}
|
||||
data = {'subject': subject.encode('utf-8'),
|
||||
'body': body.encode('utf-8'),
|
||||
'topic': self.config['topic'].encode('utf-8')}
|
||||
|
||||
auth = {}
|
||||
if self.config['username']:
|
||||
@@ -2577,8 +2578,8 @@ class PROWL(Notifier):
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'apikey': self.config['key'],
|
||||
'application': 'Tautulli',
|
||||
'event': subject.encode("utf-8"),
|
||||
'description': body.encode("utf-8"),
|
||||
'event': subject.encode('utf-8'),
|
||||
'description': body.encode('utf-8'),
|
||||
'priority': self.config['priority']}
|
||||
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'}
|
||||
@@ -2615,7 +2616,7 @@ class PUSHALOT(Notifier):
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'AuthorizationToken': self.config['api_key'],
|
||||
'Title': subject.encode('utf-8'),
|
||||
'Body': body.encode("utf-8")}
|
||||
'Body': body.encode('utf-8')}
|
||||
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'}
|
||||
|
||||
@@ -2647,14 +2648,14 @@ class PUSHBULLET(Notifier):
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'type': 'note',
|
||||
'body': body.encode("utf-8")}
|
||||
'body': body.encode('utf-8')}
|
||||
|
||||
headers = {'Content-type': 'application/json',
|
||||
'Access-Token': self.config['api_key']
|
||||
}
|
||||
|
||||
if self.config['incl_subject']:
|
||||
data['title'] = subject.encode("utf-8")
|
||||
data['title'] = subject.encode('utf-8')
|
||||
|
||||
# Can only send to a device or channel, not both.
|
||||
if self.config['device_id']:
|
||||
@@ -2783,14 +2784,14 @@ class PUSHOVER(Notifier):
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'token': self.config['api_token'],
|
||||
'user': self.config['key'],
|
||||
'message': body.encode("utf-8"),
|
||||
'message': body.encode('utf-8'),
|
||||
'sound': self.config['sound'],
|
||||
'html': self.config['html_support'],
|
||||
'priority': self.config['priority'],
|
||||
'timestamp': int(time.time())}
|
||||
|
||||
if self.config['incl_subject']:
|
||||
data['title'] = subject.encode("utf-8")
|
||||
data['title'] = subject.encode('utf-8')
|
||||
|
||||
if self.config['priority'] == 2:
|
||||
data['retry'] = max(30, self.config['retry'])
|
||||
@@ -3207,9 +3208,9 @@ class SLACK(Notifier):
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
if self.config['incl_subject']:
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode("utf-8")
|
||||
text = subject.encode('utf-8') + '\r\n' + body.encode('utf-8')
|
||||
else:
|
||||
text = body.encode("utf-8")
|
||||
text = body.encode('utf-8')
|
||||
|
||||
data = {'text': text}
|
||||
if self.config['channel'] and self.config['channel'].startswith('#'):
|
||||
@@ -3753,9 +3754,9 @@ class ZAPIER(Notifier):
|
||||
return self.agent_notify(_test_data=_test_data)
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
data = {'subject': subject.encode("utf-8"),
|
||||
'body': body.encode("utf-8"),
|
||||
'action': action.encode("utf-8")}
|
||||
data = {'subject': subject.encode('utf-8'),
|
||||
'body': body.encode('utf-8'),
|
||||
'action': action.encode('utf-8')}
|
||||
|
||||
if kwargs.get('parameters', {}).get('media_type'):
|
||||
# Grab formatted metadata
|
||||
|
@@ -336,7 +336,7 @@ class PlexTV(object):
|
||||
|
||||
def get_plextv_downloads(self, plexpass=False, output_format=''):
|
||||
if plexpass:
|
||||
uri = '/api/downloads/1.json?channel=plexpass'
|
||||
uri = '/api/downloads/5.json?channel=plexpass'
|
||||
else:
|
||||
uri = '/api/downloads/1.json'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
@@ -566,13 +566,13 @@ class PlexTV(object):
|
||||
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
|
||||
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
|
||||
|
||||
sync_details = {"device_name": helpers.sanitize(device_name),
|
||||
"platform": helpers.sanitize(device_platform),
|
||||
sync_details = {"device_name": device_name,
|
||||
"platform": device_platform,
|
||||
"user_id": device_user_id,
|
||||
"user": helpers.sanitize(device_friendly_name),
|
||||
"username": helpers.sanitize(device_username),
|
||||
"root_title": helpers.sanitize(sync_root_title),
|
||||
"sync_title": helpers.sanitize(sync_title),
|
||||
"user": device_friendly_name,
|
||||
"username": device_username,
|
||||
"root_title": sync_root_title,
|
||||
"sync_title": sync_title,
|
||||
"metadata_type": sync_metadata_type,
|
||||
"content_type": sync_content_type,
|
||||
"rating_key": rating_key,
|
||||
|
@@ -446,7 +446,10 @@ class PmsConnect(object):
|
||||
|
||||
Output: array
|
||||
"""
|
||||
if media_type in ('movie', 'show', 'artist', 'other_video'):
|
||||
media_types = ('movie', 'show', 'artist', 'other_video')
|
||||
recents_list = []
|
||||
|
||||
if media_type in media_types:
|
||||
other_video = False
|
||||
if media_type == 'movie':
|
||||
media_type = '1'
|
||||
@@ -461,7 +464,12 @@ class PmsConnect(object):
|
||||
elif section_id:
|
||||
recent = self.get_library_recently_added(section_id, start, count, output_format='xml')
|
||||
else:
|
||||
recent = self.get_recently_added(start, count, output_format='xml')
|
||||
for media_type in media_types:
|
||||
recents = self.get_recently_added_details(start, count, media_type)
|
||||
recents_list += recents['recently_added']
|
||||
|
||||
output = {'recently_added': sorted(recents_list, key=lambda k: k['added_at'], reverse=True)[:int(count)]}
|
||||
return output
|
||||
|
||||
try:
|
||||
xml_head = recent.getElementsByTagName('MediaContainer')
|
||||
@@ -469,8 +477,6 @@ class PmsConnect(object):
|
||||
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_recently_added: %s." % e)
|
||||
return []
|
||||
|
||||
recents_list = []
|
||||
|
||||
for a in xml_head:
|
||||
if a.getAttribute('size'):
|
||||
if a.getAttribute('size') == '0':
|
||||
@@ -1587,7 +1593,7 @@ class PmsConnect(object):
|
||||
transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS)
|
||||
|
||||
# Determine if a synced version is being played
|
||||
sync_id = None
|
||||
sync_id = synced_session_data = synced_item_details = None
|
||||
if media_type not in ('photo', 'clip') \
|
||||
and not session.getElementsByTagName('Session') \
|
||||
and not session.getElementsByTagName('TranscodeSession') \
|
||||
@@ -1604,6 +1610,8 @@ class PmsConnect(object):
|
||||
sync_id = synced_item_details['sync_id']
|
||||
synced_xml = self.get_sync_item(sync_id=sync_id, output_format='xml')
|
||||
synced_xml_head = synced_xml.getElementsByTagName('MediaContainer')
|
||||
|
||||
synced_xml_items = []
|
||||
if synced_xml_head[0].getElementsByTagName('Track'):
|
||||
synced_xml_items = synced_xml_head[0].getElementsByTagName('Track')
|
||||
elif synced_xml_head[0].getElementsByTagName('Video'):
|
||||
@@ -1614,7 +1622,7 @@ class PmsConnect(object):
|
||||
break
|
||||
|
||||
# Figure out which version is being played
|
||||
if sync_id:
|
||||
if sync_id and synced_session_data:
|
||||
media_info_all = synced_session_data.getElementsByTagName('Media')
|
||||
else:
|
||||
media_info_all = session.getElementsByTagName('Media')
|
||||
@@ -1688,6 +1696,7 @@ class PmsConnect(object):
|
||||
'stream_subtitle_decision': helpers.get_xml_attr(subtitle_stream_info, 'decision')
|
||||
}
|
||||
else:
|
||||
subtitle_selected = None
|
||||
subtitle_details = {'stream_subtitle_codec': '',
|
||||
'stream_subtitle_container': '',
|
||||
'stream_subtitle_format': '',
|
||||
@@ -1924,6 +1933,7 @@ class PmsConnect(object):
|
||||
quality_profile = 'Original'
|
||||
|
||||
if stream_details['optimized_version']:
|
||||
source_bitrate = helpers.cast_to_int(source_media_details.get('bitrate'))
|
||||
optimized_version_profile = '{} Mbps {}'.format(round(source_bitrate / 1000.0, 1),
|
||||
plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(source_media_details['video_resolution'],
|
||||
source_media_details['video_resolution']))
|
||||
|
@@ -289,7 +289,7 @@ def server_message(response, return_msg=False):
|
||||
message = None
|
||||
|
||||
# First attempt is to 'read' the response as HTML
|
||||
if "text/html" in response.headers.get("content-type"):
|
||||
if "text/html" in response.headers.get("content-type", ""):
|
||||
try:
|
||||
soup = BeautifulSoup(response.content, "html5lib")
|
||||
except Exception:
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "master"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.26"
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.27-beta"
|
||||
|
@@ -55,7 +55,7 @@ import users
|
||||
import versioncheck
|
||||
import web_socket
|
||||
from plexpy.api2 import API2
|
||||
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json
|
||||
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates, build_datatables_json, sanitize_out
|
||||
from plexpy.session import get_session_info, get_session_user_id, allow_session_user, allow_session_library
|
||||
from plexpy.webauth import AuthController, requireAuth, member_of
|
||||
|
||||
@@ -176,7 +176,8 @@ class WebInterface(object):
|
||||
"home_refresh_interval": plexpy.CONFIG.HOME_REFRESH_INTERVAL,
|
||||
"pms_name": plexpy.CONFIG.PMS_NAME,
|
||||
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
|
||||
"update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG
|
||||
"update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG,
|
||||
"first_run_complete": plexpy.CONFIG.FIRST_RUN_COMPLETE
|
||||
}
|
||||
return serve_template(templatename="index.html", title="Home", config=config)
|
||||
|
||||
@@ -349,6 +350,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi("get_libraries_table")
|
||||
def get_library_list(self, **kwargs):
|
||||
""" Get the data on the Tautulli libraries table.
|
||||
@@ -427,6 +429,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@sanitize_out()
|
||||
@addtoapi("get_library_names")
|
||||
def get_library_sections(self, **kwargs):
|
||||
""" Get a list of library sections and ids on the PMS.
|
||||
@@ -1014,6 +1017,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi("get_users_table")
|
||||
def get_user_list(self, **kwargs):
|
||||
""" Get the data on Tautulli users table.
|
||||
@@ -1228,6 +1232,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_user_ips(self, user_id=None, **kwargs):
|
||||
""" Get the data on Tautulli users IP table.
|
||||
@@ -1294,6 +1299,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_user_logins(self, user_id=None, **kwargs):
|
||||
""" Get the data on Tautulli user login table.
|
||||
@@ -1575,6 +1581,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_history(self, user=None, user_id=None, grouping=None, **kwargs):
|
||||
""" Get the Tautulli history.
|
||||
@@ -1821,6 +1828,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_user_names(self, **kwargs):
|
||||
""" Get a list of all user and user ids.
|
||||
@@ -2293,6 +2301,7 @@ class WebInterface(object):
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@sanitize_out()
|
||||
@requireAuth()
|
||||
def get_sync(self, machine_id=None, user_id=None, **kwargs):
|
||||
if user_id == 'null':
|
||||
@@ -2434,6 +2443,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_notification_log(self, **kwargs):
|
||||
""" Get the data on the Tautulli notification logs table.
|
||||
@@ -2495,6 +2505,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_newsletter_log(self, **kwargs):
|
||||
""" Get the data on the Tautulli newsletter logs table.
|
||||
@@ -3773,6 +3784,7 @@ class WebInterface(object):
|
||||
'update': True,
|
||||
'release': True,
|
||||
'message': 'A new release (%s) of Tautulli is available.' % plexpy.LATEST_RELEASE,
|
||||
'current_release': plexpy.common.RELEASE,
|
||||
'latest_release': plexpy.LATEST_RELEASE,
|
||||
'release_url': helpers.anon_url(
|
||||
'https://github.com/%s/%s/releases/tag/%s'
|
||||
@@ -3786,6 +3798,7 @@ class WebInterface(object):
|
||||
'update': True,
|
||||
'release': False,
|
||||
'message': 'A newer version of Tautulli is available.',
|
||||
'current_version': plexpy.CURRENT_VERSION,
|
||||
'latest_version': plexpy.LATEST_VERSION,
|
||||
'commits_behind': plexpy.COMMITS_BEHIND,
|
||||
'compare_url': helpers.anon_url(
|
||||
@@ -5228,6 +5241,7 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@sanitize_out()
|
||||
@addtoapi()
|
||||
def get_synced_items(self, machine_id='', user_id='', **kwargs):
|
||||
""" Get a list of synced items on the PMS.
|
||||
|
Reference in New Issue
Block a user