[44] add `episodes`, `seasons`, `albums`, and `tracks` subfilters

pull/858/head
meisnate12 3 years ago
parent 931e66caba
commit e2393da47e

@ -1 +1 @@
1.16.5-develop43 1.16.5-develop44

@ -157,12 +157,16 @@ Special Filters each have their own set of rules for how they're used.
### Attribute ### Attribute
| Special Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | | Special Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track |
|:--------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:-------:|:--------:|:--------:|:--------:|:--------:|:--------:| |:--------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|
| `history` | Uses the release date attribute (originally available) to match dates throughout history<br>`day`: Match the Day and Month to Today's Date<br>`month`: Match the Month to Today's Date<br>`1-30`: Match the Day and Month to Today's Date or `1-30` days before Today's Date | &#9989; | &#9989; | &#10060; | &#9989; | &#10060; | &#9989; | &#10060; | | `history` | Uses the release date attribute (originally available) to match dates throughout history<br>`day`: Match the Day and Month to Today's Date<br>`month`: Match the Month to Today's Date<br>`1-30`: Match the Day and Month to Today's Date or `1-30` days before Today's Date | &#9989; | &#9989; | &#10060; | &#9989; | &#10060; | &#9989; | &#10060; |
| `original_language`/`original_language.not`<sup>1</sup> | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match<br>Example: `original_language: en, ko` | &#10060; | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | | `episodes` | Uses the item's episodes attributes to match <br> Use the `percentage` attribute given a number between 0-100 to determine the percentage of an items episodes that must match the sub-filter. | &#10060; | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; |
| `tmdb_status`/`tmdb_status.not`<sup>1</sup> | Uses TMDb Status to match<br>**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `seasons` | Uses the item's seasons attributes to match <br> Use the `percentage` attribute given a number between 0-100 to determine the percentage of an items seasons that must match the sub-filter. | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
| `tmdb_type`/`tmdb_type.not`<sup>1</sup> | Uses TMDb Type to match<br>**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `tracks` | Uses the item's tracks attributes to match <br> Use the `percentage` attribute given a number between 0-100 to determine the percentage of an items tracks that must match the sub-filter. | &#10060; | &#10060; | &#10060; | &#10060; | &#9989; | &#9989; | &#10060; |
| `albums` | Uses the item's albums attributes to match <br> Use the `percentage` attribute given a number between 0-100 to determine the percentage of an items albums that must match the sub-filter. | &#10060; | &#10060; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; |
| `original_language`/`original_language.not`<sup>1</sup> | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match<br>Example: `original_language: en, ko` | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
| `tmdb_status`/`tmdb_status.not`<sup>1</sup> | Uses TMDb Status to match<br>**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
| `tmdb_type`/`tmdb_type.not`<sup>1</sup> | Uses TMDb Type to match<br>**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
<sup>1</sup> Also filters out missing movies/shows from being added to Radarr/Sonarr. <sup>1</sup> Also filters out missing movies/shows from being added to Radarr/Sonarr.

@ -1,5 +1,5 @@
import os, re, time import os, re, time
from datetime import datetime, timedelta from datetime import datetime
from modules import anidb, anilist, flixpatrol, icheckmovies, imdb, letterboxd, mal, plex, radarr, reciperr, sonarr, tautulli, tmdb, trakt, tvdb, mdblist, util from modules import anidb, anilist, flixpatrol, icheckmovies, imdb, letterboxd, mal, plex, radarr, reciperr, sonarr, tautulli, tmdb, trakt, tvdb, mdblist, util
from modules.util import Failed, ImageData, NotScheduled, NotScheduledRange from modules.util import Failed, ImageData, NotScheduled, NotScheduledRange
from plexapi.audio import Artist, Album, Track from plexapi.audio import Artist, Album, Track
@ -11,65 +11,6 @@ logger = util.logger
advance_new_agent = ["item_metadata_language", "item_use_original_title"] advance_new_agent = ["item_metadata_language", "item_use_original_title"]
advance_show = ["item_episode_sorting", "item_keep_episodes", "item_delete_episodes", "item_season_display", "item_episode_sorting"] advance_show = ["item_episode_sorting", "item_keep_episodes", "item_delete_episodes", "item_season_display", "item_episode_sorting"]
method_alias = {
"actors": "actor", "role": "actor", "roles": "actor",
"show_actor": "actor", "show_actors": "actor", "show_role": "actor", "show_roles": "actor",
"collections": "collection", "plex_collection": "collection",
"show_collections": "collection", "show_collection": "collection",
"content_ratings": "content_rating", "contentRating": "content_rating", "contentRatings": "content_rating",
"countries": "country",
"decades": "decade",
"directors": "director",
"genres": "genre",
"labels": "label",
"collection_minimum": "minimum_items",
"playlist_minimum": "minimum_items",
"rating": "critic_rating",
"show_user_rating": "user_rating",
"video_resolution": "resolution",
"tmdb_trending": "tmdb_trending_daily",
"play": "plays", "show_plays": "plays", "show_play": "plays", "episode_play": "episode_plays",
"originally_available": "release", "episode_originally_available": "episode_air_date",
"episode_release": "episode_air_date", "episode_released": "episode_air_date",
"show_originally_available": "release", "show_release": "release", "show_air_date": "release",
"released": "release", "show_released": "release", "max_age": "release",
"studios": "studio",
"networks": "network",
"producers": "producer",
"writers": "writer",
"years": "year", "show_year": "year", "show_years": "year",
"show_title": "title", "filter": "filters",
"seasonyear": "year", "isadult": "adult", "startdate": "start", "enddate": "end", "averagescore": "score",
"minimum_tag_percentage": "min_tag_percent", "minimumtagrank": "min_tag_percent", "minimum_tag_rank": "min_tag_percent",
"anilist_tag": "anilist_search", "anilist_genre": "anilist_search", "anilist_season": "anilist_search",
"mal_producer": "mal_studio", "mal_licensor": "mal_studio",
"trakt_recommended": "trakt_recommended_weekly", "trakt_watched": "trakt_watched_weekly", "trakt_collected": "trakt_collected_weekly",
"collection_changes_webhooks": "changes_webhooks",
"radarr_add": "radarr_add_missing", "sonarr_add": "sonarr_add_missing",
"trakt_recommended_personal": "trakt_recommendations"
}
filter_translation = {
"record_label": "studio",
"actor": "actors",
"audience_rating": "audienceRating",
"collection": "collections",
"content_rating": "contentRating",
"country": "countries",
"critic_rating": "rating",
"director": "directors",
"genre": "genres",
"label": "labels",
"producer": "producers",
"release": "originallyAvailableAt",
"added": "addedAt",
"last_played": "lastViewedAt",
"plays": "viewCount",
"user_rating": "userRating",
"writer": "writers",
"mood": "moods",
"style": "styles"
}
modifier_alias = {".greater": ".gt", ".less": ".lt"}
all_builders = anidb.builders + anilist.builders + flixpatrol.builders + icheckmovies.builders + imdb.builders + \ all_builders = anidb.builders + anilist.builders + flixpatrol.builders + icheckmovies.builders + imdb.builders + \
letterboxd.builders + mal.builders + plex.builders + reciperr.builders + tautulli.builders + \ letterboxd.builders + mal.builders + plex.builders + reciperr.builders + tautulli.builders + \
tmdb.builders + trakt.builders + tvdb.builders + mdblist.builders tmdb.builders + trakt.builders + tvdb.builders + mdblist.builders
@ -137,7 +78,10 @@ filters_by_type = {
"movie_show": ["studio", "original_language", "has_overlay", "tmdb_vote_count", "tmdb_year", "tmdb_genre", "tmdb_title", "tmdb_keyword"], "movie_show": ["studio", "original_language", "has_overlay", "tmdb_vote_count", "tmdb_year", "tmdb_genre", "tmdb_title", "tmdb_keyword"],
"movie_episode": ["director", "producer", "writer", "resolution", "audio_language", "subtitle_language", "has_dolby_vision"], "movie_episode": ["director", "producer", "writer", "resolution", "audio_language", "subtitle_language", "has_dolby_vision"],
"movie_artist": ["country"], "movie_artist": ["country"],
"show": ["tmdb_status", "tmdb_type", "origin_country", "network", "first_episode_aired", "last_episode_aired"], "show_season": ["episodes"],
"artist_album": ["tracks"],
"show": ["seasons", "tmdb_status", "tmdb_type", "origin_country", "network", "first_episode_aired", "last_episode_aired"],
"artist": ["albums"],
"album": ["record_label"] "album": ["record_label"]
} }
filters = { filters = {
@ -165,7 +109,10 @@ date_filters = ["release", "added", "last_played", "first_episode_aired", "last_
date_modifiers = ["", ".not", ".before", ".after", ".regex"] date_modifiers = ["", ".not", ".before", ".after", ".regex"]
number_filters = ["year", "tmdb_year", "critic_rating", "audience_rating", "user_rating", "tmdb_vote_count", "plays", "duration"] number_filters = ["year", "tmdb_year", "critic_rating", "audience_rating", "user_rating", "tmdb_vote_count", "plays", "duration"]
number_modifiers = [".gt", ".gte", ".lt", ".lte"] number_modifiers = [".gt", ".gte", ".lt", ".lte"]
special_filters = ["history", "original_language", "original_language.not", "tmdb_status", "tmdb_status.not", "tmdb_type", "tmdb_type.not"] special_filters = [
"history", "episodes", "seasons", "albums", "tracks", "original_language", "original_language.not",
"tmdb_status", "tmdb_status.not", "tmdb_type", "tmdb_type.not"
]
all_filters = boolean_filters + special_filters + \ all_filters = boolean_filters + special_filters + \
[f"{f}{m}" for f in string_filters for m in string_modifiers] + \ [f"{f}{m}" for f in string_filters for m in string_modifiers] + \
[f"{f}{m}" for f in tag_filters for m in tag_modifiers] + \ [f"{f}{m}" for f in tag_filters for m in tag_modifiers] + \
@ -705,7 +652,7 @@ class CollectionBuilder:
if method_key.lower() in ignored_details: if method_key.lower() in ignored_details:
continue continue
logger.debug("") logger.debug("")
method_name, method_mod, method_final = self._split(method_key) method_name, method_mod, method_final = self.library.split(method_key)
if method_name in ignored_details: if method_name in ignored_details:
continue continue
logger.debug(f"Validating Method: {method_key}") logger.debug(f"Validating Method: {method_key}")
@ -1472,7 +1419,7 @@ class CollectionBuilder:
raise Failed(f"{self.Type} Error: validate filter attribute must be either true or false") raise Failed(f"{self.Type} Error: validate filter attribute must be either true or false")
validate = dict_data.pop(dict_methods["validate"]) validate = dict_data.pop(dict_methods["validate"])
for filter_method, filter_data in dict_data.items(): for filter_method, filter_data in dict_data.items():
filter_attr, modifier, filter_final = self._split(filter_method) filter_attr, modifier, filter_final = self.library.split(filter_method)
message = None message = None
if filter_final not in all_filters: if filter_final not in all_filters:
message = f"{self.Type} Error: {filter_final} is not a valid filter attribute" message = f"{self.Type} Error: {filter_final} is not a valid filter attribute"
@ -1778,7 +1725,7 @@ class CollectionBuilder:
indent = f"\n{' ' * level}" indent = f"\n{' ' * level}"
conjunction = f"{'and' if is_all else 'or'}=1&" conjunction = f"{'and' if is_all else 'or'}=1&"
for _key, _data in filter_dict.items(): for _key, _data in filter_dict.items():
attr, modifier, final_attr = self._split(_key) attr, modifier, final_attr = self.library.split(_key)
def build_url_arg(arg, mod=None, arg_s=None, mod_s=None): def build_url_arg(arg, mod=None, arg_s=None, mod_s=None):
arg_key = plex.search_translation[attr] if attr in plex.search_translation else attr arg_key = plex.search_translation[attr] if attr in plex.search_translation else attr
@ -1857,7 +1804,7 @@ class CollectionBuilder:
base_dict = {} base_dict = {}
any_dicts = [] any_dicts = []
for alias_key, alias_value in filter_alias.items(): for alias_key, alias_value in filter_alias.items():
_, _, final = self._split(alias_key) _, _, final = self.library.split(alias_key)
if final in plex.and_searches: if final in plex.and_searches:
base_dict[alias_value[:-4]] = plex_filter[alias_value] base_dict[alias_value[:-4]] = plex_filter[alias_value]
elif final in plex.or_searches: elif final in plex.or_searches:
@ -1983,27 +1930,43 @@ class CollectionBuilder:
return util.parse(self.Type, final, data, datatype="float", minimum=0, maximum=None if attribute == "duration" else 10) return util.parse(self.Type, final, data, datatype="float", minimum=0, maximum=None if attribute == "duration" else 10)
elif attribute in plex.boolean_attributes + boolean_filters: elif attribute in plex.boolean_attributes + boolean_filters:
return util.parse(self.Type, attribute, data, datatype="bool") return util.parse(self.Type, attribute, data, datatype="bool")
elif attribute in ["seasons", "episodes", "albums", "tracks"]:
if isinstance(data, dict) and data:
percentage = 60
if "percentage" in data:
if data["percentage"] is None:
logger.warning(f"{self.Type} Warning: percentage filter attribute is blank using 60 as default")
else:
maybe = util.check_num(data["percentage"])
if maybe < 0 or maybe > 100:
logger.warning(f"{self.Type} Warning: percentage filter attribute must be a number 0-100 using 60 as default")
else:
percentage = maybe
final_filters = {"percentage": percentage}
for filter_method, filter_data in data.items():
filter_attr, filter_modifier, filter_final = self.library.split(filter_method)
message = None
if filter_final not in all_filters:
message = f"{self.Type} Error: {filter_final} is not a valid filter attribute"
elif filter_attr not in filters[attribute[:-1]] or filter_attr in ["seasons", "episodes", "albums", "tracks"]:
message = f"{self.Type} Error: {filter_final} is not a valid {attribute[:-1]} filter attribute"
elif filter_final is None:
message = f"{self.Type} Error: {filter_final} filter attribute is blank"
elif filter_final != "percentage":
final_filters[filter_final] = self.validate_attribute(filter_attr, filter_modifier, f"{attribute} {filter_final} filter", filter_data, validate)
if message:
if validate:
raise Failed(message)
else:
logger.error(message)
if not final_filters:
raise Failed(f"{self.Type} Error: no filters found under {attribute}")
return final_filters
else:
raise Failed(f"{self.Type} Error: {final} attribute must be a dictionary")
else: else:
raise Failed(f"{self.Type} Error: {final} attribute not supported") raise Failed(f"{self.Type} Error: {final} attribute not supported")
def _split(self, text):
attribute, modifier = os.path.splitext(str(text).lower())
attribute = method_alias[attribute] if attribute in method_alias else attribute
modifier = modifier_alias[modifier] if modifier in modifier_alias else modifier
if attribute == "add_to_arr":
attribute = "radarr_add_missing" if self.library.is_movie else "sonarr_add_missing"
elif attribute in ["arr_tag", "arr_folder"]:
attribute = f"{'rad' if self.library.is_movie else 'son'}{attribute}"
elif attribute in date_attributes and modifier in [".gt", ".gte"]:
modifier = ".after"
elif attribute in date_attributes and modifier in [".lt", ".lte"]:
modifier = ".before"
final = f"{attribute}{modifier}"
if text != final:
logger.warning(f"Collection Warning: {text} attribute will run as {final}")
return attribute, modifier, final
def fetch_item(self, item): def fetch_item(self, item):
if isinstance(item, (Movie, Show, Season, Episode, Artist, Album, Track)): if isinstance(item, (Movie, Show, Season, Episode, Artist, Album, Track)):
if item.ratingKey not in self.library.cached_items: if item.ratingKey not in self.library.cached_items:
@ -2114,7 +2077,7 @@ class CollectionBuilder:
if not date_to_check or date_to_check > self.current_time: if not date_to_check or date_to_check > self.current_time:
return False return False
for filter_method, filter_data in self.tmdb_filters: for filter_method, filter_data in self.tmdb_filters:
filter_attr, modifier, filter_final = self._split(filter_method) filter_attr, modifier, filter_final = self.library.split(filter_method)
if filter_attr in ["tmdb_status", "tmdb_type", "original_language"]: if filter_attr in ["tmdb_status", "tmdb_type", "original_language"]:
if filter_attr == "tmdb_status": if filter_attr == "tmdb_status":
check_value = discover_status[item.status] check_value = discover_status[item.status]
@ -2187,129 +2150,8 @@ class CollectionBuilder:
return False return False
if not self.check_tmdb_filter(t_id, item.ratingKey in self.library.movie_rating_key_map): if not self.check_tmdb_filter(t_id, item.ratingKey in self.library.movie_rating_key_map):
return False return False
for filter_method, filter_data in self.filters: if self.library.check_filters(item, self.filters, self.current_time) is False:
filter_attr, modifier, filter_final = self._split(filter_method) return False
filter_actual = filter_translation[filter_attr] if filter_attr in filter_translation else filter_attr
item_type = self.collection_level
if self.collection_level == "item":
if isinstance(item, Movie):
item_type = "movie"
elif isinstance(item, Show):
item_type = "show"
elif isinstance(item, Season):
item_type = "season"
elif isinstance(item, Episode):
item_type = "episode"
elif isinstance(item, Artist):
item_type = "artist"
elif isinstance(item, Album):
item_type = "album"
elif isinstance(item, Track):
item_type = "track"
else:
continue
if filter_attr not in filters[item_type]:
continue
elif filter_attr in date_filters:
if util.is_date_filter(getattr(item, filter_actual), modifier, filter_data, filter_final, self.current_time):
return False
elif filter_attr in string_filters:
values = []
if filter_attr == "audio_track_title":
for media in item.media:
for part in media.parts:
values.extend([a.extendedDisplayTitle for a in part.audioStreams() if a.extendedDisplayTitle])
elif filter_attr == "filepath":
values = [loc for loc in item.locations]
else:
values = [getattr(item, filter_actual)]
if util.is_string_filter(values, modifier, filter_data):
return False
elif filter_attr in boolean_filters:
filter_check = False
if filter_attr == "has_collection":
filter_check = len(item.collections) > 0
elif filter_attr == "has_overlay":
for label in item.labels:
if label.tag.lower().endswith(" overlay") or label.tag.lower() == "overlay":
filter_check = True
break
elif filter_attr == "has_dolby_vision":
for media in item.media:
for part in media.parts:
for stream in part.videoStreams():
if stream.DOVIPresent:
filter_check = True
break
if util.is_boolean_filter(filter_data, filter_check):
return False
elif filter_attr == "history":
item_date = item.originallyAvailableAt
if item_date is None:
return False
elif filter_data == "day":
if item_date.month != self.current_time.month or item_date.day != self.current_time.day:
return False
elif filter_data == "month":
if item_date.month != self.current_time.month:
return False
else:
date_match = False
for i in range(filter_data):
check_date = self.current_time - timedelta(days=i)
if item_date.month == check_date.month and item_date.day == check_date.day:
date_match = True
if date_match is False:
return False
elif modifier in [".gt", ".gte", ".lt", ".lte", ".count_gt", ".count_gte", ".count_lt", ".count_lte"]:
divider = 60000 if filter_attr == "duration" else 1
test_number = []
if filter_attr == "resolution":
for media in item.media:
test_number.append(media.videoResolution)
elif filter_attr == "audio_language":
for media in item.media:
for part in media.parts:
test_number.extend([a.language for a in part.audioStreams()])
elif filter_attr == "subtitle_language":
for media in item.media:
for part in media.parts:
test_number.extend([s.language for s in part.subtitleStreams()])
else:
test_number = getattr(item, filter_actual)
if modifier in [".count_gt", ".count_gte", ".count_lt", ".count_lte"]:
test_number = len(test_number) if test_number else 0
modifier = f".{modifier[7:]}"
if test_number is None or util.is_number_filter(test_number / divider, modifier, filter_data):
return False
else:
attrs = []
if filter_attr in ["resolution", "audio_language", "subtitle_language"]:
for media in item.media:
if filter_attr == "resolution":
attrs.append(media.videoResolution)
for part in media.parts:
if filter_attr == "audio_language":
attrs.extend([a.language for a in part.audioStreams()])
if filter_attr == "subtitle_language":
attrs.extend([s.language for s in part.subtitleStreams()])
elif filter_attr in ["content_rating", "year", "rating"]:
attrs = [getattr(item, filter_actual)]
elif filter_attr in ["actor", "country", "director", "genre", "label", "producer", "writer", "collection", "network"]:
attrs = [attr.tag for attr in getattr(item, filter_actual)]
else:
raise Failed(f"Filter Error: filter: {filter_final} not supported")
if modifier == ".regex":
has_match = False
for reg in filter_data:
for name in attrs:
if re.compile(reg).search(name):
has_match = True
if has_match is False:
return False
elif (not list(set(filter_data) & set(attrs)) and modifier == "") \
or (list(set(filter_data) & set(attrs)) and modifier == ".not"):
return False
return True return True
def run_missing(self): def run_missing(self):

@ -1,8 +1,5 @@
import os, plexapi, requests import os, plexapi, re, requests
from datetime import datetime from datetime import datetime, timedelta
from plexapi.base import PlexObject
from modules import builder, util from modules import builder, util
from modules.library import Library from modules.library import Library
from modules.util import Failed, ImageData from modules.util import Failed, ImageData
@ -119,6 +116,65 @@ modifier_translation = {
"": "", ".not": "!", ".is": "%3D", ".isnot": "!%3D", ".gt": "%3E%3E", ".gte": "%3E", ".lt": "%3C%3C", ".lte": "%3C", "": "", ".not": "!", ".is": "%3D", ".isnot": "!%3D", ".gt": "%3E%3E", ".gte": "%3E", ".lt": "%3C%3C", ".lte": "%3C",
".before": "%3C%3C", ".after": "%3E%3E", ".begins": "%3C", ".ends": "%3E", ".regex": "" ".before": "%3C%3C", ".after": "%3E%3E", ".begins": "%3C", ".ends": "%3E", ".regex": ""
} }
attribute_translation = {
"record_label": "studio",
"actor": "actors",
"audience_rating": "audienceRating",
"collection": "collections",
"content_rating": "contentRating",
"country": "countries",
"critic_rating": "rating",
"director": "directors",
"genre": "genres",
"label": "labels",
"producer": "producers",
"release": "originallyAvailableAt",
"added": "addedAt",
"last_played": "lastViewedAt",
"plays": "viewCount",
"user_rating": "userRating",
"writer": "writers",
"mood": "moods",
"style": "styles"
}
method_alias = {
"actors": "actor", "role": "actor", "roles": "actor",
"show_actor": "actor", "show_actors": "actor", "show_role": "actor", "show_roles": "actor",
"collections": "collection", "plex_collection": "collection",
"show_collections": "collection", "show_collection": "collection",
"content_ratings": "content_rating", "contentRating": "content_rating", "contentRatings": "content_rating",
"countries": "country",
"decades": "decade",
"directors": "director",
"genres": "genre",
"labels": "label",
"collection_minimum": "minimum_items",
"playlist_minimum": "minimum_items",
"rating": "critic_rating",
"show_user_rating": "user_rating",
"video_resolution": "resolution",
"tmdb_trending": "tmdb_trending_daily",
"play": "plays", "show_plays": "plays", "show_play": "plays", "episode_play": "episode_plays",
"originally_available": "release", "episode_originally_available": "episode_air_date",
"episode_release": "episode_air_date", "episode_released": "episode_air_date",
"show_originally_available": "release", "show_release": "release", "show_air_date": "release",
"released": "release", "show_released": "release", "max_age": "release",
"studios": "studio",
"networks": "network",
"producers": "producer",
"writers": "writer",
"years": "year", "show_year": "year", "show_years": "year",
"show_title": "title", "filter": "filters",
"seasonyear": "year", "isadult": "adult", "startdate": "start", "enddate": "end", "averagescore": "score",
"minimum_tag_percentage": "min_tag_percent", "minimumtagrank": "min_tag_percent", "minimum_tag_rank": "min_tag_percent",
"anilist_tag": "anilist_search", "anilist_genre": "anilist_search", "anilist_season": "anilist_search",
"mal_producer": "mal_studio", "mal_licensor": "mal_studio",
"trakt_recommended": "trakt_recommended_weekly", "trakt_watched": "trakt_watched_weekly", "trakt_collected": "trakt_collected_weekly",
"collection_changes_webhooks": "changes_webhooks",
"radarr_add": "radarr_add_missing", "sonarr_add": "sonarr_add_missing",
"trakt_recommended_personal": "trakt_recommendations"
}
modifier_alias = {".greater": ".gt", ".less": ".lt"}
album_sorting_options = {"default": -1, "newest": 0, "oldest": 1, "name": 2} album_sorting_options = {"default": -1, "newest": 0, "oldest": 1, "name": 2}
episode_sorting_options = {"default": -1, "oldest": 0, "newest": 1} episode_sorting_options = {"default": -1, "oldest": 0, "newest": 1}
keep_episodes_options = {"all": 0, "5_latest": 5, "3_latest": 3, "latest": 1, "past_3": -3, "past_7": -7, "past_30": -30} keep_episodes_options = {"all": 0, "5_latest": 5, "3_latest": 3, "latest": 1, "past_3": -3, "past_7": -7, "past_30": -30}
@ -560,6 +616,11 @@ class Plex(Library):
self._users = users self._users = users
return self._users return self._users
def manage_recommendations(self):
return [(r.title, r._data.attrib.get('identifier'), r._data.attrib.get('promotedToRecommended'),
r._data.attrib.get('promotedToOwnHome'), r._data.attrib.get('promotedToSharedHome'))
for r in self.Plex.fetchItems(f"/hubs/sections/{self.Plex.key}/manage")]
def alter_collection(self, item, collection, smart_label_collection=False, add=True): def alter_collection(self, item, collection, smart_label_collection=False, add=True):
if smart_label_collection: if smart_label_collection:
self.query_data(item.addLabel if add else item.removeLabel, collection) self.query_data(item.addLabel if add else item.removeLabel, collection)
@ -787,7 +848,7 @@ class Plex(Library):
def edit_tags(self, attr, obj, add_tags=None, remove_tags=None, sync_tags=None, do_print=True): def edit_tags(self, attr, obj, add_tags=None, remove_tags=None, sync_tags=None, do_print=True):
display = "" display = ""
final = "" final = ""
key = builder.filter_translation[attr] if attr in builder.filter_translation else attr key = attribute_translation[attr] if attr in attribute_translation else attr
attr_display = attr.replace("_", " ").title() attr_display = attr.replace("_", " ").title()
attr_call = attr_display.replace(" ", "") attr_call = attr_display.replace(" ", "")
if add_tags or remove_tags or sync_tags is not None: if add_tags or remove_tags or sync_tags is not None:
@ -873,7 +934,7 @@ class Plex(Library):
item_asset_directory = os.path.join(asset_directory[0], folder_name) item_asset_directory = os.path.join(asset_directory[0], folder_name)
os.makedirs(item_asset_directory, exist_ok=True) os.makedirs(item_asset_directory, exist_ok=True)
extra = f"\nAsset Directory Created: {item_asset_directory}" extra = f"\nAsset Directory Created: {item_asset_directory}"
raise Failed(f"Asset Warning: Unable to find asset {'folder' if self.asset_folders else 'file'}: {folder_name if self.asset_folders else file_name}{extra}") raise Failed(f"Asset Warning: Unable to find asset {'folder' if self.asset_folders else 'file'}: '{folder_name if self.asset_folders else file_name}{extra}'")
poster_filter = os.path.join(item_asset_directory, f"{file_name}.*") poster_filter = os.path.join(item_asset_directory, f"{file_name}.*")
background_filter = os.path.join(item_asset_directory, "background.*" if file_name == "poster" else f"{file_name}_background.*") background_filter = os.path.join(item_asset_directory, "background.*" if file_name == "poster" else f"{file_name}_background.*")
@ -1013,3 +1074,175 @@ class Plex(Library):
_recur("tracks") _recur("tracks")
return map_key, attrs return map_key, attrs
def split(self, text):
attribute, modifier = os.path.splitext(str(text).lower())
attribute = method_alias[attribute] if attribute in method_alias else attribute
modifier = modifier_alias[modifier] if modifier in modifier_alias else modifier
if attribute == "add_to_arr":
attribute = "radarr_add_missing" if self.is_movie else "sonarr_add_missing"
elif attribute in ["arr_tag", "arr_folder"]:
attribute = f"{'rad' if self.is_movie else 'son'}{attribute}"
elif attribute in builder.date_attributes and modifier in [".gt", ".gte"]:
modifier = ".after"
elif attribute in builder.date_attributes and modifier in [".lt", ".lte"]:
modifier = ".before"
final = f"{attribute}{modifier}"
if text != final:
logger.warning(f"Collection Warning: {text} attribute will run as {final}")
return attribute, modifier, final
def check_filters(self, item, filters_in, current_time):
for filter_method, filter_data in filters_in:
filter_attr, modifier, filter_final = self.split(filter_method)
if self.check_filter(item, filter_attr, modifier, filter_final, filter_data, current_time) is False:
return False
return True
def check_filter(self, item, filter_attr, modifier, filter_final, filter_data, current_time):
filter_actual = attribute_translation[filter_attr] if filter_attr in attribute_translation else filter_attr
if isinstance(item, Movie):
item_type = "movie"
elif isinstance(item, Show):
item_type = "show"
elif isinstance(item, Season):
item_type = "season"
elif isinstance(item, Episode):
item_type = "episode"
elif isinstance(item, Artist):
item_type = "artist"
elif isinstance(item, Album):
item_type = "album"
elif isinstance(item, Track):
item_type = "track"
else:
return True
if filter_attr not in builder.filters[item_type]:
return True
elif filter_attr in builder.date_filters:
if util.is_date_filter(getattr(item, filter_actual), modifier, filter_data, filter_final, current_time):
return False
elif filter_attr in builder.string_filters:
values = []
if filter_attr == "audio_track_title":
for media in item.media:
for part in media.parts:
values.extend(
[a.extendedDisplayTitle for a in part.audioStreams() if a.extendedDisplayTitle])
elif filter_attr == "filepath":
values = [loc for loc in item.locations]
else:
values = [getattr(item, filter_actual)]
if util.is_string_filter(values, modifier, filter_data):
return False
elif filter_attr in builder.boolean_filters:
filter_check = False
if filter_attr == "has_collection":
filter_check = len(item.collections) > 0
elif filter_attr == "has_overlay":
for label in item.labels:
if label.tag.lower().endswith(" overlay") or label.tag.lower() == "overlay":
filter_check = True
break
elif filter_attr == "has_dolby_vision":
for media in item.media:
for part in media.parts:
for stream in part.videoStreams():
if stream.DOVIPresent:
filter_check = True
break
if util.is_boolean_filter(filter_data, filter_check):
return False
elif filter_attr == "history":
item_date = item.originallyAvailableAt
if item_date is None:
return False
elif filter_data == "day":
if item_date.month != current_time.month or item_date.day != current_time.day:
return False
elif filter_data == "month":
if item_date.month != current_time.month:
return False
else:
date_match = False
for i in range(filter_data):
check_date = current_time - timedelta(days=i)
if item_date.month == check_date.month and item_date.day == check_date.day:
date_match = True
if date_match is False:
return False
elif filter_attr in ["seasons", "episodes", "albums", "tracks"]:
if filter_attr == "seasons":
sub_items = item.seasons()
elif filter_attr == "albums":
sub_items = item.albums()
elif filter_attr == "tracks":
sub_items = item.tracks()
else:
sub_items = item.episodes()
filters_in = []
percentage = 60
for sub_atr, sub_data in filter_data.items():
if sub_atr == "percentage":
percentage = sub_data
else:
filters_in.append((sub_atr, sub_data))
failure_threshold = len(sub_items) * ((100 - percentage) / 100)
failures = 0
for sub_item in sub_items:
if self.check_filters(sub_item, filters_in, current_time) is False:
failures += 1
if failures > failure_threshold:
return False
elif modifier in [".gt", ".gte", ".lt", ".lte", ".count_gt", ".count_gte", ".count_lt", ".count_lte"]:
divider = 60000 if filter_attr == "duration" else 1
test_number = []
if filter_attr == "resolution":
for media in item.media:
test_number.append(media.videoResolution)
elif filter_attr == "audio_language":
for media in item.media:
for part in media.parts:
test_number.extend([a.language for a in part.audioStreams()])
elif filter_attr == "subtitle_language":
for media in item.media:
for part in media.parts:
test_number.extend([s.language for s in part.subtitleStreams()])
else:
test_number = getattr(item, filter_actual)
if modifier in [".count_gt", ".count_gte", ".count_lt", ".count_lte"]:
test_number = len(test_number) if test_number else 0
modifier = f".{modifier[7:]}"
if test_number is None or util.is_number_filter(test_number / divider, modifier, filter_data):
return False
else:
attrs = []
if filter_attr in ["resolution", "audio_language", "subtitle_language"]:
for media in item.media:
if filter_attr == "resolution":
attrs.append(media.videoResolution)
for part in media.parts:
if filter_attr == "audio_language":
attrs.extend([a.language for a in part.audioStreams()])
if filter_attr == "subtitle_language":
attrs.extend([s.language for s in part.subtitleStreams()])
elif filter_attr in ["content_rating", "year", "rating"]:
attrs = [getattr(item, filter_actual)]
elif filter_attr in ["actor", "country", "director", "genre", "label", "producer", "writer",
"collection", "network"]:
attrs = [attr.tag for attr in getattr(item, filter_actual)]
else:
raise Failed(f"Filter Error: filter: {filter_final} not supported")
if modifier == ".regex":
has_match = False
for reg in filter_data:
for name in attrs:
if re.compile(reg).search(name):
has_match = True
if has_match is False:
return False
elif (not list(set(filter_data) & set(attrs)) and modifier == "") \
or (list(set(filter_data) & set(attrs)) and modifier == ".not"):
return False
return True
Loading…
Cancel
Save