Added AniList Builders

pull/102/head
meisnate12 4 years ago
parent 1db527df57
commit 0525f78f0f

@ -0,0 +1,210 @@
import logging, requests, time
from modules import util
from modules.util import Failed
from retrying import retry
logger = logging.getLogger("Plex Meta Manager")
class AniListAPI:
def __init__(self, config):
self.config = config
self.url = "https://graphql.anilist.co"
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def post(self, query, variables):
return requests.post(self.url, json={"query": query, "variables": variables})
@retry(stop_max_attempt_number=2, retry_on_exception=util.retry_if_not_failed)
def send_request(self, query, variables):
response = self.post(query, variables)
json_obj = response.json()
if "errors" in json_obj:
if json_obj['errors'][0]['message'] == "Too Many Requests.":
if "Retry-After" in response.headers:
time.sleep(int(response.headers["Retry-After"]))
raise ValueError
else:
raise Failed(f"AniList Error: {json_obj['errors'][0]['message']}")
else:
time.sleep(0.4)
return json_obj
def anilist_id(self, anilist_id):
query = "query ($id: Int) {Media(id: $id) {idMal title{romaji english}}}"
media = self.send_request(query, {"id": anilist_id})["data"]["Media"]
if media["idMal"]:
return media["idMal"], media["title"]["english" if media["title"]["english"] else "romaji"]
raise Failed(f"AniList Error: No MyAnimeList ID found for {anilist_id}")
def get_pagenation(self, query, limit=0, variables=None):
mal_ids = []
count = 0
page_num = 0
if variables is None:
variables = {"page": page_num}
else:
variables["page"] = page_num
next_page = True
while next_page:
page_num += 1
json_obj = self.send_request(query, variables)
next_page = json_obj["data"]["Page"]["pageInfo"]["hasNextPage"]
for media in json_obj["data"]["Page"]["media"]:
if media["idMal"]:
mal_ids.append(media["idMal"])
count += 1
if 0 < limit == count:
break
if 0 < limit == count:
break
return mal_ids
def top_rated(self, limit):
query = """
query ($page: Int) {
Page(page: $page) {
pageInfo {hasNextPage}
media(averageScore_greater: 3, sort: SCORE_DESC, type: ANIME) {idMal}
}
}
"""
return self.get_pagenation(query, limit=limit)
def popular(self, limit):
query = """
query ($page: Int) {
Page(page: $page) {
pageInfo {hasNextPage}
media(popularity_greater: 1000, sort: POPULARITY_DESC, type: ANIME) {idMal}
}
}
"""
return self.get_pagenation(query, limit=limit)
def season(self, season, year, sort, limit):
query = """
query ($page: Int, $season: String, $year: Int, $sort: String) {
Page(page: $page){
pageInfo {hasNextPage}
media(season: $season, seasonYear: $year, type: ANIME, sort: $sort){idMal}
}
}
"""
variables = {"season": season.upper(), "year": year, "sort": "SCORE_DESC" if sort == "score" else "POPULARITY_DESC"}
return self.get_pagenation(query, limit=limit, variables=variables)
def studio(self, studio_id):
query = """
query ($page: Int, $id: Int) {
Studio(id: $id) {
name
media(page: $page) {
nodes {idMal type}
pageInfo {hasNextPage}
}
}
}
"""
mal_ids = []
page_num = 0
next_page = True
name = None
while next_page:
page_num += 1
json_obj = self.send_request(query, {"id": studio_id, "page": page_num})
if not name:
name = json_obj["data"]["Studio"]["name"]
next_page = json_obj["data"]["Studio"]["media"]["pageInfo"]["hasNextPage"]
for media in json_obj["data"]["Studio"]["media"]["nodes"]:
if media["idMal"] and media["type"] == "ANIME":
mal_ids.append(media["idMal"])
return mal_ids, name
def relations(self, anilist_id, ignore_ids=None):
query = """
query ($id: Int) {
Media(id: $id) {
idMal
relations {
edges {node{id idMal type} relationType}
nodes {id idMal type}
}
}
}
"""
anilist_ids = []
mal_ids = []
name = ""
if not ignore_ids:
ignore_ids = [anilist_id]
mal_id, name = self.anilist_id(anilist_id)
mal_ids.append(mal_id)
json_obj = self.send_request(query, {"id": anilist_id})
edges = [media["node"]["id"] for media in json_obj["data"]["Media"]["relations"]["edges"]
if media["relationType"] not in ["CHARACTER", "OTHER"] and media["node"]["type"] == "ANIME"]
for media in json_obj["data"]["Media"]["relations"]["nodes"]:
if media["idMal"] and media["id"] not in ignore_ids and media["id"] in edges and media["type"] == "ANIME":
anilist_ids.append(media["id"])
ignore_ids.append(media["id"])
mal_ids.append(media["idMal"])
for next_id in anilist_ids:
new_mal_ids, ignore_ids, _ = self.relations(next_id, ignore_ids=ignore_ids)
mal_ids.extend(new_mal_ids)
return mal_ids, ignore_ids, name
def validate_anilist_ids(self, anilist_ids, studio=False):
anilist_values = []
for anilist_id in anilist_ids:
if studio: query = "query ($id: Int) {Studio(id: $id) {name}}"
else: query = "query ($id: Int) {Media(id: $id) {idMal}}"
try:
self.send_request(query, {"id": anilist_id})
anilist_values.append(anilist_id)
except Failed as e: logger.error(e)
if len(anilist_values) > 0:
return anilist_values
raise Failed(f"AniList Error: No valid AniList IDs in {anilist_ids}")
def get_items(self, method, data, status_message=True):
if status_message:
logger.debug(f"Data: {data}")
pretty = util.pretty_names[method] if method in util.pretty_names else method
if method == "anilist_id":
mal_id, name = self.anilist_id(data)
mal_ids = [mal_id]
if status_message:
logger.info(f"Processing {pretty}: ({data}) {name}")
elif method in ["anilist_popular", "anilist_top_rated"]:
mal_ids = self.popular(data) if method == "anilist_popular" else self.top_rated(data)
if status_message:
logger.info(f"Processing {pretty}: {data} Anime")
elif method == "anilist_season":
mal_ids = self.season(data["season"], data["year"], data["sort_by"], data["limit"])
if status_message:
logger.info(f"Processing {pretty}: {data['limit']} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {util.anilist_pretty[data['sort_by']]}")
elif method in ["anilist_studio", "anilist_relations"]:
if method == "anilist_studio": mal_ids, name = self.studio(data)
else: mal_ids, _, name = self.relations(data)
if status_message:
logger.info(f"Processing {pretty}: ({data}) {name} ({len(mal_ids)} Anime)")
else:
raise Failed(f"AniList Error: Method {method} not supported")
show_ids = []
movie_ids = []
for mal_id in mal_ids:
try:
ids = self.config.MyAnimeListIDList.find_mal_ids(mal_id)
if "thetvdb_id" in ids and int(ids["thetvdb_id"]) > 0: show_ids.append(int(ids["thetvdb_id"]))
elif "themoviedb_id" in ids and int(ids["themoviedb_id"]) > 0: movie_ids.append(int(ids["themoviedb_id"]))
else: raise Failed(f"MyAnimeList Error: MyAnimeList ID: {mal_id} has no other IDs associated with it")
except Failed as e:
if status_message:
logger.error(e)
if status_message:
logger.debug(f"MyAnimeList IDs Found: {mal_ids}")
logger.debug(f"Shows Found: {show_ids}")
logger.debug(f"Movies Found: {movie_ids}")
return movie_ids, show_ids

@ -325,6 +325,8 @@ class CollectionBuilder:
self.methods.append((method_name, util.get_int_list(data[m], "MyAnimeList ID"))) self.methods.append((method_name, util.get_int_list(data[m], "MyAnimeList ID")))
elif method_name in ["anidb_id", "anidb_relation"]: elif method_name in ["anidb_id", "anidb_relation"]:
self.methods.append((method_name, config.AniDB.validate_anidb_list(util.get_int_list(data[m], "AniDB ID"), self.library.Plex.language))) self.methods.append((method_name, config.AniDB.validate_anidb_list(util.get_int_list(data[m], "AniDB ID"), self.library.Plex.language)))
elif method_name in ["anilist_id", "anilist_relations", "anilist_studio"]:
self.methods.append((method_name, config.AniList.validate_anilist_ids(util.get_int_list(data[m], "AniList ID"), studio=method_name == "anilist_studio")))
elif method_name == "trakt_list": elif method_name == "trakt_list":
self.methods.append((method_name, config.Trakt.validate_trakt_list(util.get_list(data[m])))) self.methods.append((method_name, config.Trakt.validate_trakt_list(util.get_list(data[m]))))
elif method_name == "trakt_list_details": elif method_name == "trakt_list_details":
@ -542,6 +544,26 @@ class CollectionBuilder:
new_dictionary["limit"] = get_int(method_name, "limit", data[m], 100, maximum=1000) new_dictionary["limit"] = get_int(method_name, "limit", data[m], 100, maximum=1000)
self.methods.append((method_name, [new_dictionary])) self.methods.append((method_name, [new_dictionary]))
elif method_name == "anilist_season":
new_dictionary = {"sort_by": "score"}
if "sort_by" not in data[m]: logger.warning("Collection Warning: anilist_season sort_by attribute not found using score as default")
elif not data[m]["sort_by"]: logger.warning("Collection Warning: anilist_season sort_by attribute is blank using score as default")
elif data[m]["sort_by"] not in ["score", "popular"]: logger.warning(f"Collection Warning: anilist_season sort_by attribute {data[m]['sort_by']} invalid must be either 'score' or 'popular' using score as default")
else: new_dictionary["sort_by"] = data[m]["sort_by"]
if current_time.month in [12, 1, 2]: new_dictionary["season"] = "winter"
elif current_time.month in [3, 4, 5]: new_dictionary["season"] = "spring"
elif current_time.month in [6, 7, 8]: new_dictionary["season"] = "summer"
elif current_time.month in [9, 10, 11]: new_dictionary["season"] = "fall"
if "season" not in data[m]: logger.warning(f"Collection Warning: anilist_season season attribute not found using the current season: {new_dictionary['season']} as default")
elif not data[m]["season"]: logger.warning(f"Collection Warning: anilist_season season attribute is blank using the current season: {new_dictionary['season']} as default")
elif data[m]["season"] not in util.pretty_seasons: logger.warning(f"Collection Warning: anilist_season season attribute {data[m]['season']} invalid must be either 'winter', 'spring', 'summer' or 'fall' using the current season: {new_dictionary['season']} as default")
else: new_dictionary["season"] = data[m]["season"]
new_dictionary["year"] = get_int(method_name, "year", data[m], current_time.year, minimum=1917, maximum=current_time.year + 1)
new_dictionary["limit"] = get_int(method_name, "limit", data[m], 100, maximum=500)
self.methods.append((method_name, [new_dictionary]))
else: else:
raise Failed(f"Collection Error: {m} attribute is not a dictionary: {data[m]}") raise Failed(f"Collection Error: {m} attribute is not a dictionary: {data[m]}")
elif method_name in util.count_lists: elif method_name in util.count_lists:

@ -1,6 +1,7 @@
import logging, os, re, requests, time import logging, os, re, requests, time
from modules import util from modules import util
from modules.anidb import AniDBAPI from modules.anidb import AniDBAPI
from modules.anilist import AniListAPI
from modules.builder import CollectionBuilder from modules.builder import CollectionBuilder
from modules.cache import Cache from modules.cache import Cache
from modules.imdb import IMDbAPI from modules.imdb import IMDbAPI
@ -227,6 +228,7 @@ class Config:
self.TVDb = TVDbAPI(self) self.TVDb = TVDbAPI(self)
self.IMDb = IMDbAPI(self) self.IMDb = IMDbAPI(self)
self.AniDB = AniDBAPI(self) self.AniDB = AniDBAPI(self)
self.AniList = AniListAPI(self)
self.Letterboxd = LetterboxdAPI() self.Letterboxd = LetterboxdAPI()
util.separator() util.separator()

@ -1,4 +1,4 @@
import logging, math, re, requests import logging, requests
from lxml import html from lxml import html
from modules import util from modules import util
from modules.util import Failed from modules.util import Failed

@ -123,32 +123,29 @@ class MyAnimeListAPI:
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 parse_mal_ids(self, data): def request_and_parse_mal_ids(self, url):
mal_ids = [] data = self.send_request(url)
if "data" in data: return [d["node"]["id"] for d in data["data"]] if "data" in data else []
for d in data["data"]:
mal_ids.append(d["node"]["id"])
return mal_ids
def get_username(self): def get_username(self):
return self.send_request(f"{self.urls['user']}/@me")["name"] return self.send_request(f"{self.urls['user']}/@me")["name"]
def get_ranked(self, ranking_type, limit): def get_ranked(self, ranking_type, limit):
url = f"{self.urls['ranking']}?ranking_type={ranking_type}&limit={limit}" url = f"{self.urls['ranking']}?ranking_type={ranking_type}&limit={limit}"
return self.parse_mal_ids(self.send_request(url)) return self.request_and_parse_mal_ids(url)
def get_season(self, season, year, sort_by, limit): def get_season(self, season, year, sort_by, limit):
url = f"{self.urls['season']}/{year}/{season}?sort={sort_by}&limit={limit}" url = f"{self.urls['season']}/{year}/{season}?sort={sort_by}&limit={limit}"
return self.parse_mal_ids(self.send_request(url)) return self.request_and_parse_mal_ids(url)
def get_suggestions(self, limit): def get_suggestions(self, limit):
url = f"{self.urls['suggestions']}?limit={limit}" url = f"{self.urls['suggestions']}?limit={limit}"
return self.parse_mal_ids(self.send_request(url)) return self.request_and_parse_mal_ids(url)
def get_userlist(self, username, status, sort_by, limit): def get_userlist(self, username, status, sort_by, limit):
final_status = "" if status == "all" else f"status={status}&" final_status = "" if status == "all" else f"status={status}&"
url = f"{self.urls['user']}/{username}/animelist?{final_status}sort={sort_by}&limit={limit}" url = f"{self.urls['user']}/{username}/animelist?{final_status}sort={sort_by}&limit={limit}"
return self.parse_mal_ids(self.send_request(url)) return self.request_and_parse_mal_ids(url)
def get_items(self, method, data, status_message=True): def get_items(self, method, data, status_message=True):
if status_message: if status_message:

@ -1,5 +1,4 @@
import logging, math, re, requests import logging, requests
from lxml import html
from modules import util from modules import util
from modules.util import Failed from modules.util import Failed
from retrying import retry from retrying import retry

@ -95,6 +95,12 @@ pretty_names = {
"anidb_id": "AniDB ID", "anidb_id": "AniDB ID",
"anidb_relation": "AniDB Relation", "anidb_relation": "AniDB Relation",
"anidb_popular": "AniDB Popular", "anidb_popular": "AniDB Popular",
"anilist_id": "AniList ID",
"anilist_popular": "AniList Popular",
"anilist_relations": "AniList Relations",
"anilist_season": "AniList Season",
"anilist_studio": "AniList Studio",
"anilist_top_rated": "AniList Top Rated",
"imdb_list": "IMDb List", "imdb_list": "IMDb List",
"imdb_id": "IMDb ID", "imdb_id": "IMDb ID",
"letterboxd_list": "Letterboxd List", "letterboxd_list": "Letterboxd List",
@ -211,6 +217,10 @@ mal_userlist_status = [
"dropped", "dropped",
"plan_to_watch" "plan_to_watch"
] ]
anilist_pretty = {
"score": "Average Score",
"popular": "Popularity"
}
pretty_ids = { pretty_ids = {
"anidbid": "AniDB", "anidbid": "AniDB",
"imdbid": "IMDb", "imdbid": "IMDb",
@ -223,6 +233,12 @@ all_lists = [
"anidb_id", "anidb_id",
"anidb_relation", "anidb_relation",
"anidb_popular", "anidb_popular",
"anilist_id",
"anilist_popular",
"anilist_relations",
"anilist_season",
"anilist_studio",
"anilist_top_rated",
"imdb_list", "imdb_list",
"imdb_id", "imdb_id",
"letterboxd_list", "letterboxd_list",
@ -305,6 +321,7 @@ other_attributes = [
] ]
dictionary_lists = [ dictionary_lists = [
"filters", "filters",
"anilist_season",
"mal_season", "mal_season",
"mal_userlist", "mal_userlist",
"plex_collectionless", "plex_collectionless",
@ -359,6 +376,8 @@ tmdb_searches = [
] ]
count_lists = [ count_lists = [
"anidb_popular", "anidb_popular",
"anilist_popular",
"anilist_top_rated",
"mal_all", "mal_all",
"mal_airing", "mal_airing",
"mal_upcoming", "mal_upcoming",

Loading…
Cancel
Save