Compare commits

...

44 Commits

Author SHA1 Message Date
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
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
JonnyWong16
56a91de2c4 v2.1.25 2018-11-03 16:23:21 -07:00
JonnyWong16
e2d217a981 Merge pull request #1332 from samwiseg00/add/livetv_image
Add live TV images for current activity
2018-11-03 12:12:10 -07:00
samwiseg00
b484f27724 Add logic for live tv in current activity 2018-11-03 14:56:53 -04:00
samwiseg00
eb04a2e579 Fix poster image failback 2018-11-03 14:55:56 -04:00
samwiseg00
c66d8ecd5f Add new images for live tv activity 2018-11-03 14:55:30 -04:00
JonnyWong16
79b5f3c36f Merge pull request #1331 from samwiseg00/fix/pms_video_codec
Override * in video codecs
2018-11-02 19:59:48 -07:00
samwiseg00
4a78424b75 Override * in video codecs 2018-11-01 22:38:15 -04:00
JonnyWong16
4f78d0c98a Missing " in settings alert error 2018-10-31 17:49:33 -07:00
JonnyWong16
91b84b4437 Placeholder "-" for burn subtitle codec 2018-10-29 20:47:02 -07:00
JonnyWong16
8a9b3dc782 Override * in audio codecs 2018-10-29 20:46:37 -07:00
26 changed files with 260 additions and 116 deletions

View File

@@ -1,5 +1,56 @@
# Changelog
## 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:
* 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)
* Monitoring:
* Fix: Audio and video codec showing up as * on the activity cards.
* New: Poster and background image on the activity cards for live TV.
* UI:
* Fix: Alert message for invalid Tautulli Public Domain setting.
## v2.1.24-beta (2018-10-29)
* Monitoring:

View File

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

View File

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

View File

@@ -80,7 +80,9 @@ DOCUMENTATION :: END
data-rating_key="${data['rating_key']}" data-parent_rating_key="${data['parent_rating_key']}" data-grandparent_rating_key="${data['grandparent_rating_key']}">
<div class="dashboard-activity-container">
<%
if data['channel_stream'] == 0:
if data['live'] == 1:
background_url = 'images/art-live.png'
elif data['channel_stream'] == 0:
background_url = 'pms_image_proxy?img=' + data['art'] + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true'
else:
if (data['art'] and data['art'].startswith('http')) or (data['thumb'] and data['thumb'].startswith('http')):
@@ -93,7 +95,9 @@ DOCUMENTATION :: END
% if data['media_type'] == 'track':
<div id="poster-${sk}-bg" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&opacity=60&background=282828&blur=3&fallback=cover&refresh=true);"></div>
% endif
% if data['channel_stream'] == 0:
% if data['live'] == 1:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/poster-live.png);"></div>
% elif data['channel_stream'] == 0:
% if data['media_type'] == 'movie':
<a id="poster-url-${sk}" href="${href}" title="${data['title']}">
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
@@ -113,7 +117,7 @@ DOCUMENTATION :: END
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb'] or data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
% endif
% else:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/art.png);"></div>
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/poster.png);"></div>
% endif
% else:
% if data['channel_icon'].startswith('http'):
@@ -279,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']}">

View File

@@ -54,7 +54,7 @@
json_data: JSON.stringify(d),
user_id: "${data['user_id']}",
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')}"
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

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>
% 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':

View File

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

View File

@@ -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">&lt;${item['media_type']}&gt;&lt;/${item['media_type']}&gt;</span> tags
% endif
</h4>
% if agent != 'webhook':
<pre>${item['subject']}</pre>
% endif

View File

@@ -472,7 +472,7 @@
<div class="col-md-8">
<input type="text" class="form-control" id="http_base_url" name="http_base_url" value="${config['http_base_url']}" placeholder="http://mydomain.com" data-parsley-trigger="change" data-parsley-pattern="^https?:\/\/\S+$" data-parsley-errors-container="#http_base_url_error" data-parsley-error-message="Invalid URL">
</div>
<div id=http_base_url_error" class="alert alert-danger settings-alert" role="alert"></div>
<div id="http_base_url_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">
Set your public Tautulli domain for self-hosted notification images and newsletters. (e.g. http://mydomain.com)
@@ -1532,7 +1532,7 @@
<div style="padding-bottom: 10px;">
<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>
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}&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>
<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>
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}&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));
}
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) {

View File

@@ -230,7 +230,7 @@ DOCUMENTATION :: END
<tbody>
<tr>
<td>Codec</td>
<td>${data['stream_subtitle_codec'].upper()}</td>
<td>${data['stream_subtitle_codec'].upper() or '-'}</td>
<td>${data['subtitle_codec'].upper()}</td>
</tr>
</tbody>

12
init-scripts/init.systemd Executable file → Normal file
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
@@ -53,6 +55,10 @@ GuessMainPID=no
Type=forking
User=tautulli
Group=tautulli
Restart=on-abnormal
RestartSec=5
StartLimitInterval=90
StartLimitBurst=3
[Install]
WantedBy=multi-user.target

View File

@@ -266,6 +266,7 @@ class ActivityHandler(object):
last_rating_key = str(db_session['rating_key'])
last_live_uuid = db_session['live_uuid']
last_transcode_key = db_session['transcode_key'].split('/')[-1]
last_paused = db_session['last_paused']
# Make sure the same item is being played
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 == 'paused':
self.on_pause()
elif last_state == 'paused' and this_state == 'playing':
elif last_paused and this_state == 'playing':
self.on_resume()
elif this_state == 'stopped':
self.on_stop()

View File

@@ -598,7 +598,7 @@ General optional parameters:
if self._api_cmd == 'docs_md':
return out['response']['data']
elif self._api_cmd.startswith('download_'):
elif self._api_cmd and self._api_cmd.startswith('download_'):
return
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', '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': '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': '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.'},

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

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

View File

@@ -522,11 +522,28 @@ def process_json_kwargs(json_kwargs):
return params
def sanitize(string):
if string:
return unicode(string).replace('<','&lt;').replace('>','&gt;')
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('<', '&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:
return ''
return obj
def is_public_ip(host):
@@ -801,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', '')
@@ -811,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.")
@@ -823,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

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': notify_params['friendly_name'],
'username': notify_params['user'],
'user_email': notify_params['email'],
'device': notify_params['device'],
'platform': notify_params['platform'],
'product': notify_params['product'],

View File

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

View File

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

View File

@@ -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':
@@ -1504,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
@@ -1518,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'):
@@ -1587,7 +1603,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 +1620,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 +1632,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 +1706,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': '',
@@ -1888,6 +1907,18 @@ class PmsConnect(object):
stream_details['transcode_decision'] = transcode_decision
# Override * in audio codecs
if stream_details['stream_audio_codec'] == '*':
stream_details['stream_audio_codec'] = source_audio_details['audio_codec']
if transcode_details['transcode_audio_codec'] == '*':
transcode_details['transcode_audio_codec'] = source_audio_details['audio_codec']
# Override * in video codecs
if stream_details['stream_video_codec'] == '*':
stream_details['stream_video_codec'] = source_video_details['video_codec']
if transcode_details['transcode_video_codec'] == '*':
transcode_details['transcode_video_codec'] = source_video_details['video_codec']
# Get the quality profile
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
if sync_id:
@@ -1912,6 +1943,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']))

View File

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

View File

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

View File

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