Plex-Meta-Manager/modules/trakt.py

237 lines
11 KiB
Python
Raw Normal View History

2021-01-21 21:42:31 +00:00
import logging, requests, webbrowser
2021-01-20 21:37:59 +00:00
from modules import util
from modules.util import Failed, TimeoutExpired
from ruamel import yaml
logger = logging.getLogger("Plex Meta Manager")
2021-07-15 17:42:28 +00:00
redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
redirect_uri_encoded = redirect_uri.replace(":", "%3A")
base_url = "https://api.trakt.tv"
2021-03-30 05:50:53 +00:00
builders = [
"trakt_collected_daily", "trakt_collected_weekly", "trakt_collected_monthly", "trakt_collected_yearly", "trakt_collected_all",
"trakt_recommended_daily", "trakt_recommended_weekly", "trakt_recommended_monthly", "trakt_recommended_yearly", "trakt_recommended_all",
"trakt_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all",
2021-11-13 23:51:12 +00:00
"trakt_collection", "trakt_list", "trakt_list_details", "trakt_popular", "trakt_trending", "trakt_watchlist", "trakt_boxoffice"
2021-03-30 05:50:53 +00:00
]
2021-07-29 21:36:26 +00:00
sorts = [
"rank", "added", "title", "released", "runtime", "popularity",
"percentage", "votes", "random", "my_rating", "watched", "collected"
]
2021-08-22 15:54:33 +00:00
id_translation = {"movie": "tmdb", "show": "tvdb", "season": "TVDb Season", "episode": "TVDb Episode"}
2021-03-30 05:50:53 +00:00
2021-06-14 15:24:11 +00:00
class Trakt:
2021-07-26 19:03:17 +00:00
def __init__(self, config, params):
2021-07-14 14:47:20 +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"]
self.config_path = params["config_path"]
2021-07-26 19:03:17 +00:00
self.authorization = params["authorization"]
2021-05-07 19:53:54 +00:00
if not self._save(self.authorization):
if not self._refresh():
self._authorization()
2021-01-20 21:37:59 +00:00
2021-05-07 19:53:54 +00:00
def _authorization(self):
2021-07-15 17:42:28 +00:00
url = f"https://trakt.tv/oauth/authorize?response_type=code&client_id={self.client_id}&redirect_uri={redirect_uri_encoded}"
2021-02-24 06:44:06 +00:00
logger.info(f"Navigate to: {url}")
2021-01-20 21:37:59 +00:00
logger.info("If you get an OAuth error your client_id or client_secret is invalid")
webbrowser.open(url, new=2)
try: pin = util.logger_input("Trakt pin (case insensitive)", timeout=300).strip()
except TimeoutExpired: raise Failed("Input Timeout: Trakt pin required.")
if not pin: raise Failed("Trakt Error: No input Trakt pin required.")
2021-07-15 17:42:28 +00:00
json = {
"code": pin,
"client_id": self.client_id,
"client_secret": self.client_secret,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code"
}
response = self.config.post(f"{base_url}/oauth/token", json=json, headers={"Content-Type": "application/json"})
if response.status_code != 200:
2021-01-20 21:37:59 +00:00
raise Failed("Trakt Error: Invalid trakt pin. If you're sure you typed it in correctly your client_id or client_secret may be invalid")
2021-07-15 17:42:28 +00:00
elif not self._save(response.json()):
2021-01-20 21:37:59 +00:00
raise Failed("Trakt Error: New Authorization Failed")
2021-07-15 17:42:28 +00:00
def _check(self, authorization=None):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.authorization['access_token'] if authorization is None else authorization['access_token']}",
"trakt-api-version": "2",
"trakt-api-key": self.client_id
}
response = self.config.get(f"{base_url}/users/settings", headers=headers)
return response.status_code == 200
2021-01-20 21:37:59 +00:00
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...")
2021-07-15 17:42:28 +00:00
json = {
"refresh_token": self.authorization["refresh_token"],
"client_id": self.client_id,
"client_secret": self.client_secret,
"redirect_uri": redirect_uri,
"grant_type": "refresh_token"
}
response = self.config.post(f"{base_url}/oauth/token", json=json, headers={"Content-Type": "application/json"})
if response.status_code != 200:
return False
return self._save(response.json())
2021-01-20 21:37:59 +00:00
return False
2021-05-07 19:53:54 +00:00
def _save(self, authorization):
if authorization and self._check(authorization):
2021-01-20 21:37:59 +00:00
if self.authorization != authorization:
yaml.YAML().allow_duplicate_keys = True
config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path))
config["trakt"]["authorization"] = {
"access_token": authorization["access_token"],
"token_type": authorization["token_type"],
"expires_in": authorization["expires_in"],
"refresh_token": authorization["refresh_token"],
"scope": authorization["scope"],
"created_at": authorization["created_at"]
}
2021-02-24 06:44:06 +00:00
logger.info(f"Saving authorization information to {self.config_path}")
2021-01-20 21:37:59 +00:00
yaml.round_trip_dump(config, open(self.config_path, "w"), indent=ind, block_seq_indent=bsi)
2021-07-15 17:42:28 +00:00
self.authorization = authorization
2021-01-20 21:37:59 +00:00
return True
return False
2021-07-15 17:42:28 +00:00
def _request(self, url):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.authorization['access_token']}",
"trakt-api-version": "2",
"trakt-api-key": self.client_id
}
output_json = []
pages = 1
current = 1
2021-11-03 14:38:43 +00:00
if self.config.trace_mode:
logger.debug(f"URL: {base_url}{url}")
while current <= pages:
if pages == 1:
response = self.config.get(f"{base_url}{url}", headers=headers)
2021-08-05 18:17:52 +00:00
if "X-Pagination-Page-Count" in response.headers and "?" not in url:
pages = int(response.headers["X-Pagination-Page-Count"])
else:
response = self.config.get(f"{base_url}{url}?page={current}", headers=headers)
if response.status_code == 200:
2021-08-03 13:09:00 +00:00
json_data = response.json()
2021-11-03 14:38:43 +00:00
if self.config.trace_mode:
logger.debug(f"Response: {json_data}")
2021-08-03 13:09:00 +00:00
if isinstance(json_data, dict):
return json_data
else:
output_json.extend(response.json())
else:
raise Failed(f"({response.status_code}) {response.reason}")
current += 1
return output_json
def user_ratings(self, is_movie):
media = "movie" if is_movie else "show"
id_type = "tmdb" if is_movie else "tvdb"
return {int(i[media]["ids"][id_type]): i["rating"] for i in self._request(f"/users/me/ratings/{media}s")}
2021-07-15 17:42:28 +00:00
2021-05-08 00:40:07 +00:00
def convert(self, external_id, from_source, to_source, media_type):
2021-07-15 17:42:28 +00:00
path = f"/search/{from_source}/{external_id}"
if from_source in ["tmdb", "tvdb"]:
path = f"{path}?type={media_type}"
lookup = self._request(path)
2021-07-15 17:42:28 +00:00
if lookup and media_type in lookup[0] and to_source in lookup[0][media_type]["ids"]:
return lookup[0][media_type]["ids"][to_source]
2021-05-08 04:05:10 +00:00
raise Failed(f"Trakt Error: No {to_source.upper().replace('B', 'b')} ID found for {from_source.upper().replace('B', 'b')} ID: {external_id}")
2021-01-20 21:37:59 +00:00
2021-07-15 17:42:28 +00:00
def list_description(self, data):
try:
return self._request(requests.utils.urlparse(data).path)["description"]
2021-07-15 17:42:28 +00:00
except Failed:
raise Failed(f"Trakt Error: List {data} not found")
2021-03-30 05:49:10 +00:00
2021-08-22 15:54:33 +00:00
def _parse(self, items, typeless=False, item_type=None):
2021-08-04 13:42:21 +00:00
ids = []
for item in items:
2021-08-22 15:54:33 +00:00
if typeless:
2021-08-07 06:01:21 +00:00
data = item
2021-08-22 15:54:33 +00:00
current_type = None
elif item_type:
data = item[item_type]
current_type = item_type
elif "type" in item and item["type"] in id_translation:
data = item["movie" if item["type"] == "movie" else "show"]
current_type = item["type"]
2021-08-07 06:01:21 +00:00
else:
2021-08-22 15:54:33 +00:00
continue
2021-08-23 21:46:29 +00:00
id_type = "tmdb" if current_type == "movie" else "tvdb"
2021-09-18 02:07:30 +00:00
if id_type in data["ids"] and data["ids"][id_type]:
2021-08-22 15:54:33 +00:00
final_id = data["ids"][id_type]
if current_type == "episode":
final_id = f"{final_id}_{item[current_type]['season']}"
if current_type in ["episode", "season"]:
final_id = f"{final_id}_{item[current_type]['number']}"
final_type = f"{id_type}_{current_type}" if current_type in ["episode", "season"] else id_type
ids.append((final_id, final_type))
2021-08-04 13:42:21 +00:00
else:
2021-08-22 15:54:33 +00:00
logger.error(f"Trakt Error: No {id_type.upper().replace('B', 'b')} ID found for {data['title']} ({data['year']})")
2021-08-07 06:01:21 +00:00
return ids
2021-08-04 13:42:21 +00:00
2021-08-07 06:01:21 +00:00
def _user_list(self, data):
2021-07-15 17:42:28 +00:00
try:
2021-08-07 06:01:21 +00:00
items = self._request(f"{requests.utils.urlparse(data).path}/items")
2021-07-15 17:42:28 +00:00
except Failed:
2021-08-07 06:01:21 +00:00
raise Failed(f"Trakt Error: List {data} not found")
2021-07-15 17:42:28 +00:00
if len(items) == 0:
2021-08-07 06:01:21 +00:00
raise Failed(f"Trakt Error: List {data} is empty")
return self._parse(items)
def _user_items(self, list_type, data, is_movie):
try:
items = self._request(f"/users/{data}/{list_type}/{'movies' if is_movie else 'shows'}")
except Failed:
raise Failed(f"Trakt Error: User {data} not found")
if len(items) == 0:
raise Failed(f"Trakt Error: {data}'s {list_type.capitalize()} is empty")
return self._parse(items, item_type="movie" if is_movie else "show")
2021-03-27 07:30:07 +00:00
2021-05-07 19:53:54 +00:00
def _pagenation(self, pagenation, amount, is_movie):
items = self._request(f"/{'movies' if is_movie else 'shows'}/{pagenation}?limit={amount}")
2021-08-22 15:54:33 +00:00
return self._parse(items, typeless=pagenation == "popular", item_type="movie" if is_movie else "show")
2021-07-21 17:40:05 +00:00
def validate_trakt(self, trakt_lists, is_movie, trakt_type="list"):
2021-07-29 21:36:26 +00:00
values = util.get_list(trakt_lists, split=False)
2021-01-20 21:37:59 +00:00
trakt_values = []
for value in values:
try:
2021-08-07 06:01:21 +00:00
if trakt_type == "list":
self._user_list(value)
else:
self._user_items(trakt_type, value, is_movie)
2021-01-20 21:37:59 +00:00
trakt_values.append(value)
except Failed as e:
logger.error(e)
if len(trakt_values) == 0:
2021-07-15 17:42:28 +00:00
if trakt_type == "watchlist":
2021-03-27 07:30:07 +00:00
raise Failed(f"Trakt Error: No valid Trakt Watchlists in {values}")
2021-07-15 17:42:28 +00:00
elif trakt_type == "collection":
2021-03-27 07:30:07 +00:00
raise Failed(f"Trakt Error: No valid Trakt Collections in {values}")
else:
raise Failed(f"Trakt Error: No valid Trakt Lists in {values}")
2021-01-20 21:37:59 +00:00
return trakt_values
2021-08-07 06:01:21 +00:00
def get_trakt_ids(self, method, data, is_movie):
2021-08-01 04:35:42 +00:00
pretty = method.replace("_", " ").title()
2021-01-20 21:37:59 +00:00
media_type = "Movie" if is_movie else "Show"
2021-11-13 23:51:12 +00:00
if method in ["trakt_collection", "trakt_watchlist"]:
2021-05-09 05:37:45 +00:00
logger.info(f"Processing {pretty} {media_type}s for {data}")
2021-08-07 06:01:21 +00:00
return self._user_items(method[6:], data, is_movie)
2021-08-04 13:42:21 +00:00
elif method == "trakt_list":
2021-05-09 05:37:45 +00:00
logger.info(f"Processing {pretty}: {data}")
2021-08-07 06:01:21 +00:00
return self._user_list(data)
2021-11-13 23:51:12 +00:00
elif method in builders:
logger.info(f"Processing {pretty}: {data} {media_type}{'' if data == 1 else 's'}")
terms = method.split("_")
return self._pagenation(f"{terms[1]}{f'/{terms[2]}' if len(terms) > 2 else ''}", data, is_movie)
2021-07-15 17:42:28 +00:00
else:
raise Failed(f"Trakt Error: Method {method} not supported")