Compare commits
32 Commits
v2.0.23-be
...
v2.0.28
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f335ffa8d5 | ||
![]() |
793665d62a | ||
![]() |
7da5730c73 | ||
![]() |
1f587ed698 | ||
![]() |
1032fdfe7a | ||
![]() |
818e7723ff | ||
![]() |
a69008e179 | ||
![]() |
91c647f9ae | ||
![]() |
36b80aa6d3 | ||
![]() |
c35fcc727c | ||
![]() |
749e1fcebe | ||
![]() |
084732706d | ||
![]() |
2aff7713cd | ||
![]() |
683a782723 | ||
![]() |
5108e1bb09 | ||
![]() |
d8298a12eb | ||
![]() |
042b48c1fd | ||
![]() |
8fac54aa71 | ||
![]() |
244008d539 | ||
![]() |
502b807e45 | ||
![]() |
35914b9a48 | ||
![]() |
24ac34d5e2 | ||
![]() |
a5807f21b4 | ||
![]() |
e3b71a729e | ||
![]() |
ebb287e1ee | ||
![]() |
bd3497b2bf | ||
![]() |
034f3ee308 | ||
![]() |
a946879fc1 | ||
![]() |
9f964b5a87 | ||
![]() |
ed0b41cd19 | ||
![]() |
dc87591992 | ||
![]() |
d05e80e573 |
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,5 +1,46 @@
|
||||
# Changelog
|
||||
|
||||
## v2.0.28 (2018-04-02)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Homepage activity header text.
|
||||
|
||||
|
||||
## v2.0.27 (2018-04-02)
|
||||
|
||||
* Monitoring:
|
||||
* Change: Move activity refresh interval setting to the settings page.
|
||||
|
||||
|
||||
## v2.0.26-beta (2018-03-30)
|
||||
|
||||
* Monitoring:
|
||||
* New: Setting to change the refresh interval on the homepage.
|
||||
* Fix: Identify extras correctly on the activity cards.
|
||||
* Notifications:
|
||||
* Change: Send Telegram image and text separately if the caption is longer than 200 characters.
|
||||
* UI:
|
||||
* Fix: Error when clicking on synced playlist links.
|
||||
|
||||
|
||||
## v2.0.25 (2018-03-22)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Websocket not reconnecting causing activity monitoring and notifications to not work.
|
||||
* Fix: Error checking for synced streams without Plex Pass.
|
||||
|
||||
|
||||
## 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:
|
||||
|
@@ -134,9 +134,6 @@ div.form-control .selectize-input {
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
}
|
||||
.react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values.negative-operator .value-wrapper:not(:first-child):before {
|
||||
content: "and" !important;
|
||||
}
|
||||
.react-selectize.root-node .react-selectize-control .react-selectize-search-field-and-selected-values .resizable-input {
|
||||
padding-top: 3px !important;
|
||||
padding-bottom: 3px !important;
|
||||
|
@@ -64,7 +64,7 @@ DOCUMENTATION :: END
|
||||
from collections import defaultdict
|
||||
from urllib import quote
|
||||
from plexpy import helpers
|
||||
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES
|
||||
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES, EXTRA_TYPES
|
||||
import plexpy
|
||||
%>
|
||||
<%
|
||||
@@ -108,7 +108,11 @@ DOCUMENTATION :: END
|
||||
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&fallback=cover&refresh=true);"></div>
|
||||
</a>
|
||||
% elif data['media_type'] in ('photo', 'clip'):
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||
% if data['extra_type']:
|
||||
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['art'].replace('/art', '/thumb') or data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
|
||||
% else:
|
||||
<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>
|
||||
% endif
|
||||
@@ -301,14 +305,13 @@ DOCUMENTATION :: END
|
||||
<li class="dashboard-activity-info-item">
|
||||
<div class="sub-heading">Bandwidth</div>
|
||||
<div class="sub-value time-right">
|
||||
% if data['media_type'] != 'photo' and helpers.cast_to_int(data['bandwidth']):
|
||||
% if data['media_type'] != 'photo' and data['bandwidth'] != 'Unknown':
|
||||
<%
|
||||
bw = helpers.cast_to_int(data['bandwidth'])
|
||||
if bw != "Unknown":
|
||||
if bw > 1000:
|
||||
bw = str(round(bw / 1000.0, 1)) + ' Mbps'
|
||||
else:
|
||||
bw = str(bw) + ' kbps'
|
||||
if bw > 1000:
|
||||
bw = str(round(bw / 1000.0, 1)) + ' Mbps'
|
||||
else:
|
||||
bw = str(bw) + ' kbps'
|
||||
%>
|
||||
<span id="stream-bandwidth-${sk}">${bw}</span>
|
||||
<span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span>
|
||||
@@ -440,7 +443,12 @@ DOCUMENTATION :: END
|
||||
% elif data['media_type'] == 'photo':
|
||||
<span title="${data['title']}" class="sub-heading">${data['title']}</span>
|
||||
% else:
|
||||
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
|
||||
% if data['extra_type']:
|
||||
<% extra_type = EXTRA_TYPES.get(data['extra_type'], data['sub_type'].capitalize()) %>
|
||||
<span title="${data['year']} (${extra_type})" class="sub-heading">${data['year']} (${extra_type})</span>
|
||||
% else:
|
||||
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
|
||||
% endif
|
||||
% endif
|
||||
% elif data['channel_title']:
|
||||
<span title="${data['channel_title']}" class="sub-heading">${data['channel_title']}</span>
|
||||
|
@@ -113,7 +113,7 @@
|
||||
// Load user ids and names (for the selector)
|
||||
$.ajax({
|
||||
url: 'get_user_names',
|
||||
type: 'get',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success: function (data) {
|
||||
var select = $('#history-user');
|
||||
@@ -130,6 +130,7 @@
|
||||
function loadHistoryTable(media_type, selected_user_id) {
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'POST',
|
||||
data: function (d) {
|
||||
return {
|
||||
json_data: JSON.stringify(d),
|
||||
|
@@ -507,17 +507,15 @@
|
||||
|
||||
$('#location-' + key).html(s.location.toUpperCase());
|
||||
|
||||
if (s.media_type !== 'photo' && parseInt(s.bandwidth)) {
|
||||
var bw = parseInt(s.bandwidth);
|
||||
if (bw !== "Unknown") {
|
||||
if (bw > 1000) {
|
||||
bw = (bw / 1000).toFixed(1) + ' Mbps';
|
||||
} else {
|
||||
bw = bw + ' kbps'
|
||||
}
|
||||
if (s.media_type !== 'photo' && s.bandwidth !== 'Unknown') {
|
||||
var bw = parseInt(s.bandwidth) || 0;
|
||||
if (bw > 1000) {
|
||||
bw = (bw / 1000).toFixed(1) + ' Mbps';
|
||||
} else {
|
||||
bw = bw + ' kbps'
|
||||
}
|
||||
$('#stream-bandwidth-' + key).html(bw);
|
||||
}
|
||||
};
|
||||
|
||||
// Update the stream progress times
|
||||
$('#stream-eta-' + key).html(moment().add(parseInt(s.duration) - parseInt(s.view_offset), 'milliseconds').format(time_format));
|
||||
@@ -589,7 +587,7 @@
|
||||
if (!(create_instances.length) && activity_ready) {
|
||||
getCurrentActivity();
|
||||
}
|
||||
}, 2000);
|
||||
}, ${config['home_refresh_interval'] * 1000});
|
||||
|
||||
setInterval(function(){
|
||||
$('.progress_time_offset').each(function () {
|
||||
@@ -604,7 +602,7 @@
|
||||
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
|
||||
var view_offset = parseInt($(this).data('view_offset'));
|
||||
var stream_duration = parseInt($(this).data('stream_duration'));
|
||||
var progress_percent = Math.min(Math.trunc(view_offset / stream_duration * 100), 100)
|
||||
var progress_percent = Math.min(Math.trunc(view_offset / stream_duration * 100), 100);
|
||||
$(this).width(progress_percent - 3 + '%').html(progress_percent + '%')
|
||||
.attr('data-original-title', 'Stream Progress ' + progress_percent + '%')
|
||||
.data('view_offset', Math.min(view_offset + 1000, stream_duration));
|
||||
|
@@ -547,7 +547,7 @@ DOCUMENTATION :: END
|
||||
function get_history() {
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'post',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
@@ -563,7 +563,7 @@ DOCUMENTATION :: END
|
||||
function get_history() {
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'post',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
@@ -579,7 +579,7 @@ DOCUMENTATION :: END
|
||||
function get_history() {
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'post',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
|
@@ -37,7 +37,6 @@ sync_table_options = {
|
||||
"data": "state",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData === 'pending') {
|
||||
$(td).addClass('currentlyWatching');
|
||||
$(td).html('Pending...');
|
||||
} else {
|
||||
$(td).html(cellData.toProperCase());
|
||||
@@ -66,7 +65,7 @@ sync_table_options = {
|
||||
"data": "sync_title",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
if (rowData['metadata_type'] !== '') {
|
||||
if (rowData['rating_key']) {
|
||||
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||
} else {
|
||||
$(td).html(cellData);
|
||||
@@ -74,7 +73,7 @@ sync_table_options = {
|
||||
}
|
||||
},
|
||||
"className": "datatable-wrap"
|
||||
},
|
||||
},
|
||||
{
|
||||
"targets": [4],
|
||||
"data": "metadata_type",
|
||||
@@ -150,6 +149,11 @@ sync_table_options = {
|
||||
"preDrawCallback": function (settings) {
|
||||
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||
showMsg(msg, false, false, 0)
|
||||
},
|
||||
"rowCallback": function (row, rowData, rowIndex) {
|
||||
if (rowData['state'] === 'pending') {
|
||||
$(row).addClass('current-activity-row');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -91,7 +91,7 @@
|
||||
json_data: JSON.stringify(d)
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
libraries_list_table = $('#libraries_list_table').DataTable(libraries_list_table_options);
|
||||
var colvis = new $.fn.dataTable.ColVis(libraries_list_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] });
|
||||
|
@@ -374,7 +374,7 @@ DOCUMENTATION :: END
|
||||
// Build watch history table
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'post',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
@@ -406,7 +406,7 @@ DOCUMENTATION :: END
|
||||
// Build media info table
|
||||
media_info_table_options.ajax = {
|
||||
url: 'get_library_media_info',
|
||||
type: 'post',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
|
@@ -229,8 +229,8 @@
|
||||
var selected_log_level = null;
|
||||
function loadtautullilogs(logfile, selected_log_level) {
|
||||
log_table_options.ajax = {
|
||||
url: "get_log",
|
||||
type: 'post',
|
||||
url: 'get_log',
|
||||
type: 'POST',
|
||||
data: function (d) {
|
||||
return {
|
||||
logfile: logfile,
|
||||
@@ -249,7 +249,8 @@
|
||||
|
||||
function loadPlexLogs() {
|
||||
plex_log_table_options.ajax = {
|
||||
url: "get_plex_log?log_type=server"
|
||||
url: 'get_plex_log?log_type=server',
|
||||
type: 'POST'
|
||||
};
|
||||
plex_log_table_options.initComplete = bindLogLevelFilter;
|
||||
plex_log_table = $('#plex_log_table').DataTable(plex_log_table_options);
|
||||
@@ -257,7 +258,8 @@
|
||||
|
||||
function loadPlexScannerLogs() {
|
||||
plex_log_table_options.ajax = {
|
||||
url: "get_plex_log?log_type=scanner"
|
||||
url: 'get_plex_log?log_type=scanner',
|
||||
type: 'POST'
|
||||
};
|
||||
plex_log_table_options.initComplete = bindLogLevelFilter;
|
||||
plex_scanner_log_table = $('#plex_scanner_log_table').DataTable(plex_log_table_options);
|
||||
@@ -265,7 +267,8 @@
|
||||
|
||||
function loadNotificationLogs() {
|
||||
notification_log_table_options.ajax = {
|
||||
url: "get_notification_log",
|
||||
url: 'get_notification_log',
|
||||
type: 'POST',
|
||||
data: function (d) {
|
||||
return {
|
||||
json_data: JSON.stringify(d)
|
||||
@@ -278,7 +281,8 @@
|
||||
function loadLoginLogs() {
|
||||
login_log_table_options.pageLength = 50;
|
||||
login_log_table_options.ajax = {
|
||||
url: "get_user_logins",
|
||||
url: 'get_user_logins',
|
||||
type: 'POST',
|
||||
data: function (d) {
|
||||
return {
|
||||
json_data: JSON.stringify(d)
|
||||
|
@@ -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()
|
||||
|
@@ -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>
|
||||
@@ -343,21 +340,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
function setNegativeOperator(select) {
|
||||
if (select.val() === 'does not contain' || select.val() === 'is not') {
|
||||
select.closest('.form-group').find('.react-selectize-search-field-and-selected-values').addClass('negative-operator');
|
||||
} else {
|
||||
select.closest('.form-group').find('.react-selectize-search-field-and-selected-values').removeClass('negative-operator');
|
||||
}
|
||||
}
|
||||
|
||||
$('#condition-widget select[name=operator]').each(function () {
|
||||
setNegativeOperator($(this));
|
||||
});
|
||||
$('#condition-widget').on('change', 'select[name=operator]', function () {
|
||||
setNegativeOperator($(this));
|
||||
});
|
||||
|
||||
function reloadModal() {
|
||||
$.ajax({
|
||||
url: 'get_notifier_config_modal',
|
||||
@@ -433,16 +415,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 +446,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 +504,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); });
|
||||
});
|
||||
|
@@ -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>
|
||||
|
@@ -267,6 +267,21 @@
|
||||
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-homepage">
|
||||
|
||||
<div class="padded-header">
|
||||
<h3>Activity</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="home_refresh_interval">Activty Refresh Interval</label>
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" data-parsley-type="integer" id="home_refresh_interval" name="home_refresh_interval" value="${config['home_refresh_interval']}" size="5" data-parsley-min="2" data-parsley-trigger="change" data-parsley-errors-container="#home_refresh_interval_error" required>
|
||||
</div>
|
||||
<div id="home_refresh_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||
</div>
|
||||
<p class="help-block">Set the interval (in seconds) to refresh the current activity on the homepage. Minimum 2.</p>
|
||||
</div>
|
||||
|
||||
<div class="padded-header">
|
||||
<h3>Sections</h3>
|
||||
</div>
|
||||
@@ -666,12 +681,7 @@
|
||||
<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>
|
||||
<input type="text" class="form-control" id="pms_url" name="pms_url" value="${config['pms_url']}" size="30" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
@@ -969,6 +979,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>
|
||||
@@ -983,7 +996,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>
|
||||
@@ -1230,7 +1243,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>
|
||||
@@ -1858,6 +1871,9 @@ $(document).ready(function() {
|
||||
$('#pms_url_manual').prop('checked', false);
|
||||
$('#pms_url').val('Please verify your server above to retrieve the URL');
|
||||
PMSCloudCheck();
|
||||
},
|
||||
onDropdownOpen: function() {
|
||||
this.clear();
|
||||
}
|
||||
});
|
||||
var select_pms = $select_pms[0].selectize;
|
||||
@@ -1931,11 +1947,11 @@ $(document).ready(function() {
|
||||
data: {
|
||||
hostname: pms_ip,
|
||||
port: pms_port,
|
||||
identifier: pms_identifier,
|
||||
ssl: pms_ssl,
|
||||
remote: pms_is_remote,
|
||||
manual: pms_url_manual,
|
||||
get_url: serverChanged
|
||||
get_url: true,
|
||||
test_websocket: true
|
||||
},
|
||||
cache: true,
|
||||
async: true,
|
||||
@@ -1948,15 +1964,23 @@ $(document).ready(function() {
|
||||
var result = xhr;
|
||||
var identifier = result.identifier;
|
||||
var url = result.url;
|
||||
var ws = result.ws;
|
||||
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 (ws === false) {
|
||||
$("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
|
||||
$("#pms_ip_group").addClass("has-error");
|
||||
showMsg('<i class="fa fa-exclamation-circle"></i> Server found but unable to connect websocket.<br>Check the <a href="logs">logs</a> for errors.', false, true, 5000, true)
|
||||
} else {
|
||||
$("#pms_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
|
||||
$("#pms_ip_group").removeClass("has-error");
|
||||
serverChanged = false;
|
||||
}
|
||||
|
||||
if (_callback) {
|
||||
_callback();
|
||||
@@ -1984,13 +2008,6 @@ $(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');
|
||||
|
@@ -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>
|
||||
|
@@ -100,7 +100,7 @@
|
||||
// Load user ids and names (for the selector)
|
||||
$.ajax({
|
||||
url: 'get_user_names',
|
||||
type: 'get',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success: function (data) {
|
||||
var select = $('#sync-user');
|
||||
@@ -116,7 +116,8 @@
|
||||
|
||||
function loadSyncTable(selected_user_id) {
|
||||
sync_table_options.ajax = {
|
||||
url: 'get_sync?user_id=' + selected_user_id
|
||||
url: 'get_sync?user_id=' + selected_user_id,
|
||||
type: 'POST'
|
||||
};
|
||||
sync_table = $('#sync_table').DataTable(sync_table_options);
|
||||
var colvis = new $.fn.dataTable.ColVis(sync_table, {
|
||||
|
@@ -413,7 +413,7 @@ DOCUMENTATION :: END
|
||||
// Build watch history table
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'post',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
@@ -442,7 +442,8 @@ DOCUMENTATION :: END
|
||||
function loadSyncTable() {
|
||||
// Build user sync table
|
||||
sync_table_options.ajax = {
|
||||
url: 'get_sync?user_id=' + user_id
|
||||
url: 'get_sync?user_id=' + user_id,
|
||||
type: 'POST'
|
||||
};
|
||||
sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options);
|
||||
sync_table.column(2).visible(false);
|
||||
@@ -457,7 +458,7 @@ DOCUMENTATION :: END
|
||||
// Build user IP table
|
||||
user_ip_table_options.ajax = {
|
||||
url: 'get_user_ips',
|
||||
type: 'post',
|
||||
type: 'POST',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
@@ -474,6 +475,7 @@ DOCUMENTATION :: END
|
||||
// Build user login table
|
||||
login_log_table_options.ajax = {
|
||||
url: 'get_user_logins',
|
||||
type: 'POST',
|
||||
data: function(d) {
|
||||
return {
|
||||
json_data: JSON.stringify(d),
|
||||
|
@@ -94,7 +94,7 @@
|
||||
json_data: JSON.stringify(d)
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
users_list_table = $('#users_list_table').DataTable(users_list_table_options);
|
||||
var colvis = new $.fn.dataTable.ColVis(users_list_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] });
|
||||
|
@@ -374,6 +374,9 @@ $(document).ready(function() {
|
||||
$('#pms_is_remote_checkbox').prop('disabled', false);
|
||||
$('#pms_ssl_checkbox').prop('disabled', false);
|
||||
}
|
||||
},
|
||||
onDropdownOpen: function() {
|
||||
this.clear();
|
||||
}
|
||||
});
|
||||
var select_pms = $select_pms[0].selectize;
|
||||
|
@@ -1596,6 +1596,18 @@ def dbcheck():
|
||||
'ALTER TABLE poster_urls ADD COLUMN delete_hash TEXT'
|
||||
)
|
||||
|
||||
# Rename notifiers in the database
|
||||
result = c_db.execute('SELECT agent_label FROM notifiers '
|
||||
'WHERE agent_label = "XBMC" OR agent_label = "OSX Notify"').fetchone()
|
||||
if result:
|
||||
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():
|
||||
|
@@ -293,8 +293,8 @@ def connect_server(log=True, startup=False):
|
||||
|
||||
try:
|
||||
web_socket.start_thread()
|
||||
except:
|
||||
logger.error(u"Websocket :: Unable to open connection.")
|
||||
except Exception as e:
|
||||
logger.error(u"Websocket :: Unable to open connection: %s." % e)
|
||||
|
||||
|
||||
def check_server_access():
|
||||
|
@@ -171,6 +171,16 @@ HW_ENCODERS = [
|
||||
'nvenc'
|
||||
]
|
||||
|
||||
EXTRA_TYPES = {
|
||||
'1': 'Trailer',
|
||||
'2': 'Deleted Scene',
|
||||
'3': 'Interview',
|
||||
'5': 'Behind the Scenes',
|
||||
'6': 'Scene',
|
||||
'10': 'Featurette',
|
||||
'11': 'Short'
|
||||
}
|
||||
|
||||
SCHEDULER_LIST = [
|
||||
'Check GitHub for updates',
|
||||
'Check for server response',
|
||||
|
@@ -209,6 +209,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'HOME_STATS_CARDS': (list, 'General', ['top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', \
|
||||
'popular_music', 'last_watched', 'top_users', 'top_platforms', 'most_concurrent']),
|
||||
'HOME_STATS_RECENTLY_ADDED_COUNT': (int, 'General', 50),
|
||||
'HOME_REFRESH_INTERVAL': (int, 'General', 10),
|
||||
'HTTPS_CREATE_CERT': (int, 'General', 1),
|
||||
'HTTPS_CERT': (str, 'General', ''),
|
||||
'HTTPS_CERT_CHAIN': (str, 'General', ''),
|
||||
|
@@ -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()}
|
||||
|
@@ -143,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']
|
||||
@@ -159,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']
|
||||
@@ -187,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']
|
||||
@@ -199,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']
|
||||
})
|
||||
@@ -915,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:
|
||||
@@ -1441,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=''):
|
||||
@@ -1455,7 +1453,7 @@ class FACEBOOK(Notifier):
|
||||
# Request user access token
|
||||
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']
|
||||
@@ -1519,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',
|
||||
@@ -1554,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',
|
||||
@@ -1760,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',
|
||||
@@ -1862,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'
|
||||
@@ -2317,9 +2302,9 @@ class NMA(Notifier):
|
||||
|
||||
class OSX(Notifier):
|
||||
"""
|
||||
OSX notifications
|
||||
macOS notifications
|
||||
"""
|
||||
NAME = 'OSX Notify'
|
||||
NAME = 'macOS'
|
||||
_DEFAULT_CONFIG = {'notify_app': '/Applications/Tautulli'
|
||||
}
|
||||
|
||||
@@ -2402,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'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2485,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 (,).',
|
||||
@@ -2676,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
|
||||
},
|
||||
@@ -2960,6 +2951,7 @@ class SCRIPTS(Notifier):
|
||||
'TAUTULLI_URL': helpers.get_plexpy_url(hostname='localhost'),
|
||||
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY
|
||||
}
|
||||
env.update(os.environ)
|
||||
|
||||
self.script_killed = False
|
||||
output = error = ''
|
||||
@@ -3315,10 +3307,17 @@ class TELEGRAM(Notifier):
|
||||
if poster_content:
|
||||
poster_filename = 'poster_{}.jpg'.format(pretty_metadata.parameters['rating_key'])
|
||||
files = {'photo': (poster_filename, poster_content, 'image/jpeg')}
|
||||
data['caption'] = text
|
||||
|
||||
return self.make_request('https://api.telegram.org/bot{}/sendPhoto'.format(self.config['bot_token']),
|
||||
data=data, files=files)
|
||||
if len(text) > 200:
|
||||
data['disable_notification'] = True
|
||||
else:
|
||||
data['caption'] = text
|
||||
|
||||
r = self.make_request('https://api.telegram.org/bot{}/sendPhoto'.format(self.config['bot_token']),
|
||||
data=data, files=files)
|
||||
|
||||
if not data.pop('disable_notification', None):
|
||||
return r
|
||||
|
||||
data['text'] = text
|
||||
|
||||
@@ -3425,16 +3424,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.',
|
||||
@@ -3478,9 +3468,9 @@ class TWITTER(Notifier):
|
||||
|
||||
class XBMC(Notifier):
|
||||
"""
|
||||
XBMC notifications
|
||||
Kodi notifications
|
||||
"""
|
||||
NAME = 'XBMC'
|
||||
NAME = 'Kodi'
|
||||
_DEFAULT_CONFIG = {'hosts': '',
|
||||
'username': '',
|
||||
'password': '',
|
||||
@@ -3550,22 +3540,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',
|
||||
|
@@ -1096,7 +1096,9 @@ class PmsConnect(object):
|
||||
'genres': genres,
|
||||
'labels': labels,
|
||||
'collections': collections,
|
||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||
'full_title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'extra_type': helpers.get_xml_attr(metadata_main, 'extraType'),
|
||||
'sub_type': helpers.get_xml_attr(metadata_main, 'subtype')
|
||||
}
|
||||
|
||||
else:
|
||||
@@ -1475,7 +1477,8 @@ class PmsConnect(object):
|
||||
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():
|
||||
and helpers.get_xml_attr(session, 'ratingKey').isdigit() \
|
||||
and plexpy.CONFIG.PMS_PLEXPASS:
|
||||
plex_tv = plextv.PlexTV()
|
||||
parent_rating_key = helpers.get_xml_attr(session, 'parentRatingKey')
|
||||
grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey')
|
||||
@@ -1686,7 +1689,9 @@ class PmsConnect(object):
|
||||
'audio_channel_layout': common.AUDIO_CHANNELS.get(audio_channels, audio_channels),
|
||||
'channel_icon': helpers.get_xml_attr(session, 'sourceIcon'),
|
||||
'channel_title': helpers.get_xml_attr(session, 'sourceTitle'),
|
||||
'live': int(helpers.get_xml_attr(session, 'live') == '1')
|
||||
'live': int(helpers.get_xml_attr(session, 'live') == '1'),
|
||||
'extra_type': helpers.get_xml_attr(session, 'extraType'),
|
||||
'sub_type': helpers.get_xml_attr(session, 'subtype')
|
||||
}
|
||||
else:
|
||||
channel_stream = 0
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.23-beta"
|
||||
PLEXPY_BRANCH = "master"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.28"
|
||||
|
@@ -25,6 +25,7 @@ import plexpy
|
||||
import activity_handler
|
||||
import activity_pinger
|
||||
import activity_processor
|
||||
import database
|
||||
import logger
|
||||
|
||||
name = 'websocket'
|
||||
@@ -33,8 +34,14 @@ ws_shutdown = False
|
||||
|
||||
|
||||
def start_thread():
|
||||
# Check for any existing sessions on start up
|
||||
activity_pinger.check_active_sessions(ws_request=True)
|
||||
try:
|
||||
# Check for any existing sessions on start up
|
||||
activity_pinger.check_active_sessions(ws_request=True)
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli WebSocket :: Failed to check for active sessions: %s." % e)
|
||||
logger.warn(u"Tautulli WebSocket :: Attempt to fix by flushing temporary sessions...")
|
||||
database.delete_sessions()
|
||||
|
||||
# Start the websocket listener on it's own thread
|
||||
thread = threading.Thread(target=run)
|
||||
thread.daemon = True
|
||||
@@ -67,7 +74,7 @@ def on_disconnect():
|
||||
|
||||
|
||||
def reconnect():
|
||||
shutdown()
|
||||
close()
|
||||
logger.info(u"Tautulli WebSocket :: Reconnecting websocket...")
|
||||
start_thread()
|
||||
|
||||
@@ -75,7 +82,10 @@ def reconnect():
|
||||
def shutdown():
|
||||
global ws_shutdown
|
||||
ws_shutdown = True
|
||||
close()
|
||||
|
||||
|
||||
def close():
|
||||
logger.info(u"Tautulli WebSocket :: Disconnecting websocket...")
|
||||
plexpy.WEBSOCKET.close()
|
||||
plexpy.WS_CONNECTED = False
|
||||
@@ -122,7 +132,7 @@ def run():
|
||||
logger.info(u"Tautulli WebSocket :: Ready")
|
||||
plexpy.WS_CONNECTED = True
|
||||
except (websocket.WebSocketException, IOError, Exception) as e:
|
||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||
logger.error("Tautulli WebSocket :: %s." % e)
|
||||
|
||||
if plexpy.WS_CONNECTED:
|
||||
on_connect()
|
||||
@@ -155,18 +165,18 @@ def run():
|
||||
logger.info(u"Tautulli WebSocket :: Ready")
|
||||
plexpy.WS_CONNECTED = True
|
||||
except (websocket.WebSocketException, IOError, Exception) as e:
|
||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||
logger.error("Tautulli WebSocket :: %s." % e)
|
||||
|
||||
else:
|
||||
shutdown()
|
||||
close()
|
||||
break
|
||||
|
||||
except (websocket.WebSocketException, Exception) as e:
|
||||
if ws_shutdown:
|
||||
break
|
||||
|
||||
logger.error(u"Tautulli WebSocket :: %s." % e)
|
||||
shutdown()
|
||||
logger.error("Tautulli WebSocket :: %s." % e)
|
||||
close()
|
||||
break
|
||||
|
||||
if not plexpy.WS_CONNECTED and not ws_shutdown:
|
||||
|
@@ -27,6 +27,8 @@ from hashing_passwords import make_hash
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako import exceptions
|
||||
|
||||
import websocket
|
||||
|
||||
import plexpy
|
||||
import activity_pinger
|
||||
import common
|
||||
@@ -171,6 +173,7 @@ class WebInterface(object):
|
||||
"home_stats_type": plexpy.CONFIG.HOME_STATS_TYPE,
|
||||
"home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT,
|
||||
"home_stats_recently_added_count": plexpy.CONFIG.HOME_STATS_RECENTLY_ADDED_COUNT,
|
||||
"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
|
||||
@@ -2640,6 +2643,7 @@ class WebInterface(object):
|
||||
"home_sections": json.dumps(plexpy.CONFIG.HOME_SECTIONS),
|
||||
"home_stats_cards": json.dumps(plexpy.CONFIG.HOME_STATS_CARDS),
|
||||
"home_library_cards": json.dumps(plexpy.CONFIG.HOME_LIBRARY_CARDS),
|
||||
"home_refresh_interval": plexpy.CONFIG.HOME_REFRESH_INTERVAL,
|
||||
"buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD,
|
||||
"buffer_wait": plexpy.CONFIG.BUFFER_WAIT,
|
||||
"group_history_tables": checked(plexpy.CONFIG.GROUP_HISTORY_TABLES),
|
||||
@@ -3191,7 +3195,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()
|
||||
@@ -3206,7 +3210,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()
|
||||
@@ -3463,7 +3467,7 @@ class WebInterface(object):
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi()
|
||||
def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, remote=0, manual=0,
|
||||
get_url=False, **kwargs):
|
||||
get_url=False, test_websocket=False, **kwargs):
|
||||
""" Get the PMS server identifier.
|
||||
|
||||
```
|
||||
@@ -3519,6 +3523,23 @@ class WebInterface(object):
|
||||
pms_url_manual=manual,
|
||||
pms_identifier=identifier)
|
||||
result['url'] = server['pms_url']
|
||||
result['ws'] = None
|
||||
|
||||
if test_websocket == 'true':
|
||||
# Quick test websocket connection
|
||||
ws_url = result['url'].replace('http', 'ws', 1) + '/:/websockets/notifications'
|
||||
header = ['X-Plex-Token: %s' % plexpy.CONFIG.PMS_TOKEN]
|
||||
|
||||
logger.debug("Testing websocket connection...")
|
||||
try:
|
||||
test_ws = websocket.create_connection(ws_url, header=header)
|
||||
test_ws.close()
|
||||
logger.debug("Websocket connection test successful.")
|
||||
result['ws'] = True
|
||||
except (websocket.WebSocketException, IOError, Exception) as e:
|
||||
logger.error("Websocket connection test failed: %s" % e)
|
||||
result['ws'] = False
|
||||
|
||||
return result
|
||||
else:
|
||||
logger.warn('Unable to retrieve the PMS identifier.')
|
||||
@@ -3697,6 +3718,9 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@requireAuth()
|
||||
def info(self, rating_key=None, source=None, query=None, **kwargs):
|
||||
if rating_key and not str(rating_key).isdigit():
|
||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
||||
|
||||
metadata = None
|
||||
|
||||
config = {
|
||||
@@ -5126,10 +5150,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!',
|
||||
@@ -5150,7 +5174,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!',
|
||||
|
Reference in New Issue
Block a user