Compare commits
29 Commits
v2.0.22-be
...
v2.0.23-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
522684b2ab | ||
![]() |
feab16b351 | ||
![]() |
ee041db63d | ||
![]() |
2479533d07 | ||
![]() |
d045fd5834 | ||
![]() |
8407f27fed | ||
![]() |
b505286caf | ||
![]() |
feb762ce8b | ||
![]() |
8acdb5af83 | ||
![]() |
5af1294f71 | ||
![]() |
87d2d273d3 | ||
![]() |
b5c52ac71e | ||
![]() |
efe9a15f72 | ||
![]() |
525f1e4b0b | ||
![]() |
d18820b832 | ||
![]() |
7e024fd736 | ||
![]() |
c9c5989474 | ||
![]() |
ce9f96d3be | ||
![]() |
7362dd0bf4 | ||
![]() |
9905ebc144 | ||
![]() |
8f8010884b | ||
![]() |
37afd141be | ||
![]() |
a3643b4302 | ||
![]() |
02cfd8d9b7 | ||
![]() |
941ce439b4 | ||
![]() |
a08bce2073 | ||
![]() |
4e9c8322c3 | ||
![]() |
89bfe85be3 | ||
![]() |
98d994591c |
3
API.md
3
API.md
@@ -1674,7 +1674,8 @@ Optional parameters:
|
||||
remote (int): 0 or 1
|
||||
|
||||
Returns:
|
||||
string: The unique PMS identifier
|
||||
json:
|
||||
{'identifier': '08u2phnlkdshf890bhdlksghnljsahgleikjfg9t'}
|
||||
```
|
||||
|
||||
|
||||
|
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## 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:
|
||||
|
@@ -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()
|
||||
|
@@ -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;
|
||||
|
@@ -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':
|
||||
|
@@ -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']}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -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']}"
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@@ -171,7 +171,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 +333,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));
|
||||
}
|
||||
|
@@ -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">
|
||||
@@ -1062,8 +1078,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>
|
||||
@@ -1573,7 +1589,7 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
function preSaveChecks(_callback) {
|
||||
if ($("#pms_identifier").val() == "") {
|
||||
if (serverChanged) {
|
||||
verifyServer();
|
||||
}
|
||||
verifyPMSWebURL();
|
||||
@@ -1585,7 +1601,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 +1785,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 +1856,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 +1922,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 +1933,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 +1944,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 +1976,6 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
$('#verify_server_button').on('click', function(){
|
||||
$("#pms_identifier").val("");
|
||||
verifyServer();
|
||||
});
|
||||
|
||||
@@ -1959,6 +1984,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');
|
||||
|
@@ -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':
|
||||
|
@@ -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;
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
||||
@@ -1622,6 +1624,10 @@ def upgrade():
|
||||
def shutdown(restart=False, update=False, checkout=False):
|
||||
cherrypy.engine.exit()
|
||||
|
||||
# Shutdown the websocket connection
|
||||
if WEBSOCKET:
|
||||
web_socket.shutdown()
|
||||
|
||||
if SCHED.running:
|
||||
SCHED.shutdown(wait=False)
|
||||
if activity_handler.ACTIVITY_SCHED.running:
|
||||
|
@@ -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))
|
||||
|
@@ -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.'},
|
||||
|
@@ -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
|
@@ -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
|
||||
|
@@ -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 '',
|
||||
|
@@ -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',
|
||||
@@ -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 = ''
|
||||
@@ -1290,7 +1309,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()
|
||||
@@ -1434,7 +1453,7 @@ 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',
|
||||
app_id=app_id,
|
||||
@@ -1442,7 +1461,7 @@ class FACEBOOK(Notifier):
|
||||
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 +1479,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)
|
||||
@@ -1932,7 +1951,8 @@ class IFTTT(Notifier):
|
||||
"""
|
||||
NAME = 'IFTTT'
|
||||
_DEFAULT_CONFIG = {'key': '',
|
||||
'event': 'tautulli'
|
||||
'event': 'tautulli',
|
||||
'value3': '',
|
||||
}
|
||||
|
||||
def agent_notify(self, subject='', body='', action='', **kwargs):
|
||||
@@ -1941,6 +1961,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 +1988,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()
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2923,6 +2954,13 @@ 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
|
||||
}
|
||||
|
||||
self.script_killed = False
|
||||
output = error = ''
|
||||
try:
|
||||
@@ -2930,7 +2968,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,))
|
||||
@@ -2938,11 +2977,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)
|
||||
|
@@ -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
|
||||
|
@@ -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')
|
||||
@@ -2161,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')
|
||||
@@ -2228,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
|
||||
|
@@ -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():
|
||||
"""
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.22-beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.23-beta"
|
||||
|
@@ -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')
|
||||
|
@@ -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.")
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
@@ -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"))
|
||||
@@ -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()
|
||||
|
Reference in New Issue
Block a user