diff --git a/modules/anilist.py b/modules/anilist.py index f2acbee4..d73ecc76 100644 --- a/modules/anilist.py +++ b/modules/anilist.py @@ -6,17 +6,27 @@ logger = logging.getLogger("Plex Meta Manager") builders = [ "anilist_genre", "anilist_id", "anilist_popular", "anilist_relations", - "anilist_season", "anilist_studio", "anilist_tag", "anilist_top_rated" + "anilist_season", "anilist_studio", "anilist_tag", "anilist_top_rated", "anilist_search" ] pretty_names = {"score": "Average Score", "popular": "Popularity"} -search_translation = { - "season": "MediaSeason", "seasonYear": "Int", "isAdult": "Boolean", - "startDate_greater": "FuzzyDateInt", "startDate_lesser": "FuzzyDateInt", "endDate_greater": "FuzzyDateInt", "endDate_lesser": "FuzzyDateInt", - "format_in": "[MediaFormat]", "format_not_in": "[MediaFormat]", "status_in": "[MediaStatus]", "status_not_in": "[MediaStatus]", - "episodes_greater": "Int", "episodes_lesser": "Int", "duration_greater": "Int", "duration_lesser": "Int", - "genre_in": "[String]", "genre_not_in": "[String]", "tag_in": "[String]", "tag_not_in": "[String]", - "averageScore_greater": "Int", "averageScore_lesser": "Int", "popularity_greater": "Int", "popularity_lesser": "Int" +attr_translation = {"year": "seasonYear", "adult": "isAdult", "start": "startDate", "end": "endDate", "tag_category": "tagCategory", "score": "averageScore", "min_tag_percent": "minimumTagRank"} +mod_translation = {"": "in", "not": "not_in", "before": "greater", "after": "lesser", "gt": "greater", "gte": "greater", "lt": "lesser", "lte": "lesser"} +mod_searches = [ + "start.before", "start.after", "end.before", "end.after", + "format", "format.not", "status", "status.not", "genre", "genre.not", "tag", "tag.not", "tag_category", "tag_category.not", + "episodes.gt", "episodes.gte", "episodes.lt", "episodes.lte", "duration.gt", "duration.gte", "duration.lt", "duration.lte", + "score.gt", "score.gte", "score.lt", "score.lte", "popularity.gt", "popularity.gte", "popularity.lt", "popularity.lte" +] +no_mod_searches = ["season", "year", "adult", "min_tag_percent"] +searches = mod_searches + no_mod_searches +search_types = { + "season": "MediaSeason", "seasonYear": "Int", "isAdult": "Boolean", "startDate": "FuzzyDateInt", "endDate": "FuzzyDateInt", + "format": "[MediaFormat]", "status": "[MediaStatus]", "genre": "[String]", "tag": "[String]", "tagCategory": "[String]", + "episodes": "Int", "duration": "Int", "averageScore": "Int", "popularity": "Int", "minimumTagRank": "Int" } +media_season = {"winter": "WINTER", "spring": "SPRING", "summer": "SUMMER", "fall": "FALL"} +media_format = {"tv": "TV", "short": "TV_SHORT", "movie": "MOVIE", "special": "SPECIAL", "ova": "OVA", "ona": "ONA", "music": "MUSIC"} +media_status = {"finished": "FINISHED", "airing": "RELEASING", "not_yet_aired": "NOT_YET_RELEASED", "cancelled": "CANCELLED", "hiatus": "HIATUS"} base_url = "https://graphql.anilist.co" tag_query = "query{MediaTagCollection {name, category}}" genre_query = "query{GenreCollection}" @@ -24,12 +34,14 @@ genre_query = "query{GenreCollection}" class AniList: def __init__(self, config): self.config = config - self.tags = {} - self.categories = {} + self.options = { + "Tag": {}, "Tag Category": {}, + "Genre": {g.lower().replace(" ", "-"): g for g in self._request(genre_query, {})["data"]["GenreCollection"]}, + "Season": media_season, "Format": media_format, "Status": media_status + } for media_tag in self._request(tag_query, {})["data"]["MediaTagCollection"]: - self.tags[media_tag["name"].lower().replace(" ", "-")] = media_tag["name"] - self.categories[media_tag["category"].lower().replace(" ", "-")] = media_tag["category"] - self.genres = {g.lower().replace(" ", "-"): g for g in self._request(genre_query, {})["data"]["GenreCollection"]} + self.options["Tag"][media_tag["name"].lower().replace(" ", "-")] = media_tag["name"] + self.options["Tag Category"][media_tag["category"].lower().replace(" ", "-")] = media_tag["category"] def _request(self, query, variables, level=1): response = self.config.post(base_url, json={"query": query, "variables": variables}) @@ -76,32 +88,31 @@ class AniList: break return anilist_ids - def _top_rated(self, limit): - return self._search(limit=limit, averageScore_greater=3) - - def _popular(self, limit): - return self._search(sort="popular", limit=limit, popularity_greater=1000) - - def _season(self, season, year, sort, limit): - return self._search(sort=sort, limit=limit, season=season.upper(), year=year) - - def _search(self, sort="score", limit=0, **kwargs): + def _search(self, **kwargs): query_vars = "$page: Int, $sort: [MediaSort]" media_vars = "sort: $sort, type: ANIME" - variables = {"sort": "SCORE_DESC" if sort == "score" else "POPULARITY_DESC"} + variables = {"sort": "SCORE_DESC" if kwargs['sort_by'] == "score" else "POPULARITY_DESC"} for key, value in kwargs.items(): - query_vars += f", ${key}: {search_translation[key]}" - media_vars += f", {key}: ${key}" - variables[key] = value + if key not in ["sort_by", "limit"]: + if "." in key: + attr, mod = key.split(".") + else: + attr = key + mod = "" + ani_attr = attr_translation[attr] if attr in attr_translation else attr + final = ani_attr if attr in no_mod_searches else f"{ani_attr}_{mod_translation[mod]}" + if attr in ["start", "end"]: + value = int(util.validate_date(value, f"anilist_search {key}", return_as="%Y%m%d")) + if mod == "gte": + value -= 1 + elif mod == "lte": + value += 1 + query_vars += f", ${final}: {search_types[ani_attr]}" + media_vars += f", {final}: ${final}" + variables[key] = value query = f"query ({query_vars}) {{Page(page: $page){{pageInfo {{hasNextPage}}media({media_vars}){{id}}}}}}" - logger.info(query) - return self._pagenation(query, limit=limit, variables=variables) - - def _genre(self, genre, sort, limit): - return self._search(sort=sort, limit=limit, genre=genre) - - def _tag(self, tag, sort, limit): - return self._search(sort=sort, limit=limit, tag=tag) + logger.debug(query) + return self._pagenation(query, limit=kwargs["limit"], variables=variables) def _studio(self, studio_id): query = """ @@ -164,20 +175,15 @@ class AniList: return anilist_ids, ignore_ids, name - def validate_tag(self, tag): - return self._validate(tag, self.tags, "Tag") - - def validate_category(self, category): - return self._validate(category, self.categories, "Category") - - def validate_genre(self, genre): - return self._validate(genre, self.genres, "Genre") - - def _validate(self, data, options, name): - data_check = data.lower().replace(" / ", "-").replace(" ", "-") - if data_check in options: - return options[data_check] - raise Failed(f"AniList Error: {name}: {data} does not exist\nOptions: {', '.join([v for k, v in options.items()])}") + def validate(self, name, data): + valid = [] + for d in util.get_list(data): + data_check = d.lower().replace(" / ", "-").replace(" ", "-") + if data_check in self.options[name]: + valid.append(self.options[name][data_check]) + if len(valid) > 0: + return valid + raise Failed(f"AniList Error: {name}: {data} does not exist\nOptions: {', '.join([v for k, v in self.options[name].items()])}") def validate_anilist_ids(self, anilist_ids, studio=False): anilist_id_list = util.get_int_list(anilist_ids, "AniList ID") @@ -197,21 +203,6 @@ class AniList: logger.info(f"Processing AniList ID: {data}") anilist_id, name = self._validate_id(data) anilist_ids = [anilist_id] - elif method == "anilist_popular": - logger.info(f"Processing AniList Popular: {data} Anime") - anilist_ids = self._popular(data) - elif method == "anilist_top_rated": - logger.info(f"Processing AniList Top Rated: {data} Anime") - anilist_ids = self._top_rated(data) - elif method == "anilist_season": - logger.info(f"Processing AniList Season: {data['limit'] if data['limit'] > 0 else 'All'} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {pretty_names[data['sort_by']]}") - anilist_ids = self._season(data["season"], data["year"], data["sort_by"], data["limit"]) - elif method == "anilist_genre": - logger.info(f"Processing AniList Genre: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Genre: {data['genre']} sorted by {pretty_names[data['sort_by']]}") - anilist_ids = self._genre(data["genre"], data["sort_by"], data["limit"]) - elif method == "anilist_tag": - logger.info(f"Processing AniList Tag: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Tag: {data['tag']} sorted by {pretty_names[data['sort_by']]}") - anilist_ids = self._tag(data["tag"], data["sort_by"], data["limit"]) elif method == "anilist_studio": anilist_ids, name = self._studio(data) logger.info(f"Processing AniList Studio: ({data}) {name} ({len(anilist_ids)} Anime)") @@ -219,7 +210,24 @@ class AniList: anilist_ids, _, name = self._relations(data) logger.info(f"Processing AniList Relations: ({data}) {name} ({len(anilist_ids)} Anime)") else: - raise Failed(f"AniList Error: Method {method} not supported") + if method == "anilist_popular": + data = {"limit": data, "popularity.gt": 3, "sort_by": "popular"} + elif method == "anilist_top_rated": + data = {"limit": data, "score.gt": 3, "sort_by": "score"} + elif method not in builders: + raise Failed(f"AniList Error: Method {method} not supported") + message = f"Processing {method.replace('_', ' ').title().replace('Anilist', 'AniList')}:\nSort By: {pretty_names[data['sort_by']]}" + if data['limit'] > 0: + message += f"\nLimit: {data['limit']}" + for key, value in data.items(): + if "." in key: + attr, mod = key.split(".") + else: + attr = key + mod = "" + message += f"\n{attr.replace('_', ' ').title()} {util.mod_displays[mod]} {value}" + util.print_multiline(message) + anilist_ids = self._search(**data) logger.debug("") logger.debug(f"{len(anilist_ids)} AniList IDs Found: {anilist_ids}") return anilist_ids diff --git a/modules/builder.py b/modules/builder.py index 95ac721e..950650c9 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -37,7 +37,9 @@ method_alias = { "producers": "producer", "writers": "writer", "years": "year", "show_year": "year", "show_years": "year", - "show_title": "title" + "show_title": "title", + "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" } filter_translation = { "actor": "actors", @@ -757,12 +759,45 @@ class CollectionBuilder: elif self.current_time.month in [3, 4, 5]: new_dictionary["season"] = "spring" elif self.current_time.month in [6, 7, 8]: new_dictionary["season"] = "summer" elif self.current_time.month in [9, 10, 11]: new_dictionary["season"] = "fall" - new_dictionary["season"] = util.parse("season", dict_data, methods=dict_methods, parent=method_name, default=new_dictionary["season"], options=["winter", "spring", "summer", "fall"]) - new_dictionary["year"] = util.parse("year", dict_data, datatype="int", methods=dict_methods, default=self.current_time.year, parent=method_name, minimum=1917, maximum=self.current_time.year + 1) + new_dictionary["season"] = util.parse("season", dict_data, methods=dict_methods, parent=method_name, default=new_dictionary["season"], options=util.seasons) + new_dictionary["year"] = util.parse("year", dict_data, datatype="int", methods=dict_methods, default=self.current_year, parent=method_name, minimum=1917, maximum=self.current_year + 1) elif method_name == "anilist_genre": - new_dictionary["genre"] = self.config.AniList.validate_genre(util.parse("genre", dict_data, methods=dict_methods, parent=method_name)) + new_dictionary["genre"] = self.config.AniList.validate("Genre", util.parse("genre", dict_data, methods=dict_methods, parent=method_name)) elif method_name == "anilist_tag": - new_dictionary["tag"] = self.config.AniList.validate_tag(util.parse("tag", dict_data, methods=dict_methods, parent=method_name)) + new_dictionary["tag"] = self.config.AniList.validate("Tag", util.parse("tag", dict_data, methods=dict_methods, parent=method_name)) + elif method_name == "anilist_search": + for search_method, search_data in dict_data.items(): + search_attr, modifier, search_final = self._split(search_method) + if search_data is None: + raise Failed(f"Collection Error: {method_name} {search_final} attribute is blank") + elif search_final not in anilist.searches: + raise Failed(f"Collection Error: {method_name} {search_final} attribute not supported") + elif search_attr == "season": + if self.current_time.month in [12, 1, 2]: new_dictionary["season"] = "winter" + elif self.current_time.month in [3, 4, 5]: new_dictionary["season"] = "spring" + elif self.current_time.month in [6, 7, 8]: new_dictionary["season"] = "summer" + elif self.current_time.month in [9, 10, 11]: new_dictionary["season"] = "fall" + new_dictionary["season"] = util.parse("season", dict_data, parent=method_name, default=new_dictionary["season"], options=util.seasons) + if "year" not in dict_methods: + logger.warning(f"Collection Warning: {method_name} {search_final} attribute must be used with the year attribute using this year by default") + elif search_attr == "year": + if "season" not in dict_methods: + raise Failed(f"Collection Error: {method_name} {search_final} attribute must be used with the season attribute") + new_dictionary[search_attr] = util.parse(search_attr, search_data, datatype="int", parent=method_name, default=self.current_year, minimum=1917, maximum=self.current_year + 1) + elif search_attr == "adult": + new_dictionary[search_attr] = util.parse(search_attr, search_data, datatype="bool", parent=method_name) + elif search_attr in ["episodes", "duration", "score", "popularity"]: + new_dictionary[search_final] = util.parse(search_final, search_data, datatype="int", parent=method_name) + elif search_attr in ["format", "status", "genre", "tag", "tag_category"]: + new_dictionary[search_final] = self.config.AniList.validate(search_attr.replace("_", " ").title(), util.parse(search_final, search_data)) + elif search_attr in ["start", "end"]: + new_dictionary[search_final] = util.validate_date(search_data, f"{method_name} {search_final} attribute", return_as="%m/%d/%Y") + elif search_attr == "min_tag_percent": + new_dictionary[search_attr] = util.parse(search_attr, search_data, datatype="int", parent=method_name, minimum=0, maximum=100) + elif search_final not in ["sort_by", "limit"]: + raise Failed(f"Collection Error: {method_name} {search_final} attribute not supported") + if len(new_dictionary) > 0: + raise Failed(f"Collection Error: {method_name} must have at least one valid search option") new_dictionary["sort_by"] = util.parse("sort_by", dict_data, methods=dict_methods, parent=method_name, default="score", options=["score", "popular"]) new_dictionary["limit"] = util.parse("limit", dict_data, datatype="int", methods=dict_methods, default=0, parent=method_name, maximum=500) self.builders.append((method_name, new_dictionary)) @@ -808,9 +843,9 @@ class CollectionBuilder: elif self.current_time.month in [7, 8, 9]: default_season = "summer" else: default_season = "fall" self.builders.append((method_name, { - "season": util.parse("season", dict_data, methods=dict_methods, parent=method_name, default=default_season, options=["winter", "spring", "summer", "fall"]), + "season": util.parse("season", dict_data, methods=dict_methods, parent=method_name, default=default_season, options=util.seasons), "sort_by": util.parse("sort_by", dict_data, methods=dict_methods, parent=method_name, default="members", options=mal.season_sort_options, translation=mal.season_sort_translation), - "year": util.parse("year", dict_data, datatype="int", methods=dict_methods, default=self.current_time.year, parent=method_name, minimum=1917, maximum=self.current_time.year + 1), + "year": util.parse("year", dict_data, datatype="int", methods=dict_methods, default=self.current_year, parent=method_name, minimum=1917, maximum=self.current_year + 1), "limit": util.parse("limit", dict_data, datatype="int", methods=dict_methods, default=100, parent=method_name, maximum=500) })) elif method_name == "mal_userlist": @@ -866,58 +901,45 @@ class CollectionBuilder: def _tmdb(self, method_name, method_data): if method_name == "tmdb_discover": for dict_data, dict_methods in util.parse(method_name, method_data, datatype="dictlist"): - new_dictionary = {"limit": 100} - for discover_name, discover_data in dict_data.items(): - discover_final = discover_name.lower() - if discover_data: - if (self.library.is_movie and discover_final in tmdb.discover_movie) or (self.library.is_show and discover_final in tmdb.discover_tv): - if discover_final == "language": - if re.compile("([a-z]{2})-([A-Z]{2})").match(str(discover_data)): - new_dictionary[discover_final] = str(discover_data) - else: - raise Failed(f"Collection Error: {method_name} attribute {discover_final}: {discover_data} must match pattern ([a-z]{{2}})-([A-Z]{{2}}) e.g. en-US") - elif discover_final == "region": - if re.compile("^[A-Z]{2}$").match(str(discover_data)): - new_dictionary[discover_final] = str(discover_data) - else: - raise Failed(f"Collection Error: {method_name} attribute {discover_final}: {discover_data} must match pattern ^[A-Z]{{2}}$ e.g. US") - elif discover_final == "sort_by": - if (self.library.is_movie and discover_data in tmdb.discover_movie_sort) or (self.library.is_show and discover_data in tmdb.discover_tv_sort): - new_dictionary[discover_final] = discover_data - else: - raise Failed(f"Collection Error: {method_name} attribute {discover_final}: {discover_data} is invalid") - elif discover_final == "certification_country": - if "certification" in dict_data or "certification.lte" in dict_data or "certification.gte" in dict_data: - new_dictionary[discover_final] = discover_data - else: - raise Failed(f"Collection Error: {method_name} attribute {discover_final}: must be used with either certification, certification.lte, or certification.gte") - elif discover_final in ["certification", "certification.lte", "certification.gte"]: - if "certification_country" in dict_data: - new_dictionary[discover_final] = discover_data - else: - raise Failed(f"Collection Error: {method_name} attribute {discover_final}: must be used with certification_country") - elif discover_final in ["include_adult", "include_null_first_air_dates", "screened_theatrically"]: - if discover_data is True: - new_dictionary[discover_final] = discover_data - elif discover_final in tmdb.discover_dates: - new_dictionary[discover_final] = util.validate_date(discover_data, f"{method_name} attribute {discover_final}", return_as="%m/%d/%Y") - elif discover_final in ["primary_release_year", "year", "first_air_date_year"]: - new_dictionary[discover_final] = util.parse(discover_final, discover_data, datatype="int", parent=method_name, minimum=1800, maximum=self.current_year + 1) - elif discover_final in ["vote_count.gte", "vote_count.lte", "vote_average.gte", "vote_average.lte", "with_runtime.gte", "with_runtime.lte"]: - new_dictionary[discover_final] = util.parse(discover_final, discover_data, datatype="int", parent=method_name) - elif discover_final in ["with_cast", "with_crew", "with_people", "with_companies", "with_networks", "with_genres", "without_genres", "with_keywords", "without_keywords", "with_original_language", "timezone"]: - new_dictionary[discover_final] = discover_data - else: - raise Failed(f"Collection Error: {method_name} attribute {discover_final} not supported") - elif discover_final == "limit": - if isinstance(discover_data, int) and discover_data > 0: - new_dictionary[discover_final] = discover_data - else: - raise Failed(f"Collection Error: {method_name} attribute {discover_final}: must be a valid number greater then 0") + new_dictionary = {"limit": util.parse("limit", dict_data, datatype="int", methods=dict_methods, default=100, parent=method_name)} + for discover_method, discover_data in dict_data.items(): + discover_attr, modifier, discover_final = self._split(discover_method) + if discover_data is None: + raise Failed(f"Collection Error: {method_name} {discover_final} attribute is blank") + elif discover_final not in tmdb.discover_all: + raise Failed(f"Collection Error: {method_name} {discover_final} attribute not supported") + elif self.library.is_movie and discover_attr in tmdb.discover_tv_only: + raise Failed(f"Collection Error: {method_name} {discover_final} attribute only works for show libraries") + elif self.library.is_show and discover_attr in tmdb.discover_movie_only: + raise Failed(f"Collection Error: {method_name} {discover_final} attribute only works for movie libraries") + elif discover_attr in ["language", "region"]: + regex = ("([a-z]{2})-([A-Z]{2})", "en-US") if discover_attr == "language" else ("^[A-Z]{2}$", "US") + new_dictionary[discover_attr] = util.parse(discover_attr, discover_data, parent=method_name, regex=regex) + elif discover_attr == "sort_by" and self.library.is_movie: + options = tmdb.discover_movie_sort if self.library.is_movie else tmdb.discover_tv_sort + new_dictionary[discover_attr] = util.parse(discover_attr, discover_data, parent=method_name, options=options) + elif discover_attr == "certification_country": + if "certification" in dict_data or "certification.lte" in dict_data or "certification.gte" in dict_data: + new_dictionary[discover_attr] = discover_data else: - raise Failed(f"Collection Error: {method_name} attribute {discover_final} not supported") - else: - raise Failed(f"Collection Error: {method_name} parameter {discover_final} is blank") + raise Failed(f"Collection Error: {method_name} {discover_attr} attribute: must be used with either certification, certification.lte, or certification.gte") + elif discover_attr == "certification": + if "certification_country" in dict_data: + new_dictionary[discover_final] = discover_data + else: + raise Failed(f"Collection Error: {method_name} {discover_final} attribute: must be used with certification_country") + elif discover_attr in ["include_adult", "include_null_first_air_dates", "screened_theatrically"]: + new_dictionary[discover_attr] = util.parse(discover_attr, discover_data, datatype="bool", parent=method_name) + elif discover_final in tmdb.discover_dates: + new_dictionary[discover_final] = util.validate_date(discover_data, f"{method_name} {discover_final} attribute", return_as="%m/%d/%Y") + elif discover_attr in ["primary_release_year", "year", "first_air_date_year"]: + new_dictionary[discover_attr] = util.parse(discover_attr, discover_data, datatype="int", parent=method_name, minimum=1800, maximum=self.current_year + 1) + elif discover_attr in ["vote_count", "vote_average", "with_runtime"]: + new_dictionary[discover_final] = util.parse(discover_final, discover_data, datatype="int", parent=method_name) + elif discover_final in ["with_cast", "with_crew", "with_people", "with_companies", "with_networks", "with_genres", "without_genres", "with_keywords", "without_keywords", "with_original_language", "timezone"]: + new_dictionary[discover_final] = discover_data + elif discover_attr != "limit": + raise Failed(f"Collection Error: {method_name} {discover_final} attribute not supported") if len(new_dictionary) > 1: self.builders.append((method_name, new_dictionary)) else: @@ -1191,7 +1213,7 @@ class CollectionBuilder: if attr in string_filters and modifier in ["", ".not"]: mod_s = "does not contain" if modifier == ".not" else "contains" elif mod_s is None: - mod_s = plex.mod_displays[modifier] + mod_s = util.mod_displays[modifier] param_s = plex.search_display[attr] if attr in plex.search_display else attr.title().replace('_', ' ') display_line = f"{indent}{param_s} {mod_s} {arg_s}" return f"{arg_key}{mod}={arg}&", display_line diff --git a/modules/mal.py b/modules/mal.py index b86f6f13..06c4ee9f 100644 --- a/modules/mal.py +++ b/modules/mal.py @@ -208,7 +208,7 @@ class MyAnimeList: logger.info(f"Processing {mal_ranked_pretty[method]} ID: {data['producer_id']}") mal_ids = self._producer(data["producer_id"], data["limit"]) elif method == "mal_season": - logger.info(f"Processing MyAnimeList Season: {data['limit']} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {pretty_names[data['sort_by']]}") + logger.info(f"Processing MyAnimeList Season: {data['limit']} Anime from {data['season'].title()} {data['year']} sorted by {pretty_names[data['sort_by']]}") mal_ids = self._season(data["season"], data["year"], data["sort_by"], data["limit"]) elif method == "mal_suggested": logger.info(f"Processing MyAnimeList Suggested: {data} Anime") diff --git a/modules/plex.py b/modules/plex.py index 192c99d9..ba502322 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -152,10 +152,6 @@ sorts = { "added.asc": "addedAt:asc", "added.desc": "addedAt:desc" } modifiers = {".not": "!", ".begins": "<", ".ends": ">", ".before": "<<", ".after": ">>", ".gt": ">>", ".gte": "__gte", ".lt": "<<", ".lte": "__lte"} -mod_displays = { - "": "is", ".not": "is not", ".begins": "begins with", ".ends": "ends with", ".before": "is before", ".after": "is after", - ".gt": "is greater than", ".gte": "is greater than or equal", ".lt": "is less than", ".lte": "is less than or equal" -} tags = [ "actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network", "producer", "resolution", "studio", "subtitle_language", "writer" diff --git a/modules/tmdb.py b/modules/tmdb.py index 9b56e222..6bc9b28d 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -21,19 +21,23 @@ type_map = { "tmdb_network": "Network", "tmdb_person": "Person", "tmdb_producer": "Person", "tmdb_producer_details": "Person", "tmdb_show": "Show", "tmdb_show_details": "Show", "tmdb_writer": "Person", "tmdb_writer_details": "Person" } -discover_movie = [ +discover_all = [ "language", "with_original_language", "region", "sort_by", "with_cast", "with_crew", "with_people", "certification_country", "certification", "certification.lte", "certification.gte", "year", "primary_release_year", "primary_release_date.gte", "primary_release_date.lte", "release_date.gte", "release_date.lte", "vote_count.gte", "vote_count.lte", "vote_average.gte", "vote_average.lte", "with_runtime.gte", "with_runtime.lte", - "with_companies", "with_genres", "without_genres", "with_keywords", "without_keywords", "include_adult" + "with_companies", "with_genres", "without_genres", "with_keywords", "without_keywords", "include_adult", + "timezone", "screened_theatrically", "include_null_first_air_dates", "limit", + "air_date.gte", "air_date.lte", "first_air_date.gte", "first_air_date.lte", "first_air_date_year", "with_networks" ] -discover_tv = [ - "language", "with_original_language", "timezone", "sort_by", "screened_theatrically", "include_null_first_air_dates", - "air_date.gte", "air_date.lte", "first_air_date.gte", "first_air_date.lte", "first_air_date_year", - "vote_count.gte", "vote_count.lte", "vote_average.gte", "vote_average.lte", "with_runtime.gte", "with_runtime.lte", - "with_genres", "without_genres", "with_keywords", "without_keywords", "with_networks", "with_companies" +discover_movie_only = [ + "region", "with_cast", "with_crew", "with_people", "certification_country", "certification", + "year", "primary_release_year", "primary_release_date", "release_date", "include_adult" +] +discover_tv_only = [ + "timezone", "screened_theatrically", "include_null_first_air_dates", + "air_date", "first_air_date", "first_air_date_year", "with_networks", ] discover_dates = [ "primary_release_date.gte", "primary_release_date.lte", "release_date.gte", "release_date.lte", diff --git a/modules/util.py b/modules/util.py index 597edfcc..8b55a3cb 100644 --- a/modules/util.py +++ b/modules/util.py @@ -48,12 +48,16 @@ days_alias = { "saturday": 5, "sat": 5, "s": 5, "sunday": 6, "sun": 6, "su": 6, "u": 6 } +mod_displays = { + "": "is", ".not": "is not", ".begins": "begins with", ".ends": "ends with", ".before": "is before", ".after": "is after", + ".gt": "is greater than", ".gte": "is greater than or equal", ".lt": "is less than", ".lte": "is less than or equal" +} pretty_days = {0: "Monday", 1: "Tuesday", 2: "Wednesday", 3: "Thursday", 4: "Friday", 5: "Saturday", 6: "Sunday"} pretty_months = { 1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December" } -pretty_seasons = {"winter": "Winter", "spring": "Spring", "summer": "Summer", "fall": "Fall"} +seasons = ["winter", "spring", "summer", "fall"] pretty_ids = {"anidbid": "AniDB", "imdbid": "IMDb", "mal_id": "MyAnimeList", "themoviedb_id": "TMDb", "thetvdb_id": "TVDb", "tvdbid": "TVDb"} def tab_new_lines(data): @@ -283,7 +287,7 @@ def is_string_filter(values, modifier, data): if jailbreak: break return (jailbreak and modifier == ".not") or (not jailbreak and modifier in ["", ".begins", ".ends", ".regex"]) -def parse(attribute, data, datatype=None, methods=None, parent=None, default=None, options=None, translation=None, minimum=1, maximum=None): +def parse(attribute, data, datatype=None, methods=None, parent=None, default=None, options=None, translation=None, minimum=1, maximum=None, regex=None): display = f"{parent + ' ' if parent else ''}{attribute} attribute" if options is None and translation is not None: options = [o for o in translation] @@ -305,6 +309,12 @@ def parse(attribute, data, datatype=None, methods=None, parent=None, default=Non message = f"{display} not found" elif value is None: message = f"{display} is blank" + elif regex is not None: + regex_str, example = regex + if re.compile(regex_str).match(str(value)): + return str(value) + else: + message = f"{display}: {value} must match pattern {regex_str} e.g. {example}" elif datatype == "bool": if isinstance(value, bool): return value @@ -330,7 +340,7 @@ def parse(attribute, data, datatype=None, methods=None, parent=None, default=Non message = f"{pre} between {minimum} and {maximum}" elif (translation is not None and str(value).lower() not in translation) or \ (options is not None and translation is None and str(value).lower() not in options): - message = f"{display} {value} must be in {options}" + message = f"{display} {value} must be in {', '.join([str(o) for o in options])}" else: return translation[value] if translation is not None else value