Compare commits
49 Commits
v2.1.19-be
...
v2.1.22
Author | SHA1 | Date | |
---|---|---|---|
![]() |
03751abc0e | ||
![]() |
a94207691f | ||
![]() |
dbc53ca710 | ||
![]() |
4c9ddbd8b7 | ||
![]() |
045c69f5d8 | ||
![]() |
71ae314c46 | ||
![]() |
c8575bbc0f | ||
![]() |
f1b3a6f7b6 | ||
![]() |
8a94f6d63a | ||
![]() |
9b8fb73a7a | ||
![]() |
67c333e86e | ||
![]() |
cfa0b20419 | ||
![]() |
4b2930c890 | ||
![]() |
d98565ea12 | ||
![]() |
471f7c184a | ||
![]() |
3d4a5e6547 | ||
![]() |
382322d5e7 | ||
![]() |
c0ae25611b | ||
![]() |
f025533582 | ||
![]() |
fd28e5183a | ||
![]() |
185099f183 | ||
![]() |
cd6289046e | ||
![]() |
955dc795ff | ||
![]() |
1b772e60a9 | ||
![]() |
c6f4c17a81 | ||
![]() |
1e68a81fe1 | ||
![]() |
4944ce1ca0 | ||
![]() |
f04873446a | ||
![]() |
505b6b616e | ||
![]() |
87dd43d699 | ||
![]() |
a48ebef9ae | ||
![]() |
e40483525b | ||
![]() |
ebc563fd26 | ||
![]() |
5bb3e189fe | ||
![]() |
ed08df5224 | ||
![]() |
ae2584b6f6 | ||
![]() |
f0e2355231 | ||
![]() |
878c48b491 | ||
![]() |
ecfbb4de9b | ||
![]() |
731af75c54 | ||
![]() |
9817da6012 | ||
![]() |
dd3f75f154 | ||
![]() |
1eee03fa8f | ||
![]() |
02af6c4e6c | ||
![]() |
b8a9c4f5b7 | ||
![]() |
0b227dc69e | ||
![]() |
8228018dd0 | ||
![]() |
5c3086a049 | ||
![]() |
634e003bb7 |
27
API.md
27
API.md
@@ -1,9 +1,15 @@
|
||||
# API Reference
|
||||
|
||||
The API is still pretty new and needs some serious cleaning up on the backend but should be reasonably functional. There are no error codes yet.
|
||||
|
||||
## General structure
|
||||
The API endpoint is `http://ip:port + HTTP_ROOT + /api/v2?apikey=$apikey&cmd=$command`
|
||||
The API endpoint is
|
||||
```
|
||||
http://IP_ADDRESS:PORT + [/HTTP_ROOT] + /api/v2?apikey=$apikey&cmd=$command
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
http://localhost:8181/api/v2?apikey=66198313a092496b8a725867d2223b5f&cmd=get_metadata&rating_key=153037
|
||||
```
|
||||
|
||||
Response example (default `json`)
|
||||
```
|
||||
@@ -354,7 +360,8 @@ Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
session_key (int): Session key for the session info to return, OR
|
||||
session_id (str): Session ID for the session info to return
|
||||
|
||||
Returns:
|
||||
json:
|
||||
@@ -1140,7 +1147,8 @@ Returns:
|
||||
"video_language_code": "",
|
||||
"video_profile": "high",
|
||||
"video_ref_frames": "4",
|
||||
"video_width": "1920"
|
||||
"video_width": "1920",
|
||||
"selected": 0
|
||||
},
|
||||
{
|
||||
"audio_bitrate": "384",
|
||||
@@ -1153,7 +1161,8 @@ Returns:
|
||||
"audio_profile": "",
|
||||
"audio_sample_rate": "48000",
|
||||
"id": "511664",
|
||||
"type": "2"
|
||||
"type": "2",
|
||||
"selected": 1
|
||||
},
|
||||
{
|
||||
"id": "511953",
|
||||
@@ -1164,7 +1173,8 @@ Returns:
|
||||
"subtitle_language": "English",
|
||||
"subtitle_language_code": "eng",
|
||||
"subtitle_location": "external",
|
||||
"type": "3"
|
||||
"type": "3",
|
||||
"selected": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2435,7 +2445,7 @@ Required parameters:
|
||||
body (str): The body of the message
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
script_args (str): The arguments for script notifications
|
||||
|
||||
Returns:
|
||||
None
|
||||
@@ -2496,6 +2506,7 @@ Optional parameters:
|
||||
img_format (str): png
|
||||
fallback (str): "poster", "cover", "art"
|
||||
refresh (bool): True or False whether to refresh the image cache
|
||||
return_hash (bool): True or False to return the self-hosted image hash instead of the image
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,5 +1,54 @@
|
||||
# Changelog
|
||||
|
||||
## v2.1.22 (2018-10-05)
|
||||
|
||||
* Notifications:
|
||||
* Fix: Notification agent settings not loading when failed to retrieve some data.
|
||||
* UI:
|
||||
* Fix: Incorrectly showing localhost server in the setup wizard.
|
||||
* Other:
|
||||
* Fix: Incorrect redirect to HTTP when HTTPS proxy header is present.
|
||||
* Fix: Websocket not connecting automatically after the setup wizard.
|
||||
|
||||
|
||||
## v2.1.21 (2018-09-21)
|
||||
|
||||
* Notifications:
|
||||
* Fix: Content Rating notification condition always evaluating to True. (Thanks @Arcanemagus)
|
||||
* Fix: Script arguments not showing substituted values in the notification logs.
|
||||
* UI:
|
||||
* New: Unsupported browser warning when using IE or Edge.
|
||||
* Fix: Misaligned refresh image icon in album search results. (Thanks @Sheigutn)
|
||||
* Fix: Music history showing as pre-Tautulli in stream info modal.
|
||||
* Other:
|
||||
* Fix: Typo in Systemd init script group value. (Thanks @ldumont)
|
||||
* Fix: Execute permissions in Fedora/CentOS and Systemd init scripts. (Thanks @wilmardo)
|
||||
* Fix: Systemd init script instructions per Linux distro. (Thanks @samwiseg00)
|
||||
* Change: Fallback to Tautulli data directory if logs/backup/cache/newsletter directories are not writable.
|
||||
* Change: Check for alternative reverse proxy headers if X-Forwarded-Host is missing.
|
||||
|
||||
|
||||
## v2.1.20 (2018-09-05)
|
||||
* No changes.
|
||||
|
||||
|
||||
## v2.1.20-beta (2018-09-02)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Fetch messing season info when "Hide Seasons" is enabled for a show.
|
||||
* Fix: Video and Audio details sometimes missing on activity cards.
|
||||
* Notifications:
|
||||
* New: Added UTC timestamp to notification parameters. (Thanks @samwiseg00)
|
||||
* New: Added TAUTULLI_PUBLIC_URL to script environment variables. (Thanks @samwiseg00)
|
||||
* UI:
|
||||
* Change: Automatically redirect '/' to HTTP root if enabled.
|
||||
* API:
|
||||
* New: Added return_hash parameter to pms_image_proxy command.
|
||||
* New: Added session_id parameter to get_activity command.
|
||||
* Other:
|
||||
* Change: Linux systemd startup script to use the "tautulli" group permission. (Thanks @samwiseg00)
|
||||
|
||||
|
||||
## v2.1.19-beta (2018-08-19)
|
||||
|
||||
* Notifications:
|
||||
|
@@ -4162,4 +4162,16 @@ a[data-tab-destination] {
|
||||
}
|
||||
.fa-blank {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#browser-warning {
|
||||
height: 25px;
|
||||
width: 100%;
|
||||
background: #cc7b19;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
padding-top: 2px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 9999;
|
||||
}
|
@@ -439,7 +439,7 @@
|
||||
$('#transcode_container-' + key).html(transcode_container);
|
||||
|
||||
var video_decision = '';
|
||||
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.video_decision !== '') {
|
||||
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.stream_video_decision) {
|
||||
var v_res= '';
|
||||
switch (s.video_resolution.toLowerCase()) {
|
||||
case 'sd':
|
||||
@@ -477,7 +477,7 @@
|
||||
$('#video_decision-' + key).html(video_decision);
|
||||
|
||||
var audio_decision = '';
|
||||
if (['movie', 'episode', 'clip', 'track'].indexOf(s.media_type) > -1 && s.audio_decision) {
|
||||
if (['movie', 'episode', 'clip', 'track'].indexOf(s.media_type) > -1 && s.stream_audio_decision) {
|
||||
var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase();
|
||||
var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase();
|
||||
if (s.stream_audio_decision === 'transcode') {
|
||||
|
@@ -190,12 +190,12 @@ DOCUMENTATION :: END
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face cover-item style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
</div>
|
||||
<div class="item-children-instance-text-wrapper album-item">
|
||||
<div class="item-children-instance-text-wrapper cover-item">
|
||||
<h3 title="${child['title']}">${child['title']}</h3>
|
||||
</div>
|
||||
</a>
|
||||
@@ -219,7 +219,7 @@ DOCUMENTATION :: END
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
</div>
|
||||
<div class="item-children-instance-text-wrapper album-item">
|
||||
<div class="item-children-instance-text-wrapper cover-item">
|
||||
<h3 title="${child['parent_title']}">${child['parent_title']}</h3>
|
||||
<h3 title="${child['title']}">${child['title']}</h3>
|
||||
</div>
|
||||
@@ -246,11 +246,11 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
</div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
<div class="item-children-instance-text-wrapper album-item">
|
||||
<div class="item-children-instance-text-wrapper cover-item">
|
||||
<h3 title="${child['original_title'] or child['grandparent_title']}">${child['original_title'] or child['grandparent_title']}</h3>
|
||||
<h3 title="${child['title']}">${child['title']}</h3>
|
||||
<h3 title="${child['parent_title']}" class="text-muted">${child['parent_title']}</h3>
|
||||
|
@@ -1,3 +1,29 @@
|
||||
var p = {
|
||||
name: 'Unknown',
|
||||
version: 'Unknown',
|
||||
os: 'Unknown'
|
||||
};
|
||||
if (typeof platform !== 'undefined') {
|
||||
p.name = platform.name;
|
||||
p.version = platform.version;
|
||||
p.os = platform.os.toString();
|
||||
}
|
||||
|
||||
if (['IE', 'Microsoft Edge', 'IE Mobile'].indexOf(p.name) > -1) {
|
||||
$('body').prepend('<div id="browser-warning"><i class="fa fa-exclamation-circle"></i> ' +
|
||||
'Tautulli does not support Internet Explorer or Microsoft Edge! ' +
|
||||
'Please use a different browser such as Chrome or Firefox.</div>');
|
||||
var offset = $('#browser-warning').height();
|
||||
var navbar = $('.navbar-fixed-top');
|
||||
if (navbar.length) {
|
||||
navbar.offset({top: navbar.offset().top + offset});
|
||||
}
|
||||
var container = $('.body-container');
|
||||
if (container.length) {
|
||||
container.offset({top: container.offset().top + offset});
|
||||
}
|
||||
}
|
||||
|
||||
function initConfigCheckbox(elem, toggleElem, reverse) {
|
||||
toggleElem = (toggleElem === undefined) ? null : toggleElem;
|
||||
reverse = (reverse === undefined) ? false : reverse;
|
||||
@@ -141,7 +167,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
data: dataString,
|
||||
type: 'post',
|
||||
type: 'POST',
|
||||
beforeSend: function (jqXHR, settings) {
|
||||
// Start loader etc.
|
||||
feedback.prepend(loader);
|
||||
@@ -496,9 +522,10 @@ if (!localStorage.getItem('Tautulli_ClientId')) {
|
||||
}
|
||||
|
||||
function uuidv4() {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
)
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c) {
|
||||
var cryptoObj = window.crypto || window.msCrypto; // for IE 11
|
||||
return (c ^ cryptoObj.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
});
|
||||
}
|
||||
|
||||
var x_plex_headers = {
|
||||
@@ -506,10 +533,10 @@ var x_plex_headers = {
|
||||
'X-Plex-Product': 'Tautulli',
|
||||
'X-Plex-Version': 'Plex OAuth',
|
||||
'X-Plex-Client-Identifier': localStorage.getItem('Tautulli_ClientId'),
|
||||
'X-Plex-Platform': platform.name,
|
||||
'X-Plex-Platform-Version': platform.version,
|
||||
'X-Plex-Device': platform.os.toString(),
|
||||
'X-Plex-Device-Name': platform.name
|
||||
'X-Plex-Platform': p.name,
|
||||
'X-Plex-Platform-Version': p.version,
|
||||
'X-Plex-Device': p.os,
|
||||
'X-Plex-Device-Name': p.name
|
||||
};
|
||||
|
||||
var plex_oauth_window = null;
|
||||
@@ -568,7 +595,6 @@ getPlexOAuthPin = function () {
|
||||
type: 'POST',
|
||||
headers: x_plex_headers,
|
||||
success: function(data) {
|
||||
plex_oauth_window.location = 'https://app.plex.tv/auth/#!?clientID=' + x_plex_headers['X-Plex-Client-Identifier'] + '&code=' + data.code;
|
||||
deferred.resolve({pin: data.id, code: data.code});
|
||||
},
|
||||
error: function() {
|
||||
@@ -585,7 +611,6 @@ function PlexOAuth(success, error, pre) {
|
||||
if (typeof pre === "function") {
|
||||
pre()
|
||||
}
|
||||
clearTimeout(polling);
|
||||
closePlexOAuthWindow();
|
||||
plex_oauth_window = PopupCenter('', 'Plex-OAuth', 600, 700);
|
||||
$(plex_oauth_window.document.body).html(plex_oauth_loader);
|
||||
@@ -593,40 +618,38 @@ function PlexOAuth(success, error, pre) {
|
||||
getPlexOAuthPin().then(function (data) {
|
||||
const pin = data.pin;
|
||||
const code = data.code;
|
||||
var keep_polling = true;
|
||||
|
||||
plex_oauth_window.location = 'https://app.plex.tv/auth/#!?clientID=' + x_plex_headers['X-Plex-Client-Identifier'] + '&code=' + code;
|
||||
polling = pin;
|
||||
|
||||
(function poll() {
|
||||
polling = setTimeout(function () {
|
||||
$.ajax({
|
||||
url: 'https://plex.tv/api/v2/pins/' + pin,
|
||||
type: 'GET',
|
||||
headers: x_plex_headers,
|
||||
success: function (data) {
|
||||
if (data.authToken){
|
||||
keep_polling = false;
|
||||
closePlexOAuthWindow();
|
||||
if (typeof success === "function") {
|
||||
success(data.authToken)
|
||||
}
|
||||
$.ajax({
|
||||
url: 'https://plex.tv/api/v2/pins/' + pin,
|
||||
type: 'GET',
|
||||
headers: x_plex_headers,
|
||||
success: function (data) {
|
||||
if (data.authToken){
|
||||
closePlexOAuthWindow();
|
||||
if (typeof success === "function") {
|
||||
success(data.authToken)
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
keep_polling = false;
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
if (textStatus !== "timeout") {
|
||||
closePlexOAuthWindow();
|
||||
if (typeof error === "function") {
|
||||
error()
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
if (keep_polling){
|
||||
poll();
|
||||
} else {
|
||||
clearTimeout(polling);
|
||||
}
|
||||
},
|
||||
timeout: 1000
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
if (!plex_oauth_window.closed && polling === pin){
|
||||
setTimeout(function() {poll()}, 1000);
|
||||
}
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
})();
|
||||
}, function () {
|
||||
closePlexOAuthWindow();
|
||||
|
@@ -12,7 +12,6 @@
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
|
||||
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
|
||||
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
||||
<link href="${http_root}css/font-awesome.all.min.css" rel="stylesheet">
|
||||
|
@@ -779,6 +779,7 @@
|
||||
|
||||
$.ajax({
|
||||
url: 'get_notify_text_preview',
|
||||
type: 'POST',
|
||||
data: {
|
||||
notify_action: action,
|
||||
subject: subject,
|
||||
|
@@ -90,6 +90,7 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<select class="form-control pms-settings selectize-pms-ip" id="pms_ip_selectize">
|
||||
% if config['pms_identifier']:
|
||||
<option value="${config['pms_ip']}:${config['pms_port']}"
|
||||
data-identifier="${config['pms_identifier']}"
|
||||
data-ip="${config['pms_ip']}"
|
||||
@@ -99,6 +100,7 @@
|
||||
data-is_cloud="${config['pms_is_cloud']}"
|
||||
data-label="${config['pms_name'] or 'Local'}"
|
||||
selected>${config['pms_ip']}</option>
|
||||
% endif
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
0
init-scripts/init.fedora.centos.service
Normal file → Executable file
0
init-scripts/init.fedora.centos.service
Normal file → Executable file
9
init-scripts/init.systemd
Normal file → Executable file
9
init-scripts/init.systemd
Normal file → Executable file
@@ -24,9 +24,10 @@
|
||||
# - The example settings in this file assume that Tautulli is installed to: /opt/Tautulli
|
||||
#
|
||||
# - To create this user and give it ownership of the Tautulli directory:
|
||||
# sudo adduser --system --no-create-home tautulli
|
||||
# sudo chown tautulli:nogroup -R /opt/Tautulli
|
||||
#
|
||||
# Ubuntu/Debian: sudo addgroup tautulli && sudo adduser --system --no-create-home tautulli --ingroup tautulli
|
||||
# CentOS/Fedora: sudo adduser --system --no-create-home tautulli
|
||||
# sudo chown tautulli:tautulli -R /opt/Tautulli
|
||||
#
|
||||
# - Adjust ExecStart= to point to:
|
||||
# 1. Your Tautulli executable
|
||||
# - Default: /opt/Tautulli/Tautulli.py
|
||||
@@ -51,7 +52,7 @@ ExecStart=/opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir
|
||||
GuessMainPID=no
|
||||
Type=forking
|
||||
User=tautulli
|
||||
Group=nogroup
|
||||
Group=tautulli
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
@@ -140,21 +140,13 @@ def initialize(config_file):
|
||||
if not CONFIG.HTTPS_KEY:
|
||||
CONFIG.HTTPS_KEY = os.path.join(DATA_DIR, 'server.key')
|
||||
|
||||
if not CONFIG.LOG_DIR:
|
||||
CONFIG.LOG_DIR = os.path.join(DATA_DIR, 'logs')
|
||||
|
||||
if not os.path.exists(CONFIG.LOG_DIR):
|
||||
try:
|
||||
os.makedirs(CONFIG.LOG_DIR)
|
||||
except OSError:
|
||||
CONFIG.LOG_DIR = None
|
||||
|
||||
if not QUIET:
|
||||
sys.stderr.write("Unable to create the log directory. " \
|
||||
"Logging to screen only.\n")
|
||||
CONFIG.LOG_DIR, log_writable = check_folder_writable(
|
||||
CONFIG.LOG_DIR, os.path.join(DATA_DIR, 'logs'), 'logs')
|
||||
if not log_writable and not QUIET:
|
||||
sys.stderr.write("Unable to create the log directory. Logging to screen only.\n")
|
||||
|
||||
# Start the logger, disable console if needed
|
||||
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
|
||||
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR if log_writable else None,
|
||||
verbose=VERBOSE)
|
||||
|
||||
logger.info(u"Starting Tautulli {}".format(
|
||||
@@ -167,30 +159,22 @@ def initialize(config_file):
|
||||
logger.info(u"Python {}".format(
|
||||
sys.version
|
||||
))
|
||||
logger.info(u"Program Dir: {}".format(
|
||||
PROG_DIR
|
||||
))
|
||||
logger.info(u"Config File: {}".format(
|
||||
CONFIG_FILE
|
||||
))
|
||||
logger.info(u"Database File: {}".format(
|
||||
DB_FILE
|
||||
))
|
||||
|
||||
if not CONFIG.BACKUP_DIR:
|
||||
CONFIG.BACKUP_DIR = os.path.join(DATA_DIR, 'backups')
|
||||
if not os.path.exists(CONFIG.BACKUP_DIR):
|
||||
try:
|
||||
os.makedirs(CONFIG.BACKUP_DIR)
|
||||
except OSError as e:
|
||||
logger.error(u"Could not create backup dir '%s': %s" % (CONFIG.BACKUP_DIR, e))
|
||||
|
||||
if not CONFIG.CACHE_DIR:
|
||||
CONFIG.CACHE_DIR = os.path.join(DATA_DIR, 'cache')
|
||||
if not os.path.exists(CONFIG.CACHE_DIR):
|
||||
try:
|
||||
os.makedirs(CONFIG.CACHE_DIR)
|
||||
except OSError as e:
|
||||
logger.error(u"Could not create cache dir '%s': %s" % (CONFIG.CACHE_DIR, e))
|
||||
|
||||
if not CONFIG.NEWSLETTER_DIR:
|
||||
CONFIG.NEWSLETTER_DIR = os.path.join(DATA_DIR, 'newsletters')
|
||||
if not os.path.exists(CONFIG.NEWSLETTER_DIR):
|
||||
try:
|
||||
os.makedirs(CONFIG.NEWSLETTER_DIR)
|
||||
except OSError as e:
|
||||
logger.error(u"Could not create newsletter dir '%s': %s" % (CONFIG.NEWSLETTER_DIR, e))
|
||||
CONFIG.BACKUP_DIR, _ = check_folder_writable(
|
||||
CONFIG.BACKUP_DIR, os.path.join(DATA_DIR, 'backups'), 'backups')
|
||||
CONFIG.CACHE_DIR, _ = check_folder_writable(
|
||||
CONFIG.CACHE_DIR, os.path.join(DATA_DIR, 'cache'), 'cache')
|
||||
CONFIG.NEWSLETTER_DIR, _ = check_folder_writable(
|
||||
CONFIG.NEWSLETTER_DIR, os.path.join(DATA_DIR, 'newsletters'), 'newsletters')
|
||||
|
||||
# Initialize the database
|
||||
logger.info(u"Checking if the database upgrades are required...")
|
||||
@@ -1965,3 +1949,29 @@ def analytics_event(category, action, label=None, value=None, **kwargs):
|
||||
TRACKER.send('event', data)
|
||||
except Exception as e:
|
||||
logger.warn(u"Failed to send analytics event for category '%s', action '%s': %s" % (category, action, e))
|
||||
|
||||
|
||||
def check_folder_writable(folder, fallback, name):
|
||||
if not folder:
|
||||
folder = fallback
|
||||
|
||||
if not os.path.exists(folder):
|
||||
try:
|
||||
os.makedirs(folder)
|
||||
except OSError as e:
|
||||
logger.error(u"Could not create %s dir '%s': %s" % (name, folder, e))
|
||||
if folder != fallback:
|
||||
logger.warn(u"Falling back to %s dir '%s'" % (name, fallback))
|
||||
return check_folder_writable(None, fallback, name)
|
||||
else:
|
||||
return folder, None
|
||||
|
||||
if not os.access(folder, os.W_OK):
|
||||
logger.error(u"Cannot write to %s dir '%s'" % (name, folder))
|
||||
if folder != fallback:
|
||||
logger.warn(u"Falling back to %s dir '%s'" % (name, fallback))
|
||||
return check_folder_writable(None, fallback, name)
|
||||
else:
|
||||
return folder, False
|
||||
|
||||
return folder, True
|
||||
|
@@ -413,7 +413,7 @@ class API2:
|
||||
body (str): The body of the message
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
script_args (str): The arguments for script notifications
|
||||
|
||||
Returns:
|
||||
None
|
||||
@@ -496,10 +496,16 @@ class API2:
|
||||
""" Tries to make a API.md to simplify the api docs. """
|
||||
|
||||
head = '''# API Reference\n
|
||||
The API is still pretty new and needs some serious cleaning up on the backend but should be reasonably functional. There are no error codes yet.
|
||||
|
||||
## General structure
|
||||
The API endpoint is `http://ip:port + HTTP_ROOT + /api/v2?apikey=$apikey&cmd=$command`
|
||||
The API endpoint is
|
||||
```
|
||||
http://IP_ADDRESS:PORT + [/HTTP_ROOT] + /api/v2?apikey=$apikey&cmd=$command
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
http://localhost:8181/api/v2?apikey=66198313a092496b8a725867d2223b5f&cmd=get_metadata&rating_key=153037
|
||||
```
|
||||
|
||||
Response example (default `json`)
|
||||
```
|
||||
@@ -596,8 +602,9 @@ General optional parameters:
|
||||
return
|
||||
|
||||
elif self._api_cmd == 'pms_image_proxy':
|
||||
cherrypy.response.headers['Content-Type'] = 'image/jpeg'
|
||||
return out['response']['data']
|
||||
if 'return_hash' not in self._api_kwargs:
|
||||
cherrypy.response.headers['Content-Type'] = 'image/jpeg'
|
||||
return out['response']['data']
|
||||
|
||||
if self._api_out_type == 'json':
|
||||
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'
|
||||
|
@@ -321,6 +321,7 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification is triggered.'},
|
||||
{'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification is triggered.'},
|
||||
{'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification is triggered.'},
|
||||
{'name': 'UTC Time', 'type': 'int', 'value': 'utctime', 'description': 'The UTC timestamp in ISO format when the notification is triggered.'},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -432,7 +433,7 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Updated Date', 'type': 'str', 'value': 'updated_date', 'description': 'The date (in date format) the item was updated on Plex.'},
|
||||
{'name': 'Last Viewed Date', 'type': 'str', 'value': 'last_viewed_date', 'description': 'The date (in date format) the item was last viewed on Plex.'},
|
||||
{'name': 'Studio', 'type': 'str', 'value': 'studio', 'description': 'The studio for the item.'},
|
||||
{'name': 'Content Rating', 'type': 'int', 'value': 'content_rating', 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'},
|
||||
{'name': 'Content Rating', 'type': 'str', 'value': 'content_rating', 'description': 'The content rating for the item.', 'example': 'e.g. TV-MA, TV-PG, etc.'},
|
||||
{'name': 'Directors', 'type': 'str', 'value': 'directors', 'description': 'A list of directors for the item.'},
|
||||
{'name': 'Writers', 'type': 'str', 'value': 'writers', 'description': 'A list of writers for the item.'},
|
||||
{'name': 'Actors', 'type': 'str', 'value': 'actors', 'description': 'A list of actors for the item.'},
|
||||
|
@@ -926,7 +926,7 @@ class DataFactory(object):
|
||||
pre_tautulli = 0
|
||||
|
||||
# For backwards compatibility. Pick one new Tautulli key to check and override with old values.
|
||||
if not item['stream_video_resolution']:
|
||||
if not item['stream_container']:
|
||||
item['stream_video_resolution'] = item['video_resolution']
|
||||
item['stream_container'] = item['transcode_container'] or item['container']
|
||||
item['stream_video_decision'] = item['video_decision']
|
||||
|
@@ -33,6 +33,7 @@ import maxminddb
|
||||
from operator import itemgetter
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
@@ -202,17 +203,22 @@ def convert_seconds_to_minutes(s):
|
||||
def today():
|
||||
today = datetime.date.today()
|
||||
yyyymmdd = datetime.date.isoformat(today)
|
||||
|
||||
return yyyymmdd
|
||||
|
||||
|
||||
def now():
|
||||
now = datetime.datetime.now()
|
||||
|
||||
return now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def utc_now_iso():
|
||||
utcnow = datetime.datetime.utcnow()
|
||||
|
||||
return utcnow.isoformat()
|
||||
|
||||
|
||||
def human_duration(s, sig='dhms'):
|
||||
|
||||
hd = ''
|
||||
@@ -1132,3 +1138,12 @@ def traverse_map(obj, func):
|
||||
new_obj = func(obj)
|
||||
|
||||
return new_obj
|
||||
|
||||
|
||||
def split_args(args=None):
|
||||
if isinstance(args, list):
|
||||
return args
|
||||
elif isinstance(args, basestring):
|
||||
return [arg.decode(plexpy.SYS_ENCODING, 'ignore')
|
||||
for arg in shlex.split(args.encode(plexpy.SYS_ENCODING, 'ignore'))]
|
||||
return []
|
||||
|
@@ -130,6 +130,32 @@ class PublicIPFilter(logging.Filter):
|
||||
return True
|
||||
|
||||
|
||||
class PlexTokenFilter(logging.Filter):
|
||||
"""
|
||||
Log filter for X-Plex-Token
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def filter(self, record):
|
||||
try:
|
||||
tokens = re.findall(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)', record.msg)
|
||||
for token in tokens:
|
||||
record.msg = record.msg.replace(token, 8 * '*' + token[-2:])
|
||||
|
||||
args = []
|
||||
for arg in record.args:
|
||||
tokens = re.findall(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)', arg) if isinstance(arg, basestring) else []
|
||||
for token in tokens:
|
||||
arg = arg.replace(token, 8 * '*' + token[-2:])
|
||||
args.append(arg)
|
||||
record.args = tuple(args)
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def listener():
|
||||
"""
|
||||
@@ -268,6 +294,7 @@ def initLogger(console=False, log_dir=False, verbose=False):
|
||||
for handler in logger.handlers + logger_api.handlers + logger_plex_websocket.handlers:
|
||||
handler.addFilter(BlacklistFilter())
|
||||
handler.addFilter(PublicIPFilter())
|
||||
handler.addFilter(PlexTokenFilter())
|
||||
|
||||
# Install exception hooks
|
||||
initHooks()
|
||||
|
@@ -23,7 +23,6 @@ import json
|
||||
from operator import itemgetter
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
from string import Formatter
|
||||
import threading
|
||||
import time
|
||||
@@ -337,12 +336,7 @@ def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data
|
||||
if notify_action in ('test', 'api'):
|
||||
subject = kwargs.pop('subject', 'Tautulli')
|
||||
body = kwargs.pop('body', 'Test Notification')
|
||||
script_args = kwargs.pop('script_args', [])
|
||||
|
||||
if script_args and isinstance(script_args, basestring):
|
||||
# Attemps to format test script args for the user
|
||||
script_args = [arg.decode(plexpy.SYS_ENCODING, 'ignore')
|
||||
for arg in shlex.split(script_args.encode(plexpy.SYS_ENCODING, 'ignore'))]
|
||||
script_args = helpers.split_args(kwargs.pop('script_args', []))
|
||||
|
||||
else:
|
||||
# Get the subject and body strings
|
||||
@@ -749,6 +743,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'datestamp': now.format(date_format),
|
||||
'timestamp': now.format(time_format),
|
||||
'unixtime': int(time.time()),
|
||||
'utctime': helpers.utc_now_iso(),
|
||||
# Stream parameters
|
||||
'streams': stream_count,
|
||||
'user_streams': user_stream_count,
|
||||
@@ -969,6 +964,7 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
||||
'datestamp': now.format(date_format),
|
||||
'timestamp': now.format(time_format),
|
||||
'unixtime': int(time.time()),
|
||||
'utctime': helpers.utc_now_iso(),
|
||||
# Plex Media Server update parameters
|
||||
'update_version': pms_download_info['version'],
|
||||
'update_url': pms_download_info['download_url'],
|
||||
@@ -1048,8 +1044,7 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
||||
|
||||
if agent_id == 15:
|
||||
try:
|
||||
script_args = [custom_formatter.format(arg.decode(plexpy.SYS_ENCODING, 'ignore'), **parameters)
|
||||
for arg in shlex.split(subject.encode(plexpy.SYS_ENCODING, 'ignore'))]
|
||||
script_args = [custom_formatter.format(arg, **parameters) for arg in helpers.split_args(subject)]
|
||||
except LookupError as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in script argument. Using fallback." % e)
|
||||
script_args = []
|
||||
@@ -1057,7 +1052,16 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom script arguments: %s. Using fallback." % e)
|
||||
script_args = []
|
||||
|
||||
elif agent_id == 25:
|
||||
try:
|
||||
subject = custom_formatter.format(unicode(subject), **parameters)
|
||||
except LookupError as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e)
|
||||
subject = unicode(default_subject).format(**parameters)
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
|
||||
subject = unicode(default_subject).format(**parameters)
|
||||
|
||||
if agent_id == 25:
|
||||
if body:
|
||||
try:
|
||||
body = json.loads(body)
|
||||
@@ -1081,15 +1085,6 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
||||
body = ''
|
||||
|
||||
else:
|
||||
try:
|
||||
subject = custom_formatter.format(unicode(subject), **parameters)
|
||||
except LookupError as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e)
|
||||
subject = unicode(default_subject).format(**parameters)
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
|
||||
subject = unicode(default_subject).format(**parameters)
|
||||
|
||||
try:
|
||||
body = custom_formatter.format(unicode(body), **parameters)
|
||||
except LookupError as e:
|
||||
@@ -1254,7 +1249,8 @@ def get_img_info(img=None, rating_key=None, title='', width=1000, height=1500,
|
||||
|
||||
|
||||
def set_hash_image_info(img=None, rating_key=None, width=750, height=1000,
|
||||
opacity=100, background='000000', blur=0, fallback=None):
|
||||
opacity=100, background='000000', blur=0, fallback=None,
|
||||
add_to_db=True):
|
||||
if not rating_key and not img:
|
||||
return fallback
|
||||
|
||||
@@ -1272,18 +1268,19 @@ def set_hash_image_info(img=None, rating_key=None, width=750, height=1000,
|
||||
plexpy.CONFIG.PMS_UUID, img, rating_key, width, height, opacity, background, blur, fallback)
|
||||
img_hash = hashlib.sha256(img_string).hexdigest()
|
||||
|
||||
keys = {'img_hash': img_hash}
|
||||
values = {'img': img,
|
||||
'rating_key': rating_key,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'opacity': opacity,
|
||||
'background': background,
|
||||
'blur': blur,
|
||||
'fallback': fallback}
|
||||
if add_to_db:
|
||||
keys = {'img_hash': img_hash}
|
||||
values = {'img': img,
|
||||
'rating_key': rating_key,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'opacity': opacity,
|
||||
'background': background,
|
||||
'blur': blur,
|
||||
'fallback': fallback}
|
||||
|
||||
db = database.MonitorDatabase()
|
||||
db.upsert('image_hash_lookup', key_dict=keys, value_dict=values)
|
||||
db = database.MonitorDatabase()
|
||||
db.upsert('image_hash_lookup', key_dict=keys, value_dict=values)
|
||||
|
||||
return img_hash
|
||||
|
||||
|
@@ -795,6 +795,7 @@ class Notifier(object):
|
||||
pass
|
||||
|
||||
def make_request(self, url, method='POST', **kwargs):
|
||||
logger.info(u"Tautulli Notifiers :: Sending {name} notification...".format(name=self.NAME))
|
||||
response, err_msg, req_msg = request.request_response2(url, method, **kwargs)
|
||||
|
||||
if response and not err_msg:
|
||||
@@ -1145,7 +1146,7 @@ class DISCORD(Notifier):
|
||||
|
||||
# Build Discord post attachment
|
||||
attachment = {'title': title,
|
||||
'timestamp': helpers.utc_now_iso()
|
||||
'timestamp': pretty_metadata.parameters['utctime']
|
||||
}
|
||||
|
||||
if self.config['color']:
|
||||
@@ -2090,25 +2091,26 @@ class JOIN(Notifier):
|
||||
if self.config['api_key']:
|
||||
params = {'apikey': self.config['api_key']}
|
||||
|
||||
r = requests.get('https://joinjoaomgcd.appspot.com/_ah/api/registration/v1/listDevices', params=params)
|
||||
try:
|
||||
r = requests.get('https://joinjoaomgcd.appspot.com/_ah/api/registration/v1/listDevices', params=params)
|
||||
|
||||
if r.status_code == 200:
|
||||
response_data = r.json()
|
||||
if response_data.get('success'):
|
||||
response_devices = response_data.get('records', [])
|
||||
devices.update({d['deviceName']: d['deviceName'] for d in response_devices})
|
||||
else:
|
||||
error_msg = response_data.get('errorMessage')
|
||||
logger.error(u"Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=error_msg))
|
||||
|
||||
if r.status_code == 200:
|
||||
response_data = r.json()
|
||||
if response_data.get('success'):
|
||||
response_devices = response_data.get('records', [])
|
||||
devices.update({d['deviceName']: d['deviceName'] for d in response_devices})
|
||||
return devices
|
||||
else:
|
||||
error_msg = response_data.get('errorMessage')
|
||||
logger.info(u"Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=error_msg))
|
||||
return devices
|
||||
else:
|
||||
logger.error(u"Tautulli Notifiers :: Unable to retrieve {name} devices list: [{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
|
||||
logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
|
||||
return devices
|
||||
logger.error(u"Tautulli Notifiers :: Unable to retrieve {name} devices list: [{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
|
||||
logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
|
||||
|
||||
else:
|
||||
return devices
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=e))
|
||||
|
||||
return devices
|
||||
|
||||
def return_config_options(self):
|
||||
config_option = [{'label': 'Join API Key',
|
||||
@@ -2678,27 +2680,28 @@ class PUSHBULLET(Notifier):
|
||||
return self.make_request('https://api.pushbullet.com/v2/pushes', headers=headers, json=data)
|
||||
|
||||
def get_devices(self):
|
||||
devices = {'': ''}
|
||||
|
||||
if self.config['api_key']:
|
||||
headers = {'Content-type': "application/json",
|
||||
'Access-Token': self.config['api_key']
|
||||
}
|
||||
try:
|
||||
r = requests.get('https://api.pushbullet.com/v2/devices', headers=headers)
|
||||
|
||||
r = requests.get('https://api.pushbullet.com/v2/devices', headers=headers)
|
||||
if r.status_code == 200:
|
||||
response_data = r.json()
|
||||
pushbullet_devices = response_data.get('devices', [])
|
||||
devices.update({d['iden']: d['nickname'] for d in pushbullet_devices if d['active']})
|
||||
else:
|
||||
logger.error(u"Tautulli Notifiers :: Unable to retrieve {name} devices list: "
|
||||
u"[{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
|
||||
logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
|
||||
|
||||
if r.status_code == 200:
|
||||
response_data = r.json()
|
||||
devices = response_data.get('devices', [])
|
||||
devices = {d['iden']: d['nickname'] for d in devices if d['active']}
|
||||
devices.update({'': ''})
|
||||
return devices
|
||||
else:
|
||||
logger.error(u"Tautulli Notifiers :: Unable to retrieve {name} devices list: "
|
||||
u"[{r.status_code}] {r.reason}".format(name=self.NAME, r=r))
|
||||
logger.debug(u"Tautulli Notifiers :: Request response: {}".format(request.server_message(r, True)))
|
||||
return {'': ''}
|
||||
except Exception as e:
|
||||
logger.error(u"Tautulli Notifiers :: Unable to retrieve {name} devices list: {msg}".format(name=self.NAME, msg=e))
|
||||
|
||||
else:
|
||||
return {'': ''}
|
||||
return devices
|
||||
|
||||
def return_config_options(self):
|
||||
config_option = [{'label': 'Pushbullet Access Token',
|
||||
@@ -3014,6 +3017,7 @@ class SCRIPTS(Notifier):
|
||||
'PLEX_URL': plexpy.CONFIG.PMS_URL,
|
||||
'PLEX_TOKEN': plexpy.CONFIG.PMS_TOKEN,
|
||||
'TAUTULLI_URL': helpers.get_plexpy_url(hostname='localhost'),
|
||||
'TAUTULLI_PUBLIC_URL': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT,
|
||||
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY,
|
||||
'TAUTULLI_ENCODING': plexpy.SYS_ENCODING
|
||||
})
|
||||
@@ -3076,7 +3080,7 @@ class SCRIPTS(Notifier):
|
||||
logger.error(u"Tautulli Notifiers :: No script folder specified.")
|
||||
return
|
||||
|
||||
script_args = kwargs.get('script_args', [])
|
||||
script_args = helpers.split_args(kwargs.get('script_args', subject))
|
||||
|
||||
logger.debug(u"Tautulli Notifiers :: Trying to run notify script, action: %s, arguments: %s"
|
||||
% (action, script_args))
|
||||
|
@@ -809,11 +809,27 @@ class PmsConnect(object):
|
||||
elif metadata_type == 'episode':
|
||||
grandparent_rating_key = helpers.get_xml_attr(metadata_main, 'grandparentRatingKey')
|
||||
show_details = self.get_metadata_details(grandparent_rating_key)
|
||||
|
||||
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
|
||||
parent_media_index = helpers.get_xml_attr(metadata_main, 'parentIndex')
|
||||
parent_thumb = helpers.get_xml_attr(metadata_main, 'parentThumb')
|
||||
|
||||
if not parent_rating_key:
|
||||
# Try getting the parent_rating_key from the parent_thumb
|
||||
if parent_thumb.startswith('/library/metadata/'):
|
||||
parent_rating_key = parent_thumb.split('/')[3]
|
||||
|
||||
# Try getting the parent_rating_key from the grandparent's children
|
||||
if not parent_rating_key:
|
||||
children_list = self.get_item_children(grandparent_rating_key)
|
||||
parent_rating_key = next((c['rating_key'] for c in children_list['children_list']
|
||||
if c['media_index'] == parent_media_index), '')
|
||||
|
||||
metadata = {'media_type': metadata_type,
|
||||
'section_id': section_id,
|
||||
'library_name': library_name,
|
||||
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
|
||||
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
|
||||
'parent_rating_key': parent_rating_key,
|
||||
'grandparent_rating_key': helpers.get_xml_attr(metadata_main, 'grandparentRatingKey'),
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': 'Season %s' % helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
@@ -821,7 +837,7 @@ class PmsConnect(object):
|
||||
'original_title': helpers.get_xml_attr(metadata_main, 'originalTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
'parent_media_index': parent_media_index,
|
||||
'studio': show_details['studio'],
|
||||
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
|
||||
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
|
||||
@@ -834,7 +850,7 @@ class PmsConnect(object):
|
||||
'duration': helpers.get_xml_attr(metadata_main, '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'),
|
||||
'parent_thumb': parent_thumb,
|
||||
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
|
||||
'art': helpers.get_xml_attr(metadata_main, 'art'),
|
||||
'banner': show_details['banner'],
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.19-beta"
|
||||
PLEXPY_BRANCH = "master"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.22"
|
||||
|
@@ -2990,7 +2990,8 @@ class WebInterface(object):
|
||||
# Get new server URLs for SSL communications and get new server friendly name
|
||||
if server_changed:
|
||||
plextv.get_server_resources()
|
||||
web_socket.reconnect()
|
||||
if plexpy.WS_CONNECTED:
|
||||
web_socket.reconnect()
|
||||
|
||||
# If first run, start websocket
|
||||
if first_run:
|
||||
@@ -4031,7 +4032,7 @@ class WebInterface(object):
|
||||
return self.real_pms_image_proxy(**kwargs)
|
||||
|
||||
@addtoapi('pms_image_proxy')
|
||||
def real_pms_image_proxy(self, img='', rating_key=None, width=0, height=0,
|
||||
def real_pms_image_proxy(self, img=None, rating_key=None, width=750, height=1000,
|
||||
opacity=100, background='000000', blur=0, img_format='png',
|
||||
fallback=None, refresh=False, clip=False, **kwargs):
|
||||
""" Gets an image from the PMS and saves it to the image cache directory.
|
||||
@@ -4051,6 +4052,7 @@ class WebInterface(object):
|
||||
img_format (str): png
|
||||
fallback (str): "poster", "cover", "art"
|
||||
refresh (bool): True or False whether to refresh the image cache
|
||||
return_hash (bool): True or False to return the self-hosted image hash instead of the image
|
||||
|
||||
Returns:
|
||||
None
|
||||
@@ -4060,6 +4062,8 @@ class WebInterface(object):
|
||||
logger.warn('No image input received.')
|
||||
return
|
||||
|
||||
return_hash = (kwargs.get('return_hash') == 'true')
|
||||
|
||||
if rating_key and not img:
|
||||
if fallback == 'art':
|
||||
img = '/library/metadata/{}/art'.format(rating_key)
|
||||
@@ -4070,9 +4074,13 @@ class WebInterface(object):
|
||||
img = '/'.join(img_split[:5])
|
||||
rating_key = rating_key or img_split[3]
|
||||
|
||||
img_string = '{}.{}.{}.{}.{}.{}.{}.{}'.format(
|
||||
plexpy.CONFIG.PMS_UUID, img, rating_key, width, height, opacity, background, blur, fallback)
|
||||
img_hash = hashlib.sha256(img_string).hexdigest()
|
||||
img_hash = notification_handler.set_hash_image_info(
|
||||
img=img, rating_key=rating_key, width=width, height=height,
|
||||
opacity=opacity, background=background, blur=blur, fallback=fallback,
|
||||
add_to_db=return_hash)
|
||||
|
||||
if return_hash:
|
||||
return {'img_hash': img_hash}
|
||||
|
||||
fp = '{}.{}'.format(img_hash, img_format) # we want to be able to preview the thumbs
|
||||
c_dir = os.path.join(plexpy.CONFIG.CACHE_DIR, 'images')
|
||||
@@ -4899,7 +4907,7 @@ class WebInterface(object):
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@addtoapi()
|
||||
def get_activity(self, session_key=None, **kwargs):
|
||||
def get_activity(self, session_key=None, session_id=None, **kwargs):
|
||||
""" Get the current activity on the PMS.
|
||||
|
||||
```
|
||||
@@ -4907,7 +4915,8 @@ class WebInterface(object):
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
session_key (int): Session key for the session info to return, OR
|
||||
session_id (str): Session ID for the session info to return
|
||||
|
||||
Returns:
|
||||
json:
|
||||
@@ -5138,6 +5147,8 @@ class WebInterface(object):
|
||||
if result:
|
||||
if session_key:
|
||||
return next((s for s in result['sessions'] if s['session_key'] == session_key), {})
|
||||
if session_id:
|
||||
return next((s for s in result['sessions'] if s['session_id'] == session_id), {})
|
||||
|
||||
counts = {'stream_count_direct_play': 0,
|
||||
'stream_count_direct_stream': 0,
|
||||
|
@@ -67,6 +67,10 @@ def initialize(options):
|
||||
else:
|
||||
protocol = "http"
|
||||
|
||||
if options['http_proxy']:
|
||||
# Overwrite cherrypy.tools.proxy with our own proxy handler
|
||||
cherrypy.tools.proxy = cherrypy.Tool('before_handler', proxy, priority=1)
|
||||
|
||||
if options['http_password']:
|
||||
login_allowed = ["Tautulli admin (username is '%s')" % options['http_username']]
|
||||
if plexpy.CONFIG.HTTP_PLEX_ADMIN:
|
||||
@@ -80,7 +84,7 @@ def initialize(options):
|
||||
else:
|
||||
auth_enabled = True
|
||||
basic_auth_enabled = False
|
||||
cherrypy.tools.auth = cherrypy.Tool('before_handler', webauth.check_auth)
|
||||
cherrypy.tools.auth = cherrypy.Tool('before_handler', webauth.check_auth, priority=2)
|
||||
else:
|
||||
auth_enabled = basic_auth_enabled = False
|
||||
|
||||
@@ -94,7 +98,7 @@ def initialize(options):
|
||||
conf = {
|
||||
'/': {
|
||||
'tools.staticdir.root': os.path.join(plexpy.PROG_DIR, 'data'),
|
||||
'tools.proxy.on': options['http_proxy'], # pay attention to X-Forwarded-Proto header
|
||||
'tools.proxy.on': bool(options['http_proxy']),
|
||||
'tools.gzip.on': True,
|
||||
'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/css',
|
||||
'text/javascript', 'application/json',
|
||||
@@ -202,6 +206,8 @@ def initialize(options):
|
||||
# Prevent time-outs
|
||||
cherrypy.engine.timeout_monitor.unsubscribe()
|
||||
cherrypy.tree.mount(WebInterface(), options['http_root'], config=conf)
|
||||
if plexpy.HTTP_ROOT != '/':
|
||||
cherrypy.tree.mount(BaseRedirect(), '/')
|
||||
|
||||
try:
|
||||
logger.info(u"Tautulli WebStart :: Starting Tautulli web server on %s://%s:%d%s", protocol,
|
||||
@@ -218,3 +224,32 @@ def initialize(options):
|
||||
sys.exit(1)
|
||||
|
||||
cherrypy.server.wait()
|
||||
|
||||
|
||||
class BaseRedirect(object):
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
||||
|
||||
|
||||
def proxy():
|
||||
# logger.debug(u"REQUEST URI: %s, HEADER [X-Forwarded-Host]: %s, [X-Host]: %s, [Origin]: %s, [Host]: %s",
|
||||
# cherrypy.request.wsgi_environ['REQUEST_URI'],
|
||||
# cherrypy.request.headers.get('X-Forwarded-Host'),
|
||||
# cherrypy.request.headers.get('X-Host'),
|
||||
# cherrypy.request.headers.get('Origin'),
|
||||
# cherrypy.request.headers.get('Host'))
|
||||
|
||||
# Change cherrpy.tools.proxy.local header if X-Forwarded-Host header is not present
|
||||
local = 'X-Forwarded-Host'
|
||||
if not cherrypy.request.headers.get('X-Forwarded-Host'):
|
||||
if cherrypy.request.headers.get('X-Host'): # lighttpd
|
||||
local = 'X-Host'
|
||||
elif cherrypy.request.headers.get('Origin'): # Squid
|
||||
local = 'Origin'
|
||||
elif cherrypy.request.headers.get('Host'): # nginx
|
||||
local = 'Host'
|
||||
# logger.debug(u"cherrypy.tools.proxy.local set to [%s]", local)
|
||||
|
||||
# Call original cherrypy proxy tool with the new local
|
||||
cherrypy.lib.cptools.proxy(local=local)
|
||||
|
Reference in New Issue
Block a user