Skip to content

Instantly share code, notes, and snippets.

@Dr-Blank
Created January 29, 2024 10:15
Show Gist options
  • Save Dr-Blank/b583efc6dbc1301a3c7d33064c50cc08 to your computer and use it in GitHub Desktop.
Save Dr-Blank/b583efc6dbc1301a3c7d33064c50cc08 to your computer and use it in GitHub Desktop.
descriptors for making plex objects' attr able to use in filters
from __future__ import annotations
from dataclasses import dataclass
from pprint import pprint
from typing import Any, Dict, List, Type, TypeVar, Union
@dataclass
class SimpleFilter:
"""a filter for the `key` with the `value`"""
key: str
value: Any
operator: str = ""
def to_dict(self) -> Dict[str, Any]:
"""generate a dict that can be used as a filter for plexapi"""
return {f"{self.key}{self.operator}": self.value}
def __or__(self, other: SimpleFilter):
return build_logical_filter(self, other, Or)
def __and__(self, other: SimpleFilter):
return build_logical_filter(self, other, And)
class PlexFilterableAttribute:
"""Base class for Plex attributes that can be used in filters."""
def __init__(self, key: Union[str, None] = None):
self._key: str = key # type: ignore[assignment]
self._name: str
def __set_name__(self, owner: Any, name: str):
self._name = f"__{name}"
if self._key is not None: # type: ignore[unreachable]
return
if hasattr(owner, "_plex_type"):
owner_name = owner._plex_type
else:
owner_name = str(owner.__name__).lower()
self._key = f"{owner_name}.{name}" if owner_name else name
def __get__(self, instance: Any, _):
if instance is None:
return self
return getattr(instance, self._name)
def __set__(self, instance: Any, value: Any):
setattr(instance, self._name, value)
class FilterAttrSupportsEquality(PlexFilterableAttribute):
"""Base class for Plex filter attributes that support equality."""
def __eq__(self, other: Any): # type: ignore
return SimpleFilter(self._key, other, operator="")
def __ne__(self, other: Any): # type: ignore
return SimpleFilter(self._key, other, operator="!")
class FilterAttrSupportsComparison(PlexFilterableAttribute):
def __gt__(self, other: Any):
return SimpleFilter(self._key, other, operator=">>")
def __lt__(self, other: Any):
return SimpleFilter(self._key, other, operator="<<")
class IntAttr(
FilterAttrSupportsEquality,
FilterAttrSupportsComparison,
):
"""Plex attribute that is an integer."""
class StrAttr(
PlexFilterableAttribute,
):
"""Plex attribute that is a string."""
def contains(self, other: str):
"""Filter for a attr that contains the given str."""
return SimpleFilter(self._key, other, operator="")
def not_contains(self, other: Any):
"""Filter for a attr that does not contain the given str."""
return SimpleFilter(self._key, other, operator="!")
def __eq__(self, other: Any): # type: ignore
"""is exactly equal to the given str."""
return SimpleFilter(self._key, other, operator="=")
def __ne__(self, other: Any): # type: ignore
"""is not equal to the given str."""
return SimpleFilter(self._key, other, operator="!=")
def startswith(self, other: Any):
"""Filter for a attr that starts with the given str."""
return SimpleFilter(self._key, other, operator="<")
def endswith(self, other: Any):
"""Filter for a attr that ends with the given str."""
return SimpleFilter(self._key, other, operator=">")
class BoolAttr(FilterAttrSupportsEquality):
"""Plex attribute that is a boolean."""
class DatetimeAttr(FilterAttrSupportsComparison):
"""Plex attribute that is a datetime."""
class Filters:
"""builds filters for plexapi"""
def __init__(self, *args: SimpleFilter):
self.concrete_filters = args
def to_dict(self) -> Dict[str, Any]:
"""generate a dict that can be used as a filter for plexapi"""
result: Dict[str, Any] = {}
for _filter in self.concrete_filters:
result.update(_filter.to_dict())
return result
class LogicalFilter(Filters, SimpleFilter):
"""a filter that combines multiple filters with a logical operator"""
operator: str
def to_dict(self):
return {self.operator: [x.to_dict() for x in self.concrete_filters]}
LogicalFilterType = TypeVar("LogicalFilterType", bound=LogicalFilter)
def build_logical_filter(
x: SimpleFilter,
y: SimpleFilter,
of_type: Type[LogicalFilterType],
) -> LogicalFilterType:
_filters_to_combine: List[SimpleFilter] = [x]
if isinstance(x, of_type):
_filters_to_combine = list(x.concrete_filters)
if isinstance(y, of_type):
_filters_to_combine.extend(y.concrete_filters)
else:
_filters_to_combine.append(y)
return of_type(*_filters_to_combine)
class And(LogicalFilter):
"""a filter that combines multiple filters with a logical and"""
operator = "and"
class Or(LogicalFilter):
"""a filter that combines multiple filters with a logical or"""
operator = "or"
class AddedAtSupportedAttr:
addedAt = DatetimeAttr()
class DurationSupportedAttr:
duration = IntAttr()
inProgress = BoolAttr()
class Track(AddedAtSupportedAttr, DurationSupportedAttr):
"""a track in plex"""
class Show(AddedAtSupportedAttr):
collection = StrAttr()
class Episode(AddedAtSupportedAttr, DurationSupportedAttr):
"""an episode in plex"""
class Artist(AddedAtSupportedAttr):
genre = StrAttr()
mood = StrAttr()
class Album:
decade = IntAttr()
def main():
rock_artist = Artist.genre == "Rock"
pop_artist = Artist.genre == "Pop"
and_filters = Filters(
(Show.collection.contains("Rocky") & (Episode.inProgress == True))
& (rock_artist | pop_artist | (Track.addedAt > "-24h"))
)
filter2 = Filters(Track.duration > 100, Album.decade == 1980)
rock_or_pop = rock_artist | pop_artist
print(filter2.to_dict())
print(rock_or_pop.to_dict())
pprint(and_filters.to_dict())
artist_1 = Artist()
artist_1.genre = "Rock"
print(artist_1.genre)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment