Compare commits

..

46 Commits

Author SHA1 Message Date
JonnyWong16
ebb287e1ee v2.0.24 2018-03-18 17:46:17 -07:00
JonnyWong16
bd3497b2bf Rename notifiers in database 2018-03-18 17:44:24 -07:00
JonnyWong16
034f3ee308 Anon URL to FAQ for pycryptodome does not work with anchors 2018-03-18 17:33:02 -07:00
JonnyWong16
a946879fc1 Better OSX register button 2018-03-18 17:22:44 -07:00
JonnyWong16
9f964b5a87 Move notification agent instructions to wiki 2018-03-18 17:05:30 -07:00
JonnyWong16
ed0b41cd19 Add punctuation to Arnold 2018-03-17 18:44:15 -07:00
JonnyWong16
dc87591992 Show historical stream data (Fixes Tautulli/Tautulli-Issues#27) 2018-03-17 16:36:24 -07:00
JonnyWong16
d05e80e573 Make sure all exisiting environment variables are included for scripts 2018-03-17 13:30:12 -07:00
JonnyWong16
522684b2ab v2.0.23-beta 2018-03-16 19:59:06 -07:00
JonnyWong16
feab16b351 Update API docs for get_server_id 2018-03-16 19:47:49 -07:00
JonnyWong16
ee041db63d Pass common environment variable to scripts 2018-03-16 18:37:50 -07:00
JonnyWong16
2479533d07 Show Plex Server URL in settings 2018-03-16 17:43:32 -07:00
JonnyWong16
d045fd5834 Update Facebook Graph API version 2018-03-16 15:39:41 -07:00
JonnyWong16
8407f27fed Add value3 to IFTTT notifications (Closes #1279) 2018-03-16 09:45:30 -07:00
JonnyWong16
b505286caf Add season/episode/album/track count to notification parameters 2018-03-16 09:42:32 -07:00
JonnyWong16
feb762ce8b Beta/nightly update check to include non-beta releases 2018-03-16 08:37:50 -07:00
JonnyWong16
8acdb5af83 Use media stream info for transcode decision (Fixes Tautulli/Tautulli-Issues#24) 2018-03-14 19:45:47 -07:00
JonnyWong16
5af1294f71 Make websocket thread daemon 2018-03-14 16:19:22 -07:00
JonnyWong16
87d2d273d3 Attempt at fixing custom condition json error 2018-03-13 22:16:23 -07:00
JonnyWong16
b5c52ac71e Add logging for failed custom condition json 2018-03-13 20:45:41 -07:00
JonnyWong16
efe9a15f72 Cast Email username/password to string 2018-03-13 20:41:07 -07:00
JonnyWong16
525f1e4b0b Use cherrypy remote for login IP info 2018-03-13 10:00:08 -07:00
JonnyWong16
d18820b832 Use cherrypy base for login host info 2018-03-13 09:42:01 -07:00
JonnyWong16
7e024fd736 Remove test comment in c9c5989 2018-03-13 09:09:27 -07:00
JonnyWong16
c9c5989474 Fix login logs for Plex admin user 2018-03-13 09:08:09 -07:00
JonnyWong16
ce9f96d3be Exit if failed to move database instead of continuing 2018-03-12 19:43:46 -07:00
JonnyWong16
7362dd0bf4 Close websocket cleanly on shutdown 2018-03-12 19:38:19 -07:00
JonnyWong16
9905ebc144 Don't empty results if message in API response (Fixes Tautulli/Tautulli-Issues#13) 2018-03-12 08:56:43 -07:00
JonnyWong16
8f8010884b Add git pull after checkout from interface 2018-03-12 08:20:20 -07:00
JonnyWong16
37afd141be Catch invalid json for custom conditions 2018-03-11 20:59:18 -07:00
JonnyWong16
a3643b4302 Fix typos 2018-03-10 20:54:21 -08:00
JonnyWong16
02cfd8d9b7 Fix git branch select box height 2018-03-10 20:33:18 -08:00
JonnyWong16
941ce439b4 Update API message for remote app settings 2018-03-10 18:03:23 -08:00
JonnyWong16
a08bce2073 v2.0.22 2018-03-10 09:32:08 -08:00
JonnyWong16
4e9c8322c3 Don't overwrite tautulli db on move 2018-03-10 09:32:05 -08:00
JonnyWong16
89bfe85be3 Workaround for duration reported as minutes for a show 2018-03-10 08:58:15 -08:00
JonnyWong16
98d994591c Fix runtime round to minutes 2018-03-09 19:12:12 -08:00
JonnyWong16
a29bc7f4f9 v2.0.22-beta 2018-03-09 17:58:40 -08:00
JonnyWong16
288f4c5f7f Fix expanding selectize box 2018-03-09 15:50:53 -08:00
JonnyWong16
a6bf78ed56 Check is schedulers running before shutdown 2018-03-08 18:32:47 -08:00
JonnyWong16
8dbb05931e Fix library refresh when missing library 2018-03-08 18:23:12 -08:00
JonnyWong16
ac8a712ff0 Fix refreshing activity after losing connection 2018-03-06 20:01:11 -08:00
JonnyWong16
39406c25c3 Add retry and expire for Pushover priority 2 2018-03-06 09:57:06 -08:00
JonnyWong16
48d7c2c54c Fix photo library count and media info table 2018-03-05 09:49:48 -08:00
JonnyWong16
0217188274 Fix update check 2018-03-04 22:49:32 -08:00
JonnyWong16
fd762e71de Fix cherrypy sending wrong Content-Type header for svg 2018-03-04 22:32:26 -08:00
36 changed files with 594 additions and 311 deletions

3
API.md
View File

@@ -1674,7 +1674,8 @@ Optional parameters:
remote (int): 0 or 1
Returns:
string: The unique PMS identifier
json:
{'identifier': '08u2phnlkdshf890bhdlksghnljsahgleikjfg9t'}
```

View File

@@ -1,5 +1,52 @@
# Changelog
## v2.0.24 (2018-03-18)
* Monitoring:
* Fix: Fix stream data not showing for history recorded before v2.
* Notifications:
* Fix: Set all environment variables for scripts.
* Change: Moved all notification agent instructions to the wiki.
* Change: XBMC notification agent renamed to Kodi.
* Change: OSX Notify notification agent renamed to macOS Notification Center.
## v2.0.23-beta (2018-03-16)
* Monitoring:
* Fix: Certain transcode stream showing incorrectly as direct play in history. Fix is not retroactive.
* Notifications:
* New: Added season/episode/album/track count to notification parameters.
* New: Added "Value 3" setting for IFTTT notifications.
* New: Set PLEX_URL, PLEX_TOKEN, TAUTULLI_URL, and TAUTULLI_APIKEY environment variables for scripts.
* Fix: Notifications failing to send with invalid custom conditions json.
* Fix: Email notifications failing with unicode username/passwords.
* Change: Facebook Graph API version updated to v2.12.
* UI:
* New: Show the Plex Server URL in the settings.
* Fix: Incorrect info displayed in the Tautulli login logs.
* API:
* Fix: API returning empty data if a message was in the original data.
* Change: get_server_id command returns json instead of string.
* Other:
* Fix: Forgot git pull when changing branches in the web UI.
## v2.0.22 (2018-03-10)
* Tautulli v2 release!
## v2.0.22-beta (2018-03-09)
* Notifications:
* Fix: Pushover notifications failing with priority 2 is set.
* Fix: Expanding selectize box for some notification agent settings.
* Other:
* Fix: Update check failing when an update is available.
* Fix: Item count incorrect for photo libraries.
## v2.0.21-beta (2018-03-04)
* Monitoring:

View File

@@ -186,8 +186,12 @@ def main():
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME)
# Move 'plexpy.db' to 'tautulli.db'
if os.path.isfile(os.path.join(plexpy.DATA_DIR, 'plexpy.db')):
os.rename(os.path.join(plexpy.DATA_DIR, 'plexpy.db'), plexpy.DB_FILE)
if os.path.isfile(os.path.join(plexpy.DATA_DIR, 'plexpy.db')) and \
not os.path.isfile(os.path.join(plexpy.DATA_DIR, plexpy.DB_FILE)):
try:
os.rename(os.path.join(plexpy.DATA_DIR, 'plexpy.db'), plexpy.DB_FILE)
except OSError as e:
raise SystemExit("Unable to rename plexpy.db to tautulli.db: %s", e)
if plexpy.DAEMON:
plexpy.daemonize()

View File

@@ -66,7 +66,6 @@ div.form-control .selectize-input {
color: #fff;
border: 0px solid #444;
background: #555;
height: 32px;
padding: 6px 12px;
background-color: #555;
border-radius: 3px;
@@ -92,6 +91,7 @@ div.form-control .selectize-input {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
min-height: 32px !important;
height: 32px !important;
}
.input-group .selectize-control.form-control.selectize-pms-ip .selectize-input > div {
max-width: 450px;
@@ -1419,7 +1419,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
}
.dashboard-stats-info-item .sub-count {
height: 100%;
margin-left: 10px;
margin-left: 5px;
color: #f9be03;
font-size: 12px;
text-align: right;
@@ -1430,7 +1430,7 @@ a .dashboard-activity-metadata-user-thumb:hover {
}
.dashboard-stats-info-item .sub-divider {
height: 100%;
margin-left: 10px;
margin-left: 5px;
color: #aaa;
font-size: 12px;
text-align: left;
@@ -3694,6 +3694,7 @@ a:hover .overlay-refresh-image:hover {
}
.git-group select.form-control {
width: 50%;
height: 32px;
}
#changelog-modal .modal-body > h2 {
margin-bottom: 10px;

View File

@@ -163,7 +163,7 @@
}
var media_type = null;
var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}";
var selected_user_id = "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}";
loadHistoryTable(media_type, selected_user_id);
% if _session['user_group'] == 'admin':

View File

@@ -276,6 +276,7 @@
async: true,
error: function (xhr, status, error) {
console.log(status + ': ' + error);
activity_ready = true;
},
complete: function (xhr, status) {
$('#dashboard-checking-activity').remove();

View File

@@ -552,7 +552,7 @@ DOCUMENTATION :: END
return {
json_data: JSON.stringify( d ),
grandparent_rating_key: "${data['rating_key']}",
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
};
}
}
@@ -568,7 +568,7 @@ DOCUMENTATION :: END
return {
json_data: JSON.stringify( d ),
parent_rating_key: "${data['rating_key']}",
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
};
}
}
@@ -584,7 +584,7 @@ DOCUMENTATION :: END
return {
json_data: JSON.stringify( d ),
rating_key: "${data['rating_key']}",
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
};
}
}

View File

@@ -292,7 +292,11 @@ function millisecondsToMinutes(ms, roundToMinute) {
if (ms > 0) {
var minutes = Math.floor(ms / 60000);
var seconds = ((ms % 60000) / 1000).toFixed(0);
return (seconds == 60 ? (minutes+1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
if (roundToMinute) {
return (seconds >= 30 ? (minutes + 1) : minutes);
} else {
return (seconds == 60 ? (minutes + 1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
}
} else {
if (roundToMinute) {
return '0';

View File

@@ -54,7 +54,7 @@ media_info_table_options = {
} else if (rowData['media_type'] === 'album') {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Tracks"><i class="fa fa-plus-circle fa-fw"></i></span>';
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else if (rowData['media_type'] === 'photo' && rowData['parent_rating_key'] == '') {
} else if (rowData['media_type'] === 'photo_album') {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Photos"><i class="fa fa-plus-circle fa-fw"></i></span>';
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else {
@@ -77,32 +77,44 @@ media_info_table_options = {
if (rowData['media_type'] === 'movie') {
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + parent_info + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'show') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="TV Show"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'season') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Season"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">E' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">E' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'artist') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Artist"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'album') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Album"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">T' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">T' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'photo_album') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo Album"><i class="fa fa-camera fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else if (rowData['media_type'] === 'photo') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else if (rowData['media_type'] === 'clip') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Video"><i class="fa fa-video-camera fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else {
$(td).html(cellData);
}
@@ -335,7 +347,7 @@ function childTableOptionsMedia(rowData) {
case 'album':
section_type = 'track';
break;
case 'photo':
case 'photo_album':
section_type = 'picture';
break;
}

View File

@@ -379,7 +379,7 @@ DOCUMENTATION :: END
return {
json_data: JSON.stringify( d ),
section_id: section_id,
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
};
}
};

View File

@@ -29,7 +29,7 @@ DOCUMENTATION :: END
headers = {'movie': ('Movie Libraries', ('Movies', '', '')),
'show': ('TV Show Libraries', ('Shows', 'Seasons', 'Episodes')),
'artist': ('Music Libraries', ('Artists', 'Albums', 'Tracks')),
'photo': ('Photo Libraries', ('Albums', '', 'Photos'))}
'photo': ('Photo Libraries', ('Albums', 'Photos', 'Videos'))}
%>
% for section_type in types:
% if section_type in data:

View File

@@ -55,7 +55,7 @@ DOCUMENTATION :: END
})
}
return deferred;
}
};
function checkQRAddress(url) {
var parser = document.createElement('a');
@@ -82,7 +82,7 @@ DOCUMENTATION :: END
verifiedDevice = false;
getPlexPyURL().then(function (url) {
checkQRAddress(url)
checkQRAddress(url);
$.get('generate_api_key', { device: true }).then(function (token) {
$('#api_qr_address').val(url);
@@ -120,7 +120,7 @@ DOCUMENTATION :: END
$('#api_qr_address').change(function () {
var url = $(this).val();
checkQRAddress(url)
checkQRAddress(url);
$('#api_qr_code').empty().qrcode({
text: url + '|' + $('#api_qr_token').val()

View File

@@ -45,9 +45,6 @@
<div class="row">
<div class="col-md-12">
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30" ${'readonly' if item.get('readonly') else ''}>
% if item['name'] == 'osx_notify_app':
<a href="javascript:void(0)" id="osxnotifyregister">Register</a>
% endif
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
@@ -171,7 +168,7 @@
<div class="form-group">
<label for="custom_conditions_logic">Condition Logic</label>
<input type="text" class="form-control" name="custom_conditions_logic" id="custom_conditions_logic" value="${notifier['custom_conditions_logic']}" required />
<input type="text" class="form-control" name="custom_conditions_logic" id="custom_conditions_logic" value="${notifier['custom_conditions_logic']}" />
<div id="custom_conditions_logic_error" class="alert alert-danger" role="alert" style="padding-top: 5px; padding-bottom: 5px; margin: 0; display: none;"><i class="fa fa-exclamation-triangle" style="color: #a94442;"></i> <span></span></div>
<p class="help-block">
Optional: Enter custom logic to use when evaluating the conditions (e.g. <span class="inline-pre">{1} and ({2} or {3})</span>).
@@ -333,11 +330,11 @@
$('#notifier-config-modal').unbind('hidden.bs.modal');
// Need this for setting conditions since conditions contain the character "
$('#custom_conditions').val(${json.dumps(notifier["custom_conditions"]) | n});
$('#custom_conditions').val(JSON.stringify(${json.dumps(notifier["custom_conditions"]) | n}));
$('#condition-widget').filterer({
parameters: ${parameters | n},
conditions: ${notifier["custom_conditions"] | n},
parameters: ${json.dumps(parameters) | n},
conditions: ${json.dumps(notifier["custom_conditions"]) | n},
updateConditions: function(newConditions){
$('#custom_conditions').val(JSON.stringify(newConditions));
}
@@ -433,16 +430,30 @@
});
% if notifier['agent_name'] == 'facebook':
if (location.protocol !== 'https:') {
$('#tabs-config .form-group:first').prepend(
'<div class="form-group">' +
'<label>Warning</label>' +
'<p class="help-block" style="color: #eb8600;">Facebook requires HTTPS for authorization. ' +
'Please enable HTTPS for Tautulli under <a data-tab-destination="tabs-web_interface" data-dismiss="modal" style="cursor: pointer;">Web Interface</a>.</p>' +
'</div>'
);
$('#facebook_redirect_uri').val('HTTPS not enabled');
} else {
$('#facebook_redirect_uri').val(location.href.split('/settings')[0] + '/facebook_redirect');
}
function disableFacebookRequest() {
if ($('#facebook_app_id').val() !== '' && $('#facebook_app_secret').val() !== '') { $('#facebook_facebookStep1').prop('disabled', false); }
else { $('#facebook_facebookStep1').prop('disabled', true); }
if ($('#facebook_app_id').val() !== '' && $('#facebook_app_secret').val() !== '') { $('#facebook_facebook_auth').prop('disabled', false); }
else { $('#facebook_facebook_auth').prop('disabled', true); }
}
disableFacebookRequest();
$('#facebook_app_id, #facebook_app_secret').on('change', function () {
disableFacebookRequest();
});
$('#facebook_facebookStep1').click(function () {
$('#facebook_facebook_auth').click(function () {
// Remove trailing '/' from Facebook redirect URI
if ($('#facebook_redirect_uri') && $('#facebook_redirect_uri').val().endsWith('/')) {
$('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1));
@@ -450,7 +461,7 @@
var facebook_token;
$.ajax({
url: 'facebookStep1',
url: 'facebook_auth',
data: {
app_id: $('#facebook_app_id').val(),
app_secret: $('#facebook_app_secret').val(),
@@ -508,7 +519,7 @@
});
% elif notifier['agent_name'] == 'osx':
$('#osxnotifyregister').click(function () {
$('#osx_notify_register').click(function () {
var osx_notify_app = $('#osx_notify_app').val();
$.get('osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
});
@@ -606,6 +617,22 @@
});
});
% elif notifier['agent_name'] == 'pushover':
function pushoverPriority() {
if ($('#pushover_priority').val() == '2') {
$('#pushover_retry').closest('.form-group').show();
$('#pushover_expire').closest('.form-group').show();
} else {
$('#pushover_retry').closest('.form-group').hide();
$('#pushover_expire').closest('.form-group').hide();
}
}
pushoverPriority();
$('#pushover_priority').change( function () {
pushoverPriority();
});
% endif
function validateLogic() {

View File

@@ -10,7 +10,7 @@ DOCUMENTATION :: END
</%doc>
<ul class="stacked-configs list-unstyled">
% for notifier in sorted(notifiers_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])):
% for notifier in sorted(notifiers_list, key=lambda k: (k['agent_label'].lower(), k['friendly_name'], k['id'])):
<li class="notification-agent" data-id="${notifier['id']}">
<span>
<span class="toggle-left trigger-tooltip ${'active' if notifier['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Triggers ${'active' if notifier['active'] else 'inactive'}"><i class="fa fa-lg fa-bell"></i></span>

View File

@@ -642,7 +642,7 @@
<label for="pms_port">Plex Port</label>
<div class="row">
<div class="col-md-2">
<input data-parsley-type="integer" class="pms-settings form-control" type="text" id="pms_port" name="pms_port" value="${config['pms_port']}" size="30" data-parsley-trigger="change" data-parsley-errors-container="#pms_port_error" required>
<input data-parsley-type="integer" class="form-control pms-settings" type="text" id="pms_port" name="pms_port" value="${config['pms_port']}" size="30" data-parsley-trigger="change" data-parsley-errors-container="#pms_port_error" required>
</div>
<div id="pms_port_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
@@ -650,29 +650,45 @@
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle" data-id="pms_is_remote" value="1" ${checked(config['pms_is_remote'])}> Remote Server
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle pms-settings" data-id="pms_is_remote" value="1" ${checked(config['pms_is_remote'])}> Remote Server
<input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}">
</label>
<p class="help-block">Check this if your Plex Server is not on the same local network as Tautulli.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle" data-id="pms_ssl" value="1" ${checked(config['pms_ssl'])}> Use SSL
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle pms-settings" data-id="pms_ssl" value="1" ${checked(config['pms_ssl'])}> Use SSL
<input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}">
</label>
<p class="help-block">If you have secure connections enabled on your Plex Server, communicate with it securely.</p>
</div>
<div class="form-group">
<label for="pms_url">Plex Server URL</label>
<div class="row">
<div class="col-md-9">
<div class="input-group">
<input type="text" class="form-control" id="pms_url" name="pms_url" value="${config['pms_url']}" size="30" readonly>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="test_pms_url_button">Test URL</button>
</span>
</div>
</div>
</div>
<p class="help-block">
The server URL that Tautulli will use to connect to your Plex server. Retrieved automatically.
</p>
</div>
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection
<input type="checkbox" class="pms-settings" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection
</label>
<span id="cloudManualConnection" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
<p class="help-block">Use the user defined connection details. Do not retrieve the server connection URL automatically.</p>
</div>
<div class="form-group advanced-setting">
<label for="pms_logs_folder">Plex Web URL</label>
<label for="pms_web_url">Plex Web URL</label>
<div class="row">
<div class="col-md-6">
<div class="col-md-9">
<div class="input-group">
<input type="text" class="form-control" id="pms_web_url" name="pms_web_url" value="${config['pms_web_url']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^https?:\/\/\S+$|^https:\/\/app.plex.tv\/desktop$" data-parsley-errors-container="#pms_web_url_error" data-parsley-error-message="Invalid Plex Web URL.">
<span class="input-group-btn">
@@ -953,6 +969,9 @@
<p class="help-block">
Add a new notification agent, or configure an existing notification agent by clicking the settings icon on the right.
</p>
<p class="help-block">
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/Notification-Agents-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>Notification Agents Guide</a> for instructions on setting up each notification agent.
</p>
<br />
<div id="plexpy-notifiers-table">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading notification agents...</div>
@@ -967,7 +986,7 @@
<h3>Database Import</h3>
</div>
<p class="help-block">Click a button below to import an exisiting database from another app.</p>
<p class="help-block">Click a button below to import an existing database from another app.</p>
<div class="btn-group">
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexwatch">PlexWatch</button>
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexivity">Plexivity</button>
@@ -1062,8 +1081,8 @@
</div>
<p class="form-group">
<label>Registered Devices</label>
<p class="help-block">Register a new device, or configure an existing device by clicking the settings icon on the right.</p>
<p id="app_api_msg" style="color: #eb8600;">The API must be enabled under <a data-tab-destination="tabs-access_control" style="cursor: pointer;">Access Control</a> to use the app.</p>
<p class="help-block">Register a new device using a QR code, or configure an existing device by clicking the settings icon on the right.</p>
<p id="app_api_msg" style="color: #eb8600;">The API must be enabled under <a data-tab-destination="tabs-web_interface" style="cursor: pointer;">Web Interface</a> to use the app.</p>
<div class="row">
<div id="plexpy-mobile-devices-table" class="col-md-12">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading registered devices...</div>
@@ -1214,7 +1233,7 @@
<div class="row">
<div class="col-md-12">
<ul class="stacked-configs list-unstyled">
% for agent in available_notification_agents:
% for agent in sorted(available_notification_agents, key=lambda k: k['label'].lower()):
<li class="new-notification-agent" data-id="${agent['id']}">
<span>${agent['label']}</span>
</li>
@@ -1573,7 +1592,7 @@ $(document).ready(function() {
}
function preSaveChecks(_callback) {
if ($("#pms_identifier").val() == "") {
if (serverChanged) {
verifyServer();
}
verifyPMSWebURL();
@@ -1585,7 +1604,7 @@ $(document).ready(function() {
// Alert the user that their changes require a restart.
function postSaveChecks() {
if (serverChanged || authChanged || httpChanged || directoryChanged) {
if (authChanged || httpChanged || directoryChanged) {
$('#restart-modal').modal('show');
}
$("#http_hashed_password").val($("#http_hash_password").is(":checked") ? 1 : 0);
@@ -1769,9 +1788,8 @@ $(document).ready(function() {
$( ".pms-settings" ).change(function() {
serverChanged = true;
$("#pms_identifier").val("");
$("#server_changed").prop('checked', true);
verifyServer();
$("#pms_verify").hide();
});
$('.checkbox-toggle').click(function () {
@@ -1841,6 +1859,7 @@ $(document).ready(function() {
$('#pms_ssl').val(ssl !== 'undefined' && ssl === 1 ? 1 : 0);
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
$('#pms_url_manual').prop('checked', false);
$('#pms_url').val('Please verify your server above to retrieve the URL');
PMSCloudCheck();
}
});
@@ -1906,6 +1925,7 @@ $(document).ready(function() {
var pms_identifier = $("#pms_identifier").val();
var pms_ssl = $("#pms_ssl").val();
var pms_is_remote = $("#pms_is_remote").val();
var pms_url_manual = $("#pms_url_manual").is(':checked') ? 1 : 0;
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
$("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
@@ -1916,7 +1936,9 @@ $(document).ready(function() {
port: pms_port,
identifier: pms_identifier,
ssl: pms_ssl,
remote: pms_is_remote
remote: pms_is_remote,
manual: pms_url_manual,
get_url: serverChanged
},
cache: true,
async: true,
@@ -1925,13 +1947,20 @@ $(document).ready(function() {
$("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
$("#pms_ip_group").addClass("has-error");
},
success: function (json) {
var machine_identifier = json;
if (machine_identifier) {
$("#pms_identifier").val(machine_identifier);
success: function(xhr, status) {
var result = xhr;
var identifier = result.identifier;
var url = result.url;
if (identifier) {
$("#pms_identifier").val(identifier);
if (url) {
$("#pms_url").val(url);
}
$("#pms_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
$("#pms_ip_group").removeClass("has-error");
serverChanged = false;
if (_callback) {
_callback();
}
@@ -1950,7 +1979,6 @@ $(document).ready(function() {
}
$('#verify_server_button').on('click', function(){
$("#pms_identifier").val("");
verifyServer();
});
@@ -1959,6 +1987,13 @@ $(document).ready(function() {
$("#pms_web_url").val(pms_web_url || 'https://app.plex.tv/desktop');
}
$('#test_pms_url_button').on('click', function(){
var pms_url = $.trim($("#pms_url").val());
if (pms_url.startsWith('http')) {
window.open(pms_url + '/web', '_blank');
}
});
$('#test_pms_web_button').on('click', function(){
var pms_web_url = $.trim($("#pms_web_url").val());
window.open(pms_web_url, '_blank');

View File

@@ -58,6 +58,10 @@ DOCUMENTATION :: END
<div class="col-sm-12 text-muted stream-info-current">
<i class="fa fa-exclamation-circle"></i> Current session. Updated stream details below may be delayed.
</div>
% elif data['pre_tautulli']:
<div class="col-sm-12 text-muted stream-info-current">
<i class="fa fa-exclamation-circle"></i> Pre-Tautulli history. Stream details below may be incorrect.
</div>
% endif
<table class="stream-info" style="margin-top: 0;">
<thead>
@@ -84,8 +88,8 @@ DOCUMENTATION :: END
<tbody>
<tr>
<td>Bitrate</td>
<td>${data['stream_bitrate']} kbps</td>
<td>${data['bitrate']} kbps</td>
<td>${data['stream_bitrate']} ${'kbps' if data['stream_bitrate'] else ''}</td>
<td>${data['bitrate']} ${'kbps' if data['bitrate'] else ''}</td>
</tr>
% if data['media_type'] != 'track':
<tr>
@@ -154,8 +158,8 @@ DOCUMENTATION :: END
</tr>
<tr>
<td>Bitrate</td>
<td>${data['stream_video_bitrate']} kbps</td>
<td>${data['video_bitrate']} kbps</td>
<td>${data['stream_video_bitrate']} ${'kbps' if data['stream_video_bitrate'] else ''}</td>
<td>${data['video_bitrate']} ${'kbps' if data['video_bitrate'] else ''}</td>
</tr>
<tr>
<td>Width</td>
@@ -199,8 +203,8 @@ DOCUMENTATION :: END
</tr>
<tr>
<td>Bitrate</td>
<td>${data['stream_audio_bitrate']} kbps</td>
<td>${data['audio_bitrate']} kbps</td>
<td>${data['stream_audio_bitrate']} ${'kbps' if data['stream_audio_bitrate'] else ''}</td>
<td>${data['audio_bitrate']} ${'kbps' if data['audio_bitrate'] else ''}</td>
</tr>
<tr>
<td>Channels</td>

View File

@@ -134,7 +134,7 @@
});
}
var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}";
var selected_user_id = "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}";
loadSyncTable(selected_user_id);
% if _session['user_group'] == 'admin':

View File

@@ -94,7 +94,7 @@
<label for="pms_ip">Plex IP or Hostname</label>
<div class="row">
<div class="col-xs-12">
<select class="form-control selectize-pms-ip" id="pms_ip" name="pms_ip">
<select class="form-control pms-settings selectize-pms-ip" id="pms_ip" name="pms_ip">
<option value="${config['pms_ip']}" selected>${config['pms_ip']}</option>
</select>
</div>
@@ -104,12 +104,12 @@
<label for="pms_port">Plex Port</label>
<div class="row">
<div class="col-xs-3">
<input type="text" class="form-control pms_settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
<input type="text" class="form-control pms-settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
</div>
<div class="col-xs-4">
<div class="checkbox">
<label>
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle" data-id="pms_ssl" value="1" ${helpers.checked(config['pms_ssl'])}> Use SSL
<input type="checkbox" id="pms_ssl_checkbox" class="checkbox-toggle pms-settings" data-id="pms_ssl" value="1" ${helpers.checked(config['pms_ssl'])}> Use SSL
<input type="hidden" id="pms_ssl" name="pms_ssl" value="${config['pms_ssl']}">
</label>
</div>
@@ -117,16 +117,16 @@
<div class="col-xs-4">
<div class="checkbox">
<label>
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle" data-id="pms_is_remote" value="1" ${helpers.checked(config['pms_is_remote'])}> Remote Server
<input type="checkbox" id="pms_is_remote_checkbox" class="checkbox-toggle pms-settings" data-id="pms_is_remote" value="1" ${helpers.checked(config['pms_is_remote'])}> Remote Server
<input type="hidden" id="pms_is_remote" name="pms_is_remote" value="${config['pms_is_remote']}">
</label>
</div>
</div>
</div>
</div>
<input type="hidden" class="form-control pms-settings" id="pms_valid" data-validate="validatePMSip" value="">
<input type="hidden" id="pms_valid" data-validate="validatePMSip" value="">
<input type="hidden" id="pms_is_cloud" name="pms_is_cloud" value="${config['pms_is_cloud']}">
<input type="hidden" class="form-control pms-settings" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
</div>
@@ -419,7 +419,8 @@ $(document).ready(function() {
port: pms_port,
identifier: pms_identifier,
ssl: pms_ssl,
remote: pms_is_remote },
remote: pms_is_remote
},
cache: true,
async: true,
timeout: 5000,
@@ -427,10 +428,11 @@ $(document).ready(function() {
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> This is not a Plex Server!');
$('#pms-verify-status').fadeIn('fast');
},
success: function (json) {
var machine_identifier = json;
if (machine_identifier) {
$("#pms_identifier").val(machine_identifier);
success: function(xhr, status) {
var result = xhr;
var identifier = result.identifier;
if (identifier) {
$("#pms_identifier").val(identifier);
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!');
$('#pms-verify-status').fadeIn('fast');
pms_verified = true;

View File

@@ -45,7 +45,8 @@ __version__ = version.__version__
FACEBOOK_GRAPH_URL = "https://graph.facebook.com/"
FACEBOOK_OAUTH_DIALOG_URL = "https://www.facebook.com/dialog/oauth?"
VALID_API_VERSIONS = ["2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9"]
VALID_API_VERSIONS = [
"2.5", "2.6", "2.7", "2.8", "2.9", "2.10", "2.11", "2.12"]
VALID_SEARCH_TYPES = ["page", "event", "group", "place", "placetopic", "user"]
@@ -89,7 +90,7 @@ class GraphAPI(object):
self.session = session or requests.Session()
if version:
version_regex = re.compile("^\d\.\d$")
version_regex = re.compile("^\d\.\d{1,2}$")
match = version_regex.search(str(version))
if match is not None:
if str(version) not in VALID_API_VERSIONS:
@@ -229,7 +230,7 @@ class GraphAPI(object):
try:
headers = response.headers
version = headers["facebook-api-version"].replace("v", "")
return float(version)
return str(version)
except Exception:
raise GraphAPIError("API version number not available")
@@ -369,24 +370,24 @@ class GraphAPIError(Exception):
self.code = None
try:
self.type = result["error_code"]
except:
except (KeyError, TypeError):
self.type = ""
# OAuth 2.0 Draft 10
try:
self.message = result["error_description"]
except:
except (KeyError, TypeError):
# OAuth 2.0 Draft 00
try:
self.message = result["error"]["message"]
self.code = result["error"].get("code")
if not self.type:
self.type = result["error"].get("type", "")
except:
except (KeyError, TypeError):
# REST server style
try:
self.message = result["error_msg"]
except:
except (KeyError, TypeError):
self.message = result
Exception.__init__(self, self.message)

View File

@@ -46,6 +46,7 @@ import notifiers
import plextv
import users
import versioncheck
import web_socket
import plexpy.config
PROG_DIR = None
@@ -95,6 +96,7 @@ HTTP_ROOT = None
DEV = False
WEBSOCKET = None
WS_CONNECTED = False
PLEX_SERVER_UP = None
@@ -1594,6 +1596,15 @@ def dbcheck():
'ALTER TABLE poster_urls ADD COLUMN delete_hash TEXT'
)
# Rename notifiers in the database
logger.debug(u"Altering database. Renaming notifiers.")
c_db.execute(
'UPDATE notifiers SET agent_label = "Kodi" WHERE agent_label = "XBMC"'
)
c_db.execute(
'UPDATE notifiers SET agent_label = "macOS Notification Center" WHERE agent_label = "OSX Notify"'
)
# Add "Local" user to database as default unauthenticated user.
result = c_db.execute('SELECT id FROM users WHERE username = "Local"')
if not result.fetchone():
@@ -1621,8 +1632,15 @@ def upgrade():
def shutdown(restart=False, update=False, checkout=False):
cherrypy.engine.exit()
SCHED.shutdown(wait=False)
activity_handler.ACTIVITY_SCHED.shutdown(wait=False)
# Shutdown the websocket connection
if WEBSOCKET:
web_socket.shutdown()
if SCHED.running:
SCHED.shutdown(wait=False)
if activity_handler.ACTIVITY_SCHED.running:
activity_handler.ACTIVITY_SCHED.shutdown(wait=False)
# Stop the notification threads
for i in range(CONFIG.NOTIFICATION_THREADS):

View File

@@ -652,10 +652,9 @@ General optional parameters:
# {result: error, message: 'Some shit happend'}
if isinstance(ret, dict):
if ret.get('message'):
self._api_msg = ret.get('message', {})
ret = {}
self._api_msg = ret.pop('message', None)
if ret.get('result'):
self._api_result_type = ret.get('result')
self._api_result_type = ret.pop('result', None)
return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))

View File

@@ -391,6 +391,10 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Episode Number 00', 'type': 'int', 'value': 'episode_num00', 'description': 'The two digit episode number.', 'example': 'e.g. 06, or 06-10'},
{'name': 'Track Number', 'type': 'int', 'value': 'track_num', 'description': 'The track number.', 'example': 'e.g. 4, or 4-10'},
{'name': 'Track Number 00', 'type': 'int', 'value': 'track_num00', 'description': 'The two digit track number.', 'example': 'e.g. 04, or 04-10'},
{'name': 'Season Count', 'type': 'int', 'value': 'season_count', 'description': 'The number of seasons.'},
{'name': 'Episode Count', 'type': 'int', 'value': 'episode_count', 'description': 'The number of episodes.'},
{'name': 'Album Count', 'type': 'int', 'value': 'album_count', 'description': 'The number of albums.'},
{'name': 'Track Count', 'type': 'int', 'value': 'track_count', 'description': 'The number of tracks.'},
{'name': 'Year', 'type': 'int', 'value': 'year', 'description': 'The release year for the item.'},
{'name': 'Release Date', 'type': 'int', 'value': 'release_date', 'description': 'The release date (in date format) for the item.'},
{'name': 'Air Date', 'type': 'str', 'value': 'air_date', 'description': 'The air date (in date format) for the item.'},

View File

@@ -885,6 +885,9 @@ class DataFactory(object):
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \
'video_decision, audio_decision, transcode_decision, width, height, container, ' \
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
'transcode_width, transcode_height, ' \
'session_history_metadata.media_type, title, grandparent_title ' \
'FROM session_history_media_info ' \
'JOIN session_history ON session_history_media_info.id = session_history.id ' \
@@ -903,6 +906,9 @@ class DataFactory(object):
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \
'video_decision, audio_decision, transcode_decision, width, height, container, ' \
'transcode_container, transcode_video_codec, transcode_audio_codec, transcode_audio_channels, ' \
'transcode_width, transcode_height, ' \
'media_type, title, grandparent_title ' \
'FROM sessions ' \
'WHERE session_key = ? %s' % user_cond
@@ -913,6 +919,23 @@ class DataFactory(object):
stream_output = {}
for item in result:
pre_tautulli = 0
# For backwards compatibility. Pick one new Tautulli key to check and override with old values.
if not item['stream_video_resolution']:
item['stream_video_resolution'] = item['video_resolution']
item['stream_container'] = item['transcode_container'] or item['container']
item['stream_video_decision'] = item['video_decision']
item['stream_video_codec'] = item['transcode_video_codec'] or item['video_codec']
item['stream_video_width'] = item['transcode_width'] or item['width']
item['stream_video_height'] = item['transcode_height'] or item['height']
item['stream_audio_decision'] = item['audio_decision']
item['stream_audio_codec'] = item['transcode_audio_codec'] or item['audio_codec']
item['stream_audio_channels'] = item['transcode_audio_channels'] or item['audio_channels']
item['video_width'] = item['width']
item['video_height'] = item['height']
pre_tautulli = 1
stream_output = {'bitrate': item['bitrate'],
'video_resolution': item['video_resolution'],
'optimized_version': item['optimized_version'],
@@ -951,10 +974,13 @@ class DataFactory(object):
'stream_subtitle_codec': item['stream_subtitle_codec'],
'transcode_hw_decoding': item['transcode_hw_decoding'],
'transcode_hw_encoding': item['transcode_hw_encoding'],
'video_decision': item['video_decision'],
'audio_decision': item['audio_decision'],
'media_type': item['media_type'],
'title': item['title'],
'grandparent_title': item['grandparent_title'],
'current_session': 1 if session_key else 0
'current_session': 1 if session_key else 0,
'pre_tautulli': pre_tautulli
}
stream_output = {k: v or '' for k, v in stream_output.iteritems()}

View File

@@ -933,3 +933,36 @@ def eval_logic_groups_to_bool(logic_groups, eval_conds):
result = result or eval_cond
return result
def get_plexpy_url(hostname=None):
if plexpy.CONFIG.ENABLE_HTTPS:
scheme = 'https'
else:
scheme = 'http'
if hostname is None and plexpy.CONFIG.HTTP_HOST == '0.0.0.0':
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.connect(('<broadcast>', 0))
hostname = s.getsockname()[0]
except socket.error:
hostname = socket.gethostbyname(socket.gethostname())
if not hostname:
hostname = 'localhost'
else:
hostname = hostname or plexpy.CONFIG.HTTP_HOST
if plexpy.CONFIG.HTTP_PORT not in (80, 443):
port = ':' + str(plexpy.CONFIG.HTTP_PORT)
else:
port = ''
if plexpy.CONFIG.HTTP_ROOT.strip('/'):
root = '/' + plexpy.CONFIG.HTTP_ROOT.strip('/')
else:
root = ''
return scheme + '://' + hostname + port + root

View File

@@ -125,8 +125,8 @@ def update_section_ids():
library_children = pms_connect.get_library_children_details(section_id=section_id,
section_type=section_type)
if library_children:
children_list = library_children['childern_list']
key_mappings.update({child['rating_key']:child['section_id'] for child in children_list})
children_list = library_children['children_list']
key_mappings.update({child['rating_key']: child['section_id'] for child in children_list})
else:
logger.warn(u"Tautulli Libraries :: Unable to get a list of library items for section_id %s." % section_id)
@@ -198,7 +198,7 @@ def update_labels():
label_key=label['label_key'])
if library_children:
children_list = library_children['childern_list']
children_list = library_children['children_list']
# rating_key_list = [child['rating_key'] for child in children_list]
for rating_key in [child['rating_key'] for child in children_list]:
@@ -456,7 +456,7 @@ class Libraries(object):
get_media_info=True)
if library_children:
library_count = library_children['library_count']
children_list = library_children['childern_list']
children_list = library_children['children_list']
else:
logger.warn(u"Tautulli Libraries :: Unable to get a list of library items.")
return default_return
@@ -744,7 +744,7 @@ class Libraries(object):
logger.warn(u"Tautulli Libraries :: Unable to retrieve library %s from database. Requesting library list refresh."
% section_id)
# Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet
pmsconnect.refresh_libraries()
refresh_libraries()
library_details = get_library_details(section_id=section_id)

View File

@@ -208,7 +208,7 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
custom_conditions_logic = notifier_config['custom_conditions_logic']
custom_conditions = json.loads(notifier_config['custom_conditions']) or []
custom_conditions = notifier_config['custom_conditions']
if custom_conditions_logic or any(c for c in custom_conditions if c['value']):
logger.debug(u"Tautulli NotificationHandler :: Checking custom notification conditions for notifier_id %s."
@@ -507,9 +507,9 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
user_stream_count = len(user_sessions)
# Generate a combined transcode decision value
if session.get('video_decision','') == 'transcode' or session.get('audio_decision','') == 'transcode':
if session.get('stream_video_decision', '') == 'transcode' or session.get('stream_audio_decision', '') == 'transcode':
transcode_decision = 'Transcode'
elif session.get('video_decision','') == 'copy' or session.get('audio_decision','') == 'copy':
elif session.get('stream_video_decision', '') == 'copy' or session.get('stream_audio_decision', '') == 'copy':
transcode_decision = 'Direct Stream'
else:
transcode_decision = 'Direct Play'
@@ -640,13 +640,17 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
album_name = ''
track_name = ''
num, num00 = format_group_index([helpers.cast_to_int(d['media_index'])
for d in child_metadata if d['parent_rating_key'] == rating_key])
child_num = [helpers.cast_to_int(
d['media_index']) for d in child_metadata if d['parent_rating_key'] == rating_key]
num, num00 = format_group_index(child_num)
season_num, season_num00 = num, num00
episode_num, episode_num00 = '', ''
track_num, track_num00 = '', ''
child_count = len(child_num)
grandchild_count = ''
elif ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT)
and notify_params['media_type'] in ('season', 'album')):
show_name = notify_params['parent_title']
@@ -654,14 +658,19 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
artist_name = notify_params['parent_title']
album_name = notify_params['title']
track_name = ''
season_num = str(notify_params['media_index']).zfill(1)
season_num00 = str(notify_params['media_index']).zfill(2)
num, num00 = format_group_index([helpers.cast_to_int(d['media_index'])
for d in child_metadata if d['parent_rating_key'] == rating_key])
grandchild_num = [helpers.cast_to_int(
d['media_index']) for d in child_metadata if d['parent_rating_key'] == rating_key]
num, num00 = format_group_index(grandchild_num)
episode_num, episode_num00 = num, num00
track_num, track_num00 = num, num00
child_count = 1
grandchild_count = len(grandchild_num)
else:
show_name = notify_params['grandparent_title']
episode_name = notify_params['title']
@@ -674,6 +683,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
episode_num00 = str(notify_params['media_index']).zfill(2)
track_num = str(notify_params['media_index']).zfill(1)
track_num00 = str(notify_params['media_index']).zfill(2)
child_count = 1
grandchild_count = 1
available_params = {
# Global paramaters
@@ -783,6 +794,10 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'episode_num00': episode_num00,
'track_num': track_num,
'track_num00': track_num00,
'season_count': child_count,
'episode_count': grandchild_count,
'album_count': child_count,
'track_count': grandchild_count,
'year': notify_params['year'],
'release_date': arrow.get(notify_params['originally_available_at']).format(date_format)
if notify_params['originally_available_at'] else '',

View File

@@ -54,6 +54,7 @@ import twitter
import pynma
import plexpy
import common
import database
import helpers
import logger
@@ -94,6 +95,8 @@ AGENT_IDS = {'growl': 0,
'zapier': 24
}
DEFAULT_CUSTOM_CONDITIONS = [{'parameter': '', 'operator': '', 'value': ''}]
def available_notification_agents():
agents = [{'label': 'Tautulli Remote Android App',
@@ -140,6 +143,10 @@ def available_notification_agents():
'name': 'join',
'id': AGENT_IDS['join']
},
{'label': 'Kodi',
'name': 'xbmc',
'id': AGENT_IDS['xbmc']
},
{'label': 'Notify My Android',
'name': 'nma',
'id': AGENT_IDS['nma']
@@ -156,10 +163,10 @@ def available_notification_agents():
'name': 'prowl',
'id': AGENT_IDS['prowl']
},
{'label': 'Pushalot',
'name': 'pushalot',
'id': AGENT_IDS['pushalot']
},
# {'label': 'Pushalot',
# 'name': 'pushalot',
# 'id': AGENT_IDS['pushalot']
# },
{'label': 'Pushbullet',
'name': 'pushbullet',
'id': AGENT_IDS['pushbullet']
@@ -184,10 +191,6 @@ def available_notification_agents():
'name': 'twitter',
'id': AGENT_IDS['twitter']
},
{'label': 'XBMC',
'name': 'xbmc',
'id': AGENT_IDS['xbmc']
},
{'label': 'Zapier',
'name': 'zapier',
'id': AGENT_IDS['zapier']
@@ -196,7 +199,7 @@ def available_notification_agents():
# OSX Notifications should only be visible if it can be used
if OSX().validate():
agents.append({'label': 'OSX Notify',
agents.append({'label': 'macOS Notification Center',
'name': 'osx',
'id': AGENT_IDS['osx']
})
@@ -446,7 +449,6 @@ def get_notifier_config(notifier_id=None):
db = database.MonitorDatabase()
result = db.select_single('SELECT * FROM notifiers WHERE id = ?',
args=[notifier_id])
if not result:
return None
@@ -468,6 +470,14 @@ def get_notifier_config(notifier_id=None):
notifier_text[k] = {'subject': result.pop(k + '_subject'),
'body': result.pop(k + '_body')}
try:
result['custom_conditions'] = json.loads(result['custom_conditions'])
except (ValueError, TypeError):
result['custom_conditions'] = DEFAULT_CUSTOM_CONDITIONS
if not result['custom_conditions_logic']:
result['custom_conditions_logic'] = ''
result['config'] = config
result['config_options'] = notifier_config
result['actions'] = notifier_actions
@@ -494,7 +504,9 @@ def add_notifier_config(agent_id=None, **kwargs):
'agent_name': agent['name'],
'agent_label': agent['label'],
'friendly_name': '',
'notifier_config': json.dumps(get_agent_class(agent_id=agent['id']).config)
'notifier_config': json.dumps(get_agent_class(agent_id=agent['id']).config),
'custom_conditions': json.dumps(DEFAULT_CUSTOM_CONDITIONS),
'custom_conditions_logic': ''
}
if agent['name'] == 'scripts':
for a in available_notification_actions():
@@ -549,7 +561,7 @@ def set_notifier_config(notifier_id=None, agent_id=None, **kwargs):
'agent_label': agent['label'],
'friendly_name': kwargs.get('friendly_name', ''),
'notifier_config': json.dumps(notifier_config),
'custom_conditions': kwargs.get('custom_conditions', ''),
'custom_conditions': kwargs.get('custom_conditions', json.dumps(DEFAULT_CUSTOM_CONDITIONS)),
'custom_conditions_logic': kwargs.get('custom_conditions_logic', ''),
}
values.update(actions)
@@ -718,6 +730,13 @@ class PrettyMetadata(object):
def get_plex_url(self):
return self.parameters['plex_url']
@staticmethod
def get_parameters():
parameters = {param['value']: param['name']
for category in common.NOTIFICATION_PARAMETERS for param in category['parameters']}
parameters[''] = ''
return parameters
class Notifier(object):
NAME = ''
@@ -896,11 +915,9 @@ class ANDROIDAPP(Notifier):
'The content of your notifications will be sent unencrypted!</strong><br>'
'Please install the library to encrypt the notification contents. '
'Instructions can be found in the '
'<a href="' + helpers.anon_url(
'https://github.com/%s/%s-Wiki/wiki/'
'Frequently-Asked-Questions#notifications-pycryptodome'
% (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO)) +
'" target="_blank">FAQ</a>.',
'<a href="https://github.com/%s/%s-Wiki/wiki/'
'Frequently-Asked-Questions#notifications-pycryptodome'
'" target="_blank">FAQ</a>.' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO),
'input_type': 'help'
})
else:
@@ -1290,7 +1307,7 @@ class EMAIL(Notifier):
mailserver.ehlo()
if self.config['smtp_user']:
mailserver.login(self.config['smtp_user'], self.config['smtp_password'])
mailserver.login(str(self.config['smtp_user']), str(self.config['smtp_password']))
mailserver.sendmail(self.config['from'], recipients, msg.as_string())
mailserver.quit()
@@ -1422,7 +1439,7 @@ class FACEBOOK(Notifier):
plexpy.CONFIG.FACEBOOK_TOKEN = 'temp'
return facebook.auth_url(app_id=app_id,
canvas_url=redirect_uri + '/facebookStep2',
canvas_url=redirect_uri,
perms=['user_managed_groups','publish_actions'])
def _get_credentials(self, code=''):
@@ -1434,15 +1451,15 @@ class FACEBOOK(Notifier):
try:
# Request user access token
api = facebook.GraphAPI(version='2.5')
api = facebook.GraphAPI(version='2.12')
response = api.get_access_token_from_code(code=code,
redirect_uri=redirect_uri + '/facebookStep2',
redirect_uri=redirect_uri,
app_id=app_id,
app_secret=app_secret)
access_token = response['access_token']
# Request extended user access token
api = facebook.GraphAPI(access_token=access_token, version='2.5')
api = facebook.GraphAPI(access_token=access_token, version='2.12')
response = api.extend_access_token(app_id=app_id,
app_secret=app_secret)
@@ -1460,7 +1477,7 @@ class FACEBOOK(Notifier):
def _post_facebook(self, **data):
if self.config['group_id']:
api = facebook.GraphAPI(access_token=self.config['access_token'], version='2.5')
api = facebook.GraphAPI(access_token=self.config['access_token'], version='2.12')
try:
api.put_object(parent_object=self.config['group_id'], connection_name='feed', **data)
@@ -1500,25 +1517,11 @@ class FACEBOOK(Notifier):
return self._post_facebook(**data)
def return_config_options(self):
config_option = [{'label': 'Instructions',
'description': 'Step 1: Visit <a href="' + helpers.anon_url('https://developers.facebook.com/apps') + '" target="_blank">'
'Facebook Developers</a> to add a new app using <strong>basic setup</strong>.<br>'
'Step 2: Click <strong>Add Product</strong> on the left, then <strong>Get Started</strong>'
'for <strong>Facebook Login</strong>.<br>'
'Step 3: Fill in <strong>Valid OAuth redirect URIs</strong> with your Tautulli URL (e.g. http://localhost:8181).<br>'
'Step 4: Click <strong>App Review</strong> on the left and toggle "make public" to <strong>Yes</strong>.<br>'
'Step 5: Fill in the <strong>Tautulli URL</strong> below with the exact same URL from Step 3.<br>'
'Step 6: Fill in the <strong>App ID</strong> and <strong>App Secret</strong> below.<br>'
'Step 7: Click the <strong>Request Authorization</strong> button below to retrieve your access token.<br>'
'Step 8: Fill in your <strong>Access Token</strong> below if it is not filled in automatically.<br>'
'Step 9: Fill in your <strong>Group ID</strong> number below. It can be found in the URL of your group page.',
'input_type': 'help'
},
{'label': 'Tautulli URL',
config_option = [{'label': 'OAuth Redirect URI',
'value': self.config['redirect_uri'],
'name': 'facebook_redirect_uri',
'description': 'Your Tautulli URL. This will tell Facebook where to redirect you after authorization.\
(e.g. http://localhost:8181)',
'description': 'Fill in this address for the "Valid OAuth redirect URIs" '
'in your Facebook App.',
'input_type': 'text'
},
{'label': 'Facebook App ID',
@@ -1535,14 +1538,15 @@ class FACEBOOK(Notifier):
},
{'label': 'Request Authorization',
'value': 'Request Authorization',
'name': 'facebook_facebookStep1',
'name': 'facebook_facebook_auth',
'description': 'Request Facebook authorization. (Ensure you allow the browser pop-up).',
'input_type': 'button'
},
{'label': 'Facebook Access Token',
'value': self.config['access_token'],
'name': 'facebook_access_token',
'description': 'Your Facebook access token. Automatically filled in after requesting authorization.',
'description': 'Your Facebook access token. '
'Automatically filled in after requesting authorization.',
'input_type': 'text'
},
{'label': 'Facebook Group ID',
@@ -1741,7 +1745,7 @@ class GROWL(Notifier):
config_option = [{'label': 'Growl Host',
'value': self.config['host'],
'name': 'growl_host',
'description': 'Your Growl hostname.',
'description': 'Your Growl hostname or IP address.',
'input_type': 'text'
},
{'label': 'Growl Password',
@@ -1843,7 +1847,7 @@ class HIPCHAT(Notifier):
return self.make_request(self.config['hook'], headers=headers, json=data)
def return_config_options(self):
config_option = [{'label': 'Hipchat Custom Integrations Full URL',
config_option = [{'label': 'Hipchat Custom Integrations URL',
'value': self.config['hook'],
'name': 'hipchat_hook',
'description': 'Your Hipchat BYO integration URL. You can get a key from'
@@ -1932,7 +1936,8 @@ class IFTTT(Notifier):
"""
NAME = 'IFTTT'
_DEFAULT_CONFIG = {'key': '',
'event': 'tautulli'
'event': 'tautulli',
'value3': '',
}
def agent_notify(self, subject='', body='', action='', **kwargs):
@@ -1941,6 +1946,10 @@ class IFTTT(Notifier):
data = {'value1': subject.encode("utf-8"),
'value2': body.encode("utf-8")}
if self.config['value3']:
pretty_metadata = PrettyMetadata(kwargs['parameters'])
data['value3'] = pretty_metadata.parameters.get(self.config['value3'], '')
headers = {'Content-type': 'application/json'}
return self.make_request('https://maker.ifttt.com/trigger/{}/with/key/{}'.format(event, self.config['key']),
@@ -1964,6 +1973,13 @@ class IFTTT(Notifier):
' as <span class="inline-pre">value1</span>'
' and <span class="inline-pre">value2</span> respectively.',
'input_type': 'text'
},
{'label': 'Value 3',
'value': self.config['value3'],
'name': 'ifttt_value3',
'description': 'Optional: Select a parameter to send as <span class="inline-pre">value3</span>.',
'input_type': 'select',
'select_options': PrettyMetadata().get_parameters()
}
]
@@ -2286,9 +2302,9 @@ class NMA(Notifier):
class OSX(Notifier):
"""
OSX notifications
macOS notifications
"""
NAME = 'OSX Notify'
NAME = 'macOS'
_DEFAULT_CONFIG = {'notify_app': '/Applications/Tautulli'
}
@@ -2371,9 +2387,15 @@ class OSX(Notifier):
config_option = [{'label': 'Register Notify App',
'value': self.config['notify_app'],
'name': 'osx_notify_app',
'description': 'Enter the path/application name to be registered with the '
'Notification Center, default is /Applications/Tautulli.',
'description': 'Enter the path/application name to be registered with the Notification Center. '
'Default is <span class="inline-pre">/Applications/Tautulli</span>.',
'input_type': 'text'
},
{'label': 'Register App',
'value': 'Register App',
'name': 'osx_notify_register',
'description': 'Register Tautulli with the Notification Center.',
'input_type': 'button'
}
]
@@ -2454,7 +2476,7 @@ class PLEX(Notifier):
return True
def return_config_options(self):
config_option = [{'label': 'Plex Home Theater Host:Port',
config_option = [{'label': 'Plex Home Theater Host Address',
'value': self.config['hosts'],
'name': 'plex_hosts',
'description': 'Host running Plex Home Theater (eg. http://localhost:3005). Separate multiple hosts with commas (,).',
@@ -2645,10 +2667,10 @@ class PUSHBULLET(Notifier):
return {'': ''}
def return_config_options(self):
config_option = [{'label': 'Pushbullet API Key',
config_option = [{'label': 'Pushbullet Access Token',
'value': self.config['api_key'],
'name': 'pushbullet_api_key',
'description': 'Your Pushbullet API key.',
'description': 'Your Pushbullet access token.',
'input_type': 'text',
'refresh': True
},
@@ -2691,8 +2713,10 @@ class PUSHOVER(Notifier):
_DEFAULT_CONFIG = {'api_token': '',
'key': '',
'html_support': 1,
'priority': 0,
'sound': '',
'priority': 0,
'retry': 30,
'expire': 3600,
'incl_url': 1,
'incl_subject': 1,
'incl_poster': 0,
@@ -2713,6 +2737,10 @@ class PUSHOVER(Notifier):
if self.config['incl_subject']:
data['title'] = subject.encode("utf-8")
if self.config['priority'] == 2:
data['retry'] = max(30, self.config['retry'])
data['expire'] = max(30, self.config['expire'])
headers = {'Content-type': 'application/x-www-form-urlencoded'}
files = {}
@@ -2789,6 +2817,13 @@ class PUSHOVER(Notifier):
'description': 'Your Pushover user or group key.',
'input_type': 'text'
},
{'label': 'Sound',
'value': self.config['sound'],
'name': 'pushover_sound',
'description': 'Set the notification sound. Leave blank for the default sound.',
'input_type': 'select',
'select_options': self.get_sounds()
},
{'label': 'Priority',
'value': self.config['priority'],
'name': 'pushover_priority',
@@ -2796,12 +2831,19 @@ class PUSHOVER(Notifier):
'input_type': 'select',
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
},
{'label': 'Sound',
'value': self.config['sound'],
'name': 'pushover_sound',
'description': 'Set the notification sound. Leave blank for the default sound.',
'input_type': 'select',
'select_options': self.get_sounds()
{'label': 'Retry Interval',
'value': self.config['retry'],
'name': 'pushover_retry',
'description': 'Set the interval in seconds to keep retrying the notification.<br>'
'Note: For priority 2 only. Minimum 30 seconds.',
'input_type': 'number'
},
{'label': 'Expire Duration',
'value': self.config['expire'],
'name': 'pushover_expire',
'description': 'Set the duration in seconds when the notification will stop retrying.<br>'
'Note: For priority 2 only. Minimum 30 seconds.',
'input_type': 'number'
},
{'label': 'Enable HTML Support',
'value': self.config['html_support'],
@@ -2903,6 +2945,14 @@ class SCRIPTS(Notifier):
process.kill()
self.script_killed = True
# Common environment variables
env = {'PLEX_URL': plexpy.CONFIG.PMS_URL,
'PLEX_TOKEN': plexpy.CONFIG.PMS_TOKEN,
'TAUTULLI_URL': helpers.get_plexpy_url(hostname='localhost'),
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY
}
env.update(os.environ)
self.script_killed = False
output = error = ''
try:
@@ -2910,7 +2960,8 @@ class SCRIPTS(Notifier):
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=self.config['script_folder'])
cwd=self.config['script_folder'],
env=env)
if self.config['timeout'] > 0:
timer = threading.Timer(self.config['timeout'], kill_script, (process,))
@@ -2918,11 +2969,13 @@ class SCRIPTS(Notifier):
timer = None
try:
if timer: timer.start()
if timer:
timer.start()
output, error = process.communicate()
status = process.returncode
finally:
if timer: timer.cancel()
if timer:
timer.cancel()
except OSError as e:
logger.error(u"Tautulli Notifiers :: Failed to run script: %s" % e)
@@ -3364,16 +3417,7 @@ class TWITTER(Notifier):
return self._send_tweet(body, attachment=poster_url)
def return_config_options(self):
config_option = [{'label': 'Instructions',
'description': 'Step 1: Visit <a href="' + helpers.anon_url('https://apps.twitter.com') + '" target="_blank">'
'Twitter Apps</a> to <strong>Create New App</strong>. A vaild "Website" is not required.<br>'
'Step 2: Go to <strong>Keys and Access Tokens</strong> and click '
'<strong>Create my access token</strong>.<br>'
'Step 3: Fill in the <strong>Consumer Key</strong>, <strong>Consumer Secret</strong>, '
'<strong>Access Token</strong>, and <strong>Access Token Secret</strong> below.',
'input_type': 'help'
},
{'label': 'Twitter Consumer Key',
config_option = [{'label': 'Twitter Consumer Key',
'value': self.config['consumer_key'],
'name': 'twitter_consumer_key',
'description': 'Your Twitter consumer key.',
@@ -3417,9 +3461,9 @@ class TWITTER(Notifier):
class XBMC(Notifier):
"""
XBMC notifications
Kodi notifications
"""
NAME = 'XBMC'
NAME = 'Kodi'
_DEFAULT_CONFIG = {'hosts': '',
'username': '',
'password': '',
@@ -3489,22 +3533,22 @@ class XBMC(Notifier):
return True
def return_config_options(self):
config_option = [{'label': 'XBMC Host:Port',
config_option = [{'label': 'Kodi Host Address',
'value': self.config['hosts'],
'name': 'xbmc_hosts',
'description': 'Host running XBMC (e.g. http://localhost:8080). Separate multiple hosts with commas (,).',
'description': 'Host running Kodi (e.g. http://localhost:8080). Separate multiple hosts with commas (,).',
'input_type': 'text'
},
{'label': 'XBMC Username',
{'label': 'Kodi Username',
'value': self.config['username'],
'name': 'xbmc_username',
'description': 'Username of your XBMC client API (blank for none).',
'description': 'Username of your Kodi client API (blank for none).',
'input_type': 'text'
},
{'label': 'XBMC Password',
{'label': 'Kodi Password',
'value': self.config['password'],
'name': 'xbmc_password',
'description': 'Password of your XBMC client API (blank for none).',
'description': 'Password of your Kodi client API (blank for none).',
'input_type': 'password'
},
{'label': 'Notification Duration',

View File

@@ -29,7 +29,7 @@ import pmsconnect
import session
def get_server_resources(return_presence=False):
def get_server_resources(return_presence=False, return_server=False, **kwargs):
if not return_presence:
logger.info(u"Tautulli PlexTV :: Requesting resources for server...")
@@ -42,9 +42,15 @@ def get_server_resources(return_presence=False):
'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE,
'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD,
'pms_url': plexpy.CONFIG.PMS_URL,
'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL
'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL,
'pms_identifier': plexpy.CONFIG.PMS_IDENTIFIER
}
if kwargs:
server.update(kwargs)
for k in ['pms_ssl', 'pms_is_remote', 'pms_is_cloud', 'pms_url_manual']:
server[k] = int(server[k])
if server['pms_url_manual'] and server['pms_ssl'] or server['pms_is_cloud']:
scheme = 'https'
else:
@@ -55,7 +61,7 @@ def get_server_resources(return_presence=False):
port=server['pms_port'])
plex_tv = PlexTV()
result = plex_tv.get_server_connections(pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
result = plex_tv.get_server_connections(pms_identifier=server['pms_identifier'],
pms_ip=server['pms_ip'],
pms_port=server['pms_port'],
include_https=server['pms_ssl'])
@@ -103,6 +109,9 @@ def get_server_resources(return_presence=False):
server['pms_url'] = fallback_url
logger.info(u"Tautulli PlexTV :: Using user-defined URL.")
if return_server:
return server
plexpy.CONFIG.process_kwargs(server)
plexpy.CONFIG.write()
@@ -645,6 +654,7 @@ class PlexTV(object):
'label': helpers.get_xml_attr(d, 'name'),
'ip': helpers.get_xml_attr(c, 'address'),
'port': helpers.get_xml_attr(c, 'port'),
'uri': helpers.get_xml_attr(c, 'uri'),
'local': helpers.get_xml_attr(c, 'local'),
'value': helpers.get_xml_attr(c, 'address'),
'is_cloud': is_cloud

View File

@@ -666,6 +666,11 @@ class PmsConnect(object):
}
elif metadata_type == 'show':
# Workaround for for duration sometimes reported in minutes for a show
duration = helpers.get_xml_attr(metadata_main, 'duration')
if duration.isdigit() and int(duration) < 1000:
duration = unicode(int(duration) * 60 * 1000)
metadata = {'media_type': metadata_type,
'section_id': section_id,
'library_name': library_name,
@@ -685,7 +690,7 @@ class PmsConnect(object):
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'duration': duration,
'year': helpers.get_xml_attr(metadata_main, 'year'),
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
@@ -1465,18 +1470,12 @@ class PmsConnect(object):
transcode_details['transcode_hw_decoding'] = int(transcode_details['transcode_hw_decode'].lower() in common.HW_DECODERS)
transcode_details['transcode_hw_encoding'] = int(transcode_details['transcode_hw_encode'].lower() in common.HW_ENCODERS)
# Generate a combined transcode decision value
if transcode_details['video_decision'] == 'transcode' or transcode_details['audio_decision'] == 'transcode':
transcode_decision = 'transcode'
elif transcode_details['video_decision'] == 'copy' or transcode_details['audio_decision'] == 'copy':
transcode_decision = 'copy'
else:
transcode_decision = 'direct play'
# Determine if a synced version is being played
sync_id = None
if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \
and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play':
if media_type not in ('photo', 'clip') \
and not session.getElementsByTagName('Session') \
and not session.getElementsByTagName('TranscodeSession') \
and helpers.get_xml_attr(session, 'ratingKey').isdigit():
plex_tv = plextv.PlexTV()
parent_rating_key = helpers.get_xml_attr(session, 'parentRatingKey')
grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey')
@@ -1582,6 +1581,14 @@ class PmsConnect(object):
'stream_subtitle_decision': ''
}
# Generate a combined transcode decision value
if video_details['stream_video_decision'] == 'transcode' or audio_details['stream_audio_decision'] == 'transcode':
transcode_decision = 'transcode'
elif video_details['stream_video_decision'] == 'copy' or audio_details['stream_audio_decision'] == 'copy':
transcode_decision = 'copy'
else:
transcode_decision = 'direct play'
# Get the bif thumbnail
indexes = helpers.get_xml_attr(stream_media_parts_info, 'indexes')
view_offset = helpers.get_xml_attr(session, 'viewOffset')
@@ -2138,10 +2145,12 @@ class PmsConnect(object):
sort_type = '&type=10'
elif section_type == 'photo':
sort_type = ''
elif section_type == 'photoAlbum':
elif section_type == 'photo_album':
sort_type = '&type=14'
elif section_type == 'picture':
sort_type = '&type=13'
sort_type = '&type=13&clusterZoomLevel=1'
elif section_type == 'clip':
sort_type = '&type=12&clusterZoomLevel=1'
else:
sort_type = ''
@@ -2159,16 +2168,16 @@ class PmsConnect(object):
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_library_children_details: %s." % e)
return []
childern_list = []
children_list = []
for a in xml_head:
if a.getAttribute('size'):
if a.getAttribute('size') == '0':
logger.debug(u"Tautulli Pmsconnect :: No library data.")
childern_list = {'library_count': '0',
'childern_list': []
children_list = {'library_count': '0',
'children_list': []
}
return childern_list
return children_list
if rating_key:
library_count = helpers.get_xml_attr(xml_head[0], 'size')
@@ -2226,10 +2235,10 @@ class PmsConnect(object):
}
item_info.update(media_info)
childern_list.append(item_info)
children_list.append(item_info)
output = {'library_count': library_count,
'childern_list': childern_list
'children_list': children_list
}
return output
@@ -2284,12 +2293,12 @@ class PmsConnect(object):
library_stats.update(child_stats)
if section_type == 'photo':
parent_list = self.get_library_children_details(section_id=section_id, section_type='photoAlbum', count='1')
parent_list = self.get_library_children_details(section_id=section_id, section_type='picture', count='1')
if parent_list:
parent_stats = {'parent_count': parent_list['library_count']}
library_stats.update(parent_stats)
child_list = self.get_library_children_details(section_id=section_id, section_type='picture', count='1')
child_list = self.get_library_children_details(section_id=section_id, section_type='clip', count='1')
if child_list:
child_stats = {'child_count': child_list['library_count']}
library_stats.update(child_stats)

View File

@@ -38,14 +38,14 @@ def get_session_user():
Returns the user_id for the current logged in session
"""
_session = get_session_info()
return _session['user'] if _session and _session['user'] else None
return _session['user'] if _session['user_group'] == 'guest' and _session['user'] else None
def get_session_user_id():
"""
Returns the user_id for the current logged in session
"""
_session = get_session_info()
return str(_session['user_id']) if _session and _session['user_id'] else None
return str(_session['user_id']) if _session['user_group'] == 'guest' and _session['user_id'] else None
def get_session_shared_libraries():
"""
@@ -79,7 +79,7 @@ def get_session_library_filters_type(filters, media_type=None):
filters = filters.get('filter_tv', ())
elif media_type == 'artist' or media_type == 'album' or media_type == 'track':
filters = filters.get('filter_music', ())
elif media_type == 'photo' or media_type == 'photoAlbum' or media_type == 'picture':
elif media_type == 'photo' or media_type == 'photo_album' or media_type == 'picture' or media_type == 'clip':
filters = filters.get('filter_photos', ())
else:
filters = filters.get('filter_all', ())

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.0.21-beta"
PLEXPY_BRANCH = "master"
PLEXPY_RELEASE_VERSION = "v2.0.24"

View File

@@ -190,9 +190,9 @@ def checkGithub(auto_update=False):
if plexpy.CONFIG.GIT_BRANCH == 'master':
release = next((r for r in releases if not r['prerelease']), releases[0])
elif plexpy.CONFIG.GIT_BRANCH == 'beta':
release = next((r for r in releases if r['prerelease'] and '-beta' in r['tag_name']), releases[0])
release = next((r for r in releases if not r['tag_name'].endswith('-nightly')), releases[0])
elif plexpy.CONFIG.GIT_BRANCH == 'nightly':
release = next((r for r in releases if r['prerelease'] and '-nightly' in r['tag_name']), releases[0])
release = next((r for r in releases), releases[0])
else:
release = releases[0]
@@ -292,8 +292,8 @@ def update():
def checkout_git_branch():
if plexpy.INSTALL_TYPE == 'git':
output, err = runGit('fetch ' + plexpy.CONFIG.GIT_REMOTE)
output, err = runGit('checkout ' + plexpy.CONFIG.GIT_BRANCH)
output, err = runGit('fetch %s' % plexpy.CONFIG.GIT_REMOTE)
output, err = runGit('checkout %s' % plexpy.CONFIG.GIT_BRANCH)
if not output:
logger.error('Unable to change git branch.')
@@ -304,6 +304,8 @@ def checkout_git_branch():
logger.error('Unable to checkout from git: ' + line)
logger.info('Output: ' + str(output))
output, err = runGit('pull %s %s' % (plexpy.CONFIG.GIT_REMOTE, plexpy.CONFIG.GIT_BRANCH))
def read_changelog(latest_only=False, since_prev_release=False):
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')

View File

@@ -29,14 +29,16 @@ import logger
name = 'websocket'
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
ws_reconnect = False
ws_shutdown = False
def start_thread():
# Check for any existing sessions on start up
activity_pinger.check_active_sessions(ws_request=True)
# Start the websocket listener on it's own thread
threading.Thread(target=run).start()
thread = threading.Thread(target=run)
thread.daemon = True
thread.start()
def on_connect():
@@ -65,8 +67,18 @@ def on_disconnect():
def reconnect():
global ws_reconnect
ws_reconnect = True
shutdown()
logger.info(u"Tautulli WebSocket :: Reconnecting websocket...")
start_thread()
def shutdown():
global ws_shutdown
ws_shutdown = True
logger.info(u"Tautulli WebSocket :: Disconnecting websocket...")
plexpy.WEBSOCKET.close()
plexpy.WS_CONNECTED = False
def run():
@@ -88,8 +100,8 @@ def run():
else:
header = []
global ws_reconnect
ws_reconnect = False
global ws_shutdown
ws_shutdown = False
reconnects = 0
# Try an open the websocket connection
@@ -106,7 +118,7 @@ def run():
logger.info(u"Tautulli WebSocket :: Connection attempt %s." % str(reconnects))
try:
ws = create_connection(uri, header=header)
plexpy.WEBSOCKET = create_connection(uri, header=header)
logger.info(u"Tautulli WebSocket :: Ready")
plexpy.WS_CONNECTED = True
except (websocket.WebSocketException, IOError, Exception) as e:
@@ -117,12 +129,15 @@ def run():
while plexpy.WS_CONNECTED:
try:
process(*receive(ws))
process(*receive(plexpy.WEBSOCKET))
# successfully received data, reset reconnects counter
reconnects = 0
except websocket.WebSocketConnectionClosedException:
if ws_shutdown:
break
if reconnects == 0:
logger.warn(u"Tautulli WebSocket :: Connection has closed.")
@@ -136,31 +151,25 @@ def run():
logger.warn(u"Tautulli WebSocket :: Reconnection attempt %s." % str(reconnects))
try:
ws = create_connection(uri, header=header)
plexpy.WEBSOCKET = create_connection(uri, header=header)
logger.info(u"Tautulli WebSocket :: Ready")
plexpy.WS_CONNECTED = True
except (websocket.WebSocketException, IOError, Exception) as e:
logger.error(u"Tautulli WebSocket :: %s." % e)
else:
ws.shutdown()
plexpy.WS_CONNECTED = False
shutdown()
break
except (websocket.WebSocketException, Exception) as e:
if ws_shutdown:
break
logger.error(u"Tautulli WebSocket :: %s." % e)
ws.shutdown()
plexpy.WS_CONNECTED = False
shutdown()
break
# Check if we recieved a restart notification and close websocket connection cleanly
if ws_reconnect:
logger.info(u"Tautulli WebSocket :: Reconnecting websocket...")
ws.shutdown()
plexpy.WS_CONNECTED = False
start_thread()
if not plexpy.WS_CONNECTED and not ws_reconnect:
if not plexpy.WS_CONNECTED and not ws_shutdown:
on_disconnect()
logger.debug(u"Tautulli WebSocket :: Leaving thread.")

View File

@@ -106,10 +106,10 @@ def check_credentials(username, password, admin_login='0'):
if plexpy.CONFIG.HTTP_PASSWORD:
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
return True, 'admin'
return True, 'tautulli admin'
elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
return True, 'admin'
return True, 'tautulli admin'
if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS):
plex_login = user_login(username, password)
@@ -215,12 +215,12 @@ class AuthController(object):
return
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
def on_login(self, user_id, username, user_group):
def on_login(self, username, user_id=None, user_group=None, success=0):
"""Called on successful login"""
# Save login to the database
ip_address = cherrypy.request.headers.get('X-Forwarded-For', cherrypy.request.headers.get('Remote-Addr'))
host = cherrypy.request.headers.get('Host', cherrypy.request.headers.get('Origin'))
ip_address = cherrypy.request.remote.ip
host = cherrypy.request.base
user_agent = cherrypy.request.headers.get('User-Agent')
Users().set_user_login(user_id=user_id,
@@ -229,28 +229,15 @@ class AuthController(object):
ip_address=ip_address,
host=host,
user_agent=user_agent,
success=1)
success=success)
logger.debug(u"Tautulli WebAuth :: %s user '%s' logged into Tautulli." % (user_group.capitalize(), username))
if success == 1:
logger.debug(u"Tautulli WebAuth :: %s user '%s' logged into Tautulli." % (user_group.capitalize(), username))
def on_logout(self, username, user_group):
"""Called on logout"""
logger.debug(u"Tautulli WebAuth :: %s user '%s' logged out of Tautulli." % (user_group.capitalize(), username))
def on_login_failed(self, username):
"""Called on failed login"""
# Save login attempt to the database
ip_address = cherrypy.request.headers.get('X-Forwarded-For', cherrypy.request.headers.get('Remote-Addr'))
host = cherrypy.request.headers.get('Origin')
user_agent = cherrypy.request.headers.get('User-Agent')
Users().set_user_login(user=username,
ip_address=ip_address,
host=host,
user_agent=user_agent,
success=0)
def get_loginform(self):
from plexpy.webserve import serve_template
return serve_template(templatename="login.html", title="Login")
@@ -293,15 +280,16 @@ class AuthController(object):
valid_login, user_group = check_credentials(username, password, admin_login)
if valid_login:
if user_group == 'guest':
if user_group == 'tautulli admin':
user_group = 'admin'
user_id = None
else:
if re.match(r"[^@]+@[^@]+\.[^@]+", username):
user_details = Users().get_details(email=username)
else:
user_details = Users().get_details(user=username)
user_id = user_details['user_id']
else:
user_id = None
time_delta = timedelta(days=30) if remember_me == '1' else timedelta(minutes=60)
expiry = datetime.utcnow() + time_delta
@@ -315,7 +303,10 @@ class AuthController(object):
jwt_token = jwt.encode(payload, plexpy.CONFIG.JWT_SECRET, algorithm=JWT_ALGORITHM)
self.on_login(user_id, username, user_group)
self.on_login(username=username,
user_id=user_id,
user_group=user_group,
success=1)
jwt_cookie = JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID
cherrypy.response.cookie[jwt_cookie] = jwt_token
@@ -327,13 +318,13 @@ class AuthController(object):
return {'status': 'success', 'token': jwt_token.decode('utf-8'), 'uuid': plexpy.CONFIG.PMS_UUID}
elif admin_login == '1':
self.on_login_failed(username)
self.on_login(username=username)
logger.debug(u"Tautulli WebAuth :: Invalid admin login attempt from '%s'." % username)
cherrypy.response.status = 401
return error_message
else:
self.on_login_failed(username)
self.on_login(username=username)
logger.debug(u"Tautulli WebAuth :: Invalid login attempt from '%s'." % username)
cherrypy.response.status = 401
return error_message

View File

@@ -2611,6 +2611,7 @@ class WebInterface(object):
"pms_ssl": plexpy.CONFIG.PMS_SSL,
"pms_is_remote": plexpy.CONFIG.PMS_IS_REMOTE,
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
"pms_url": plexpy.CONFIG.PMS_URL,
"pms_url_manual": checked(plexpy.CONFIG.PMS_URL_MANUAL),
"pms_uuid": plexpy.CONFIG.PMS_UUID,
"pms_web_url": plexpy.CONFIG.PMS_WEB_URL,
@@ -2812,6 +2813,12 @@ class WebInterface(object):
return {'result': 'success', 'message': 'Settings saved.'}
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
def get_server_resources(self, **kwargs):
return plextv.get_server_resources(return_server=True, **kwargs)
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@@ -3004,18 +3011,12 @@ class WebInterface(object):
def get_notifier_config_modal(self, notifier_id=None, **kwargs):
result = notifiers.get_notifier_config(notifier_id=notifier_id)
if not result['custom_conditions']:
result['custom_conditions'] = json.dumps([{'parameter': '', 'operator': '', 'value': ''}])
if not result['custom_conditions_logic']:
result['custom_conditions_logic'] = ''
parameters = [
{'name': param['name'], 'type': param['type'], 'value': param['value']}
for category in common.NOTIFICATION_PARAMETERS for param in category['parameters']
]
return serve_template(templatename="notifier_config.html", notifier=result, parameters=json.dumps(parameters))
return serve_template(templatename="notifier_config.html", notifier=result, parameters=parameters)
@cherrypy.expose
@cherrypy.tools.json_out()
@@ -3190,7 +3191,7 @@ class WebInterface(object):
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
def facebookStep1(self, app_id='', app_secret='', redirect_uri='', **kwargs):
def facebook_auth(self, app_id='', app_secret='', redirect_uri='', **kwargs):
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
facebook_notifier = notifiers.FACEBOOK()
@@ -3205,7 +3206,7 @@ class WebInterface(object):
@cherrypy.expose
@requireAuth(member_of("admin"))
def facebookStep2(self, code='', **kwargs):
def facebook_redirect(self, code='', **kwargs):
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
facebook = notifiers.FACEBOOK()
@@ -3461,7 +3462,8 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, remote=0, **kwargs):
def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, remote=0, manual=0,
get_url=False, **kwargs):
""" Get the PMS server identifier.
```
@@ -3474,7 +3476,8 @@ class WebInterface(object):
remote (int): 0 or 1
Returns:
string: The unique PMS identifier
json:
{'identifier': '08u2phnlkdshf890bhdlksghnljsahgleikjfg9t'}
```
"""
# Attempt to get the pms_identifier from plex.tv if the server is published
@@ -3505,11 +3508,21 @@ class WebInterface(object):
xml_head = request.getElementsByTagName('MediaContainer')[0]
identifier = xml_head.getAttribute('machineIdentifier')
result = {'identifier': identifier}
if identifier:
return identifier
if get_url == 'true':
server = self.get_server_resources(pms_ip=hostname,
pms_port=port,
pms_ssl=ssl,
pms_is_remote=remote,
pms_url_manual=manual,
pms_identifier=identifier)
result['url'] = server['pms_url']
return result
else:
logger.warn('Unable to retrieve the PMS identifier.')
return None
return result
@cherrypy.expose
@requireAuth(member_of("admin"))
@@ -3585,7 +3598,7 @@ class WebInterface(object):
return {'result': 'success',
'update': True,
'release': True,
'message': 'A new release (%) of Tautulli is available.' % plexpy.LATEST_RELEASE,
'message': 'A new release (%s) of Tautulli is available.' % plexpy.LATEST_RELEASE,
'latest_release': plexpy.LATEST_RELEASE,
'release_url': helpers.anon_url(
'https://github.com/%s/%s/releases/tag/%s'
@@ -5113,10 +5126,10 @@ class WebInterface(object):
quote_list = ['To crush your enemies, see them driven before you, and to hear the lamentation of their women!',
'Your clothes, give them to me, now!',
'Do it!',
'If it bleeds, we can kill it',
'If it bleeds, we can kill it.',
'See you at the party Richter!',
'Let off some steam, Bennett',
'I\'ll be back',
'Let off some steam, Bennett.',
'I\'ll be back.',
'Get to the chopper!',
'Hasta La Vista, Baby!',
'It\'s not a tumor!',
@@ -5137,7 +5150,7 @@ class WebInterface(object):
'What killed the dinosaurs? The Ice Age!',
'That\'s for sleeping with my wife!',
'Remember when I said I\'d kill you last... I lied!',
'You want to be a farmer? Here\'s a couple of acres',
'You want to be a farmer? Here\'s a couple of acres.',
'Now, this is the plan. Get your ass to Mars.',
'I just had a terrible thought... What if this is a dream?',
'Well, listen to this one: Rubber baby buggy bumpers!',
@@ -5271,34 +5284,4 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth()
def get_plexpy_url(self, **kwargs):
if plexpy.CONFIG.ENABLE_HTTPS:
scheme = 'https'
else:
scheme = 'http'
# Have to return some hostname if socket fails even if 127.0.0.1 won't work
hostname = '127.0.0.1'
if plexpy.CONFIG.HTTP_HOST == '0.0.0.0':
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.connect(('<broadcast>', 0))
hostname = s.getsockname()[0]
except socket.error:
hostname = socket.gethostbyname(socket.gethostname())
else:
hostname = plexpy.CONFIG.HTTP_HOST
if plexpy.CONFIG.HTTP_PORT not in (80, 443):
port = ':' + str(plexpy.CONFIG.HTTP_PORT)
else:
port = ''
if plexpy.CONFIG.HTTP_ROOT.strip('/'):
root = '/' + plexpy.CONFIG.HTTP_ROOT.strip('/')
else:
root = ''
return scheme + '://' + hostname + port + root
return helpers.get_plexpy_url()

View File

@@ -122,6 +122,7 @@ def initialize(options):
'/images': {
'tools.staticdir.on': True,
'tools.staticdir.dir': "interfaces/default/images",
'tools.staticdir.content_types': {'svg': 'image/svg+xml'},
'tools.caching.on': True,
'tools.caching.force': True,
'tools.caching.delay': 0,