Plex-Meta-Manager/modules/mal.py

269 lines
13 KiB
Python
Raw Normal View History

2022-05-12 19:10:03 +00:00
import re, secrets, time, webbrowser
from json import JSONDecodeError
2021-01-20 21:37:59 +00:00
from modules import util
2022-05-12 19:10:03 +00:00
from modules.util import Failed, TimeoutExpired, YAML
2021-01-20 21:37:59 +00:00
logger = util.logger
2021-01-20 21:37:59 +00:00
2021-03-30 05:50:53 +00:00
builders = [
"mal_id", "mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_ova", "mal_movie", "mal_special",
2022-05-03 18:44:19 +00:00
"mal_popular", "mal_favorite", "mal_season", "mal_suggested", "mal_userlist", "mal_genre", "mal_studio", "mal_search"
2021-03-30 05:50:53 +00:00
]
mal_ranked_name = {
2021-07-23 19:44:21 +00:00
"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"
2021-03-30 05:50:53 +00:00
}
2021-08-01 04:35:42 +00:00
mal_ranked_pretty = {
2022-05-03 18:44:19 +00:00
"mal_all": "MyAnimeList All", "mal_airing": "MyAnimeList Airing", "mal_search": "MyAnimeList Search",
2021-08-01 04:35:42 +00:00
"mal_upcoming": "MyAnimeList Upcoming", "mal_tv": "MyAnimeList TV", "mal_ova": "MyAnimeList OVA",
"mal_movie": "MyAnimeList Movie", "mal_special": "MyAnimeList Special", "mal_popular": "MyAnimeList Popular",
2021-08-17 00:07:35 +00:00
"mal_favorite": "MyAnimeList Favorite", "mal_genre": "MyAnimeList Genre", "mal_studio": "MyAnimeList Studio"
2021-08-01 04:35:42 +00:00
}
2021-07-23 19:44:21 +00:00
season_sort_translation = {"score": "anime_score", "anime_score": "anime_score", "members": "anime_num_list_users", "anime_num_list_users": "anime_num_list_users"}
2021-07-23 18:45:49 +00:00
season_sort_options = ["score", "members"]
2021-03-30 05:50:53 +00:00
pretty_names = {
2021-07-23 19:44:21 +00:00
"anime_score": "Score", "list_score": "Score", "anime_num_list_users": "Members", "list_updated_at": "Last Updated",
"anime_title": "Title", "anime_start_date": "Start Date", "all": "All Anime", "watching": "Currently Watching",
"completed": "Completed", "on_hold": "On Hold", "dropped": "Dropped", "plan_to_watch": "Plan to Watch"
2021-03-30 05:50:53 +00:00
}
2021-07-23 18:45:49 +00:00
userlist_sort_translation = {
2021-07-23 19:44:21 +00:00
"score": "list_score", "list_score": "list_score",
"last_updated": "list_updated_at", "list_updated": "list_updated_at", "list_updated_at": "list_updated_at",
"title": "anime_title", "anime_title": "anime_title",
"start_date": "anime_start_date", "anime_start_date": "anime_start_date"
2021-03-30 05:50:53 +00:00
}
2021-07-23 18:45:49 +00:00
userlist_sort_options = ["score", "last_updated", "title", "start_date"]
2021-07-23 19:44:21 +00:00
userlist_status = ["all", "watching", "completed", "on_hold", "dropped", "plan_to_watch"]
2022-05-03 18:44:19 +00:00
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/"
2022-07-04 06:49:52 +00:00
uni_code_verifier = "k_UHwN_eHAPQVXiceC-rYGkozKqrJmKxPUIUOBIKo1noq_4XGRVCViP_dGcwB-fkPql8f56mmWj5aWCa2HDeugf6sRvnc9Rjhbb1vKGYLY0IwWsDNXRqXdksaVGJthux"
2021-07-14 14:47:20 +00:00
urls = {
2022-05-03 18:44:19 +00:00
"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"
2021-07-14 14:47:20 +00:00
}
2021-07-15 17:42:28 +00:00
2021-06-14 15:24:11 +00:00
class MyAnimeList:
2021-07-26 19:03:17 +00:00
def __init__(self, config, params):
2021-04-15 20:45:35 +00:00
self.config = config
2021-01-20 21:37:59 +00:00
self.client_id = params["client_id"]
self.client_secret = params["client_secret"]
2022-07-04 06:49:52 +00:00
self.localhost_url = params["localhost_url"]
2021-01-20 21:37:59 +00:00
self.config_path = params["config_path"]
2021-07-26 19:03:17 +00:00
self.authorization = params["authorization"]
logger.secret(self.client_secret)
2021-05-07 19:53:54 +00:00
if not self._save(self.authorization):
if not self._refresh():
self._authorization()
2022-06-23 14:10:03 +00:00
self._genres = {}
self._studios = {}
2022-05-03 18:44:19 +00:00
@property
def genres(self):
if not 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"])
2022-06-01 05:57:32 +00:00
self._genres[str(data["mal_id"])] = int(data["mal_id"])
2022-05-03 18:44:19 +00:00
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"])
2022-06-01 05:57:32 +00:00
self._studios[str(data["mal_id"])] = int(data["mal_id"])
2022-05-03 18:44:19 +00:00
self._studios[int(data["mal_id"])] = data["name"]
return self._studios
2021-01-20 21:37:59 +00:00
2021-05-07 19:53:54 +00:00
def _authorization(self):
2022-07-04 06:49:52 +00:00
if self.localhost_url:
code_verifier = uni_code_verifier
url = self.localhost_url
else:
code_verifier = secrets.token_urlsafe(100)[:128]
url = f"{urls['oauth_authorize']}?response_type=code&client_id={self.client_id}&code_challenge={code_verifier}"
logger.info("")
logger.info(f"Navigate to: {url}")
logger.info("")
logger.info("Login and click the Allow option. You will then be redirected to a localhost")
logger.info("url that most likely won't load, which is fine. Copy the URL and paste it below")
webbrowser.open(url, new=2)
try: url = util.logger_input("URL").strip()
except TimeoutExpired: raise Failed("Input Timeout: URL required.")
if not url: raise Failed("MyAnimeList Error: No input MyAnimeList code required.")
2021-01-20 21:37:59 +00:00
match = re.search("code=([^&]+)", str(url))
if not match:
raise Failed("MyAnimeList Error: Invalid URL")
code = match.group(1)
data = {
"client_id": self.client_id,
"client_secret": self.client_secret,
"code": code,
"code_verifier": code_verifier,
"grant_type": "authorization_code"
}
2021-05-07 19:53:54 +00:00
new_authorization = self._oauth(data)
2021-01-20 21:37:59 +00:00
if "error" in new_authorization:
raise Failed("MyAnimeList Error: Invalid code")
2021-05-07 19:53:54 +00:00
if not self._save(new_authorization):
2021-01-20 21:37:59 +00:00
raise Failed("MyAnimeList Error: New Authorization Failed")
2021-05-07 19:53:54 +00:00
def _check(self, authorization):
2021-01-20 21:37:59 +00:00
try:
2021-07-14 14:47:20 +00:00
self._request(urls["suggestions"], authorization=authorization)
2021-01-20 21:37:59 +00:00
return True
except Failed as e:
logger.debug(e)
return False
2021-05-07 19:53:54 +00:00
def _refresh(self):
2021-01-20 21:37:59 +00:00
if self.authorization and "refresh_token" in self.authorization and self.authorization["refresh_token"]:
logger.info("Refreshing Access Token...")
data = {
"client_id": self.client_id,
"client_secret": self.client_secret,
"refresh_token": self.authorization["refresh_token"],
"grant_type": "refresh_token"
}
2021-05-07 19:53:54 +00:00
refreshed_authorization = self._oauth(data)
return self._save(refreshed_authorization)
2021-01-20 21:37:59 +00:00
return False
2021-05-07 19:53:54 +00:00
def _save(self, authorization):
if authorization is not None and "access_token" in authorization and authorization["access_token"] and self._check(authorization):
2021-12-28 00:01:43 +00:00
if self.authorization != authorization and not self.config.read_only:
2022-05-12 19:10:03 +00:00
yaml = YAML(self.config_path)
yaml.data["mal"]["authorization"] = {
2021-01-20 21:37:59 +00:00
"access_token": authorization["access_token"],
"token_type": authorization["token_type"],
"expires_in": authorization["expires_in"],
"refresh_token": authorization["refresh_token"]
}
2021-02-24 06:44:06 +00:00
logger.info(f"Saving authorization information to {self.config_path}")
2022-05-12 19:10:03 +00:00
yaml.save()
2022-05-03 18:44:19 +00:00
logger.secret(authorization["access_token"])
2021-01-20 21:37:59 +00:00
self.authorization = authorization
return True
return False
2021-05-07 19:53:54 +00:00
def _oauth(self, data):
2021-07-14 14:47:20 +00:00
return self.config.post_json(urls["oauth_token"], data=data)
2021-01-20 21:37:59 +00:00
2021-05-07 19:53:54 +00:00
def _request(self, url, authorization=None):
token = authorization["access_token"] if authorization else self.authorization["access_token"]
2022-09-07 19:32:52 +00:00
logger.trace(f"URL: {url}")
try:
response = self.config.get_json(url, headers={"Authorization": f"Bearer {token}"})
2022-09-07 19:32:52 +00:00
logger.trace(f"Response: {response}")
if "error" in response: raise Failed(f"MyAnimeList Error: {response['error']}")
else: return response
except JSONDecodeError:
raise Failed(f"MyAnimeList Error: Connection Failed")
2021-01-20 21:37:59 +00:00
2022-05-03 18:44:19 +00:00
def _jiken_request(self, url, params=None):
data = self.config.get_json(f"{jiken_base_url}{url}", params=params)
time.sleep(2)
return data
2021-05-07 19:53:54 +00:00
def _parse_request(self, url):
data = self._request(url)
2021-03-10 16:51:27 +00:00
return [d["node"]["id"] for d in data["data"]] if "data" in data else []
2021-01-20 21:37:59 +00:00
2021-05-07 19:53:54 +00:00
def _username(self):
2021-07-14 14:47:20 +00:00
return self._request(f"{urls['user']}/@me")["name"]
2021-01-20 21:37:59 +00:00
2021-05-07 19:53:54 +00:00
def _ranked(self, ranking_type, limit):
2021-07-14 14:47:20 +00:00
url = f"{urls['ranking']}?ranking_type={ranking_type}&limit={limit}"
2021-05-07 19:53:54 +00:00
return self._parse_request(url)
2021-01-20 21:37:59 +00:00
2021-05-07 19:53:54 +00:00
def _season(self, season, year, sort_by, limit):
2021-07-14 14:47:20 +00:00
url = f"{urls['season']}/{year}/{season}?sort={sort_by}&limit={limit}"
2021-05-07 19:53:54 +00:00
return self._parse_request(url)
2021-01-20 21:37:59 +00:00
2021-05-07 19:53:54 +00:00
def _suggestions(self, limit):
2021-07-14 14:47:20 +00:00
url = f"{urls['suggestions']}?limit={limit}"
2021-05-07 19:53:54 +00:00
return self._parse_request(url)
2021-01-20 21:37:59 +00:00
2021-05-07 19:53:54 +00:00
def _userlist(self, username, status, sort_by, limit):
2021-02-24 06:44:06 +00:00
final_status = "" if status == "all" else f"status={status}&"
2021-07-14 14:47:20 +00:00
url = f"{urls['user']}/{username}/animelist?{final_status}sort={sort_by}&limit={limit}"
2021-05-07 19:53:54 +00:00
return self._parse_request(url)
2021-01-20 21:37:59 +00:00
2022-05-03 18:44:19 +00:00
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 = []
2021-08-16 05:41:04 +00:00
current_page = 1
chances = 0
2022-05-03 18:44:19 +00:00
while current_page <= last_visible_page:
2021-08-16 05:41:04 +00:00
if chances > 6:
logger.debug(data)
raise Failed("AniList Error: Connection Failed")
2022-05-03 18:44:19 +00:00
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}")
2021-08-16 05:41:04 +00:00
if current_page > 1:
2022-05-03 18:44:19 +00:00
if params is None:
params = {}
params["page"] = current_page
data = self._jiken_request(endpoint, params)
if "data" in data:
2021-08-16 05:41:04 +00:00
chances = 0
2022-05-03 18:44:19 +00:00
mal_ids.extend(data["data"])
if limit and len(mal_ids) >= limit:
2021-08-16 05:41:04 +00:00
return mal_ids[:limit]
current_page += 1
else:
chances += 1
logger.exorcise()
return mal_ids
2021-08-07 06:01:21 +00:00
def get_mal_ids(self, method, data):
2021-01-20 21:37:59 +00:00
if method == "mal_id":
2021-08-01 04:35:42 +00:00
logger.info(f"Processing MyAnimeList ID: {data}")
2021-01-20 21:37:59 +00:00
mal_ids = [data]
2021-03-30 05:50:53 +00:00
elif method in mal_ranked_name:
2021-08-01 04:35:42 +00:00
logger.info(f"Processing {mal_ranked_pretty[method]}: {data} Anime")
2021-05-07 19:53:54 +00:00
mal_ids = self._ranked(mal_ranked_name[method], data)
2022-05-03 18:44:19 +00:00
elif method == "mal_search":
logger.info(f"Processing {data[1]}")
2022-05-09 15:22:41 +00:00
mal_ids = [mal_data["mal_id"] for mal_data in 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']}")
2022-05-09 15:22:41 +00:00
mal_ids = [mal_data["mal_id"] for mal_data in self._pagination("anime", params={"genres": data["genre_id"]}, limit=data["limit"])]
2021-08-17 00:07:35 +00:00
elif method == "mal_studio":
logger.info(f"Processing {mal_ranked_pretty[method]} ID: {data['studio_id']}")
2022-05-09 15:22:41 +00:00
mal_ids = [mal_data["mal_id"] for mal_data in self._pagination("anime", params={"producers": data["studio_id"]}, limit=data["limit"])]
2021-01-20 21:37:59 +00:00
elif method == "mal_season":
2021-08-14 22:59:35 +00:00
logger.info(f"Processing MyAnimeList Season: {data['limit']} Anime from {data['season'].title()} {data['year']} sorted by {pretty_names[data['sort_by']]}")
2021-05-07 19:53:54 +00:00
mal_ids = self._season(data["season"], data["year"], data["sort_by"], data["limit"])
2021-01-20 21:37:59 +00:00
elif method == "mal_suggested":
2021-08-01 04:35:42 +00:00
logger.info(f"Processing MyAnimeList Suggested: {data} Anime")
2021-05-07 19:53:54 +00:00
mal_ids = self._suggestions(data)
2021-01-20 21:37:59 +00:00
elif method == "mal_userlist":
2021-08-01 04:35:42 +00:00
logger.info(f"Processing MyAnimeList Userlist: {data['limit']} Anime from {self._username() if data['username'] == '@me' else data['username']}'s {pretty_names[data['status']]} list sorted by {pretty_names[data['sort_by']]}")
2021-05-07 19:53:54 +00:00
mal_ids = self._userlist(data["username"], data["status"], data["sort_by"], data["limit"])
2021-01-20 21:37:59 +00:00
else:
2021-02-24 06:44:06 +00:00
raise Failed(f"MyAnimeList Error: Method {method} not supported")
2021-05-24 03:38:46 +00:00
logger.debug("")
2021-07-03 01:47:09 +00:00
logger.debug(f"{len(mal_ids)} MyAnimeList IDs Found: {mal_ids}")
2021-08-07 06:01:21 +00:00
return mal_ids