From e6cc58e88f1d7ec4dc9279ec6b60e667609bad60 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 3 May 2022 14:44:19 -0400 Subject: [PATCH] [59] #785 add `mal_search` --- VERSION | 2 +- docs/conf.py | 2 + docs/metadata/builders/myanimelist.md | 118 ++++++++++++-------------- docs/metadata/metadata.md | 2 + modules/builder.py | 74 +++++++++++++++- modules/mal.py | 114 +++++++++++++++---------- modules/plex.py | 2 +- 7 files changed, 199 insertions(+), 115 deletions(-) diff --git a/VERSION b/VERSION index 58061bdd..3505bf84 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.5-develop58 +1.16.5-develop59 diff --git a/docs/conf.py b/docs/conf.py index 314716c7..fa6713ba 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -182,6 +182,8 @@ html_theme_options = { ("IMDb Builders", "metadata/builders/imdb"), ("Trakt Builders", "metadata/builders/trakt"), ("Tautulli Builders", "metadata/builders/tautulli"), + ("Radarr Builders", "metadata/builders/radarr"), + ("Sonarr Builders", "metadata/builders/sonarr"), ("MdbList Builders", "metadata/builders/mdblist"), ("Letterboxd Builders", "metadata/builders/letterboxd"), ("ICheckMovies Builders", "metadata/builders/icheckmovies"), diff --git a/docs/metadata/builders/myanimelist.md b/docs/metadata/builders/myanimelist.md index 247da916..366e87ba 100644 --- a/docs/metadata/builders/myanimelist.md +++ b/docs/metadata/builders/myanimelist.md @@ -6,6 +6,7 @@ You can find anime using the features of [MyAnimeList.net](https://myanimelist.n | Attribute | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | |:----------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------:|:----------------:|:------------------------------------:| +| [`mal_search`](#myanimelist-search) | Finds every anime in a MyAnimeList Search list | ✅ | ✅ | ✅ | | [`mal_all`](#myanimelist-top-all-anime) | Finds every anime in MyAnimeList's [Top All Anime](https://myanimelist.net/topanime.php) list | ✅ | ✅ | ✅ | | [`mal_airing`](#myanimelist-top-airing-anime) | Finds every anime in MyAnimeList's [Top Airing Anime](https://myanimelist.net/topanime.php?type=airing) list | ✅ | ✅ | ✅ | | [`mal_upcoming`](#myanimelist-top-upcoming-anime) | Finds every anime in MyAnimeList's [Top Upcoming Anime](https://myanimelist.net/topanime.php?type=upcoming) list | ✅ | ✅ | ✅ | @@ -19,30 +20,51 @@ You can find anime using the features of [MyAnimeList.net](https://myanimelist.n | [`mal_id`](#myanimelist-id) | Finds the anime specified by the MyAnimeList ID | ✅ | ✅ | ❌ | | [`mal_userlist`](#myanimelist-user-anime-list) | Finds anime in MyAnimeList User's Anime list the options are detailed below | ✅ | ✅ | ✅ | | [`mal_season`](#myanimelist-seasonal-anime) | Finds anime in MyAnimeList's [Seasonal Anime](https://myanimelist.net/anime/season) list the options are detailed below | ✅ | ✅ | ✅ | -| [`mal_genre`](#myanimelist-genre) | Finds every anime tagged with the specified genre id. Genre options can be found on [MyAnimeList's Search](https://myanimelist.net/anime.php) | ✅ | ✅ | ✅ | -| [`mal_studio`](#myanimelist-studio) | Finds every anime tagged with the specified studio/producer/licensor id. Studio options can be found on [MyAnimeList's Search](https://myanimelist.net/anime.php) | ✅ | ✅ | ✅ | -## Expected Input +## MyAnimeList Search -The builders below are expected to have a single integer value of how many movies/shows to query. -* [MyAnimeList Top All Anime](#myanimelist-top-all-anime) -* [MyAnimeList Top Airing Anime](#myanimelist-top-airing-anime) -* [MyAnimeList Top Upcoming Anime](#myanimelist-top-upcoming-anime) -* [MyAnimeList Top Anime TV Series](#myanimelist-top-anime-tv-series) -* [MyAnimeList Top Anime Movies](#myanimelist-top-anime-movies) -* [MyAnimeList Top Anime OVA Series](#myanimelist-top-anime-ova-series) -* [MyAnimeList Top Anime Specials](#myanimelist-top-anime-specials) -* [MyAnimeList Most Popular Anime](#myanimelist-most-popular-anime) -* [MyAnimeList Most Favorited Anime](#myanimelist-most-favorited-anime) -* [MyAnimeList Suggested Anime](#myanimelist-suggested-anime) +Gets every anime in a MyAnimeList search. The different sub-attributes are detailed below. At least one attribute is required. -The attributes of [MyAnimeList ID](#myanimelist-id), [MyAnimeList Seasonal Anime](#myanimelist-seasonal-anime), [MyAnimeList User Anime List](#myanimelist-user-anime-list), [MyAnimeList Genre](#myanimelist-genre), and [MyAnimeList Studio](#myanimelist-studio) are detailed in their sections below. +The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. + +| Attribute | Description | +|:-----------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `sort_by` | **Description:** Sort Order to return
**Values:** `mal_id.desc`, `mal_id.asc`, `title.desc`, `title.asc`, `type.desc`, `type.asc`, `rating.desc`, `rating.asc`, `start_date.desc`, `start_date.asc`, `end_date.desc`, `end_date.asc`, `episodes.desc`, `episodes.asc`, `score.desc`, `score.asc`, `scored_by.desc`, `scored_by.asc`, `rank.desc`, `rank.asc`, `popularity.desc`, `popularity.asc`, `members.desc`, `members.asc`, `favorites.desc`, `favorites.asc` | +| `limit` | **Description:** Don't return more then this number
**Values:** Number of Anime to query from MyAnimeList | +| `query` | **Description:** Text query to search for | +| `prefix` | **Description:** Results must begin with this prefix | +| `type` | **Description:** Type of Anime to search for
**Values:** `tv`, `movie`, `ova`, `special`, `ona`, `music` | +| `status` | **Description:** Status to search for
**Values:** `airing`, `complete`, `upcoming` | +| `genre` | **Description:** List of Genres to include
**Values:** Genre Name or ID | +| `genre.not` | **Description:** List of Genres to exclude
**Values:** Genre Name or ID | +| `studio` | **Description:** List of Studios to include
**Values:** Studio Name or ID | +| `content_rating` | **Description:** Content Rating to search for
**Values:** `g`, `pg`, `pg13`, `r17`, `r`, `rx` | +| `score.gt`/`score.gte` | **Description:** Score must be greater then the given number
**Values:** Float between `0.00`-`10.00` | +| `score.lt`/`score.lte` | **Description:** Score must be less then the given number
**Values:** Float between `0.00`-`10.00` | +| `sfw` | **Description:** Results must be Safe for Work
**Value:** `true` | +* Studio options can be found on [MyAnimeList's Search](https://myanimelist.net/anime.php) Page. +* Genre options can be found on [MyAnimeList's Search](https://myanimelist.net/anime.php) Page. +* To find the ID click on a Studio or Genre in the link above and there should be a number in the URL that's the `id`. +* For example if the url is `https://myanimelist.net/anime/producer/4/Bones` the `id` would be `4` or if the url is `https://myanimelist.net/anime/genre/1/Action` the `id` would be `1`. + +```yaml +collections: + Top Action Anime: + mal_search: + limit: 100 + sort_by: score.desc + genre: Action + collection_order: custom + sync_mode: sync +``` ## MyAnimeList Top All Anime Gets every anime in MyAnimeList's [Top Airing Anime](https://myanimelist.net/topanime.php?type=airing) list. (Maximum: 500) +The expected input value is a single integer value of how many movies/shows to query. + The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. ```yaml @@ -57,6 +79,8 @@ collections: Gets every anime in MyAnimeList's [Top Airing Anime](https://myanimelist.net/topanime.php?type=airing) list. (Maximum: 500) +The expected input value is a single integer value of how many movies/shows to query. + The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. ```yaml @@ -71,6 +95,8 @@ collections: Gets every anime in MyAnimeList's [Top Upcoming Anime](https://myanimelist.net/topanime.php?type=upcoming) list. (Maximum: 500) +The expected input value is a single integer value of how many movies/shows to query. + The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. ```yaml @@ -85,6 +111,8 @@ collections: Gets every anime in MyAnimeList's [Top Anime TV Series](https://myanimelist.net/topanime.php?type=tv) list. (Maximum: 500) +The expected input value is a single integer value of how many movies/shows to query. + The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. ```yaml @@ -99,6 +127,8 @@ collections: Gets every anime in MyAnimeList's [Top Anime Movies](https://myanimelist.net/topanime.php?type=movie) list. (Maximum: 500) +The expected input value is a single integer value of how many movies/shows to query. + The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. ```yaml @@ -113,6 +143,8 @@ collections: Gets every anime in MyAnimeList's [Top Anime OVA Series](https://myanimelist.net/topanime.php?type=ova) list. (Maximum: 500) +The expected input value is a single integer value of how many movies/shows to query. + The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. ```yaml @@ -127,6 +159,8 @@ collections: Gets every anime in MyAnimeList's [Top Anime Specials](https://myanimelist.net/topanime.php?type=special) list. (Maximum: 500) +The expected input value is a single integer value of how many movies/shows to query. + The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. ```yaml @@ -141,6 +175,8 @@ collections: Gets every anime in MyAnimeList's [Most Popular Anime](https://myanimelist.net/topanime.php?type=bypopularity) list. (Maximum: 500) +The expected input value is a single integer value of how many movies/shows to query. + The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. ```yaml @@ -155,6 +191,8 @@ collections: Gets every anime in MyAnimeList's [Most Favorited Anime](https://myanimelist.net/topanime.php?type=favorite) list. (Maximum: 500) +The expected input value is a single integer value of how many movies/shows to query. + The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. ```yaml @@ -169,6 +207,8 @@ collections: Gets the suggested anime in by MyAnimeList for the authorized user. (Maximum: 100) +The expected input value is a single integer value of how many movies/shows to query. + The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. ```yaml @@ -248,51 +288,3 @@ collections: collection_order: custom sync_mode: sync ``` - -## MyAnimeList Genre - -Gets every anime tagged with the specified genre ID sorted by members the options are detailed below. `genre_id` is the only required attribute. - -The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. - -* Genre options can be found on [MyAnimeList's Search](https://myanimelist.net/anime.php) Page. -* To find the ID click on a Genre in the link above and there should be a number in the URL that's the `genre_id`. -* For example if the url is `https://myanimelist.net/anime/genre/1/Action` the `genre_id` would be `1`. - -| Attribute | Description | -|:-----------|:--------------------------------------------------------------------| -| `genre_id` | The ID of Genre from MyAnimeList | -| `limit` | Number of Anime to query from MyAnimeList
**Default:** `0` (All) | - -```yaml -collections: - Sports Anime: - mal_genre: - genre_id: 30 - collection_order: custom - sync_mode: sync -``` - -## MyAnimeList Studio - -Gets every anime tagged with the specified studio/producer/licensor ID sorted by members the options are detailed below. `studio_id` is the only required attribute. - -The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. - -* Studio options can be found on [MyAnimeList's Search](https://myanimelist.net/anime.php) Page. -* To find the ID click on a Studio in the link above and there should be a number in the URL that's the `studio_id`. -* For example if the url is `https://myanimelist.net/anime/producer/4/Bones` the `studio_id` would be `4`. - -| Attribute | Description | -|:------------|:--------------------------------------------------------------------| -| `studio_id` | The ID of Studio/Producer/Licensor from MyAnimeList | -| `limit` | Number of Anime to query from MyAnimeList
**Default:** `0` (All) | - -```yaml -collections: - Bones Studio Anime: - mal_studio: - studio_id: 4 - collection_order: custom - sync_mode: sync -``` diff --git a/docs/metadata/metadata.md b/docs/metadata/metadata.md index 7d56be2e..1f77a25f 100644 --- a/docs/metadata/metadata.md +++ b/docs/metadata/metadata.md @@ -53,6 +53,8 @@ Builders use third-party services to source items to be added to the collection. * [IMDb Builders](builders/imdb) * [Trakt Builders](builders/trakt) * [Tautulli Builders](builders/tautulli) +* [Radarr Builders](builders/radarr) +* [Sonarr Builders](builders/sonarr) * [Letterboxd Builders](builders/letterboxd) * [ICheckMovies Builders](builders/icheckmovies) * [FlixPatrol Builders](builders/flixpatrol) diff --git a/modules/builder.py b/modules/builder.py index fb8f2490..79da38c1 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -138,7 +138,7 @@ custom_sort_builders = [ "trakt_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all", "tautulli_popular", "tautulli_watched", "mdblist_list", "letterboxd_list", "icheckmovies_list", "anilist_top_rated", "anilist_popular", "anilist_trending", "anilist_search", "anilist_userlist", - "mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_movie", "mal_ova", "mal_special", + "mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_movie", "mal_ova", "mal_special", "mal_search", "mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio" ] episode_parts_only = ["plex_pilots"] @@ -1202,7 +1202,7 @@ class CollectionBuilder: self.builders.append((method_name, mal_id)) elif method_name in ["mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_ova", "mal_movie", "mal_special", "mal_popular", "mal_favorite", "mal_suggested"]: self.builders.append((method_name, util.parse(self.Type, method_name, method_data, datatype="int", default=10, maximum=100 if method_name == "mal_suggested" else 500))) - elif method_name in ["mal_season", "mal_userlist"]: + elif method_name in ["mal_season", "mal_userlist", "mal_search"]: for dict_data in util.parse(self.Type, method_name, method_data, datatype="listdict"): dict_methods = {dm.lower(): dm for dm in dict_data} if method_name == "mal_season": @@ -1226,12 +1226,80 @@ class CollectionBuilder: "sort_by": util.parse(self.Type, "sort_by", dict_data, methods=dict_methods, parent=method_name, default="score", options=mal.userlist_sort_options, translation=mal.userlist_sort_translation), "limit": util.parse(self.Type, "limit", dict_data, datatype="int", methods=dict_methods, default=100, parent=method_name, maximum=1000) })) + elif method_name == "mal_search": + final_attributes = {} + final_text = "MyAnimeList Search" + if "sort_by" in dict_methods: + sort = util.parse(self.Type, "sort_by", dict_data, methods=dict_methods, parent=method_name, options=mal.search_combos) + sort_type, sort_direction = sort.split(".") + final_text += f"\nSorted By: {sort}" + final_attributes["order_by"] = sort_type + final_attributes["sort"] = sort_direction + limit = 0 + if "limit" in dict_methods: + limit = util.parse(self.Type, "limit", dict_data, datatype="int", default=0, methods=dict_methods, parent=method_name) + final_text += f"\nLimit: {limit if limit else 'None'}" + if "query" in dict_methods: + final_attributes["q"] = util.parse(self.Type, "query", dict_data, methods=dict_methods, parent=method_name) + final_text += f"\nQuery: {final_attributes['q']}" + if "prefix" in dict_methods: + final_attributes["letter"] = util.parse(self.Type, "prefix", dict_data, methods=dict_methods, parent=method_name) + final_text += f"\nPrefix: {final_attributes['letter']}" + if "type" in dict_methods: + type_list = util.parse(self.Type, "type", dict_data, datatype="commalist", methods=dict_methods, parent=method_name, options=mal.search_types) + final_attributes["type"] = ",".join(type_list) + final_text += f"\nType: {' or '.join(type_list)}" + if "status" in dict_methods: + final_attributes["status"] = util.parse(self.Type, "status", dict_data, methods=dict_methods, parent=method_name, options=mal.search_status) + final_text += f"\nStatus: {final_attributes['status']}" + if "genre" in dict_methods: + genre_list = util.parse(self.Type, "genre", dict_data, datatype="commalist", methods=dict_methods, parent=method_name) + final_genres = [self.config.MyAnimeList.genres[g] for g in genre_list if g in self.config.MyAnimeList.genres] + final_attributes["genres"] = ",".join(final_genres) + final_text += f"\nGenre: {' or '.join([self.config.MyAnimeList.genres[g] for g in final_genres])}" + if "genre.not" in dict_methods: + genre_list = util.parse(self.Type, "genre.not", dict_data, datatype="commalist", methods=dict_methods, parent=method_name) + final_genres = [self.config.MyAnimeList.genres[g] for g in genre_list if g in self.config.MyAnimeList.genres] + final_attributes["genres_exclude"] = ",".join(final_genres) + final_text += f"\nNot Genre: {' or '.join([self.config.MyAnimeList.genres[g] for g in final_genres])}" + if "studio" in dict_methods: + studio_list = util.parse(self.Type, "studio", dict_data, datatype="commalist", methods=dict_methods, parent=method_name) + final_studios = [self.config.MyAnimeList.studios[s] for s in studio_list if s in self.config.MyAnimeList.studios] + final_attributes["producers"] = ",".join(final_studios) + final_text += f"\nStudio: {' or '.join([self.config.MyAnimeList.studios[s] for s in final_studios])}" + if "content_rating" in dict_methods: + final_attributes["rating"] = util.parse(self.Type, "content_rating", dict_data, methods=dict_methods, parent=method_name, options=mal.search_ratings) + final_text += f"\nContent Rating: {final_attributes['rating']}" + if "score.gte" in dict_methods: + final_attributes["min_score"] = util.parse(self.Type, "score.gte", dict_data, datatype="float", methods=dict_methods, parent=method_name, minimum=0, maximum=10) + final_text += f"\nScore Greater Than or Equal: {final_attributes['min_score']}" + elif "score.gt" in dict_methods: + original_score = util.parse(self.Type, "score.gt", dict_data, datatype="float", methods=dict_methods, parent=method_name, minimum=0, maximum=10) + final_attributes["min_score"] = original_score + 0.01 + final_text += f"\nScore Greater Than: {original_score}" + if "score.lte" in dict_methods: + final_attributes["max_score"] = util.parse(self.Type, "score.lte", dict_data, datatype="float", methods=dict_methods, parent=method_name, minimum=0, maximum=10) + final_text += f"\nScore Less Than or Equal: {final_attributes['max_score']}" + elif "score.lt" in dict_methods: + original_score = util.parse(self.Type, "score.lt", dict_data, datatype="float", methods=dict_methods, parent=method_name, minimum=0, maximum=10) + final_attributes["max_score"] = original_score - 0.01 + final_text += f"\nScore Less Than: {original_score}" + if "min_score" in final_attributes and "max_score" in final_attributes and final_attributes["max_score"] <= final_attributes["min_score"]: + raise Failed(f"{self.Type} Error: mal_search score.lte/score.lt attribute must be greater then score.gte/score.gt") + if "sfw" in dict_methods: + sfw = util.parse(self.Type, "sfw", dict_data, datatype="bool", methods=dict_methods, parent=method_name) + if sfw: + final_attributes["sfw"] = 1 + final_text += f"\nSafe for Work: {final_attributes['sfw']}" + if not final_attributes: + raise Failed(f"{self.Type} Error: no mal_search attributes found") + self.builders.append((method_name, (final_attributes, final_text, limit))) elif method_name in ["mal_genre", "mal_studio"]: id_name = f"{method_name[4:]}_id" final_data = [] for data in util.get_list(method_data): final_data.append(data if isinstance(data, dict) else {id_name: data, "limit": 0}) - for dict_data in util.parse(self.Type, method_name, method_data, datatype="listdict"): + for dict_data in final_data: dict_methods = {dm.lower(): dm for dm in dict_data} self.builders.append((method_name, { id_name: util.parse(self.Type, id_name, dict_data, datatype="int", methods=dict_methods, parent=method_name, maximum=999999), diff --git a/modules/mal.py b/modules/mal.py index bffb3443..2b2f50f2 100644 --- a/modules/mal.py +++ b/modules/mal.py @@ -7,14 +7,14 @@ logger = util.logger builders = [ "mal_id", "mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_ova", "mal_movie", "mal_special", - "mal_popular", "mal_favorite", "mal_season", "mal_suggested", "mal_userlist", "mal_genre", "mal_studio" + "mal_popular", "mal_favorite", "mal_season", "mal_suggested", "mal_userlist", "mal_genre", "mal_studio", "mal_search" ] mal_ranked_name = { "mal_all": "all", "mal_airing": "airing", "mal_upcoming": "upcoming", "mal_tv": "tv", "mal_ova": "ova", "mal_movie": "movie", "mal_special": "special", "mal_popular": "bypopularity", "mal_favorite": "favorite" } mal_ranked_pretty = { - "mal_all": "MyAnimeList All", "mal_airing": "MyAnimeList Airing", + "mal_all": "MyAnimeList All", "mal_airing": "MyAnimeList Airing", "mal_search": "MyAnimeList Search", "mal_upcoming": "MyAnimeList Upcoming", "mal_tv": "MyAnimeList TV", "mal_ova": "MyAnimeList OVA", "mal_movie": "MyAnimeList Movie", "mal_special": "MyAnimeList Special", "mal_popular": "MyAnimeList Popular", "mal_favorite": "MyAnimeList Favorite", "mal_genre": "MyAnimeList Genre", "mal_studio": "MyAnimeList Studio" @@ -34,15 +34,20 @@ userlist_sort_translation = { } userlist_sort_options = ["score", "last_updated", "title", "start_date"] userlist_status = ["all", "watching", "completed", "on_hold", "dropped", "plan_to_watch"] -base_url = "https://api.myanimelist.net" -jiken_base_url = "https://api.jikan.moe/v3" +search_types = ["tv", "movie", "ova", "special", "ona", "music"] +search_status = ["airing", "complete", "upcoming"] +search_ratings = ["g", "pg", "pg13", "r17", "r", "rx"] +search_sorts = ["mal_id", "title", "type", "rating", "start_date", "end_date", "episodes", "score", "scored_by", "rank", "popularity", "members", "favorites"] +search_combos = [f"{s}.{d}" for s in search_sorts for d in ["desc", "asc"]] +base_url = "https://api.myanimelist.net/v2/" +jiken_base_url = "https://api.jikan.moe/v4/" urls = { - "oauth_token": f"https://myanimelist.net/v1/oauth2/token", - "oauth_authorize": f"https://myanimelist.net/v1/oauth2/authorize", - "ranking": f"{base_url}/v2/anime/ranking", - "season": f"{base_url}/v2/anime/season", - "suggestions": f"{base_url}/v2/anime/suggestions", - "user": f"{base_url}/v2/users" + "oauth_token": "https://myanimelist.net/v1/oauth2/token", + "oauth_authorize": "https://myanimelist.net/v1/oauth2/authorize", + "ranking": f"{base_url}anime/ranking", + "season": f"{base_url}anime/season", + "suggestions": f"{base_url}anime/suggestions", + "user": f"{base_url}users" } class MyAnimeList: @@ -56,6 +61,29 @@ class MyAnimeList: if not self._save(self.authorization): if not self._refresh(): self._authorization() + self._genres = None + self._studios = None + + @property + def genres(self): + if not self._genres: + self._genres = {} + for data in self._jiken_request("genres/anime")["data"]: + self._genres[data["name"]] = int(data["mal_id"]) + self._genres[data["name"].lower()] = int(data["mal_id"]) + self._genres[data["mal_id"]] = int(data["mal_id"]) + self._genres[int(data["mal_id"])] = data["name"] + return self._genres + + @property + def studios(self): + if not self._studios: + for data in self._jiken_request("producers")["data"]: + self._studios[data["name"]] = int(data["mal_id"]) + self._studios[data["name"].lower()] = int(data["mal_id"]) + self._studios[data["mal_id"]] = int(data["mal_id"]) + self._studios[int(data["mal_id"])] = data["name"] + return self._studios def _authorization(self): code_verifier = secrets.token_urlsafe(100)[:128] @@ -120,6 +148,7 @@ class MyAnimeList: } logger.info(f"Saving authorization information to {self.config_path}") yaml.round_trip_dump(config, open(self.config_path, "w"), indent=ind, block_seq_indent=bsi) + logger.secret(authorization["access_token"]) self.authorization = authorization return True return False @@ -129,7 +158,6 @@ class MyAnimeList: def _request(self, url, authorization=None): token = authorization["access_token"] if authorization else self.authorization["access_token"] - logger.secret(token) if self.config.trace_mode: logger.debug(f"URL: {url}") response = self.config.get_json(url, headers={"Authorization": f"Bearer {token}"}) @@ -138,8 +166,8 @@ class MyAnimeList: if "error" in response: raise Failed(f"MyAnimeList Error: {response['error']}") else: return response - def _jiken_request(self, url): - data = self.config.get_json(f"{jiken_base_url}{url}") + def _jiken_request(self, url, params=None): + data = self.config.get_json(f"{jiken_base_url}{url}", params=params) time.sleep(2) return data @@ -167,29 +195,35 @@ class MyAnimeList: url = f"{urls['user']}/{username}/animelist?{final_status}sort={sort_by}&limit={limit}" return self._parse_request(url) - def _genre(self, genre_id, limit): - data = self._jiken_request(f"/genre/anime/{genre_id}") - if "item_count" not in data: - raise Failed(f"MyAnimeList Error: No MyAnimeList IDs for Genre ID: {genre_id}") - total_items = data["item_count"] - if total_items < limit or limit <= 0: - limit = total_items + def _pagination(self, endpoint, params=None, limit=None): + data = self._jiken_request(endpoint, params) + last_visible_page = data["pagination"]["last_visible_page"] + if limit is not None: + total_items = data["pagination"]["items"]["total"] + if total_items == 0: + raise Failed("MyAnimeList Error: No MyAnimeList IDs for Search") + if total_items < limit or limit <= 0: + limit = total_items + per_page = len(data["data"]) mal_ids = [] - num_of_pages = math.ceil(int(limit) / 100) current_page = 1 chances = 0 - while current_page <= num_of_pages: + while current_page <= last_visible_page: if chances > 6: logger.debug(data) raise Failed("AniList Error: Connection Failed") - start_num = (current_page - 1) * 100 + 1 - logger.ghost(f"Parsing Page {current_page}/{num_of_pages} {start_num}-{limit if current_page == num_of_pages else current_page * 100}") + start_num = (current_page - 1) * per_page + 1 + end_num = limit if limit and (current_page == last_visible_page or limit < start_num + per_page) else current_page * per_page + logger.ghost(f"Parsing Page {current_page}/{last_visible_page} {start_num}-{end_num}") if current_page > 1: - data = self._jiken_request(f"/genre/anime/{genre_id}/{current_page}") - if "anime" in data: + if params is None: + params = {} + params["page"] = current_page + data = self._jiken_request(endpoint, params) + if "data" in data: chances = 0 - mal_ids.extend([anime["mal_id"] for anime in data["anime"]]) - if len(mal_ids) > limit: + mal_ids.extend(data["data"]) + if limit and len(mal_ids) >= limit: return mal_ids[:limit] current_page += 1 else: @@ -197,23 +231,6 @@ class MyAnimeList: logger.exorcise() return mal_ids - def _studio(self, studio_id, limit): - data = self._jiken_request(f"/producer/{studio_id}") - if "anime" not in data: - raise Failed(f"MyAnimeList Error: No MyAnimeList IDs for Studio ID: {studio_id}") - mal_ids = [] - count = 1 - while True: - if count > 1: - data = self._jiken_request(f"/producer/{studio_id}/{count}") - if "anime" not in data: - break - mal_ids.extend([anime["mal_id"] for anime in data["anime"]]) - if len(mal_ids) > limit > 0: - return mal_ids[:limit] - count += 1 - return mal_ids - def get_mal_ids(self, method, data): if method == "mal_id": logger.info(f"Processing MyAnimeList ID: {data}") @@ -221,12 +238,15 @@ class MyAnimeList: elif method in mal_ranked_name: logger.info(f"Processing {mal_ranked_pretty[method]}: {data} Anime") mal_ids = self._ranked(mal_ranked_name[method], data) + elif method == "mal_search": + logger.info(f"Processing {data[1]}") + mal_ids = self._pagination("anime", params=data[0], limit=data[2]) elif method == "mal_genre": logger.info(f"Processing {mal_ranked_pretty[method]} ID: {data['genre_id']}") - mal_ids = self._genre(data["genre_id"], data["limit"]) + mal_ids = self._pagination("anime", params={"genres": data["genre_id"]}, limit=data["limit"]) elif method == "mal_studio": logger.info(f"Processing {mal_ranked_pretty[method]} ID: {data['studio_id']}") - mal_ids = self._studio(data["studio_id"], data["limit"]) + mal_ids = self._pagination("anime", params={"producers": data["studio_id"]}, limit=data["limit"]) elif method == "mal_season": 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"]) diff --git a/modules/plex.py b/modules/plex.py index d2393007..0045c460 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -748,7 +748,7 @@ class Plex(Library): except NotFound: logger.warning(f"Plex Warning: {item.title} has no Season 1 Episode 1 ") elif method == "plex_search": - logger.info(data[1]) + logger.info(f"Processing {data[1]}") items = self.get_filter_items(data[2]) elif method == "plex_collectionless": good_collections = []