[12] add .regex modifier for tags

pull/858/head
meisnate12 3 years ago
parent 9cf79d2a91
commit e887bcbffb

@ -1 +1 @@
1.16.5-develop11 1.16.5-develop12

@ -7,6 +7,7 @@ libraries: # This is called out once within
- folder: config/Movies/ # This is a local directory on the system - folder: config/Movies/ # This is a local directory on the system
- git: meisnate12/MovieCharts # This is a file within the GitHub Repository - git: meisnate12/MovieCharts # This is a file within the GitHub Repository
overlay_path: 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 - file: config/Overlays.yml # This is a local file on the system
TV Shows: TV Shows:
metadata_path: metadata_path:

@ -143,7 +143,7 @@ libraries:
### Remove Overlays ### 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 ```yaml
libraries: libraries:
@ -151,8 +151,8 @@ libraries:
metadata_path: metadata_path:
- file: config/TV Shows.yml - file: config/TV Shows.yml
overlay_path: overlay_path:
- remove_overlays: true
- file: config/Overlays.yml - file: config/Overlays.yml
- remove_overlays: ture
``` ```
* This will remove all overlays when run and not generate new ones. * This will remove all overlays when run and not generate new ones.

@ -165,10 +165,11 @@ Tag search can take multiple values as a **list or a comma-separated string**.
### Tag Modifiers ### Tag Modifiers
| Tag Modifier | Description | Plex Web UI Display | | Tag Modifier | Description | Plex Web UI Display |
|:-------------|:-----------------------------------------------------------------------|:-------------------:| |:-------------|:------------------------------------------------------------------------|:-------------------:|
| No Modifier | Matches every item where the attribute matches the given string | `is` | | 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` | | `.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 ### Tag Attributes

@ -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 | | 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 | | `.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_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_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 | | `.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 ### Attribute
| Tag Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | | Tag Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track |
|:--------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| |:-----------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|
| `actor` | Uses the actor tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | | `actor` | Uses the actor tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| `collection` | Uses the collection tags to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `collection` | Uses the collection tags to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `content_rating` | Uses the content rating tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | | `content_rating` | Uses the content rating tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| `network` | Uses the network tags to match | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | `network` | Uses the network tags to match | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| `country` | Uses the country tags to match | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | | `country` | Uses the country tags to match | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
| `director` | Uses the director tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | | `director` | Uses the director tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| `genre` | Uses the genre tags to match | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | | `genre` | Uses the genre tags to match | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ |
| `tmdb_genre`<sup>1</sup> | Uses the genre from TMDb to match | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `label` | Uses the label tags to match | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#9989; | &#10060; |
| `tmdb_keyword`<sup>1</sup> | Uses the keyword from TMDb to match | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `producer` | Uses the actor tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `label` | Uses the label tags to match | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#9989; | &#10060; | | `year` | Uses the year tag to match | &#9989; | &#9989; | &#9989; | &#9989; | &#10060; | &#9989; | &#9989; |
| `producer` | Uses the actor tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `writer` | Uses the writer tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `year` | Uses the year tag to match | &#9989; | &#9989; | &#9989; | &#9989; | &#10060; | &#9989; | &#9989; | | `resolution` | Uses the resolution tag to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `writer` | Uses the writer tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `audio_language` | Uses the audio language tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `resolution` | Uses the resolution tag to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `subtitle_language` | Uses the subtitle language tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `audio_language` | Uses the audio language tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `tmdb_genre`<sup>1</sup> | Uses the genre from TMDb to match | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
| `subtitle_language` | Uses the subtitle language tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `tmdb_keyword`<sup>1</sup> | Uses the keyword from TMDb to match | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
| `original_language`<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; | | `origin_country`<sup>1</sup> | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match<br>Example: `origin_country: us` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
| `origin_country`<sup>1</sup> | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match<br>Example: `origin_country: us` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
| `tmdb_status`<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`<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. These Values also cannot use the `count` modifiers. <sup>1</sup> 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 ### 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; |
| `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.
## Collection Filter Examples ## Collection Filter Examples

