Skip to content

SmartFilterMixin._parseFilters algorithm fails when multi-level filters are to be parsed #1274

@Dr-Blank

Description

@Dr-Blank

Describe the Bug

For multi-level filters in smart playlist, the parsing would return erroneous operator for nodes that are not leaves.

Example

# input
push=1  # <--- Top level nested filters starts
  push=1  # <--- Second level nested filters starts
    track.userRating>>=5
    and=1
    track.lastViewedAt<<=-12mon
    and=1
    track.userRating<<=8
  pop=1  # <--- Second level nested filters ends
 or=1  # <--- Operator for Top level
  push=1
    track.userRating=8
    and=1  # <--- Operator for Second level
    track.lastViewedAt<<=-9w
  pop=1
pop=1  # <--- Top level nested filters ends

Expected Result

In this case the expected filters dict would be

{"or": [{"and":[...]}, {"and":[...]}]}

but it returns

{"and": [{"and":[...]}, {"and":[...]}]}

Reason

while looping when it is time to pop, filterGroups[-1].insert(0, filterOp) inserts the stack using the filterOp that was set most recently.

Hence even though the operator between the nested filters is or, since the last operator it encountered was and in the ultimate nested filters, it sets the operator erroneously as and between the both nested filters.

push=1  # <--- Top level nested filters starts
    push=1
       .............
    pop=1
  or=1  # <---- will forget
    push=1  # <--- Second level nested filters starts
       .............
      and=1  # <---- last seen operator
       .............
    pop=1  # <--- Second level nested filters ends
pop=1  # <--- Top level nested filters ends

Code Snippets

playlist = server.playlist('playlist with multiple nested filters with highest being or, last group being and')
playlist.filters()

Additional Context

I have implemented an algorithm for this using recursion

from collections import deque

def parse_filter_groups(input_list: list[tuple[str, str]] | deque) -> dict:
    """parse filter groups from input lines between push and pop"""
    current_filters_stack: list[dict] = []
    operator_for_stack = None
    if not isinstance(input_list, deque):
        input_list = deque(input_list)

    allowed_logical_operators = ["and", "or"]
    while input_list:
        key, value = input_list.popleft()  # consume the first item
        if key == "push":
            # recurse and add the result to the current stack
            current_filters_stack.append(parse_filter_groups(input_list))
        elif key == "pop":
            # stop iterating and return the current stack
            break

        elif key in allowed_logical_operators:
            # set the operator
            if operator_for_stack and not operator_for_stack == key:
                raise ValueError("cannot have two logical operators in a row")
            operator_for_stack = key

        else:
            # add the key value pair to the current filter
            current_filters_stack.append({key: value})

    if not operator_for_stack and len(current_filters_stack) > 1:
        raise ValueError("no logical operator found for multiple filters")

    if operator_for_stack:
        return {operator_for_stack: current_filters_stack}
    return current_filters_stack.pop()

Operating System and Version

Windows 10 64-bit

Plex Media Server Version

1.32.6.7557

Python Version

3.11.0b1

PlexAPI Version

4.15.4

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions