Compare commits
15 Commits
v2.0.10-be
...
v2.0.12-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
644fea6665 | ||
![]() |
a1349ff8a6 | ||
![]() |
71c20002b8 | ||
![]() |
157af84226 | ||
![]() |
9b4536f132 | ||
![]() |
29ab470e42 | ||
![]() |
c67fa480a7 | ||
![]() |
0a1a691c73 | ||
![]() |
48588f23bf | ||
![]() |
cf14fbc3f0 | ||
![]() |
e471d5207d | ||
![]() |
5722a52082 | ||
![]() |
08c32e875e | ||
![]() |
7d3ee3afb3 | ||
![]() |
def8600f5c |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.0.12-beta (2018-01-07)
|
||||||
|
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Incorrect Plex URL parameter value.
|
||||||
|
* Change: Custom condition logic is now optional. An implicit "and" is applied between all conditions if the logic is blank.
|
||||||
|
* UI:
|
||||||
|
* New: Added separate required LAN/WAN bandwidth in the activity header.
|
||||||
|
* API:
|
||||||
|
* Fix: Notify API command not sending notifications.
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.11-beta (2018-01-05)
|
||||||
|
|
||||||
|
* Notifications:
|
||||||
|
* Fix: Some notification parameters showing up blank.
|
||||||
|
* UI:
|
||||||
|
* Fix: Stream data showing up as "None" for pre-v2 history.
|
||||||
|
* Other:
|
||||||
|
* Fix: Ability to login using the hashed password.
|
||||||
|
|
||||||
|
|
||||||
## v2.0.10-beta (2018-01-04)
|
## v2.0.10-beta (2018-01-04)
|
||||||
|
|
||||||
* Monitoring:
|
* Monitoring:
|
||||||
|
@@ -71,11 +71,13 @@ select.form-control {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
transition: background-color .3s;
|
transition: background-color .3s;
|
||||||
}
|
}
|
||||||
.react-selectize.root-node .react-selectize-control {
|
.react-selectize.root-node .react-selectize-control,
|
||||||
|
.selectize-control.form-control .selectize-input {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
border: 0px solid #444 !important;
|
border: 0px solid #444 !important;
|
||||||
background: #555 !important;
|
background: #555 !important;
|
||||||
padding: 1px 2px;
|
padding: 1px 2px;
|
||||||
|
transition: background-color .3s;
|
||||||
}
|
}
|
||||||
.react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
|
.react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
@@ -83,6 +85,13 @@ select.form-control {
|
|||||||
.react-selectize.root-node .react-selectize-control .react-selectize-toggle-button path {
|
.react-selectize.root-node .react-selectize-control .react-selectize-toggle-button path {
|
||||||
fill: #fff !important;
|
fill: #fff !important;
|
||||||
}
|
}
|
||||||
|
.react-selectize.root-node .simple-value,
|
||||||
|
.selectize-control.multi .selectize-input > div {
|
||||||
|
background: #444444 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
padding-bottom: 2px !important;
|
||||||
|
transition: background-color .3s;
|
||||||
|
}
|
||||||
.react-selectize.root-node .simple-value span {
|
.react-selectize.root-node .simple-value span {
|
||||||
padding-bottom: 2px !important;
|
padding-bottom: 2px !important;
|
||||||
}
|
}
|
||||||
@@ -90,13 +99,25 @@ select.form-control {
|
|||||||
padding-top: 3px !important;
|
padding-top: 3px !important;
|
||||||
padding-bottom: 3px !important;
|
padding-bottom: 3px !important;
|
||||||
}
|
}
|
||||||
select.form-control:focus {
|
select.form-control:focus,
|
||||||
|
.react-selectize.root-node.open .react-selectize-control,
|
||||||
|
.selectize-control.form-control .selectize-input.focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
outline: thin dotted \9;
|
outline: thin dotted \9;
|
||||||
color: #555;
|
color: #555 !important;
|
||||||
background-color: #fff;
|
background-color: #fff !important;
|
||||||
transition: background-color .3s;
|
transition: background-color .3s;
|
||||||
}
|
}
|
||||||
|
.react-selectize.root-node.open .simple-value,
|
||||||
|
.selectize-control.multi .selectize-input.focus > div,
|
||||||
|
.selectize-control.multi .selectize-input > div.active{
|
||||||
|
background: #efefef !important;
|
||||||
|
color: #333333 !important;
|
||||||
|
transition: background-color .3s;
|
||||||
|
}
|
||||||
|
.react-selectize.root-node.open .react-selectize-control .react-selectize-toggle-button path {
|
||||||
|
fill: #999 !important;
|
||||||
|
}
|
||||||
select.form-control option {
|
select.form-control option {
|
||||||
color: #555;
|
color: #555;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
@@ -3722,7 +3743,11 @@ a:hover .overlay-refresh-image:hover {
|
|||||||
.no-image {
|
.no-image {
|
||||||
background-image: none !important;
|
background-image: none !important;
|
||||||
}
|
}
|
||||||
|
#info-modal .stream-info-current {
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
#info-modal .stream-info-item {
|
#info-modal .stream-info-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@@ -292,7 +292,9 @@
|
|||||||
var sc_dp = current_activity.stream_count_direct_play,
|
var sc_dp = current_activity.stream_count_direct_play,
|
||||||
sc_ds = current_activity.stream_count_direct_stream,
|
sc_ds = current_activity.stream_count_direct_stream,
|
||||||
sc_tc = current_activity.stream_count_transcode,
|
sc_tc = current_activity.stream_count_transcode,
|
||||||
total_bw = current_activity.total_bandwidth;
|
total_bw = current_activity.total_bandwidth,
|
||||||
|
lan_bw = current_activity.lan_bandwidth,
|
||||||
|
wan_bw = current_activity.wan_bandwidth;
|
||||||
var streams_header = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
|
var streams_header = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
|
||||||
if (sc_dp) {
|
if (sc_dp) {
|
||||||
streams_header += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', ';
|
streams_header += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', ';
|
||||||
@@ -306,7 +308,14 @@
|
|||||||
streams_header = streams_header.replace(/, $/, '') + ')';
|
streams_header = streams_header.replace(/, $/, '') + ')';
|
||||||
$('#currentActivityHeader-streams').text(streams_header);
|
$('#currentActivityHeader-streams').text(streams_header);
|
||||||
|
|
||||||
var bandwidth_header = (total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps');
|
var bandwidth_header = ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps')) + ' (';
|
||||||
|
if (lan_bw) {
|
||||||
|
bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', ';
|
||||||
|
}
|
||||||
|
if (wan_bw) {
|
||||||
|
bandwidth_header += 'WAN: ' + ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps')) + ', ';
|
||||||
|
}
|
||||||
|
bandwidth_header = bandwidth_header.replace(/, $/, '') + ')';
|
||||||
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
|
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
|
||||||
|
|
||||||
$('#currentActivityHeader').show();
|
$('#currentActivityHeader').show();
|
||||||
|
@@ -132,12 +132,9 @@
|
|||||||
<div role="tabpanel" class="tab-pane" id="tabs-notify_conditions">
|
<div role="tabpanel" class="tab-pane" id="tabs-notify_conditions">
|
||||||
<label>Notification Conditions</label>
|
<label>Notification Conditions</label>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
Add custom notification conditions.
|
Add custom conditions to filter out notifications.
|
||||||
<a href="#notify-text-sub-modal" data-toggle="modal">Click here</a> for a description of all the parameters.
|
<a href="#notify-text-sub-modal" data-toggle="modal">Click here</a> for a description of all the parameters.
|
||||||
</p>
|
</p>
|
||||||
<p class="help-block">
|
|
||||||
Note: Conditions are checked after the notification trigger and the notification will only be sent if the condition logic is satisfied.
|
|
||||||
</p>
|
|
||||||
<div id="condition-widget"></div>
|
<div id="condition-widget"></div>
|
||||||
<input type="hidden" name="custom_conditions" id="custom_conditions" />
|
<input type="hidden" name="custom_conditions" id="custom_conditions" />
|
||||||
|
|
||||||
@@ -146,7 +143,8 @@
|
|||||||
<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']}" required />
|
||||||
<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>
|
<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">
|
<p class="help-block">
|
||||||
Enter the logic to use when evaluating the conditions (e.g. <span class="inline-pre">{1} and ({2} or {3})</span>).
|
Optional: Enter custom logic to use when evaluating the conditions (e.g. <span class="inline-pre">{1} and ({2} or {3})</span>).
|
||||||
|
Leave blank for implicit <span class="inline-pre">and</span> between all conditions.
|
||||||
</p>
|
</p>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
Note: Only the keywords <span class="inline-pre">and</span>/<span class="inline-pre">or</span> and brackets <span class="inline-pre">()</span> are supported.
|
Note: Only the keywords <span class="inline-pre">and</span>/<span class="inline-pre">or</span> and brackets <span class="inline-pre">()</span> are supported.
|
||||||
|
@@ -63,7 +63,7 @@ DOCUMENTATION :: END
|
|||||||
<h3 class="text-muted"> </h3>
|
<h3 class="text-muted"> </h3>
|
||||||
</div>
|
</div>
|
||||||
% elif item['media_type'] == 'show':
|
% elif item['media_type'] == 'show':
|
||||||
<a href="info?rating_key=${item['rating_key']}" title="${item['parent_title']}">
|
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">
|
||||||
<div class="dashboard-recent-media-poster">
|
<div class="dashboard-recent-media-poster">
|
||||||
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||||
<div class="dashboard-recent-media-overlay">
|
<div class="dashboard-recent-media-overlay">
|
||||||
|
@@ -918,7 +918,7 @@
|
|||||||
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="get_file_sizes" name="get_file_sizes" value="1" ${config['get_file_sizes']}> Calculate Total File Sizes <span style="color: #eb8600; padding-left: 10px;">[experimental]</span>
|
<input type="checkbox" id="get_file_sizes" name="get_file_sizes" value="1" ${config['get_file_sizes']}> Calculate Total File Sizes
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">Enable if you want Tautulli to calculate the total file size for TV Shows/Seasons and Artists/Albums on the media info tables.</p>
|
<p class="help-block">Enable if you want Tautulli to calculate the total file size for TV Shows/Seasons and Artists/Albums on the media info tables.</p>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -54,6 +54,11 @@ DOCUMENTATION :: END
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
% if data['current_session']:
|
||||||
|
<div class="col-sm-12 text-muted stream-info-current">
|
||||||
|
<i class="fa fa-exclamation-circle"></i> Current session. Updated stream details below may be delayed.
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
<table class="stream-info" style="margin-top: 0;">
|
<table class="stream-info" style="margin-top: 0;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@@ -1189,6 +1189,27 @@ def dbcheck():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Upgrade session_history_media_info table from earlier versions
|
||||||
|
try:
|
||||||
|
result = c_db.execute('SELECT stream_container FROM session_history_media_info '
|
||||||
|
'WHERE stream_container IS NULL').fetchall()
|
||||||
|
if len(result) > 0:
|
||||||
|
logger.debug(u"Altering database. Removing NULL values from session_history_media_info table.")
|
||||||
|
c_db.execute(
|
||||||
|
'UPDATE session_history_media_info SET stream_container = "" WHERE stream_container IS NULL '
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'UPDATE session_history_media_info SET stream_video_codec = "" WHERE stream_video_codec IS NULL '
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'UPDATE session_history_media_info SET stream_audio_codec = "" WHERE stream_audio_codec IS NULL '
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'UPDATE session_history_media_info SET stream_subtitle_codec = "" WHERE stream_subtitle_codec IS NULL '
|
||||||
|
)
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.warn(u"Unable to remove NULL values from session_history_media_info table.")
|
||||||
|
|
||||||
# Upgrade users table from earlier versions
|
# Upgrade users table from earlier versions
|
||||||
try:
|
try:
|
||||||
c_db.execute('SELECT do_notify FROM users')
|
c_db.execute('SELECT do_notify FROM users')
|
||||||
@@ -1370,8 +1391,8 @@ def dbcheck():
|
|||||||
|
|
||||||
# Upgrade library_sections table from earlier versions (remove duplicated libraries)
|
# Upgrade library_sections table from earlier versions (remove duplicated libraries)
|
||||||
try:
|
try:
|
||||||
result = c_db.execute('SELECT * FROM library_sections WHERE server_id = ""')
|
result = c_db.execute('SELECT * FROM library_sections WHERE server_id = ""').fetchall()
|
||||||
if result.rowcount > 0:
|
if len(result) > 0:
|
||||||
logger.debug(u"Altering database. Removing duplicate libraries from library_sections table.")
|
logger.debug(u"Altering database. Removing duplicate libraries from library_sections table.")
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'DELETE FROM library_sections WHERE server_id = ""'
|
'DELETE FROM library_sections WHERE server_id = ""'
|
||||||
|
@@ -33,6 +33,7 @@ ACTIVITY_SCHED = BackgroundScheduler()
|
|||||||
|
|
||||||
RECENTLY_ADDED_QUEUE = {}
|
RECENTLY_ADDED_QUEUE = {}
|
||||||
|
|
||||||
|
|
||||||
class ActivityHandler(object):
|
class ActivityHandler(object):
|
||||||
|
|
||||||
def __init__(self, timeline):
|
def __init__(self, timeline):
|
||||||
@@ -229,9 +230,11 @@ class ActivityHandler(object):
|
|||||||
# Update the session state and viewOffset
|
# Update the session state and viewOffset
|
||||||
if this_state == 'playing':
|
if this_state == 'playing':
|
||||||
# Update the session in our temp session table
|
# Update the session in our temp session table
|
||||||
session = self.get_live_session()
|
# if the last set temporary stopped time exceeds 15 seconds
|
||||||
if session:
|
if int(time.time()) - db_session['stopped'] > 60:
|
||||||
self.update_db_session(session=session)
|
session = self.get_live_session()
|
||||||
|
if session:
|
||||||
|
self.update_db_session(session=session)
|
||||||
|
|
||||||
# Start our state checks
|
# Start our state checks
|
||||||
if this_state != last_state:
|
if this_state != last_state:
|
||||||
|
@@ -35,6 +35,8 @@ import database
|
|||||||
import libraries
|
import libraries
|
||||||
import logger
|
import logger
|
||||||
import mobile_app
|
import mobile_app
|
||||||
|
import notification_handler
|
||||||
|
import notifiers
|
||||||
import users
|
import users
|
||||||
|
|
||||||
|
|
||||||
@@ -397,6 +399,50 @@ class API2:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def notify(self, notifier_id='', subject='Tautulli', body='Test notification', **kwargs):
|
||||||
|
""" Send a notification using Tautulli.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
notifier_id (int): The ID number of the notification agent
|
||||||
|
subject (str): The subject of the message
|
||||||
|
body (str): The body of the message
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
if not notifier_id:
|
||||||
|
self._api_msg = 'Notification failed: no notifier id provided.'
|
||||||
|
self._api_result_type = 'error'
|
||||||
|
return
|
||||||
|
|
||||||
|
notifier = notifiers.get_notifier_config(notifier_id=notifier_id)
|
||||||
|
|
||||||
|
if not notifier:
|
||||||
|
self._api_msg = 'Notification failed: invalid notifier_id provided %s.' % notifier_id
|
||||||
|
self._api_result_type = 'error'
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.api_debug(u'Tautulli APIv2 :: Sending notification.')
|
||||||
|
success = notification_handler.notify(notifier_id=notifier_id,
|
||||||
|
notify_action='api',
|
||||||
|
subject=subject,
|
||||||
|
body=body,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self._api_msg = 'Notification sent.'
|
||||||
|
self._api_result_type = 'success'
|
||||||
|
else:
|
||||||
|
self._api_msg = 'Notification failed.'
|
||||||
|
self._api_result_type = 'error'
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
def _api_make_md(self):
|
def _api_make_md(self):
|
||||||
""" Tries to make a API.md to simplify the api docs. """
|
""" Tries to make a API.md to simplify the api docs. """
|
||||||
|
|
||||||
@@ -581,8 +627,8 @@ General optional parameters:
|
|||||||
if isinstance(result, (dict, list)):
|
if isinstance(result, (dict, list)):
|
||||||
ret = result
|
ret = result
|
||||||
else:
|
else:
|
||||||
raise
|
raise Exception
|
||||||
except:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
ret = json.loads(result)
|
ret = json.loads(result)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
|
@@ -289,6 +289,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'LOG_BLACKLIST': (int, 'General', 1),
|
'LOG_BLACKLIST': (int, 'General', 1),
|
||||||
'LOG_DIR': (str, 'General', ''),
|
'LOG_DIR': (str, 'General', ''),
|
||||||
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
|
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
|
||||||
|
'METADATA_CACHE_SECONDS': (int, 'Advanced', 1800),
|
||||||
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
||||||
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
||||||
'MOVIE_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
'MOVIE_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
||||||
|
@@ -951,9 +951,11 @@ class DataFactory(object):
|
|||||||
'transcode_hw_encoding': item['transcode_hw_encoding'],
|
'transcode_hw_encoding': item['transcode_hw_encoding'],
|
||||||
'media_type': item['media_type'],
|
'media_type': item['media_type'],
|
||||||
'title': item['title'],
|
'title': item['title'],
|
||||||
'grandparent_title': item['grandparent_title']
|
'grandparent_title': item['grandparent_title'],
|
||||||
|
'current_session': 1 if session_key else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stream_output = {k: v or '' for k, v in stream_output.iteritems()}
|
||||||
return stream_output
|
return stream_output
|
||||||
|
|
||||||
def get_metadata_details(self, rating_key):
|
def get_metadata_details(self, rating_key):
|
||||||
|
@@ -206,19 +206,21 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
|||||||
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
|
notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id)
|
||||||
|
|
||||||
custom_conditions_logic = notifier_config['custom_conditions_logic']
|
custom_conditions_logic = notifier_config['custom_conditions_logic']
|
||||||
|
custom_conditions = json.loads(notifier_config['custom_conditions']) or []
|
||||||
|
|
||||||
if custom_conditions_logic:
|
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." % notifier_id)
|
logger.debug(u"Tautulli NotificationHandler :: Checking custom notification conditions for notifier_id %s."
|
||||||
|
% notifier_id)
|
||||||
|
|
||||||
custom_conditions = json.loads(notifier_config['custom_conditions'])
|
logic_groups = None
|
||||||
|
if custom_conditions_logic:
|
||||||
try:
|
try:
|
||||||
# Parse and validate the custom conditions logic
|
# Parse and validate the custom conditions logic
|
||||||
logic_groups = helpers.parse_condition_logic_string(custom_conditions_logic, len(custom_conditions))
|
logic_groups = helpers.parse_condition_logic_string(custom_conditions_logic, len(custom_conditions))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom condition logic '%s': %s."
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom condition logic '%s': %s."
|
||||||
% (custom_conditions_logic, e))
|
% (custom_conditions_logic, e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
evaluated_conditions = [None] # Set condition {0} to None
|
evaluated_conditions = [None] # Set condition {0} to None
|
||||||
|
|
||||||
@@ -227,10 +229,11 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
|||||||
operator = condition['operator']
|
operator = condition['operator']
|
||||||
values = condition['value']
|
values = condition['value']
|
||||||
parameter_type = condition['type']
|
parameter_type = condition['type']
|
||||||
|
parameter_value = parameters.get(parameter, "")
|
||||||
|
|
||||||
# Set blank conditions to None
|
# Set blank conditions to True (skip)
|
||||||
if not parameter or not operator or not values:
|
if not parameter or not operator or not values:
|
||||||
evaluated_conditions.append(None)
|
evaluated_conditions.append(True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Make sure the condition values is in a list
|
# Make sure the condition values is in a list
|
||||||
@@ -248,25 +251,25 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
|||||||
elif parameter_type == 'float':
|
elif parameter_type == 'float':
|
||||||
values = [float(v) for v in values]
|
values = [float(v) for v in values]
|
||||||
|
|
||||||
except Exception as e:
|
except ValueError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to cast condition '%s' to type '%s'."
|
logger.error(u"Tautulli NotificationHandler :: Unable to cast condition '%s', values '%s', to type '%s'."
|
||||||
% (parameter, parameter_type))
|
% (parameter, values, parameter_type))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Cast the parameter value to the correct type
|
# Cast the parameter value to the correct type
|
||||||
try:
|
try:
|
||||||
if parameter_type == 'str':
|
if parameter_type == 'str':
|
||||||
parameter_value = unicode(parameters[parameter]).lower()
|
parameter_value = unicode(parameter_value).lower()
|
||||||
|
|
||||||
elif parameter_type == 'int':
|
elif parameter_type == 'int':
|
||||||
parameter_value = int(parameters[parameter])
|
parameter_value = int(parameter_value)
|
||||||
|
|
||||||
elif parameter_type == 'float':
|
elif parameter_type == 'float':
|
||||||
parameter_value = float(parameters[parameter])
|
parameter_value = float(parameter_value)
|
||||||
|
|
||||||
except Exception as e:
|
except ValueError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to cast parameter '%s' to type '%s'."
|
logger.error(u"Tautulli NotificationHandler :: Unable to cast parameter '%s', value '%s', to type '%s'."
|
||||||
% (parameter, parameter_type))
|
% (parameter, parameter_value, parameter_type))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check each condition
|
# Check each condition
|
||||||
@@ -298,12 +301,15 @@ def notify_custom_conditions(notifier_id=None, parameters=None):
|
|||||||
logger.warn(u"Tautulli NotificationHandler :: Invalid condition operator '%s'." % operator)
|
logger.warn(u"Tautulli NotificationHandler :: Invalid condition operator '%s'." % operator)
|
||||||
evaluated_conditions.append(None)
|
evaluated_conditions.append(None)
|
||||||
|
|
||||||
# Format and evaluate the logic string
|
if logic_groups:
|
||||||
try:
|
# Format and evaluate the logic string
|
||||||
evaluated_logic = helpers.eval_logic_groups_to_bool(logic_groups, evaluated_conditions)
|
try:
|
||||||
except Exception as e:
|
evaluated_logic = helpers.eval_logic_groups_to_bool(logic_groups, evaluated_conditions)
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to evaluate custom condition logic: %s." % e)
|
except Exception as e:
|
||||||
return False
|
logger.error(u"Tautulli NotificationHandler :: Unable to evaluate custom condition logic: %s." % e)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
evaluated_logic = all(evaluated_conditions[1:])
|
||||||
|
|
||||||
logger.debug(u"Tautulli NotificationHandler :: Custom condition evaluated to '%s'." % str(evaluated_logic))
|
logger.debug(u"Tautulli NotificationHandler :: Custom condition evaluated to '%s'." % str(evaluated_logic))
|
||||||
return evaluated_logic
|
return evaluated_logic
|
||||||
@@ -326,7 +332,7 @@ def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data
|
|||||||
if not notifier_config:
|
if not notifier_config:
|
||||||
return
|
return
|
||||||
|
|
||||||
if notify_action == 'test':
|
if notify_action in ('test', 'api'):
|
||||||
subject = kwargs.pop('subject', 'Tautulli')
|
subject = kwargs.pop('subject', 'Tautulli')
|
||||||
body = kwargs.pop('body', 'Test Notification')
|
body = kwargs.pop('body', 'Test Notification')
|
||||||
script_args = kwargs.pop('script_args', [])
|
script_args = kwargs.pop('script_args', [])
|
||||||
@@ -344,8 +350,8 @@ def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data
|
|||||||
|
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
notification_id = set_notify_state(session=stream_data or timeline_data,
|
notification_id = set_notify_state(session=stream_data or timeline_data,
|
||||||
notify_action=notify_action,
|
|
||||||
notifier=notifier_config,
|
notifier=notifier_config,
|
||||||
|
notify_action=notify_action,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
body=body,
|
body=body,
|
||||||
script_args=script_args)
|
script_args=script_args)
|
||||||
@@ -384,9 +390,9 @@ def get_notify_state(session):
|
|||||||
return notify_states
|
return notify_states
|
||||||
|
|
||||||
|
|
||||||
def set_notify_state(notify_action, notifier, subject, body, script_args, session=None):
|
def set_notify_state(notifier, notify_action, subject='', body='', script_args='', session=None):
|
||||||
|
|
||||||
if notify_action and notifier:
|
if notifier and notify_action:
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
session = session or {}
|
session = session or {}
|
||||||
@@ -451,7 +457,11 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
|
|
||||||
notify_params = defaultdict(str)
|
notify_params = defaultdict(str)
|
||||||
if session:
|
if session:
|
||||||
|
# Reload json from raw stream info
|
||||||
|
if session.get('raw_stream_info'):
|
||||||
|
session.update(json.loads(session['raw_stream_info']))
|
||||||
notify_params.update(session)
|
notify_params.update(session)
|
||||||
|
|
||||||
if timeline:
|
if timeline:
|
||||||
notify_params.update(timeline)
|
notify_params.update(timeline)
|
||||||
|
|
||||||
@@ -513,7 +523,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
remaining_duration = duration - view_offset
|
remaining_duration = duration - view_offset
|
||||||
|
|
||||||
# Build Plex URL
|
# Build Plex URL
|
||||||
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fnotify_params%2F{rating_key}'.format(
|
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
|
||||||
web_url=plexpy.CONFIG.PMS_WEB_URL,
|
web_url=plexpy.CONFIG.PMS_WEB_URL,
|
||||||
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
rating_key=rating_key)
|
rating_key=rating_key)
|
||||||
@@ -937,7 +947,7 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
|||||||
try:
|
try:
|
||||||
script_args = [custom_formatter.format(unicode(arg), **parameters) for arg in subject.split()]
|
script_args = [custom_formatter.format(unicode(arg), **parameters) for arg in subject.split()]
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse field %s in script argument. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in script argument. Using fallback." % e)
|
||||||
script_args = []
|
script_args = []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom script arguments: %s. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom script arguments: %s. Using fallback." % e)
|
||||||
@@ -948,7 +958,7 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
|||||||
try:
|
try:
|
||||||
subject = custom_formatter.format(unicode(subject), **parameters)
|
subject = custom_formatter.format(unicode(subject), **parameters)
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse field %s in notification subject. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in notification subject. Using fallback." % e)
|
||||||
subject = unicode(default_subject).format(**parameters)
|
subject = unicode(default_subject).format(**parameters)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification subject: %s. Using fallback." % e)
|
||||||
@@ -957,7 +967,7 @@ def build_notify_text(subject='', body='', notify_action=None, parameters=None,
|
|||||||
try:
|
try:
|
||||||
body = custom_formatter.format(unicode(body), **parameters)
|
body = custom_formatter.format(unicode(body), **parameters)
|
||||||
except LookupError as e:
|
except LookupError as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse field %s in notification body. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse parameter %s in notification body. Using fallback." % e)
|
||||||
body = unicode(default_body).format(**parameters)
|
body = unicode(default_body).format(**parameters)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e)
|
logger.error(u"Tautulli NotificationHandler :: Unable to parse custom notification body: %s. Using fallback." % e)
|
||||||
|
@@ -61,7 +61,6 @@ import mobile_app
|
|||||||
import pmsconnect
|
import pmsconnect
|
||||||
import request
|
import request
|
||||||
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
||||||
from plexpy.helpers import checked
|
|
||||||
|
|
||||||
|
|
||||||
AGENT_IDS = {'growl': 0,
|
AGENT_IDS = {'growl': 0,
|
||||||
|
@@ -542,8 +542,8 @@ class PmsConnect(object):
|
|||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
_cache_time = metadata.pop('_cache_time', 0)
|
_cache_time = metadata.pop('_cache_time', 0)
|
||||||
# Return cached metadata if less than 30 minutes ago
|
# Return cached metadata if less than METADATA_CACHE_SECONDS ago
|
||||||
if int(time.time()) - _cache_time <= 1800:
|
if int(time.time()) - _cache_time <= plexpy.CONFIG.METADATA_CACHE_SECONDS:
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
if rating_key:
|
if rating_key:
|
||||||
@@ -1155,9 +1155,9 @@ class PmsConnect(object):
|
|||||||
metadata['media_info'] = medias
|
metadata['media_info'] = medias
|
||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
metadata['_cache_time'] = int(time.time())
|
|
||||||
|
|
||||||
if cache_key:
|
if cache_key:
|
||||||
|
metadata['_cache_time'] = int(time.time())
|
||||||
|
|
||||||
out_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % cache_key)
|
out_file_path = os.path.join(plexpy.CONFIG.CACHE_DIR, 'metadata-sessionKey-%s.json' % cache_key)
|
||||||
try:
|
try:
|
||||||
with open(out_file_path, 'w') as outFile:
|
with open(out_file_path, 'w') as outFile:
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
PLEXPY_BRANCH = "beta"
|
PLEXPY_BRANCH = "beta"
|
||||||
PLEXPY_RELEASE_VERSION = "v2.0.10-beta"
|
PLEXPY_RELEASE_VERSION = "v2.0.12-beta"
|
||||||
|
@@ -96,7 +96,8 @@ def check_credentials(username, password, admin_login='0'):
|
|||||||
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||||
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
|
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
|
||||||
return True, u'admin'
|
return True, u'admin'
|
||||||
elif username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||||
|
username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
||||||
return True, u'admin'
|
return True, u'admin'
|
||||||
elif not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS and user_login(username, password):
|
elif not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS and user_login(username, password):
|
||||||
return True, u'guest'
|
return True, u'guest'
|
||||||
|
@@ -3079,7 +3079,6 @@ class WebInterface(object):
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi("notify")
|
|
||||||
def send_notification(self, notifier_id=None, subject='Tautulli', body='Test notification', notify_action='', **kwargs):
|
def send_notification(self, notifier_id=None, subject='Tautulli', body='Test notification', notify_action='', **kwargs):
|
||||||
""" Send a notification using Tautulli.
|
""" Send a notification using Tautulli.
|
||||||
|
|
||||||
@@ -4440,7 +4439,9 @@ class WebInterface(object):
|
|||||||
counts = {'stream_count_direct_play': 0,
|
counts = {'stream_count_direct_play': 0,
|
||||||
'stream_count_direct_stream': 0,
|
'stream_count_direct_stream': 0,
|
||||||
'stream_count_transcode': 0,
|
'stream_count_transcode': 0,
|
||||||
'total_bandwidth': 0}
|
'total_bandwidth': 0,
|
||||||
|
'lan_bandwidth': 0,
|
||||||
|
'wan_bandwidth': 0}
|
||||||
|
|
||||||
for s in result['sessions']:
|
for s in result['sessions']:
|
||||||
if s['transcode_decision'] == 'transcode':
|
if s['transcode_decision'] == 'transcode':
|
||||||
@@ -4451,6 +4452,10 @@ class WebInterface(object):
|
|||||||
counts['stream_count_direct_play'] += 1
|
counts['stream_count_direct_play'] += 1
|
||||||
|
|
||||||
counts['total_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
counts['total_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||||
|
if s['location'] == 'lan':
|
||||||
|
counts['lan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||||
|
elif s['location'] == 'wan':
|
||||||
|
counts['wan_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||||
|
|
||||||
result.update(counts)
|
result.update(counts)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user