@ -158,20 +158,23 @@ string_filters = ["title", "summary", "studio", "record_label", "filepath", "aud
string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends", ".regex"] string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends", ".regex"]
tag_filters = [ tag_filters = [
"actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "year", "origin_country", "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"] boolean_filters = ["has_collection", "has_overlay", "has_dolby_vision"]
date_filters = ["release", "added", "last_played", "first_episode_aired", "last_episode_aired"] date_filters = ["release", "added", "last_played", "first_episode_aired", "last_episode_aired"]
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"] special_filters = ["history", "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] + \
[f"{f}{m}" for f in date_filters for m in date_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] [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_invalid = ["collection_order", "collection_level"]
smart_only = ["collection_filtering"] smart_only = ["collection_filtering"]
smart_url_invalid = ["filters", "run_again", "sync_mode", "show_filtered", "show_missing", "save_missing", "smart_label"] + radarr_details + sonarr_details 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 display_add += inside_display
results += f"{conjunction if len(results) > 0 else ''}push=1&{inside_filter}pop=1&" results += f"{conjunction if len(results) > 0 else ''}push=1&{inside_filter}pop=1&"
else: 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: if validation is not False and not validation:
continue continue
elif attr in plex.date_attributes and modifier in ["", ".not"]: elif attr in plex.date_attributes and modifier in ["", ".not"]:
@ -1786,7 +1789,7 @@ class CollectionBuilder:
bool_mod = "" if validation else "!" bool_mod = "" if validation else "!"
bool_arg = "true" if validation else "false" bool_arg = "true" if validation else "false"
results, display_add = build_url_arg(1, mod=bool_mod, arg_s=bool_arg, mod_s="is") 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 = "" results = ""
display_add = "" display_add = ""
for og_value, result in validation: for og_value, result in validation:
@ -1839,24 +1842,11 @@ class CollectionBuilder:
return type_key, filter_details, filter_url 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): def smart_pair(list_to_pair):
return [(t, t) for t in list_to_pair] if pairs else list_to_pair return [(t, t) for t in list_to_pair] if plex_search else list_to_pair
if modifier == ".regex": if modifier == ".regex" and not plex_search:
regex_list = util.get_list(data, split=False) return util.validate_regex(data, self.Type, validate=validate)
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
elif attribute in plex.string_attributes + string_filters and modifier in ["", ".not", ".is", ".isnot", ".begins", ".ends"]: 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)) return smart_pair(util.get_list(data, split=False))
elif attribute == "origin_country": elif attribute == "origin_country":
@ -1871,12 +1861,23 @@ class CollectionBuilder:
except Failed: except Failed:
if str(data).lower() in ["day", "month"]: if str(data).lower() in ["day", "month"]:
return data.lower() 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": elif attribute == "tmdb_type":
return util.parse(self.Type, final, data, datatype="commalist", options=[v for k, v in discover_types.items()]) return util.parse(self.Type, final, data, datatype="commalist", options=[v for k, v in discover_types.items()])
elif attribute == "tmdb_status": elif attribute == "tmdb_status":
return util.parse(self.Type, final, data, datatype="commalist", options=[v for k, v in discover_status.items()]) 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: if attribute in plex.tmdb_attributes:
final_values = [] final_values = []
for value in util.get_list(data): for value in util.get_list(data):
@ -1887,12 +1888,11 @@ class CollectionBuilder:
final_values.append(value) final_values.append(value)
else: else:
final_values = util.get_list(data) final_values = util.get_list(data)
use_title = not pairs search_choices, names = self.library.get_search_choices(attribute, title=not plex_search)
search_choices, names = self.library.get_search_choices(attribute, title=use_title)
valid_list = [] valid_list = []
for value in final_values: for value in final_values:
if str(value).lower() in search_choices: if str(value).lower() in search_choices:
if pairs: if plex_search:
valid_list.append((value, search_choices[str(value).lower()])) valid_list.append((value, search_choices[str(value).lower()]))
else: else:
valid_list.append(search_choices[str(value).lower()]) valid_list.append(search_choices[str(value).lower()])
@ -1901,7 +1901,7 @@ class CollectionBuilder:
if attribute in ["actor", "director", "producer", "writer"]: if attribute in ["actor", "director", "producer", "writer"]:
actor_id = self.library.get_actor_id(value) actor_id = self.library.get_actor_id(value)
if actor_id: if actor_id:
if pairs: if plex_search:
valid_list.append((value, actor_id)) valid_list.append((value, actor_id))
else: else:
valid_list.append(actor_id) valid_list.append(actor_id)
@ -1914,18 +1914,18 @@ class CollectionBuilder:
else: else:
logger.error(error) logger.error(error)
return valid_list 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": if data == "today":
return datetime.strftime(datetime.now(), "%Y-%m-%d") return datetime.strftime(datetime.now(), "%Y-%m-%d")
else: else:
return util.validate_date(data, final, return_as="%Y-%m-%d") 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 = [] final_years = []
values = util.get_list(data) values = util.get_list(data)
for value in values: for value in values:
final_years.append(util.parse(self.Type, final, value, datatype="int")) final_years.append(util.parse(self.Type, final, value, datatype="int"))
return smart_pair(final_years) 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"]): 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") return util.parse(self.Type, final, data, datatype="int")
elif attribute in plex.float_attributes and modifier in [".gt", ".gte", ".lt", ".lte"]: 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" attribute = "radarr_add_missing" if self.library.is_movie else "sonarr_add_missing"
elif attribute in ["arr_tag", "arr_folder"]: elif attribute in ["arr_tag", "arr_folder"]:
attribute = f"{'rad' if self.library.is_movie else 'son'}{attribute}" 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" 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" modifier = ".before"
final = f"{attribute}{modifier}" final = f"{attribute}{modifier}"
if text != final: if text != final:
@ -2092,7 +2092,15 @@ class CollectionBuilder:
attrs = [c.iso_3166_1 for c in item.countries] attrs = [c.iso_3166_1 for c in item.countries]
else: else:
raise Failed 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"): or (list(set(filter_data) & set(attrs)) and modifier == ".not"):
return False return False
elif filter_attr == "tmdb_title": elif filter_attr == "tmdb_title":
@ -2227,11 +2235,19 @@ class CollectionBuilder:
attrs.extend([s.language for s in part.subtitleStreams()]) attrs.extend([s.language for s in part.subtitleStreams()])
elif filter_attr in ["content_rating", "year", "rating"]: elif filter_attr in ["content_rating", "year", "rating"]:
attrs = [getattr(item, filter_actual)] 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)] attrs = [attr.tag for attr in getattr(item, filter_actual)]
else: else:
raise Failed(f"Filter Error: filter: {filter_final} not supported") 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"): or (list(set(filter_data) & set(attrs)) and modifier == ".not"):
return False return False
logger.ghost(f"Filtering {display} {item.title}") logger.ghost(f"Filtering {display} {item.title}")

