diff --git a/plexpy/helpers.py b/plexpy/helpers.py index 187bd34a..0c330c86 100644 --- a/plexpy/helpers.py +++ b/plexpy/helpers.py @@ -798,3 +798,72 @@ def humanFileSize(bytes, si=False): u += 1 return "{0:.1f} {1}".format(bytes, units[u]) + +def parse_condition_logic_string(s, num_cond=0): + """ Parse a logic string into a nested list + Based on http://stackoverflow.com/a/23185606 + """ + valid_tokens = re.compile(r'(\(|\)|and|or)') + conditions_pattern = re.compile(r'{\d+}') + + tokens = [x.strip() for x in re.split(valid_tokens, s.lower()) if x.strip()] + + stack = [[]] + + cond_next = True + bool_next = False + open_bracket_next = True + close_bracket_next = False + + for i, x in enumerate(tokens): + if open_bracket_next and x == '(': + stack[-1].append([]) + stack.append(stack[-1][-1]) + cond_next = True + bool_next = False + open_bracket_next = True + close_bracket_next = False + + elif close_bracket_next and x == ')': + stack.pop() + if not stack: + raise ValueError('opening bracket is missing') + cond_next = False + bool_next = True + open_bracket_next = False + close_bracket_next = True + + elif cond_next and re.match(conditions_pattern, x): + try: + num = int(x[1:-1]) + except: + raise ValueError('invalid condition logic') + if not 0 < num <= num_cond: + raise ValueError('invalid condition number in condition logic') + stack[-1].append(x) + cond_next = False + bool_next = True + open_bracket_next = False + close_bracket_next = True + + elif bool_next and x in ('and', 'or') and i < len(tokens)-1: + stack[-1].append(x) + cond_next = True + bool_next = False + open_bracket_next = True + close_bracket_next = False + + else: + raise ValueError('invalid condition logic') + + if len(stack) > 1: + raise ValueError('closing bracket is missing') + + return stack.pop() + +def nested_list_to_string(l): + for i, x in enumerate(l): + if isinstance(x, list): + l[i] = nested_list_to_string(x) + s = '(' + ' '.join(l) + ')' + return s \ No newline at end of file diff --git a/plexpy/notification_handler.py b/plexpy/notification_handler.py index e23ea356..55dac2d8 100644 --- a/plexpy/notification_handler.py +++ b/plexpy/notification_handler.py @@ -105,15 +105,19 @@ def add_notifier_each(notify_action=None, stream_data=None, timeline_data=None, logger.error(u"PlexPy NotificationHandler :: Failed to build notification parameters.") return - # Add each notifier to the queue for notifier in notifiers_enabled: - data = {'notifier_id': notifier['id'], - 'notify_action': notify_action, - 'stream_data': stream_data, - 'timeline_data': timeline_data, - 'parameters': parameters} - data.update(kwargs) - plexpy.NOTIFY_QUEUE.put(data) + # Check custom user conditions + if notify_custom_conditions(notifier_id=notifier['id'], parameters=parameters): + # Add each notifier to the queue + data = {'notifier_id': notifier['id'], + 'notify_action': notify_action, + 'stream_data': stream_data, + 'timeline_data': timeline_data, + 'parameters': parameters} + data.update(kwargs) + plexpy.NOTIFY_QUEUE.put(data) + else: + logger.debug(u"PlexPy NotificationHandler :: Custom notification conditions not satisfied, skipping notifier_id %s." % notifier['id']) # Add on_concurrent and on_newdevice to queue if action is on_play if notify_action == 'on_play': @@ -121,7 +125,7 @@ def add_notifier_each(notify_action=None, stream_data=None, timeline_data=None, plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data, 'notify_action': 'on_newdevice'}) -def notify_conditions(notifier=None, notify_action=None, stream_data=None, timeline_data=None): +def notify_conditions(notify_action=None, stream_data=None, timeline_data=None): # Activity notifications if stream_data: @@ -188,7 +192,120 @@ def notify_conditions(notifier=None, notify_action=None, stream_data=None, timel return True +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'] + + if custom_conditions_logic: + logger.debug(u"PlexPy NotificationHandler :: Checking custom notification conditions for notifier_id %s." % notifier_id) + + custom_conditions = json.loads(notifier_config['custom_conditions']) + + try: + # Parse and validate the custom conditions logic + logic_groups = helpers.parse_condition_logic_string(custom_conditions_logic, len(custom_conditions)) + logic_string = helpers.nested_list_to_string(logic_groups) + except ValueError as e: + logger.error(u"PlexPy NotificationHandler :: Unable to parse custom condition logic: %s." % e) + return False + + param_types = {param['value']: param['type'] + for category in common.NOTIFICATION_PARAMETERS for param in category['parameters']} + + evaluated_conditions = [None] # Set condition {0} to None + + for condition in custom_conditions: + parameter = condition['parameter'] + operator = condition['operator'] + values = condition['value'] + + # Set blank conditions to None + if not values: + evaluated_conditions.append(None) + continue + + # Make sure the condition values is in a list + if not isinstance(values, list): + values = [values] + + parameter_type = param_types[parameter] + + # Cast the condition values to the correct type + try: + if parameter_type == 'str': + values = [unicode(v).lower() for v in values] + + elif parameter_type == 'int': + values = [int(v) for v in values] + + elif parameter_type == 'float': + values = [float(v) for v in values] + + except Exception as e: + logger.error(u"PlexPy NotificationHandler :: Unable to cast condition '%s' to type '%s'." + % (parameter, parameter_type)) + return False + + # Cast the parameter value to the correct type + try: + if parameter_type == 'str': + parameter_value = unicode(parameters[parameter]).lower() + + elif parameter_type == 'int': + parameter_value = int(parameters[parameter]) + + elif parameter_type == 'float': + parameter_value = float(parameters[parameter]) + + except Exception as e: + logger.error(u"PlexPy NotificationHandler :: Unable to cast parameter '%s' to type '%s'." + % (parameter, parameter_type)) + return False + + condition_value = values[0] + + # Check each condition + if operator == 'contains': + evaluated_conditions.append(condition_value in parameter_value) + + elif operator == 'does not contain': + evaluated_conditions.append(condition_value not in parameter_value) + + elif operator == 'is': + evaluated_conditions.append(parameter_value == condition_value) + + elif operator == 'is not': + evaluated_conditions.append(parameter_value != condition_value) + + elif operator == 'begins with': + evaluated_conditions.append(parameter_value.startswith(condition_value)) + + elif operator == 'ends with': + evaluated_conditions.append(parameter_value.endswith(condition_value)) + + elif operator == 'greater than': + evaluated_conditions.append(parameter_value > condition_value) + + elif operator == 'less than': + evaluated_conditions.append(parameter_value < condition_value) + + # Format and evaluate the logic string + try: + evaluated_logic = bool(eval(logic_string.format(*evaluated_conditions))) + except Exception as e: + logger.error(u"PlexPy NotificationHandler :: Unable to evaluate custom condition logic: %s." % e) + return False + + logger.debug(u"PlexPy NotificationHandler :: Custom condition evaluated to '%s'." % str(evaluated_logic)) + return evaluated_logic + + return True + + def notify(notifier_id=None, notify_action=None, stream_data=None, timeline_data=None, parameters=None, **kwargs): + logger.debug(u"PlexPy NotificationHandler :: Preparing notifications for notifier_id %s." % notifier_id) + notifier_config = notifiers.get_notifier_config(notifier_id=notifier_id) if not notifier_config: