From e887bcbffb1005785bb0e21de2ec250f35e999e6 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 20 Apr 2022 12:03:08 -0400 Subject: [PATCH] [12] add .regex modifier for tags --- VERSION | 2 +- config/config.yml.template | 1 + docs/config/libraries.md | 4 +- docs/metadata/builders/plex.md | 9 +-- docs/metadata/filters.md | 53 +++++++++-------- modules/builder.py | 88 ++++++++++++++++------------ modules/plex.py | 101 ++++++--------------------------- modules/util.py | 15 +++++ 8 files changed, 120 insertions(+), 153 deletions(-) diff --git a/VERSION b/VERSION index 4282b3ff..6096d0dc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.5-develop11 +1.16.5-develop12 diff --git a/config/config.yml.template b/config/config.yml.template index c7c1a283..a6103487 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -7,6 +7,7 @@ libraries: # This is called out once within - folder: config/Movies/ # This is a local directory on the system - git: meisnate12/MovieCharts # This is a file within the GitHub Repository overlay_path: + - remove_overlays: false # Set this to true to remove all overlays - file: config/Overlays.yml # This is a local file on the system TV Shows: metadata_path: diff --git a/docs/config/libraries.md b/docs/config/libraries.md index 4efc4115..f841fdd2 100644 --- a/docs/config/libraries.md +++ b/docs/config/libraries.md @@ -143,7 +143,7 @@ libraries: ### Remove Overlays -You can remove overlays from a library by adding `remove_overlays: true` to overlay_path +You can remove overlays from a library by adding `remove_overlays: true` to `overlay_path`. ```yaml libraries: @@ -151,8 +151,8 @@ libraries: metadata_path: - file: config/TV Shows.yml overlay_path: + - remove_overlays: true - file: config/Overlays.yml - - remove_overlays: ture ``` * This will remove all overlays when run and not generate new ones. diff --git a/docs/metadata/builders/plex.md b/docs/metadata/builders/plex.md index b39f6035..1f3f0ca2 100644 --- a/docs/metadata/builders/plex.md +++ b/docs/metadata/builders/plex.md @@ -165,10 +165,11 @@ Tag search can take multiple values as a **list or a comma-separated string**. ### Tag Modifiers -| Tag Modifier | Description | Plex Web UI Display | -|:-------------|:-----------------------------------------------------------------------|:-------------------:| -| No Modifier | Matches every item where the attribute matches the given string | `is` | -| `.not` | Matches every item where the attribute does not match the given string | `is not` | +| Tag Modifier | Description | Plex Web UI Display | +|:-------------|:------------------------------------------------------------------------|:-------------------:| +| No Modifier | Matches every item where the attribute matches the given string | `is` | +| `.not` | Matches every item where the attribute does not match the given string | `is not` | +| `.regex` | Matches every item where one value of this attribute matches the regex. | `N/A` | ### Tag Attributes diff --git a/docs/metadata/filters.md b/docs/metadata/filters.md index 131801f1..6b564378 100644 --- a/docs/metadata/filters.md +++ b/docs/metadata/filters.md @@ -51,6 +51,7 @@ Tag filters can take multiple values as a **list or a comma-separated string**. |:-------------|:------------------------------------------------------------------------------------------| | No Modifier | Matches every item where the attribute matches the given string | | `.not` | Matches every item where the attribute does not match the given string | +| `.regex` | Matches every item where one value of this attribute matches the regex. | | `.count_gt` | Matches every item where the attribute count is greater then the given number | | `.count_gte` | Matches every item where the attribute count is greater then or equal to the given number | | `.count_lt` | Matches every item where the attribute count is less then the given number | @@ -58,28 +59,25 @@ Tag filters can take multiple values as a **list or a comma-separated string**. ### Attribute -| Tag Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | -|:--------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| -| `actor` | Uses the actor tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `collection` | Uses the collection tags to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| `content_rating` | Uses the content rating tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `network` | Uses the network tags to match | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `country` | Uses the country tags to match | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | -| `director` | Uses the director tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `genre` | Uses the genre tags to match | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | -| `tmdb_genre`1 | Uses the genre from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `tmdb_keyword`1 | Uses the keyword from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `label` | Uses the label tags to match | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | -| `producer` | Uses the actor tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `year` | Uses the year tag to match | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | -| `writer` | Uses the writer tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `resolution` | Uses the resolution tag to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `audio_language` | Uses the audio language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `subtitle_language` | Uses the subtitle language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `original_language`1 | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match
Example: `original_language: en, ko` | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `origin_country`1 | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match
Example: `origin_country: us` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `tmdb_status`1 | Uses TMDb Status to match
**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `tmdb_type`1 | Uses TMDb Type to match
**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Tag Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | +|:-----------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| +| `actor` | Uses the actor tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `collection` | Uses the collection tags to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `content_rating` | Uses the content rating tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `network` | Uses the network tags to match | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `country` | Uses the country tags to match | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| `director` | Uses the director tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `genre` | Uses the genre tags to match | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | +| `label` | Uses the label tags to match | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | +| `producer` | Uses the actor tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `year` | Uses the year tag to match | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| `writer` | Uses the writer tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `resolution` | Uses the resolution tag to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `audio_language` | Uses the audio language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `subtitle_language` | Uses the subtitle language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `tmdb_genre`1 | Uses the genre from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `tmdb_keyword`1 | Uses the keyword from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `origin_country`1 | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match
Example: `origin_country: us` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | 1 Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. @@ -159,9 +157,14 @@ Special Filters each have their own set of rules for how they're used. ### Attribute -| Special Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | -|:----------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------:|:-------:|:--------:|:--------:|:--------:|:-------:|:--------:| -| `history` | Uses the release date attribute (originally available) to match dates throughout history
`day`: Match the Day and Month to Today's Date
`month`: Match the Month to Today's Date
`1-30`: Match the Day and Month to Today's Date or `1-30` days before Today's Date | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | +| Special Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | +|:--------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:-------:|:--------:|:--------:|:--------:|:--------:|:--------:| +| `history` | Uses the release date attribute (originally available) to match dates throughout history
`day`: Match the Day and Month to Today's Date
`month`: Match the Month to Today's Date
`1-30`: Match the Day and Month to Today's Date or `1-30` days before Today's Date | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | +| `original_language`/`original_language.not`1 | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match
Example: `original_language: en, ko` | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| `tmdb_status`/`tmdb_status.not`1 | Uses TMDb Status to match
**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `tmdb_type`/`tmdb_type.not`1 | Uses TMDb Type to match
**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | + +1 Also filters out missing movies/shows from being added to Radarr/Sonarr. ## Collection Filter Examples diff --git a/modules/builder.py b/modules/builder.py index 22159c5d..db190cbe 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -158,20 +158,23 @@ string_filters = ["title", "summary", "studio", "record_label", "filepath", "aud string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends", ".regex"] tag_filters = [ "actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "year", "origin_country", - "writer", "original_language", "resolution", "audio_language", "subtitle_language", "tmdb_keyword", "tmdb_genre", "tmdb_status", "tmdb_type" + "writer", "resolution", "audio_language", "subtitle_language", "tmdb_keyword", "tmdb_genre" ] -tag_modifiers = ["", ".not", ".count_gt", ".count_gte", ".count_lt", ".count_lte"] +tag_modifiers = ["", ".not", ".regex", ".count_gt", ".count_gte", ".count_lt", ".count_lte"] boolean_filters = ["has_collection", "has_overlay", "has_dolby_vision"] date_filters = ["release", "added", "last_played", "first_episode_aired", "last_episode_aired"] date_modifiers = ["", ".not", ".before", ".after", ".regex"] number_filters = ["year", "tmdb_year", "critic_rating", "audience_rating", "user_rating", "tmdb_vote_count", "plays", "duration"] number_modifiers = [".gt", ".gte", ".lt", ".lte"] -special_filters = ["history"] +special_filters = ["history", "original_language", "original_language.not", "tmdb_status", "tmdb_status.not", "tmdb_type", "tmdb_type.not"] 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 tag_filters for m in tag_modifiers] + \ [f"{f}{m}" for f in date_filters for m in date_modifiers] + \ [f"{f}{m}" for f in number_filters for m in number_modifiers] +date_attributes = plex.date_attributes + ["first_episode_aired", "last_episode_aired"] +year_attributes = plex.year_attributes + ["tmdb_year"] +number_attributes = plex.number_attributes + ["tmdb_vote_count"] smart_invalid = ["collection_order", "collection_level"] smart_only = ["collection_filtering"] smart_url_invalid = ["filters", "run_again", "sync_mode", "show_filtered", "show_missing", "save_missing", "smart_label"] + radarr_details + sonarr_details @@ -1773,7 +1776,7 @@ class CollectionBuilder: display_add += inside_display results += f"{conjunction if len(results) > 0 else ''}push=1&{inside_filter}pop=1&" else: - validation = self.validate_attribute(attr, modifier, final_attr, _data, validate, pairs=True) + validation = self.validate_attribute(attr, modifier, final_attr, _data, validate, plex_search=True) if validation is not False and not validation: continue elif attr in plex.date_attributes and modifier in ["", ".not"]: @@ -1786,7 +1789,7 @@ class CollectionBuilder: bool_mod = "" if validation else "!" bool_arg = "true" if validation else "false" results, display_add = build_url_arg(1, mod=bool_mod, arg_s=bool_arg, mod_s="is") - elif (attr in plex.tag_attributes + plex.string_attributes + plex.year_attributes) and modifier in ["", ".is", ".isnot", ".not", ".begins", ".ends"]: + elif (attr in plex.tag_attributes + plex.string_attributes + plex.year_attributes) and modifier in ["", ".is", ".isnot", ".not", ".begins", ".ends", ".regex"]: results = "" display_add = "" for og_value, result in validation: @@ -1839,24 +1842,11 @@ class CollectionBuilder: return type_key, filter_details, filter_url - def validate_attribute(self, attribute, modifier, final, data, validate, pairs=False): + def validate_attribute(self, attribute, modifier, final, data, validate, plex_search=False): def smart_pair(list_to_pair): - return [(t, t) for t in list_to_pair] if pairs else list_to_pair - if modifier == ".regex": - regex_list = util.get_list(data, split=False) - valid_regex = [] - for reg in regex_list: - try: - re.compile(reg) - valid_regex.append(reg) - except re.error: - logger.stacktrace() - err = f"{self.Type} Error: Regular Expression Invalid: {reg}" - if validate: - raise Failed(err) - else: - logger.error(err) - return valid_regex + return [(t, t) for t in list_to_pair] if plex_search else list_to_pair + if modifier == ".regex" and not plex_search: + return util.validate_regex(data, self.Type, validate=validate) elif attribute in plex.string_attributes + string_filters and modifier in ["", ".not", ".is", ".isnot", ".begins", ".ends"]: return smart_pair(util.get_list(data, split=False)) elif attribute == "origin_country": @@ -1871,12 +1861,23 @@ class CollectionBuilder: except Failed: if str(data).lower() in ["day", "month"]: return data.lower() - raise Failed(f"{self.Type} Error: history attribute invalid: {data} must be a number between 1-30, day, or month") + else: + raise Failed(f"{self.Type} Error: history attribute invalid: {data} must be a number between 1-30, day, or month") elif attribute == "tmdb_type": return util.parse(self.Type, final, data, datatype="commalist", options=[v for k, v in discover_types.items()]) elif attribute == "tmdb_status": return util.parse(self.Type, final, data, datatype="commalist", options=[v for k, v in discover_status.items()]) - elif attribute in plex.tag_attributes and modifier in ["", ".not"]: + elif attribute in plex.tag_attributes and modifier in [".regex"]: + _, names = self.library.get_search_choices(attribute, title=not plex_search, name_pairs=True) + valid_list = [] + used = [] + if plex_search and modifier == ".regex": + for reg in util.validate_regex(data, self.Type, validate=validate): + for name, key in names: + if name not in used and re.compile(reg).search(name): + valid_list.append((name, key) if plex_search else key) + return valid_list + elif attribute in plex.tag_attributes and modifier in ["", ".not", ".regex"]: if attribute in plex.tmdb_attributes: final_values = [] for value in util.get_list(data): @@ -1887,12 +1888,11 @@ class CollectionBuilder: final_values.append(value) else: final_values = util.get_list(data) - use_title = not pairs - search_choices, names = self.library.get_search_choices(attribute, title=use_title) + search_choices, names = self.library.get_search_choices(attribute, title=not plex_search) valid_list = [] for value in final_values: if str(value).lower() in search_choices: - if pairs: + if plex_search: valid_list.append((value, search_choices[str(value).lower()])) else: valid_list.append(search_choices[str(value).lower()]) @@ -1901,7 +1901,7 @@ class CollectionBuilder: if attribute in ["actor", "director", "producer", "writer"]: actor_id = self.library.get_actor_id(value) if actor_id: - if pairs: + if plex_search: valid_list.append((value, actor_id)) else: valid_list.append(actor_id) @@ -1914,18 +1914,18 @@ class CollectionBuilder: else: logger.error(error) return valid_list - elif attribute in plex.date_attributes and modifier in [".before", ".after"]: + elif attribute in date_attributes and modifier in [".before", ".after"]: if data == "today": return datetime.strftime(datetime.now(), "%Y-%m-%d") else: return util.validate_date(data, final, return_as="%Y-%m-%d") - elif attribute in plex.year_attributes + ["tmdb_year"] and modifier in ["", ".not"]: + elif attribute in year_attributes and modifier in ["", ".not"]: final_years = [] values = util.get_list(data) for value in values: final_years.append(util.parse(self.Type, final, value, datatype="int")) return smart_pair(final_years) - elif (attribute in plex.number_attributes + plex.date_attributes + plex.year_attributes + ["tmdb_year"] and modifier in ["", ".not", ".gt", ".gte", ".lt", ".lte"]) \ + elif (attribute in number_attributes + date_attributes + year_attributes and modifier in ["", ".not", ".gt", ".gte", ".lt", ".lte"]) \ or (attribute in plex.tag_attributes and modifier in [".count_gt", ".count_gte", ".count_lt", ".count_lte"]): return util.parse(self.Type, final, data, datatype="int") elif attribute in plex.float_attributes and modifier in [".gt", ".gte", ".lt", ".lte"]: @@ -1944,9 +1944,9 @@ class CollectionBuilder: 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 plex.date_attributes and modifier in [".gt", ".gte"]: + elif attribute in date_attributes and modifier in [".gt", ".gte"]: modifier = ".after" - elif attribute in plex.date_attributes and modifier in [".lt", ".lte"]: + elif attribute in date_attributes and modifier in [".lt", ".lte"]: modifier = ".before" final = f"{attribute}{modifier}" if text != final: @@ -2092,7 +2092,15 @@ class CollectionBuilder: attrs = [c.iso_3166_1 for c in item.countries] else: raise Failed - if (not list(set(filter_data) & set(attrs)) and modifier == "") \ + 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 elif filter_attr == "tmdb_title": @@ -2227,11 +2235,19 @@ class CollectionBuilder: 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"]: + 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 (not list(set(filter_data) & set(attrs)) and modifier == "") \ + 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 logger.ghost(f"Filtering {display} {item.title}") diff --git a/modules/plex.py b/modules/plex.py index 151eb3e2..acc79dfd 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -114,7 +114,7 @@ show_translation = { } modifier_translation = { "": "", ".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" + ".before": "%3C%3C", ".after": "%3E%3E", ".begins": "%3C", ".ends": "%3E", ".regex": "" } album_sorting_options = {"default": -1, "newest": 0, "oldest": 1, "name": 2} episode_sorting_options = {"default": -1, "oldest": 0, "newest": 1} @@ -152,84 +152,6 @@ item_advance_keys = { "item_use_original_title": ("useOriginalTitle", use_original_title_options) } new_plex_agents = ["tv.plex.agents.movie", "tv.plex.agents.series"] -music_searches = [ - "artist_title", "artist_title.not", "artist_title.is", "artist_title.isnot", "artist_title.begins", "artist_title.ends", - "artist_user_rating.gt", "artist_user_rating.gte", "artist_user_rating.lt", "artist_user_rating.lte", - "artist_genre", "artist_genre.not", - "artist_collection", "artist_collection.not", - "artist_country", "artist_country.not", - "artist_mood", "artist_mood.not", - "artist_style", "artist_style.not", - "artist_added", "artist_added.not", "artist_added.before", "artist_added.after", - "artist_last_played", "artist_last_played.not", "artist_last_played.before", "artist_last_played.after", - "artist_unmatched", - "album_title", "album_title.not", "album_title.is", "album_title.isnot", "album_title.begins", "album_title.ends", - "album_year.gt", "album_year.gte", "album_year.lt", "album_year.lte", - "album_decade", - "album_genre", "album_genre.not", - "album_plays.gt", "album_plays.gte", "album_plays.lt", "album_plays.lte", - "album_last_played", "album_last_played.not", "album_last_played.before", "album_last_played.after", - "album_user_rating.gt", "album_user_rating.gte", "album_user_rating.lt", "album_user_rating.lte", - "album_critic_rating.gt", "album_critic_rating.gte", "album_critic_rating.lt", "album_critic_rating.lte", - "album_record_label", "album_record_label.not", "album_record_label.is", "album_record_label.isnot", "album_record_label.begins", "album_record_label.ends", - "album_mood", "album_mood.not", - "album_style", "album_style.not", - "album_format", "album_format.not", - "album_type", "album_type.not", - "album_collection", "album_collection.not", - "album_added", "album_added.not", "album_added.before", "album_added.after", - "album_released", "album_released.not", "album_released.before", "album_released.after", - "album_unmatched", - "album_source", "album_source.not", - "album_label", "album_label.not", - "track_mood", "track_mood.not", - "track_title", "track_title.not", "track_title.is", "track_title.isnot", "track_title.begins", "track_title.ends", - "track_plays.gt", "track_plays.gte", "track_plays.lt", "track_plays.lte", - "track_last_played", "track_last_played.not", "track_last_played.before", "track_last_played.after", - "track_skips.gt", "track_skips.gte", "track_skips.lt", "track_skips.lte", - "track_last_skipped", "track_last_skipped.not", "track_last_skipped.before", "track_last_skipped.after", - "track_user_rating.gt", "track_user_rating.gte", "track_user_rating.lt", "track_user_rating.lte", - "track_last_rated", "track_last_rated.not", "track_last_rated.before", "track_last_rated.after", - "track_added", "track_added.not", "track_added.before", "track_added.after", - "track_trash", - "track_source", "track_source.not" -] -searches = [ - "title", "title.not", "title.is", "title.isnot", "title.begins", "title.ends", - "studio", "studio.not", "studio.is", "studio.isnot", "studio.begins", "studio.ends", - "actor", "actor.not", - "audio_language", "audio_language.not", - "collection", "collection.not", - "season_collection", "season_collection.not", - "episode_collection", "episode_collection.not", - "content_rating", "content_rating.not", - "country", "country.not", - "director", "director.not", - "genre", "genre.not", - "label", "label.not", - "network", "network.not", - "producer", "producer.not", - "subtitle_language", "subtitle_language.not", - "writer", "writer.not", - "decade", "resolution", "hdr", "unmatched", "duplicate", "unplayed", "progress", "trash", - "last_played", "last_played.not", "last_played.before", "last_played.after", - "added", "added.not", "added.before", "added.after", - "release", "release.not", "release.before", "release.after", - "duration.gt", "duration.gte", "duration.lt", "duration.lte", - "plays.gt", "plays.gte", "plays.lt", "plays.lte", - "user_rating.gt", "user_rating.gte", "user_rating.lt", "user_rating.lte", - "critic_rating.gt", "critic_rating.gte", "critic_rating.lt", "critic_rating.lte", - "audience_rating.gt", "audience_rating.gte", "audience_rating.lt", "audience_rating.lte", - "year", "year.not", "year.gt", "year.gte", "year.lt", "year.lte", - "unplayed_episodes", "episode_unplayed", "episode_duplicate", "episode_progress", "episode_unmatched", "show_unmatched", - "episode_title", "episode_title.not", "episode_title.is", "episode_title.isnot", "episode_title.begins", "episode_title.ends", - "episode_added", "episode_added.not", "episode_added.before", "episode_added.after", - "episode_air_date", "episode_air_date.not", "episode_air_date.before", "episode_air_date.after", - "episode_last_played", "episode_last_played.not", "episode_last_played.before", "episode_last_played.after", - "episode_plays.gt", "episode_plays.gte", "episode_plays.lt", "episode_plays.lte", - "episode_user_rating.gt", "episode_user_rating.gte", "episode_user_rating.lt", "episode_user_rating.lte", - "episode_year", "episode_year.not", "episode_year.gt", "episode_year.gte", "episode_year.lt", "episode_year.lte" -] + music_searches and_searches = [ "title.and", "studio.and", "actor.and", "audio_language.and", "collection.and", "content_rating.and", "country.and", "director.and", "genre.and", "label.and", @@ -260,6 +182,7 @@ show_only_searches = [ "unplayed_episodes", "episode_unplayed", "episode_duplicate", "episode_progress", "episode_unmatched", "show_unmatched", ] string_attributes = ["title", "studio", "episode_title", "artist_title", "album_title", "album_record_label", "track_title"] +string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends"] float_attributes = [ "user_rating", "episode_user_rating", "critic_rating", "audience_rating", "duration", "artist_user_rating", "album_user_rating", "album_critic_rating", "track_user_rating" @@ -271,11 +194,13 @@ boolean_attributes = [ tmdb_attributes = ["actor", "director", "producer", "writer"] date_attributes = [ "added", "episode_added", "release", "episode_air_date", "last_played", "episode_last_played", - "first_episode_aired", "last_episode_aired", "artist_added", "artist_last_played", "album_last_played", + "artist_added", "artist_last_played", "album_last_played", "album_added", "album_released", "track_last_played", "track_last_skipped", "track_last_rated", "track_added" ] +date_modifiers = ["", ".not", ".before", ".after"] year_attributes = ["decade", "year", "episode_year", "album_year", "album_decade"] -number_attributes = ["plays", "episode_plays", "tmdb_vote_count", "album_plays", "track_plays", "track_skips"] + year_attributes +number_attributes = ["plays", "episode_plays", "album_plays", "track_plays", "track_skips"] + year_attributes +number_modifiers = [".gt", ".gte", ".lt", ".lte"] search_display = {"added": "Date Added", "release": "Release Date", "hdr": "HDR", "progress": "In Progress", "episode_progress": "Episode In Progress"} tag_attributes = [ "actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network", @@ -283,6 +208,14 @@ tag_attributes = [ "artist_genre", "artist_collection", "artist_country", "artist_mood", "artist_style", "album_genre", "album_mood", "album_style", "album_format", "album_type", "album_collection", "album_source", "album_label", "track_mood", "track_source" ] +tag_modifiers = ["", ".not", ".regex"] +no_mods = ["resolution", "decade", "album_decade"] +searches = boolean_attributes + no_mods + \ + [f"{f}{m}" for f in string_attributes for m in string_modifiers] + \ + [f"{f}{m}" for f in tag_attributes + year_attributes for m in tag_modifiers if f not in no_mods] + \ + [f"{f}{m}" for f in date_attributes for m in date_modifiers] + \ + [f"{f}{m}" for f in number_attributes + float_attributes for m in number_modifiers if f not in no_mods] +music_searches = [a for a in searches if a.startswith(("artist", "album", "track"))] movie_sorts = { "title.asc": "titleSort", "title.desc": "titleSort%3Adesc", "year.asc": "year", "year.desc": "year%3Adesc", @@ -580,7 +513,7 @@ class Plex(Library): return result.id @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) - def get_search_choices(self, search_name, title=True): + def get_search_choices(self, search_name, title=True, name_pairs=False): final_search = search_translation[search_name] if search_name in search_translation else search_name final_search = show_translation[final_search] if self.is_show and final_search in show_translation else final_search try: @@ -589,9 +522,7 @@ class Plex(Library): use_title = title and final_search not in ["contentRating", "audioLanguage", "subtitleLanguage", "resolution"] for choice in self.Plex.listFilterChoices(final_search): if choice.title not in names: - names.append(choice.title) - if choice.key not in names: - names.append(choice.key) + names.append((choice.title, choice.key) if name_pairs else choice.title) choices[choice.title] = choice.title if use_title else choice.key choices[choice.key] = choice.title if use_title else choice.key choices[choice.title.lower()] = choice.title if use_title else choice.key diff --git a/modules/util.py b/modules/util.py index a01409ac..cb51a7d8 100644 --- a/modules/util.py +++ b/modules/util.py @@ -143,6 +143,21 @@ def validate_date(date_text, method, return_as=None): raise Failed(f"Collection Error: {method}: {date_text} must match pattern YYYY-MM-DD (e.g. 2020-12-25) or MM/DD/YYYY (e.g. 12/25/2020)") return datetime.strftime(date_obg, return_as) if return_as else date_obg +def validate_regex(data, col_type, validate=True): + regex_list = get_list(data, split=False) + valid_regex = [] + for reg in regex_list: + try: + re.compile(reg) + valid_regex.append(reg) + except re.error: + err = f"{col_type} Error: Regular Expression Invalid: {reg}" + if validate: + raise Failed(err) + else: + logger.error(err) + return valid_regex + def logger_input(prompt, timeout=60): if windows: return windows_input(prompt, timeout) elif hasattr(signal, "SIGALRM"): return unix_input(prompt, timeout)