[59] #785 add `mal_search`

pull/858/head
meisnate12 3 years ago
parent cacb62305f
commit e6cc58e88f

@ -1 +1 @@
1.16.5-develop58 1.16.5-develop59

@ -182,6 +182,8 @@ html_theme_options = {
("IMDb Builders", "metadata/builders/imdb"), ("IMDb Builders", "metadata/builders/imdb"),
("Trakt Builders", "metadata/builders/trakt"), ("Trakt Builders", "metadata/builders/trakt"),
("Tautulli Builders", "metadata/builders/tautulli"), ("Tautulli Builders", "metadata/builders/tautulli"),
("Radarr Builders", "metadata/builders/radarr"),
("Sonarr Builders", "metadata/builders/sonarr"),
("MdbList Builders", "metadata/builders/mdblist"), ("MdbList Builders", "metadata/builders/mdblist"),
("Letterboxd Builders", "metadata/builders/letterboxd"), ("Letterboxd Builders", "metadata/builders/letterboxd"),
("ICheckMovies Builders", "metadata/builders/icheckmovies"), ("ICheckMovies Builders", "metadata/builders/icheckmovies"),

@ -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 | | 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_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_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 | ✅ | ✅ | ✅ | | [`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_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_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_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. Gets every anime in a MyAnimeList search. The different sub-attributes are detailed below. At least one attribute is required.
* [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)
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<br>**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<br>**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<br>**Values:** `tv`, `movie`, `ova`, `special`, `ona`, `music` |
| `status` | **Description:** Status to search for<br>**Values:** `airing`, `complete`, `upcoming` |
| `genre` | **Description:** List of Genres to include<br>**Values:** Genre Name or ID |
| `genre.not` | **Description:** List of Genres to exclude<br>**Values:** Genre Name or ID |
| `studio` | **Description:** List of Studios to include<br>**Values:** Studio Name or ID |
| `content_rating` | **Description:** Content Rating to search for<br>**Values:** `g`, `pg`, `pg13`, `r17`, `r`, `rx` |
| `score.gt`/`score.gte` | **Description:** Score must be greater then the given number<br>**Values:** Float between `0.00`-`10.00` |
| `score.lt`/`score.lte` | **Description:** Score must be less then the given number<br>**Values:** Float between `0.00`-`10.00` |
| `sfw` | **Description:** Results must be Safe for Work<br>**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 ## MyAnimeList Top All Anime
Gets every anime in MyAnimeList's [Top Airing Anime](https://myanimelist.net/topanime.php?type=airing) list. (Maximum: 500) 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. The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
```yaml ```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) 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. The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
```yaml ```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) 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. The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
```yaml ```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) 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. The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
```yaml ```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) 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. The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
```yaml ```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) 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. The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
```yaml ```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) 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. The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
```yaml ```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) 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. The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
```yaml ```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) 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. The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
```yaml ```yaml
@ -169,6 +207,8 @@ collections:
Gets the suggested anime in by MyAnimeList for the authorized user. (Maximum: 100) 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. The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
```yaml ```yaml
@ -248,51 +288,3 @@ collections:
collection_order: custom collection_order: custom
sync_mode: sync 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<br>**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<br>**Default:** `0` (All) |
```yaml
collections:
Bones Studio Anime:
mal_studio:
studio_id: 4
collection_order: custom
sync_mode: sync
```

@ -53,6 +53,8 @@ Builders use third-party services to source items to be added to the collection.
* [IMDb Builders](builders/imdb) * [IMDb Builders](builders/imdb)
* [Trakt Builders](builders/trakt) * [Trakt Builders](builders/trakt)
* [Tautulli Builders](builders/tautulli) * [Tautulli Builders](builders/tautulli)
* [Radarr Builders](builders/radarr)
* [Sonarr Builders](builders/sonarr)
* [Letterboxd Builders](builders/letterboxd) * [Letterboxd Builders](builders/letterboxd)
* [ICheckMovies Builders](builders/icheckmovies) * [ICheckMovies Builders](builders/icheckmovies)
* [FlixPatrol Builders](builders/flixpatrol) * [FlixPatrol Builders](builders/flixpatrol)

@ -138,7 +138,7 @@ custom_sort_builders = [
"trakt_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all", "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", "tautulli_popular", "tautulli_watched", "mdblist_list", "letterboxd_list", "icheckmovies_list",
"anilist_top_rated", "anilist_popular", "anilist_trending", "anilist_search", "anilist_userlist", "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" "mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio"
] ]
episode_parts_only = ["plex_pilots"] episode_parts_only = ["plex_pilots"]
@ -1202,7 +1202,7 @@ class CollectionBuilder:
self.builders.append((method_name, mal_id)) 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"]: 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))) 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"): for dict_data in util.parse(self.Type, method_name, method_data, datatype="listdict"):
dict_methods = {dm.lower(): dm for dm in dict_data} dict_methods = {dm.lower(): dm for dm in dict_data}
if method_name == "mal_season": 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), "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) "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"]: elif method_name in ["mal_genre", "mal_studio"]:
id_name = f"{method_name[4:]}_id" id_name = f"{method_name[4:]}_id"
final_data = [] final_data = []
for data in util.get_list(method_data): for data in util.get_list(method_data):
final_data.append(data if isinstance(data, dict) else {id_name: data, "limit": 0}) 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} dict_methods = {dm.lower(): dm for dm in dict_data}
self.builders.append((method_name, { 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), id_name: util.parse(self.Type, id_name, dict_data, datatype="int", methods=dict_methods, parent=method_name, maximum=999999),

@ -7,14 +7,14 @@ logger = util.logger
builders = [ builders = [
"mal_id", "mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_ova", "mal_movie", "mal_special", "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_ranked_name = {
"mal_all": "all", "mal_airing": "airing", "mal_upcoming": "upcoming", "mal_tv": "tv", "mal_ova": "ova", "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_movie": "movie", "mal_special": "special", "mal_popular": "bypopularity", "mal_favorite": "favorite"
} }
mal_ranked_pretty = { 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_upcoming": "MyAnimeList Upcoming", "mal_tv": "MyAnimeList TV", "mal_ova": "MyAnimeList OVA",
"mal_movie": "MyAnimeList Movie", "mal_special": "MyAnimeList Special", "mal_popular": "MyAnimeList Popular", "mal_movie": "MyAnimeList Movie", "mal_special": "MyAnimeList Special", "mal_popular": "MyAnimeList Popular",
"mal_favorite": "MyAnimeList Favorite", "mal_genre": "MyAnimeList Genre", "mal_studio": "MyAnimeList Studio" "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_sort_options = ["score", "last_updated", "title", "start_date"]
userlist_status = ["all", "watching", "completed", "on_hold", "dropped", "plan_to_watch"] userlist_status = ["all", "watching", "completed", "on_hold", "dropped", "plan_to_watch"]
base_url = "https://api.myanimelist.net" search_types = ["tv", "movie", "ova", "special", "ona", "music"]
jiken_base_url = "https://api.jikan.moe/v3" 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 = { urls = {
"oauth_token": f"https://myanimelist.net/v1/oauth2/token", "oauth_token": "https://myanimelist.net/v1/oauth2/token",
"oauth_authorize": f"https://myanimelist.net/v1/oauth2/authorize", "oauth_authorize": "https://myanimelist.net/v1/oauth2/authorize",
"ranking": f"{base_url}/v2/anime/ranking", "ranking": f"{base_url}anime/ranking",
"season": f"{base_url}/v2/anime/season", "season": f"{base_url}anime/season",
"suggestions": f"{base_url}/v2/anime/suggestions", "suggestions": f"{base_url}anime/suggestions",
"user": f"{base_url}/v2/users" "user": f"{base_url}users"
} }
class MyAnimeList: class MyAnimeList:
@ -56,6 +61,29 @@ class MyAnimeList:
if not self._save(self.authorization): if not self._save(self.authorization):
if not self._refresh(): if not self._refresh():
self._authorization() 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): def _authorization(self):
code_verifier = secrets.token_urlsafe(100)[:128] code_verifier = secrets.token_urlsafe(100)[:128]
@ -120,6 +148,7 @@ class MyAnimeList:
} }
logger.info(f"Saving authorization information to {self.config_path}") 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) yaml.round_trip_dump(config, open(self.config_path, "w"), indent=ind, block_seq_indent=bsi)
logger.secret(authorization["access_token"])
self.authorization = authorization self.authorization = authorization
return True return True
return False return False
@ -129,7 +158,6 @@ class MyAnimeList:
def _request(self, url, authorization=None): def _request(self, url, authorization=None):
token = authorization["access_token"] if authorization else self.authorization["access_token"] token = authorization["access_token"] if authorization else self.authorization["access_token"]
logger.secret(token)
if self.config.trace_mode: if self.config.trace_mode:
logger.debug(f"URL: {url}") logger.debug(f"URL: {url}")
response = self.config.get_json(url, headers={"Authorization": f"Bearer {token}"}) 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']}") if "error" in response: raise Failed(f"MyAnimeList Error: {response['error']}")
else: return response else: return response
def _jiken_request(self, url): def _jiken_request(self, url, params=None):
data = self.config.get_json(f"{jiken_base_url}{url}") data = self.config.get_json(f"{jiken_base_url}{url}", params=params)
time.sleep(2) time.sleep(2)
return data return data
@ -167,29 +195,35 @@ class MyAnimeList:
url = f"{urls['user']}/{username}/animelist?{final_status}sort={sort_by}&limit={limit}" url = f"{urls['user']}/{username}/animelist?{final_status}sort={sort_by}&limit={limit}"
return self._parse_request(url) return self._parse_request(url)
def _genre(self, genre_id, limit): def _pagination(self, endpoint, params=None, limit=None):
data = self._jiken_request(f"/genre/anime/{genre_id}") data = self._jiken_request(endpoint, params)
if "item_count" not in data: last_visible_page = data["pagination"]["last_visible_page"]
raise Failed(f"MyAnimeList Error: No MyAnimeList IDs for Genre ID: {genre_id}") if limit is not None:
total_items = data["item_count"] 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: if total_items < limit or limit <= 0:
limit = total_items limit = total_items
per_page = len(data["data"])
mal_ids = [] mal_ids = []
num_of_pages = math.ceil(int(limit) / 100)
current_page = 1 current_page = 1
chances = 0 chances = 0
while current_page <= num_of_pages: while current_page <= last_visible_page:
if chances > 6: if chances > 6:
logger.debug(data) logger.debug(data)
raise Failed("AniList Error: Connection Failed") raise Failed("AniList Error: Connection Failed")
start_num = (current_page - 1) * 100 + 1 start_num = (current_page - 1) * per_page + 1
logger.ghost(f"Parsing Page {current_page}/{num_of_pages} {start_num}-{limit if current_page == num_of_pages else current_page * 100}") 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: if current_page > 1:
data = self._jiken_request(f"/genre/anime/{genre_id}/{current_page}") if params is None:
if "anime" in data: params = {}
params["page"] = current_page
data = self._jiken_request(endpoint, params)
if "data" in data:
chances = 0 chances = 0
mal_ids.extend([anime["mal_id"] for anime in data["anime"]]) mal_ids.extend(data["data"])
if len(mal_ids) > limit: if limit and len(mal_ids) >= limit:
return mal_ids[:limit] return mal_ids[:limit]
current_page += 1 current_page += 1
else: else:
@ -197,23 +231,6 @@ class MyAnimeList:
logger.exorcise() logger.exorcise()
return mal_ids 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): def get_mal_ids(self, method, data):
if method == "mal_id": if method == "mal_id":
logger.info(f"Processing MyAnimeList ID: {data}") logger.info(f"Processing MyAnimeList ID: {data}")
@ -221,12 +238,15 @@ class MyAnimeList:
elif method in mal_ranked_name: elif method in mal_ranked_name:
logger.info(f"Processing {mal_ranked_pretty[method]}: {data} Anime") logger.info(f"Processing {mal_ranked_pretty[method]}: {data} Anime")
mal_ids = self._ranked(mal_ranked_name[method], data) 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": elif method == "mal_genre":
logger.info(f"Processing {mal_ranked_pretty[method]} ID: {data['genre_id']}") 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": elif method == "mal_studio":
logger.info(f"Processing {mal_ranked_pretty[method]} ID: {data['studio_id']}") 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": 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']]}") 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"]) mal_ids = self._season(data["season"], data["year"], data["sort_by"], data["limit"])

@ -748,7 +748,7 @@ class Plex(Library):
except NotFound: except NotFound:
logger.warning(f"Plex Warning: {item.title} has no Season 1 Episode 1 ") logger.warning(f"Plex Warning: {item.title} has no Season 1 Episode 1 ")
elif method == "plex_search": elif method == "plex_search":
logger.info(data[1]) logger.info(f"Processing {data[1]}")
items = self.get_filter_items(data[2]) items = self.get_filter_items(data[2])
elif method == "plex_collectionless": elif method == "plex_collectionless":
good_collections = [] good_collections = []

Loading…
Cancel
Save