@ -114,7 +114,7 @@ show_translation = {
} }
modifier_translation = { 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" ".before": "%3C%3C", ".after": "%3E%3E", ".begins": "%3C", ".ends": "%3E", ".regex": ""
} }
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}
@ -152,84 +152,6 @@ item_advance_keys = {
"item_use_original_title": ("useOriginalTitle", use_original_title_options) "item_use_original_title": ("useOriginalTitle", use_original_title_options)
} }
new_plex_agents = ["tv.plex.agents.movie", "tv.plex.agents.series"] 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 = [ and_searches = [
"title.and", "studio.and", "actor.and", "audio_language.and", "collection.and", "title.and", "studio.and", "actor.and", "audio_language.and", "collection.and",
"content_rating.and", "country.and", "director.and", "genre.and", "label.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", "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_attributes = ["title", "studio", "episode_title", "artist_title", "album_title", "album_record_label", "track_title"]
string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends"]
float_attributes = [ float_attributes = [
"user_rating", "episode_user_rating", "critic_rating", "audience_rating", "duration", "user_rating", "episode_user_rating", "critic_rating", "audience_rating", "duration",
"artist_user_rating", "album_user_rating", "album_critic_rating", "track_user_rating" "artist_user_rating", "album_user_rating", "album_critic_rating", "track_user_rating"
@ -271,11 +194,13 @@ boolean_attributes = [
tmdb_attributes = ["actor", "director", "producer", "writer"] tmdb_attributes = ["actor", "director", "producer", "writer"]
date_attributes = [ date_attributes = [
"added", "episode_added", "release", "episode_air_date", "last_played", "episode_last_played", "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" "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"] 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"} search_display = {"added": "Date Added", "release": "Release Date", "hdr": "HDR", "progress": "In Progress", "episode_progress": "Episode In Progress"}
tag_attributes = [ tag_attributes = [
"actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network", "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", "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" "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 = { movie_sorts = {
"title.asc": "titleSort", "title.desc": "titleSort%3Adesc", "title.asc": "titleSort", "title.desc": "titleSort%3Adesc",
"year.asc": "year", "year.desc": "year%3Adesc", "year.asc": "year", "year.desc": "year%3Adesc",
@ -580,7 +513,7 @@ class Plex(Library):
return result.id return result.id
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) @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 = 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 final_search = show_translation[final_search] if self.is_show and final_search in show_translation else final_search
try: try:
@ -589,9 +522,7 @@ class Plex(Library):
use_title = title and final_search not in ["contentRating", "audioLanguage", "subtitleLanguage", "resolution"] use_title = title and final_search not in ["contentRating", "audioLanguage", "subtitleLanguage", "resolution"]
for choice in self.Plex.listFilterChoices(final_search): for choice in self.Plex.listFilterChoices(final_search):
if choice.title not in names: if choice.title not in names:
names.append(choice.title) names.append((choice.title, choice.key) if name_pairs else choice.title)
if choice.key not in names:
names.append(choice.key)
choices[choice.title] = choice.title if use_title else choice.key choices[choice.title] = choice.title if use_title else choice.key
choices[choice.key] = 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 choices[choice.title.lower()] = choice.title if use_title else choice.key

@ -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)") 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 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): def logger_input(prompt, timeout=60):
if windows: return windows_input(prompt, timeout) if windows: return windows_input(prompt, timeout)
elif hasattr(signal, "SIGALRM"): return unix_input(prompt, timeout) elif hasattr(signal, "SIGALRM"): return unix_input(prompt, timeout)

Loading…
Cancel
Save