def _update_status(self): status = self.debrid.torrent_info(self.transfer_info["id"]) downloading_status = [ "queued", "downloading", "compressing", "magnet_conversion", "waiting_files_selection", ] if "error" in status or status.get("status", "") in ["", "magnet_error"]: g.log("Failure to create cache: {} - {}".format( self.debrid_readable, status)) raise FailureAtRemoteParty(status["error"]) if status["status"] == "waiting_files_selection": self._select_files() if status["status"] in downloading_status: self.status = "downloading" elif status["status"] == "downloaded": self.status = "finished" else: g.log("invalid status: {}".format(status["status"])) self.status = "failed" self.seeds = status.get("seeders", 0) self.download_speed = status.get("speed", 0) self.previous_percent = self.current_percent self.current_percent = tools.safe_round(status["progress"], 2)
def get_remaining_time_display(self): """ Returns a display friendly version of the remaining time :return: String """ seconds = self.remaining_seconds categories = ["s", "m", "h"] for i in categories: if seconds / 60 < 1: return "{}{}".format(tools.safe_round(seconds, 1), i) else: seconds = seconds / 60
def get_display_speed(self): """ Returns a display friendly version of the current speed :return: String eg: (125.54 KB/s) """ speed = self.download_speed speed_categories = ["B/s", "KB/s", "MB/s"] for i in speed_categories: if speed / 1024 < 1: return "{} {}".format(tools.safe_round(speed, 2), i) else: speed = speed / 1024
def get_display_speed(self): """ Returns a display friendly version of the current speed :return: String """ speed = self.speed speed_categories = ["B/s", "KB/s", "MB/s"] if self.progress >= 100: return "-" for i in speed_categories: if speed / 1024 < 1: return "{} {}".format(tools.safe_round(speed, 2), i) else: speed = speed / 1024
def _update_progress(self, offset=None): self.current_time = self.getTime() if not self.total_time: return 0 self.watched_percentage = 0 if offset is not None: self.current_time += offset if self.total_time > 0: try: self.watched_percentage = tools.safe_round( float(self.current_time) / float(self.total_time) * 100, 2) if self.watched_percentage > 100: self.watched_percentage = 100 except TypeError: pass
def _update_status(self): status = self.debrid.magnet_status(self.transfer_id)["magnets"] if status["status"] == "Downloading": self.status = "downloading" elif status["status"] == "Ready": self.status = "finished" else: self.status = "failed" self.previous_percent = self.current_percent self.seeds = status["seeders"] self.download_speed = status["downloadSpeed"] total_size = status["size"] downloaded = status["downloaded"] if downloaded > 0: self.current_percent = tools.safe_round( (float(downloaded) / total_size) * 100, 2)
def _update_status(self): transfer_status = [ i for i in self.debrid.list_transfers()["transfers"] if i["id"] == self.transfer_id ][0] self.status = "downloading" if transfer_status["status"] == "running" else transfer_status["status"] self.previous_percent = self.current_percent self.current_percent = tools.safe_round(transfer_status["progress"] * 100, 2) if transfer_status["message"]: message = re.findall( r"(\d+\.\d+\s+[a-zA-Z]{1,2}/s)\s+from\s+(\d+)", transfer_status["message"] ) try: self.download_speed = message[0][0].replace('\\', '') self.seeds = message[0][1] except IndexError: self.download_speed = "0.00 B/s" self.seeds = "0"
def _update_status(self): transfer_status = [ i for i in self.debrid.list_transfers()["transfers"] if i["id"] == self.transfer_id ][0] if transfer_status["status"] == "running": transfer_status["status"] = "downloading" self.previous_percent = self.current_percent self.current_percent = tools.safe_round( transfer_status["progress"] * 100, 2) message = re.findall(r"(\d*.\d* .{1,2}\/s) from (\d*)", transfer_status["message"]) try: self.download_speed = message[0][0] self.seeds = message[0][1] except IndexError: self.download_speed = "0.00 B/s" self.seeds = "0" self.status = transfer_status["status"]
class TMDBAPI(ApiBase): baseUrl = "https://api.themoviedb.org/3/" imageBaseUrl = "https://image.tmdb.org/t/p/" normalization = [ ("overview", ("plot", "overview", "plotoutline"), None), ("release_date", ("premiered", "aired"), lambda t: tools.validate_date(t)), ( "keywords", "tag", lambda t: sorted(OrderedDict.fromkeys(v["name"] for v in t["keywords"])), ), ( "genres", "genre", lambda t: sorted( OrderedDict.fromkeys(x.strip() for v in t for x in v["name"].split("&")) ), ), ("certification", "mpaa", None), ("imdb_id", ("imdbnumber", "imdb_id"), None), (("external_ids", "imdb_id"), ("imdbnumber", "imdb_id"), None), ("show_id", "tmdb_show_id", None), ("id", "tmdb_id", None), ("network", "studio", None), ("runtime", "duration", lambda d: d * 60), ( None, "rating.tmdb", ( ("vote_average", "vote_count"), lambda a, c: {"rating": tools.safe_round(a, 2), "votes": c}, ), ), ("tagline", "tagline", None), ("status", "status", None), ("trailer", "trailer", None), ("belongs_to_collection", "set", lambda t: t.get("name") if t else None), ( "production_companies", "studio", lambda t: sorted( OrderedDict.fromkeys(v["name"] if "name" in v else v for v in t) ), ), ( "production_countries", "country", lambda t: sorted( OrderedDict.fromkeys(v["name"] if "name" in v else v for v in t) ), ), ("aliases", "aliases", None), ("mediatype", "mediatype", None), ] show_normalization = tools.extend_array( [ ("name", ("tite", "tvshowtitle", "sorttitle"), None), ("original_name", "originaltitle", None), ( "first_air_date", "year", lambda t: tools.validate_date(t)[:4] if tools.validate_date(t) else None, ), ( "networks", "studio", lambda t: sorted(OrderedDict.fromkeys(v["name"] for v in t)), ), ( "origin_country", "studio", lambda t: sorted( OrderedDict.fromkeys(v["name"] if "name" in v else v for v in t) ), ), ( ("credits", "crew"), "director", lambda t: sorted( OrderedDict.fromkeys( v["name"] if "name" in v else v for v in t if v.get("job") == "Director" ) ), ), ( ("credits", "crew"), "writer", lambda t: sorted( OrderedDict.fromkeys( v["name"] if "name" in v else v for v in t if v.get("department") == "Writing" ) ), ), (("external_ids", "tvdb_id"), "tvdb_id", None), ( "origin_country", "country_origin", lambda t: t[0].upper() if t is not None and t[0] is not None else None, ), ], normalization, ) season_normalization = tools.extend_array( [ ("name", ("title", "sorttitle"), None), ("season_number", ("season", "sortseason"), None), ("episodes", "episode_count", lambda t: len(t) if t is not None else None), ( ("credits", "crew"), "director", lambda t: sorted( OrderedDict.fromkeys( v["name"] if "name" in v else v for v in t if v.get("job") == "Director" ) ), ), ( ("credits", "crew"), "writer", lambda t: sorted( OrderedDict.fromkeys( v["name"] if "name" in v else v for v in t if v.get("department") == "Writing" ) ), ), (("external_ids", "tvdb_id"), "tvdb_id", None), ], normalization, ) episode_normalization = tools.extend_array( [ ("name", ("title", "sorttitle"), None), ("episode_number", ("episode", "sortepisode"), None), ("season_number", ("season", "sortseason"), None), ( "crew", "director", lambda t: sorted( OrderedDict.fromkeys( v["name"] for v in t if v.get("job") == "Director" ) ), ), ( "crew", "writer", lambda t: sorted( OrderedDict.fromkeys( v["name"] for v in t if v.get("department") == "Writing" ) ), ), ], normalization, ) movie_normalization = tools.extend_array( [ ("title", ("title", "sorttitle"), None), ("original_title", "originaltitle", None), ("premiered", "year", lambda t: t[:4]), ], normalization, ) meta_objects = { "movie": movie_normalization, "tvshow": show_normalization, "season": season_normalization, "episode": episode_normalization, } http_codes = { 200: "Success", 201: "Success - new resource created (POST)", 401: "Invalid API key: You must be granted a valid key.", 404: "The resource you requested could not be found.", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", } session = requests.Session() retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504]) session.mount("https://", HTTPAdapter(max_retries=retries)) append_to_response = [ "credits", "images", "release_dates", "content_ratings", "external_ids", "movie_credits", "tv_credits", "videos", "alternative_titles", ] def __init__(self): self.apiKey = g.get_setting("tmdb.apikey", "9f3ca569aa46b6fb13931ec96ab8ae7e") self.lang_code = g.get_language_code() self.lang_full_code = g.get_language_code(True) self.lang_region_code = self.lang_full_code.split("-")[1] if self.lang_region_code == "": self.lang_full_code = self.lang_full_code.strip("-") self.include_languages = OrderedDict.fromkeys([self.lang_code, "en", "null"]) self.preferred_artwork_size = g.get_int_setting("artwork.preferredsize") self.artwork_size = {} self._set_artwork() self.art_normalization = [ ("backdrops", "fanart", None), ( "posters", "poster", lambda x: x["iso_639_1"] != "xx" and x["iso_639_1"] is not None, ), ( "posters", "keyart", lambda x: x["iso_639_1"] == "xx" or x["iso_639_1"] is None, ), ("stills", "fanart", None), ] self.meta_hash = tools.md5_hash( ( self.lang_code, self.lang_full_code, self.lang_region_code, self.include_languages, self.preferred_artwork_size, self.append_to_response, self.baseUrl, self.imageBaseUrl, ) ) def _set_artwork(self): if self.preferred_artwork_size == 0: self.artwork_size["fanart"] = 2160 self.artwork_size["poster"] = 780 self.artwork_size["keyart"] = 780 self.artwork_size["thumb"] = 780 self.artwork_size["icon"] = 780 self.artwork_size["cast"] = 780 elif self.preferred_artwork_size == 1: self.artwork_size["fanart"] = 1280 self.artwork_size["poster"] = 500 self.artwork_size["keyart"] = 500 self.artwork_size["thumb"] = 500 self.artwork_size["icon"] = 342 self.artwork_size["cast"] = 500 elif self.preferred_artwork_size == 2: self.artwork_size["fanart"] = 780 self.artwork_size["poster"] = 342 self.artwork_size["keyart"] = 342 self.artwork_size["thumb"] = 300 self.artwork_size["icon"] = 185 self.artwork_size["cast"] = 342 @tmdb_guard_response def get(self, url, **params): return requests.get( tools.urljoin(self.baseUrl, url), params=self._add_api_key(params), headers={"Accept": "application/json"}, timeout=3, ) def get_json(self, url, **params): response = self.get(url, **params) if response is None: return None return self._handle_response(response.json()) @wrap_tmdb_object def get_movie(self, tmdb_id): return self.get_json( "movie/{}".format(tmdb_id), language=self.lang_full_code, append_to_response=",".join(self.append_to_response), include_image_language=",".join(self.include_languages), region=self.lang_region_code, ) @wrap_tmdb_object def get_movie_rating(self, tmdb_id): result = tools.filter_dictionary( tools.safe_dict_get(self.get_json("movie/{}".format(tmdb_id)), "info"), "rating", ) return {"info": result} if result else None @wrap_tmdb_object def get_movie_cast(self, tmdb_id): result = tools.safe_dict_get( self.get_json("movie/{}/credits".format(tmdb_id)), "cast" ) return {"cast": result} if result else None @wrap_tmdb_object def get_movie_art(self, tmdb_id): return self.get_json( "movie/{}/images".format(tmdb_id), include_image_language=",".join(self.include_languages), ) @wrap_tmdb_object def get_show(self, tmdb_id): return self.get_json( "tv/{}".format(tmdb_id), language=self.lang_full_code, append_to_response=",".join(self.append_to_response), include_image_language=",".join(self.include_languages), region=self.lang_region_code, ) @wrap_tmdb_object def get_show_art(self, tmdb_id): return self.get_json( "tv/{}/images".format(tmdb_id), include_image_language=",".join(self.include_languages), ) @wrap_tmdb_object def get_show_rating(self, tmdb_id): result = tools.filter_dictionary( tools.safe_dict_get(self.get_json("tv/{}".format(tmdb_id)), "info"), "rating", ) return {"info": result} if result else None @wrap_tmdb_object def get_show_cast(self, tmdb_id): result = tools.safe_dict_get( self.get_json("tv/{}/credits".format(tmdb_id)), "cast" ) return {"cast": result} if result else None @wrap_tmdb_object def get_season(self, tmdb_id, season): return self.get_json( "tv/{}/season/{}".format(tmdb_id, season), language=self.lang_full_code, append_to_response=",".join(self.append_to_response), include_image_language=",".join(self.include_languages), region=self.lang_region_code, ) @wrap_tmdb_object def get_season_art(self, tmdb_id, season): return self.get_json( "tv/{}/season/{}/images".format(tmdb_id, season), include_image_language=",".join(self.include_languages), ) @wrap_tmdb_object def get_episode(self, tmdb_id, season, episode): return self.get_json( "tv/{}/season/{}/episode/{}".format(tmdb_id, season, episode), language=self.lang_full_code, append_to_response=",".join(self.append_to_response), include_image_language=",".join(self.include_languages), region=self.lang_region_code, ) @wrap_tmdb_object def get_episode_art(self, tmdb_id, season, episode): return self.get_json( "tv/{}/season/{}/episode/{}/images".format(tmdb_id, season, episode), include_image_language=",".join(self.include_languages), ) @wrap_tmdb_object def get_episode_rating(self, tmdb_id, season, episode): result = tools.filter_dictionary( tools.safe_dict_get( self.get_json( "tv/{}/season/{}/episode/{}".format(tmdb_id, season, episode) ), "info", ), "rating", ) return {"info": result} if result else None def _add_api_key(self, params): if "api_key" not in params: params.update({"api_key": self.apiKey}) return params @handle_single_item_or_list def _handle_response(self, item): result = {} self._try_detect_type(item) self._apply_localized_alternative_titles(item) self._apply_releases(item) self._apply_content_ratings(item) self._apply_release_dates(item) self._apply_trailers(item) result.update({"art": self._handle_artwork(item)}) result.update({"cast": self._handle_cast(item)}) if item.get("mediatype"): result.update( { "info": self._normalize_info( self.meta_objects[item["mediatype"]], item ) } ) return result def _apply_localized_alternative_titles(self, item): if "alternative_titles" in item: item["aliases"] = [] for t in item["alternative_titles"].get( "titles", item["alternative_titles"].get("results", []) ): if "iso_3166_1" in t and t["iso_3166_1"] in [ self.lang_region_code, "US", ]: if t.get("title") not in [None, ""]: item["aliases"].append(t["title"]) if "iso_3166_1" in t and t["iso_3166_1"] in [self.lang_region_code]: if t.get("title") not in [None, ""] and t.get("type") not in [ None, "", ]: item.update({"title": t["title"]}) return item def _apply_trailers(self, item): if "videos" not in item: return item if not TMDBAPI._apply_trailer(item, self.lang_region_code): TMDBAPI._apply_trailer(item, "US") @staticmethod def _apply_trailer(item, region_code): for t in sorted( item["videos"].get("results", []), key=lambda k: k["size"], reverse=True ): if ( "iso_3166_1" in t and t["iso_3166_1"] == region_code and t["site"] == "YouTube" and t["type"] == "Trailer" ): if t.get("key"): item.update({"trailer": tools.youtube_url.format(t["key"])}) return True return False def _apply_releases(self, item): if "releases" not in item: return item if not TMDBAPI._apply_release(item, self.lang_region_code): TMDBAPI._apply_release(item, "US") @staticmethod def _apply_release(item, region_code): for t in item["releases"]["countries"]: if "iso_3166_1" in t and t["iso_3166_1"] == region_code: if t.get("certification"): item.update({"certification": t["certification"]}) if t.get("release_date"): item.update({"release_date": t["release_date"]}) return True return False def _apply_content_ratings(self, item): if "content_ratings" not in item: return item if not TMDBAPI._apply_content_rating(item, self.lang_region_code): TMDBAPI._apply_content_rating(item, "US") return item @staticmethod def _apply_content_rating(item, region_code): for rating in item["content_ratings"]["results"]: if "iso_3166_1" in rating and rating["iso_3166_1"] == region_code: if rating.get("rating"): item.update({"rating": rating["rating"]}) return True return False def _apply_release_dates(self, item): if "release_dates" not in item: return item if not TMDBAPI._apply_release_date(item, self.lang_region_code): TMDBAPI._apply_release_date(item, "US") return item @staticmethod def _apply_release_date(item, region_code): for rating in item["release_dates"]["results"]: if "iso_3166_1" in rating and rating["iso_3166_1"] == region_code: if ( "release_dates" in rating and rating["release_dates"][0] and rating["release_dates"][0]["certification"] ): item.update( {"certification": rating["release_dates"][0]["certification"]} ) if ( "release_dates" in rating and rating["release_dates"][0] and rating["release_dates"][0]["release_date"] ): item.update({"rating": rating["release_dates"][0]["release_date"]}) return True return False @staticmethod def _try_detect_type(item): if "still_path" in item: item.update({"mediatype": "episode"}) elif "season_number" in item and "episode_count" in item or "episodes" in item: item.update({"mediatype": "season"}) elif "number_of_seasons" in item: item.update({"mediatype": "tvshow"}) elif "imdb_id" in item: item.update({"mediatype": "movie"}) return item def _handle_artwork(self, item): result = {} if item.get("still_path") is not None: result.update( { "thumb": self._get_absolute_image_path( item["still_path"], self._create_tmdb_image_size(self.artwork_size["thumb"]), ) } ) if item.get("backdrop_path") is not None: result.update( { "fanart": self._get_absolute_image_path( item["backdrop_path"], self._create_tmdb_image_size(self.artwork_size["fanart"]), ) } ) if item.get("poster_path") is not None: result.update( { "poster": self._get_absolute_image_path( item["poster_path"], self._create_tmdb_image_size(self.artwork_size["poster"]), ) } ) images = item.get("images", item) for tmdb_type, kodi_type, selector in self.art_normalization: if tmdb_type not in images or not images[tmdb_type]: continue result.update( { kodi_type: [ { "url": self._get_absolute_image_path( i["file_path"], self._create_tmdb_image_size( self.artwork_size[kodi_type] ), ), "language": i["iso_639_1"] if i["iso_639_1"] != "xx" else None, "rating": self._normalize_rating(i), "size": int( i["width" if tmdb_type != "posters" else "height"] ) if int(i["width" if tmdb_type != "posters" else "height"]) < self.artwork_size[kodi_type] else self.artwork_size[kodi_type], } for i in images[tmdb_type] if selector is None or selector(i) ] } ) return result @staticmethod def _create_tmdb_image_size(size): if size == 2160 or size == 1080: return "original" else: return "w{}".format(size) def _handle_cast(self, item): cast = item.get("credits", item) if (not cast.get("cast")) and not item.get("guest_stars"): return return [ { "name": item["name"], "role": item["character"], "order": idx, "thumbnail": self._get_absolute_image_path( item["profile_path"], self._create_tmdb_image_size(self.artwork_size["cast"]), ), } for idx, item in enumerate( tools.extend_array( sorted(cast.get("cast", []), key=lambda k: k["order"],), sorted(cast.get("guest_stars", []), key=lambda k: k["order"]), ) ) if "name" in item and "character" in item and "profile_path" in item ] @staticmethod def _normalize_rating(image): if image["vote_count"]: rating = image["vote_average"] rating = 5 + (rating - 5) * 2 return rating return 5 def _get_absolute_image_path(self, relative_path, size="orginal"): if not relative_path: return None return "/".join( [self.imageBaseUrl.strip("/"), size.strip("/"), relative_path.strip("/")] )
def __init__(self): self.api_key = g.get_setting("omdb.apikey", None) self.omdb_support = False if not self.api_key else True self.meta_hash = tools.md5_hash((self.omdb_support, self.ApiUrl)) self.normalization = [ ( "@title", ("title", "sorttitle"), lambda d: d if not self._is_value_none(d) else None, ), ("@rated", "mpaa", lambda d: d if not self._is_value_none(d) else None), ( "@released", ("premiered", "aired"), lambda d: g.validate_date(d) if not self._is_value_none(d) else None, ), ( "@runtime", "duration", lambda d: int(d[:-4]) * 60 if not self._is_value_none(d) and len(d) > 4 and d[:-4].isdigit() else None, ), ( "@genre", "genre", lambda d: sorted( OrderedDict.fromkeys({x.strip() for x in d.split(",")})) if not self._is_value_none(d) else None, ), ( "@director", "director", lambda d: sorted( OrderedDict.fromkeys({ re.sub(r"\(.*?\)", "", x).strip() for x in d.split(",") })) if not self._is_value_none(d) else None, ), ( "@writer", "writer", lambda d: sorted( OrderedDict.fromkeys({ re.sub(r"\(.*?\)", "", x).strip() for x in d.split(",") })) if not self._is_value_none(d) else None, ), ("@plot", ("plot", "overview", "plotoutline"), None), ( "@country", "country", lambda d: d if not self._is_value_none(d) else None, ), ("@imdbID", ("imdbnumber", "imdb_id"), None), ( None, "rating.imdb", ( ("@imdbRating", "@imdbVotes"), lambda a, c: { "rating": tools.safe_round(tools.get_clean_number(a), 2 ), "votes": tools.get_clean_number(c), } if not self._is_value_none(a) and not self. _is_value_none(c) else None, ), ), ( "@Production", "studio", lambda d: d if not self._is_value_none(d) else None, ), ("@awards", "awards", lambda d: d if not self._is_value_none(d) else None), ( "@awards", "oscar_wins", lambda d: self._extract_awards(d, ("Won", "Oscar")), ), ( "@awards", "oscar_nominations", lambda d: self._extract_awards(d, ("Nominated for", "Oscar")), ), ( "@awards", "award_wins", lambda d: self._extract_awards(d, ("Another", "wins"), ("", "wins")), ), ( "@awards", "award_nominations", lambda d: self._extract_awards(d, ("wins &", "nominations"), ("", "nominations")), ), ( "@metascore", "metacritic_rating", lambda d: d if not self._is_value_none(d) else None, ), ( "@tomatoMeter", "rottentomatoes_rating", lambda d: d if not self._is_value_none(d) else None, ), ( "@tomatoImage", "rottentomatoes_image", lambda d: d if not self._is_value_none(d) else None, ), ( "@tomatoReviews", "rottentomatoes_reviewstotal", lambda d: tools.get_clean_number(d) if not self._is_value_none(d) else None, ), ( "@tomatoFresh", "rottentomatoes_reviewsfresh", lambda d: tools.get_clean_number(d) if not self._is_value_none(d) else None, ), ( "@tomatoRotten", "rottentomatoes_reviewsrotten", lambda d: tools.get_clean_number(d) if not self._is_value_none(d) else None, ), ( "@tomatoConsensus", "rottentomatoes_consensus", lambda d: d if not self._is_value_none(d) else None, ), ( "@tomatoUserMeter", "rottentomatoes_usermeter", lambda d: d if not self._is_value_none(d) else None, ), ( "@tomatoUserReviews", "rottentomatoes_userreviews", lambda d: tools.get_clean_number(d) if not self._is_value_none(d) else None, ), ] self.session = requests.Session() retries = Retry( total=5, backoff_factor=0.1, status_forcelist=[500, 503, 504, 520, 521, 522, 524], ) self.session.mount("https://", HTTPAdapter(max_retries=retries))
def __init__(self): self.access_token = None self.refresh_token = None self.token_expires = 0 self.username = None self._load_settings() self.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" self.try_refresh_token() self.language = g.get_language_code() self.country = g.get_language_code(True).split("-")[-1].lower() self.TranslationNormalization = [ ("title", ("title", "originaltitle", "sorttitle"), None), ("language", "language", None), ("overview", ("plot", "plotoutline"), None), ] self.UserRatingNormalization = [( "rating", "user_rating", lambda t: tools.safe_round(tools.get_clean_number(t), 2), ), ("rated_at", "rated_at", lambda t: g.validate_date(t))] self.PlayBackNormalization = [("progress", "percentplayed", None), ("paused_at", "paused_at", lambda t: g.validate_date(t)), ("id", "playback_id", None)] self.PlayBackHistoryNormalization = [("action", "action", None), ("watched_at", "watched_at", lambda t: g.validate_date(t)), ("id", "playback_id", None)] self.CalendarNormalization = [("first_aired", "first_aired", lambda t: g.validate_date(t))] self.Normalization = tools.extend_array( [("certification", "mpaa", None), ("genres", "genre", None), (("ids", "imdb"), ("imdbnumber", "imdb_id"), None), (("ids", "trakt"), "trakt_id", None), (("ids", "slug"), "trakt_slug", None), (("ids", "tvdb"), "tvdb_id", None), (("ids", "tmdb"), "tmdb_id", None), ("playback_id", "playback_id", None), (("show", "ids", "trakt"), "trakt_show_id", None), ("network", "studio", lambda n: [n]), ("runtime", "duration", lambda d: d * 60), ("progress", "percentplayed", None), ("percentplayed", "percentplayed", None), ("updated_at", "dateadded", lambda t: g.validate_date(t)), ("last_updated_at", "dateadded", lambda t: g.validate_date(t)), ("last_watched_at", "last_watched_at", lambda t: g.validate_date(t)), ("watched_at", "watched_at", lambda t: g.validate_date(t)), ("paused_at", "paused_at", lambda t: g.validate_date(t)), ( "rating", "rating", lambda t: tools.safe_round(tools.get_clean_number(t), 2), ), ("votes", "votes", lambda t: tools.get_clean_number(t)), ( None, "rating.trakt", ( ("rating", "votes"), lambda r, v: { "rating": tools.safe_round(tools.get_clean_number(r), 2), "votes": tools.get_clean_number(v), }, ), ), ("tagline", "tagline", None), ( "trailer", "trailer", lambda t: tools.youtube_url.format(t.split("?v=")[-1]) if t else None, ), ("type", "mediatype", lambda t: t if "show" not in t else "tvshow"), ("available_translations", "available_translations", None), ("score", "score", None), ("action", "action", None), ("added", "added", None), ("rank", "rank", None), ("listed_at", "listed_at", None), ( "country", "country_origin", lambda t: t.upper() if t is not None else None, ), ("user_rating", "user_rating", None), ("rated_at", "rated_at", None)], self.TranslationNormalization, ) self.MoviesNormalization = tools.extend_array( [ ("plays", "playcount", None), ("year", "year", None), ("released", ("premiered", "aired"), lambda t: g.validate_date(t)), ("collected_at", "collected_at", lambda t: g.validate_date(t)), ], self.Normalization, ) self.ShowNormalization = tools.extend_array( [("status", "status", None), ("status", "is_airing", lambda t: not t == "ended"), ("title", "tvshowtitle", None), ("first_aired", "year", lambda t: g.validate_date(t)[:4] if g.validate_date(t) else None), ( "first_aired", ("premiered", "aired"), lambda t: g.validate_date(t), ), ("last_collected_at", "last_collected_at", lambda t: g.validate_date(t))], self.Normalization) self.SeasonNormalization = tools.extend_array( [("number", ("season", "sortseason"), None), ("episode_count", "episode_count", None), ("aired_episodes", "aired_episodes", None), ("first_aired", "year", lambda t: g.validate_date(t)[:4] if g.validate_date(t) else None), ( "first_aired", ("premiered", "aired"), lambda t: g.validate_date(t), ), ("last_collected_at", "last_collected_at", lambda t: g.validate_date(t))], self.Normalization, ) self.EpisodeNormalization = tools.extend_array( [("number", ("episode", "sortepisode"), None), ("season", ("season", "sortseason"), None), ("collected_at", "collected", lambda t: 1), ("plays", "playcount", None), ("first_aired", "year", lambda t: g.validate_date(t)[:4] if g.validate_date(t) else None), ( "first_aired", ("premiered", "aired"), lambda t: g.validate_date(t), ), ("collected_at", "collected_at", lambda t: g.validate_date(t))], self.Normalization, ) self.ListNormalization = [ ("updated_at", "dateadded", lambda t: g.validate_date(t)), (("ids", "trakt"), "trakt_id", None), (("ids", "slug"), "slug", None), ("sort_by", "sort_by", None), ("sort_how", "sort_how", None), (("user", "ids", "slug"), "username", None), ("name", ("name", "title"), None), ("type", "mediatype", None), ] self.MixedEpisodeNormalization = [ (("show", "ids", "trakt"), "trakt_show_id", None), (("episode", "ids", "trakt"), "trakt_id", None) ] self.MixedSeasonNormalization = [ (("show", "ids", "trakt"), "trakt_show_id", None), (("season", "ids", "trakt"), "trakt_id", None) ] self.MetaNormalization = { "movie": self.MoviesNormalization, "list": self.ListNormalization, "show": self.ShowNormalization, "season": self.SeasonNormalization, "episode": self.EpisodeNormalization, "mixedepisode": self.MixedEpisodeNormalization, "mixedseason": self.MixedSeasonNormalization, "playback": self.PlayBackNormalization, "playbackhistory": self.PlayBackHistoryNormalization, "user_rating": self.UserRatingNormalization, "calendar": self.CalendarNormalization } self.MetaObjects = ("movie", "tvshow", "show", "season", "episode", "list") self.MetaCollections = ("movies", "shows", "seasons", "episodes", "cast")
class TVDBAPI(ApiBase): baseUrl = "https://api.thetvdb.com/" normalization = [ ("imdbId", ("imdbnumber", "imdb_id"), None), ("id", "tvdb_id", None), ( None, "rating.tvdb", ( ("siteRating", "siteRatingCount"), lambda r, c: { "rating": tools.safe_round(r, 2), "votes": c }, ), ), ("firstAired", ("premiered", "aired"), lambda t: g.validate_date(t)), ("overview", ("plot", "plotoutline"), None), ("mediatype", "mediatype", None), ] show_normalization = tools.extend_array( [ ( "firstAired", "year", lambda t: g.validate_date(t)[:4] if g.validate_date(t) else None, ), ("status", "status", None), ("runtime", "runtime", lambda d: int(d) * 60), ("network", "studio", None), ("genre", "genre", lambda t: sorted(OrderedDict.fromkeys(t))), ("seriesName", ("title", "tvshowtitle"), None), ("rating", "mpaa", None), ("language", "language", None), ("aliases", "aliases", None), ( "country", "country_origin", lambda t: t.upper() if t is not None else None, ), ], normalization, ) episode_normalization = tools.extend_array( [ ("episodeName", "title", None), ("seriesId", "tvdb_show_id", None), ("airedEpisodeNumber", ("episode", "sortepisode"), None), ("airedSeason", ("season", "sortseason"), None), ("directors", "director", lambda t: sorted(OrderedDict.fromkeys(t))), ("writers", "writer", lambda t: sorted(OrderedDict.fromkeys(t))), ("overview", "plot", None), ("contentRating", "mpaa", None), ], normalization, ) meta_objects = { "movie": normalization, "tvshow": show_normalization, "episode": episode_normalization, } # These are a duplicate of the TMDB codes, should be used as a very rough reference # Until we can find a good source from TVDB about their codes we can use this in place http_codes = { 200: "Success", 201: "Success - new resource created (POST)", 401: "Returned if your JWT token is missing or expired", 404: "Returned if the given ID does not exist.", 409: "Returned if requested record could not be updated/deleted", 405: "Missing query params are given", 422: "Invalid query params provided", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", } imageBaseUrl = "https://www.thetvdb.com/banners/" art_map = { "fanart": "fanart", "poster": "poster", "season": "poster", "seasonwide": "banner", "series": "banner", } supported_languages = [ { "id": 101, "abbreviation": "aa", "name": "Afaraf", "englishName": "Afar" }, { "id": 102, "abbreviation": "ab", "name": "аҧсуа бызшәа", "englishName": "Abkhaz", }, { "id": 103, "abbreviation": "af", "name": "Afrikaans", "englishName": "Afrikaans", }, { "id": 104, "abbreviation": "ak", "name": "Akan", "englishName": "Akan" }, { "id": 105, "abbreviation": "am", "name": "አማርኛ", "englishName": "Amharic" }, { "id": 106, "abbreviation": "ar", "name": "العربية", "englishName": "Arabic", }, { "id": 107, "abbreviation": "an", "name": "aragonés", "englishName": "Aragonese", }, { "id": 108, "abbreviation": "as", "name": "অসমীয়া", "englishName": "Assamese", }, { "id": 109, "abbreviation": "av", "name": "авар мацӀ", "englishName": "Avaric", }, { "id": 110, "abbreviation": "ae", "name": "avesta", "englishName": "Avestan", }, { "id": 111, "abbreviation": "ay", "name": "aymar aru", "englishName": "Aymara", }, { "id": 112, "abbreviation": "az", "name": "azərbaycan dili", "englishName": "Azerbaijani", }, { "id": 113, "abbreviation": "ba", "name": "башҡорт теле", "englishName": "Bashkir", }, { "id": 114, "abbreviation": "bm", "name": "bamanankan", "englishName": "Bambara", }, { "id": 115, "abbreviation": "be", "name": "беларуская мова", "englishName": "Belarusian", }, { "id": 116, "abbreviation": "bn", "name": "বাংলা", "englishName": "Bengali", }, { "id": 117, "abbreviation": "bh", "name": "भोजपुरी", "englishName": "Bihari", }, { "id": 118, "abbreviation": "bi", "name": "Bislama", "englishName": "Bislama", }, { "id": 119, "abbreviation": "bo", "name": "བོད་ཡིག", "englishName": "Tibetan Standard", }, { "id": 120, "abbreviation": "bs", "name": "bosanski jezik", "englishName": "Bosnian", }, { "id": 121, "abbreviation": "br", "name": "brezhoneg", "englishName": "Breton", }, { "id": 122, "abbreviation": "bg", "name": "български език", "englishName": "Bulgarian", }, { "id": 123, "abbreviation": "ca", "name": "català", "englishName": "Catalan", }, { "id": 28, "abbreviation": "cs", "name": "čeština", "englishName": "Czech" }, { "id": 124, "abbreviation": "ch", "name": "Chamoru", "englishName": "Chamorro", }, { "id": 125, "abbreviation": "ce", "name": "нохчийн мотт", "englishName": "Chechen", }, { "id": 126, "abbreviation": "cu", "name": "ѩзыкъ словѣньскъ", "englishName": "Old Church Slavonic", }, { "id": 127, "abbreviation": "cv", "name": "чӑваш чӗлхи", "englishName": "Chuvash", }, { "id": 128, "abbreviation": "kw", "name": "Kernewek", "englishName": "Cornish", }, { "id": 129, "abbreviation": "co", "name": "corsu", "englishName": "Corsican", }, { "id": 130, "abbreviation": "cr", "name": "ᓀᐦᐃᔭᐍᐏᐣ", "englishName": "Cree" }, { "id": 131, "abbreviation": "cy", "name": "Cymraeg", "englishName": "Welsh", }, { "id": 10, "abbreviation": "da", "name": "dansk", "englishName": "Danish" }, { "id": 14, "abbreviation": "de", "name": "Deutsch", "englishName": "German", }, { "id": 132, "abbreviation": "dv", "name": "ދިވެހި", "englishName": "Divehi", }, { "id": 133, "abbreviation": "dz", "name": "རྫོང་ཁ", "englishName": "Dzongkha", }, { "id": 20, "abbreviation": "el", "name": "ελληνική γλώσσα", "englishName": "Greek", }, { "id": 7, "abbreviation": "en", "name": "English", "englishName": "English", }, { "id": 134, "abbreviation": "eo", "name": "Esperanto", "englishName": "Esperanto", }, { "id": 135, "abbreviation": "et", "name": "eesti", "englishName": "Estonian", }, { "id": 136, "abbreviation": "eu", "name": "euskara", "englishName": "Basque", }, { "id": 137, "abbreviation": "ee", "name": "Eʋegbe", "englishName": "Ewe" }, { "id": 138, "abbreviation": "fo", "name": "føroyskt", "englishName": "Faroese", }, { "id": 139, "abbreviation": "fa", "name": "فارسی", "englishName": "Persian", }, { "id": 140, "abbreviation": "fj", "name": "vosa Vakaviti", "englishName": "Fijian", }, { "id": 11, "abbreviation": "fi", "name": "suomi", "englishName": "Finnish" }, { "id": 17, "abbreviation": "fr", "name": "français", "englishName": "French", }, { "id": 141, "abbreviation": "fy", "name": "Frysk", "englishName": "Western Frisian", }, { "id": 142, "abbreviation": "ff", "name": "Fulfulde", "englishName": "Fula", }, { "id": 143, "abbreviation": "gd", "name": "Gàidhlig", "englishName": "Scottish Gaelic", }, { "id": 144, "abbreviation": "ga", "name": "Gaeilge", "englishName": "Irish", }, { "id": 145, "abbreviation": "gl", "name": "galego", "englishName": "Galician", }, { "id": 146, "abbreviation": "gv", "name": "Gaelg", "englishName": "Manx" }, { "id": 147, "abbreviation": "gn", "name": "Avañe'ẽ", "englishName": "Guaraní", }, { "id": 148, "abbreviation": "gu", "name": "ગુજરાતી", "englishName": "Gujarati", }, { "id": 149, "abbreviation": "ht", "name": "Kreyòl ayisyen", "englishName": "Haitian", }, { "id": 150, "abbreviation": "ha", "name": "هَوُسَ", "englishName": "Hausa" }, { "id": 24, "abbreviation": "he", "name": "עברית", "englishName": "Hebrew" }, { "id": 151, "abbreviation": "hz", "name": "Otjiherero", "englishName": "Herero", }, { "id": 152, "abbreviation": "hi", "name": "हिन्दी", "englishName": "Hindi" }, { "id": 153, "abbreviation": "ho", "name": "Hiri Motu", "englishName": "Hiri Motu", }, { "id": 31, "abbreviation": "hr", "name": "hrvatski jezik", "englishName": "Croatian", }, { "id": 19, "abbreviation": "hu", "name": "Magyar", "englishName": "Hungarian", }, { "id": 154, "abbreviation": "hy", "name": "Հայերեն", "englishName": "Armenian", }, { "id": 155, "abbreviation": "ig", "name": "Asụsụ Igbo", "englishName": "Igbo", }, { "id": 156, "abbreviation": "io", "name": "Ido", "englishName": "Ido" }, { "id": 157, "abbreviation": "ii", "name": "Nuosuhxop", "englishName": "Nuosu", }, { "id": 158, "abbreviation": "iu", "name": "ᐃᓄᒃᑎᑐᑦ", "englishName": "Inuktitut", }, { "id": 159, "abbreviation": "ie", "name": "Interlingue", "englishName": "Interlingue", }, { "id": 160, "abbreviation": "ia", "name": "Interlingua", "englishName": "Interlingua", }, { "id": 161, "abbreviation": "id", "name": "Bahasa Indonesia", "englishName": "Indonesian", }, { "id": 162, "abbreviation": "ik", "name": "Iñupiaq", "englishName": "Inupiaq", }, { "id": 163, "abbreviation": "is", "name": "Íslenska", "englishName": "Icelandic", }, { "id": 15, "abbreviation": "it", "name": "italiano", "englishName": "Italian", }, { "id": 164, "abbreviation": "jv", "name": "basa Jawa", "englishName": "Javanese", }, { "id": 25, "abbreviation": "ja", "name": "日本語", "englishName": "Japanese" }, { "id": 165, "abbreviation": "kl", "name": "kalaallisut", "englishName": "Kalaallisut", }, { "id": 166, "abbreviation": "kn", "name": "ಕನ್ನಡ", "englishName": "Kannada", }, { "id": 167, "abbreviation": "ks", "name": "कश्मीरी", "englishName": "Kashmiri", }, { "id": 168, "abbreviation": "ka", "name": "ქართული", "englishName": "Georgian", }, { "id": 169, "abbreviation": "kr", "name": "Kanuri", "englishName": "Kanuri", }, { "id": 170, "abbreviation": "kk", "name": "қазақ тілі", "englishName": "Kazakh", }, { "id": 171, "abbreviation": "km", "name": "ខ្មែរ", "englishName": "Khmer" }, { "id": 172, "abbreviation": "ki", "name": "Gĩkũyũ", "englishName": "Kikuyu", }, { "id": 173, "abbreviation": "rw", "name": "Ikinyarwanda", "englishName": "Kinyarwanda", }, { "id": 174, "abbreviation": "ky", "name": "кыргыз тили", "englishName": "Kirghiz", }, { "id": 175, "abbreviation": "kv", "name": "коми кыв", "englishName": "Komi", }, { "id": 176, "abbreviation": "kg", "name": "KiKongo", "englishName": "Kongo", }, { "id": 32, "abbreviation": "ko", "name": "한국어", "englishName": "Korean" }, { "id": 177, "abbreviation": "kj", "name": "Kuanyama", "englishName": "Kwanyama", }, { "id": 178, "abbreviation": "ku", "name": "Kurdî", "englishName": "Kurdish", }, { "id": 179, "abbreviation": "lo", "name": "ພາສາລາວ", "englishName": "Lao" }, { "id": 180, "abbreviation": "la", "name": "latine", "englishName": "Latin" }, { "id": 181, "abbreviation": "lv", "name": "latviešu valoda", "englishName": "Latvian", }, { "id": 182, "abbreviation": "li", "name": "Limburgs", "englishName": "Limburgish", }, { "id": 183, "abbreviation": "ln", "name": "Lingála", "englishName": "Lingala", }, { "id": 184, "abbreviation": "lt", "name": "lietuvių kalba", "englishName": "Lithuanian", }, { "id": 185, "abbreviation": "lb", "name": "Lëtzebuergesch", "englishName": "Luxembourgish", }, { "id": 186, "abbreviation": "lu", "name": "Luba-Katanga", "englishName": "Luba-Katanga", }, { "id": 187, "abbreviation": "lg", "name": "Luganda", "englishName": "Luganda", }, { "id": 188, "abbreviation": "mh", "name": "Kajin M̧ajeļ", "englishName": "Marshallese", }, { "id": 189, "abbreviation": "ml", "name": "മലയാളം", "englishName": "Malayalam", }, { "id": 190, "abbreviation": "mr", "name": "मराठी", "englishName": "Marathi", }, { "id": 191, "abbreviation": "mk", "name": "македонски јазик", "englishName": "Macedonian", }, { "id": 192, "abbreviation": "mg", "name": "Malagasy fiteny", "englishName": "Malagasy", }, { "id": 193, "abbreviation": "mt", "name": "Malti", "englishName": "Maltese", }, { "id": 194, "abbreviation": "mn", "name": "монгол", "englishName": "Mongolian", }, { "id": 195, "abbreviation": "mi", "name": "te reo Māori", "englishName": "Māori", }, { "id": 196, "abbreviation": "ms", "name": "bahasa Melayu", "englishName": "Malay", }, { "id": 197, "abbreviation": "my", "name": "Burmese", "englishName": "Burmese", }, { "id": 198, "abbreviation": "na", "name": "Ekakairũ Naoero", "englishName": "Nauru", }, { "id": 199, "abbreviation": "nv", "name": "Diné bizaad", "englishName": "Navajo", }, { "id": 200, "abbreviation": "nr", "name": "isiNdebele", "englishName": "South Ndebele", }, { "id": 201, "abbreviation": "nd", "name": "isiNdebele", "englishName": "North Ndebele", }, { "id": 202, "abbreviation": "ng", "name": "Owambo", "englishName": "Ndonga", }, { "id": 203, "abbreviation": "ne", "name": "नेपाली", "englishName": "Nepali", }, { "id": 13, "abbreviation": "nl", "name": "Nederlands", "englishName": "Dutch", }, { "id": 9, "abbreviation": "no", "name": "Norsk bokmål", "englishName": "Norwegian", }, { "id": 206, "abbreviation": "ny", "name": "chiCheŵa", "englishName": "Chichewa", }, { "id": 207, "abbreviation": "oc", "name": "occitan", "englishName": "Occitan", }, { "id": 208, "abbreviation": "oj", "name": "ᐊᓂᔑᓈᐯᒧᐎᓐ", "englishName": "Ojibwe", }, { "id": 209, "abbreviation": "or", "name": "ଓଡ଼ିଆ", "englishName": "Oriya" }, { "id": 210, "abbreviation": "om", "name": "Afaan Oromoo", "englishName": "Oromo", }, { "id": 211, "abbreviation": "os", "name": "ирон æвзаг", "englishName": "Ossetian", }, { "id": 212, "abbreviation": "pa", "name": "ਪੰਜਾਬੀ", "englishName": "Panjabi", }, { "id": 213, "abbreviation": "pi", "name": "पाऴि", "englishName": "Pāli" }, { "id": 18, "abbreviation": "pl", "name": "język polski", "englishName": "Polish", }, { "id": 214, "abbreviation": "pt", "name": "Português - Portugal", "englishName": "Portuguese - Portugal", }, { "id": 26, "abbreviation": "pt", "name": "Português - Brasil", "englishName": "Portuguese - Brazil", }, { "id": 215, "abbreviation": "ps", "name": "پښتو", "englishName": "Pashto" }, { "id": 216, "abbreviation": "qu", "name": "Runa Simi", "englishName": "Quechua", }, { "id": 217, "abbreviation": "rm", "name": "rumantsch grischun", "englishName": "Romansh", }, { "id": 218, "abbreviation": "ro", "name": "limba română", "englishName": "Romanian", }, { "id": 219, "abbreviation": "rn", "name": "Ikirundi", "englishName": "Kirundi", }, { "id": 22, "abbreviation": "ru", "name": "русский язык", "englishName": "Russian", }, { "id": 220, "abbreviation": "sg", "name": "yângâ tî sängö", "englishName": "Sango", }, { "id": 221, "abbreviation": "sa", "name": "संस्कृतम्", "englishName": "Sanskrit", }, { "id": 222, "abbreviation": "si", "name": "සිංහල", "englishName": "Sinhala", }, { "id": 30, "abbreviation": "sk", "name": "slovenčina", "englishName": "Slovak", }, { "id": 223, "abbreviation": "sl", "name": "slovenski jezik", "englishName": "Slovene", }, { "id": 224, "abbreviation": "se", "name": "Davvisámegiella", "englishName": "Northern Sami", }, { "id": 225, "abbreviation": "sm", "name": "gagana fa'a Samoa", "englishName": "Samoan", }, { "id": 226, "abbreviation": "sn", "name": "chiShona", "englishName": "Shona", }, { "id": 227, "abbreviation": "sd", "name": "सिन्धी", "englishName": "Sindhi", }, { "id": 228, "abbreviation": "so", "name": "Soomaaliga", "englishName": "Somali", }, { "id": 229, "abbreviation": "st", "name": "Sesotho", "englishName": "Southern Sotho", }, { "id": 16, "abbreviation": "es", "name": "español", "englishName": "Spanish", }, { "id": 230, "abbreviation": "sq", "name": "gjuha shqipe", "englishName": "Albanian", }, { "id": 231, "abbreviation": "sc", "name": "sardu", "englishName": "Sardinian", }, { "id": 232, "abbreviation": "sr", "name": "српски језик", "englishName": "Serbian", }, { "id": 233, "abbreviation": "ss", "name": "SiSwati", "englishName": "Swati", }, { "id": 234, "abbreviation": "su", "name": "Basa Sunda", "englishName": "Sundanese", }, { "id": 235, "abbreviation": "sw", "name": "Kiswahili", "englishName": "Swahili", }, { "id": 8, "abbreviation": "sv", "name": "svenska", "englishName": "Swedish", }, { "id": 236, "abbreviation": "ty", "name": "Reo Tahiti", "englishName": "Tahitian", }, { "id": 237, "abbreviation": "ta", "name": "தமிழ்", "englishName": "Tamil" }, { "id": 238, "abbreviation": "tt", "name": "татар теле", "englishName": "Tatar", }, { "id": 239, "abbreviation": "te", "name": "తెలుగు", "englishName": "Telugu", }, { "id": 240, "abbreviation": "tg", "name": "тоҷикӣ", "englishName": "Tajik" }, { "id": 241, "abbreviation": "tl", "name": "Wikang Tagalog", "englishName": "Tagalog", }, { "id": 242, "abbreviation": "th", "name": "ไทย", "englishName": "Thai" }, { "id": 243, "abbreviation": "ti", "name": "ትግርኛ", "englishName": "Tigrinya", }, { "id": 244, "abbreviation": "to", "name": "faka Tonga", "englishName": "Tonga", }, { "id": 245, "abbreviation": "tn", "name": "Setswana", "englishName": "Tswana", }, { "id": 246, "abbreviation": "ts", "name": "Xitsonga", "englishName": "Tsonga", }, { "id": 247, "abbreviation": "tk", "name": "Türkmen", "englishName": "Turkmen", }, { "id": 21, "abbreviation": "tr", "name": "Türkçe", "englishName": "Turkish", }, { "id": 248, "abbreviation": "tw", "name": "Twi", "englishName": "Twi" }, { "id": 249, "abbreviation": "ug", "name": "Uyƣurqə", "englishName": "Uighur", }, { "id": 250, "abbreviation": "uk", "name": "українська мова", "englishName": "Ukrainian", }, { "id": 251, "abbreviation": "ur", "name": "اردو", "englishName": "Urdu" }, { "id": 252, "abbreviation": "uz", "name": "Ozbek", "englishName": "Uzbek" }, { "id": 253, "abbreviation": "ve", "name": "Tshivenḓa", "englishName": "Venda", }, { "id": 254, "abbreviation": "vi", "name": "Tiếng Việt", "englishName": "Vietnamese", }, { "id": 255, "abbreviation": "vo", "name": "Volapük", "englishName": "Volapük", }, { "id": 256, "abbreviation": "wa", "name": "walon", "englishName": "Walloon", }, { "id": 257, "abbreviation": "wo", "name": "Wollof", "englishName": "Wolof" }, { "id": 258, "abbreviation": "xh", "name": "isiXhosa", "englishName": "Xhosa", }, { "id": 259, "abbreviation": "yi", "name": "ייִדיש", "englishName": "Yiddish", }, { "id": 260, "abbreviation": "yo", "name": "Yorùbá", "englishName": "Yoruba", }, { "id": 261, "abbreviation": "za", "name": "Saɯ cueŋƅ", "englishName": "Zhuang", }, { "id": 27, "abbreviation": "zh", "name": "大陆简体", "englishName": "Chinese - China", }, { "id": 262, "abbreviation": "zu", "name": "isiZulu", "englishName": "Zulu" }, ] retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504]) def __init__(self): self.session = requests.Session() self.session.mount("https://", HTTPAdapter(max_retries=self.retries)) self._load_settings() self.lang_code = g.get_language_code(False) self.languages = ([None, self.lang_code] if self.lang_code != "en" and any(self.lang_code == i["abbreviation"] for i in self.supported_languages) else [None]) if not self.jwToken: self.init_token() else: self.try_refresh_token() self.preferred_artwork_size = g.get_int_setting( "artwork.preferredsize") self.meta_hash = tools.md5_hash(( self.lang_code, self.art_map, self.baseUrl, self.imageBaseUrl, self.preferred_artwork_size, )) def __del__(self): self.session.close() def _get_headers(self, lang=None): headers = {"content-type": "application/json"} if self.jwToken: headers["Authorization"] = "Bearer {}".format(self.jwToken) if lang is not None: headers["Accept-Language"] = lang return headers def try_refresh_token(self, force=False): if not force and self.tokenExpires > float(time.time()): return try: with GlobalLock(self.__class__.__name__, True, self.jwToken) as lock: try: g.log("TVDB Token requires refreshing...") response = self.session.post( tools.urljoin(self.baseUrl, "refresh_token"), headers=self._get_headers(), ).json() if "Error" in response: response = self.session.post( self.baseUrl + "login", json={ "apikey": self.apiKey }, headers=self._get_headers(), ).json() self._save_settings(response) g.log("Refreshed Tvdbs Token") except Exception: g.log("Failed to refresh Tvdb Access Token", "error") return except RanOnceAlready: return def _load_settings(self): self.apiKey = g.get_setting("tvdb.apikey", "43VPI0R8323FB7TI") self.jwToken = g.get_setting("tvdb.jw") self.tokenExpires = g.get_float_setting("tvdb.expiry") def _save_settings(self, response): if "token" in response: g.set_setting("tvdb.jw", response["token"]) self.jwToken = response["token"] self.tokenExpires = time.time() + (24 * (60 * 60)) g.set_setting("tvdb.expiry", g.UNICODE(self.tokenExpires)) def init_token(self): try: with GlobalLock(self.__class__.__name__, True): response = self.session.post( self.baseUrl + "login", json={ "apikey": self.apiKey }, headers=self._get_headers(), ).json() self._save_settings(response) except RanOnceAlready: return @tvdb_guard_response def get(self, url, **params): if "language" in params: language = params.pop("language") else: language = None timeout = params.pop("timeout", 10) return self.session.get( self.baseUrl + url, params=params, headers=self._get_headers(language), timeout=timeout, ) @staticmethod def _flatten(response): if "data" in response: response = response["data"] if not response: return [] return response def get_json(self, url, **params): response = self.get(url, **params) if response is None: return None try: return self._handle_response(params.get("language"), self._flatten(response.json())) except (ValueError, AttributeError): traceback.print_exc() g.log( "Failed to receive JSON from Tvdb response - response: {}". format(response), "error", ) return None @use_cache() def get_json_cached(self, url, **params): return self.get_json(url, **params) @staticmethod def _get_all_pages(func, url, **params): if "progress" in params: progress_callback = params.pop("progress") else: progress_callback = None response = func(url, **params) yield response if "links" not in response.headers: return for i in range(2, int(response.headers["X-Pagination-Page-Count"]) + 1): if progress_callback is not None: progress_callback( (i / (int(response.headers["X-Pagination-Page-Count"]) + 1)) * 100) params.update({"page": i}) yield func(url, **params) @handle_single_item_or_list def _handle_response(self, language, item): result = {} item = self._try_detect_type(item) if "mediatype" not in item: return item result.update({"art": self._extract_art(item, language)}) result.update({ "info": self._normalize_info(self.meta_objects[item["mediatype"]], item) }) return result @handle_single_item_or_list def _handle_cast(self, item): if item is None or "name" not in item: return {} return { "name": item["name"], "role": item["role"], "thumbnail": self._get_absolute_image_path(item["image"]), "order": item["sortOrder"], } @staticmethod def _try_detect_type(item): if "airedSeasons" in item or "seriesName" in item: item.update({"mediatype": "tvshow"}) elif "episodeName" in item: item.update({"mediatype": "episode"}) return item def _extract_art(self, item, language, key_name=None, sub_key=None): result = {} if item is None: return result if not isinstance(item, (list, set)): if item.get("filename"): result.update( {"thumb": self._get_absolute_image_path(item["filename"])}) if key_name is not None and isinstance(item, (set, list)): art = [ self._get_image(i, self.art_map[key_name], language) for i in item if "fileName" in i and i.get("keyType") == key_name and ( sub_key is None or i.get("subKey") == g.UNICODE(sub_key)) ] if len(art) > 0: result.update({self.art_map[key_name]: art}) return result @staticmethod def _get_rating(image): if image["ratingsInfo"]["count"]: info = image["ratingsInfo"] rating = info["average"] if info["count"] < 5: rating = 5 + (rating - 5) * sin(info["count"] / pi) return rating else: return 5 @staticmethod def _parse_size(image, art_type): if art_type in ("series", "seasonwide"): return 758 elif art_type == "season": return 1000 try: return int(image["resolution"].split("x") [0 if art_type != "poster" else 1]) except (ValueError, IndexError): return 0 def _get_image(self, image, art_type, language): return { "url": self._get_absolute_image_path(image["fileName"]), "language": language if language else "en", "rating": self._get_rating(image), "size": self._parse_size(image, art_type), } @wrap_tvdb_object def get_show(self, tvdb_id): item = {} art_types = self._get_show_art_types(tvdb_id) if art_types: art_types = [i for i in art_types if not i.startswith("season")] else: art_types = [] thread_pool = ThreadPool() thread_pool.put(self._get_series_cast, tvdb_id) for language in self.languages: thread_pool.put(self._get_show_info, tvdb_id, language) for art_type in art_types: thread_pool.put(self._get_show_art, tvdb_id, art_type, language) item = tools.smart_merge_dictionary(item, thread_pool.wait_completion()) if not item or len(item.keys()) == 0: return None return item @wrap_tvdb_object def get_show_art(self, tvdb_id): item = {} art_types = self._get_show_art_types(tvdb_id) if art_types: art_types = [i for i in art_types if not i.startswith("season")] else: art_types = [] thread_pool = ThreadPool() for language in self.languages: for art_type in art_types: thread_pool.put(self._get_show_art, tvdb_id, art_type, language) item = tools.smart_merge_dictionary(item, thread_pool.wait_completion()) if not item or len(item.keys()) == 0: return None return item @wrap_tvdb_object def get_show_info(self, tvdb_id): item = {} thread_pool = ThreadPool() thread_pool.put(self._get_series_cast, tvdb_id) for language in self.languages: thread_pool.put(self._get_show_info, tvdb_id, language) item = tools.smart_merge_dictionary(item, thread_pool.wait_completion()) if not item or len(item.keys()) == 0: return None return item @wrap_tvdb_object def get_show_rating(self, tvdb_id): item = self.get_json_cached( "series/{}/filter".format(tvdb_id), keys="siteRating,siteRatingCount,seriesName", ) if not item or len(item.keys()) == 0: return None return {"info": tools.filter_dictionary(item["info"], "rating")} @wrap_tvdb_object def get_show_cast(self, tvdb_id): cast = self._get_series_cast(tvdb_id) if cast.get("cast"): return cast else: return None @use_cache() def _get_show_art_types(self, tvdb_id): result = self.get_json_cached( "series/{}/images/query/params".format(tvdb_id)) return (result if not result else sorted( [i["keyType"] for i in result if "keyType" in i])) def _get_show_art(self, tvdb_id, key_type, language): return { "art": self._extract_art( self.get_json_cached( "series/{}/images/query?keyType={}".format( tvdb_id, key_type), language=language, ), language, key_type, ) } def _get_show_info(self, tvdb_id, language): return self.get_json_cached("series/{}".format(tvdb_id), language=language) def _get_series_cast(self, tvdb_id): return { "cast": self._handle_cast( self.get_json_cached("series/{}/actors".format(tvdb_id))) } @wrap_tvdb_object def get_season_art(self, tvdb_id, season): item = {} art_types = self._get_show_art_types(tvdb_id) if not art_types: return None art_types = [i for i in art_types if i.startswith("season")] thread_pool = ThreadPool() for language in self.languages: for art_type in art_types: thread_pool.put(self._get_season_art, tvdb_id, art_type, season, language) item = tools.smart_merge_dictionary(item, thread_pool.wait_completion()) if not item or len(item.keys()) == 0: return None return item def _get_season_art(self, tvdb_id, art_name, season, language): return { "art": self._extract_art( self.get_json_cached( "series/{}/images/query".format(tvdb_id), keyType=art_name, language=language, ), language, art_name, season, ) } def _get_episode_info(self, tvdb_id, season, episode, language): result = self.get_json_cached( "series/{}/episodes/query".format(tvdb_id), airedSeason=season, airedEpisode=episode, language=language, ) if result is None: return None if isinstance(result, (list, set)): return next(iter(result)) return result @wrap_tvdb_object def get_episode(self, tvdb_id, season, episode): item = {} thread_pool = ThreadPool() for language in self.languages: thread_pool.put(self._get_episode_info, tvdb_id, season, episode, language) item = tools.smart_merge_dictionary(item, thread_pool.wait_completion()) if not item or len(item.keys()) == 0: return None return item @wrap_tvdb_object def get_episode_rating(self, tvdb_id, season, episode): item = self._get_episode_info(tvdb_id, season, episode, None) if not item or len(item.keys()) == 0: return None return {"info": tools.filter_dictionary(item["info"], "rating")} def _get_absolute_image_path(self, relative_path): if not relative_path: return None if self.preferred_artwork_size > 1: splitted_path = relative_path.split(".") relative_path = "{}_t.{}".format(splitted_path[0], splitted_path[1]) return "/".join( [self.imageBaseUrl.strip("/"), relative_path.strip("/")])
def __init__(self): self.api_key = g.get_setting("omdb.apikey", None) self.omdb_support = False if not self.api_key else True self.normalization = [ ( "@title", ("title", "sorttitle"), lambda d: d if not self._is_value_none(d) else None, ), ("@rated", "mpaa", lambda d: d if not self._is_value_none(d) else None), ( "@released", ("premiered", "aired"), lambda d: g.validate_date(d) if not self._is_value_none(d) else None, ), ( "@runtime", "duration", lambda d: int(d[:-4]) * 60 if not self._is_value_none(d) and len(d) > 4 and d[:-4].isdigit() else None, ), ( "@genre", "genre", lambda d: sorted(set(x.strip() for x in d.split(","))) if not self._is_value_none(d) else None, ), ( "@director", "director", lambda d: sorted( set( re.sub(r"\(.*?\)", "", x).strip() for x in d.split(","))) if not self._is_value_none(d) else None, ), ( "@writer", "writer", lambda d: sorted( set( re.sub(r"\(.*?\)", "", x).strip() for x in d.split(","))) if not self._is_value_none(d) else None, ), ("@plot", ("plot", "overview", "plotoutline"), None), ( "@country", "country", lambda d: d if not self._is_value_none(d) else None, ), ("@imdbID", ("imdbnumber", "imdb_id"), None), ( None, "rating.imdb", ( ("@imdbRating", "@imdbVotes"), lambda a, c: { "rating": tools.safe_round(tools.get_clean_number(a), 2 ), "votes": tools.get_clean_number(c), } if not self._is_value_none(a) and not self. _is_value_none(c) else None, ), ), ( "@Production", "studio", lambda d: d if not self._is_value_none(d) else None, ), ("@awards", "awards", lambda d: d if not self._is_value_none(d) else None), ( "@awards", "oscar_wins", lambda d: self._extract_awards(d, ("Won", "Oscar")), ), ( "@awards", "oscar_nominations", lambda d: self._extract_awards(d, ("Nominated for", "Oscar")), ), ( "@awards", "award_wins", lambda d: self._extract_awards(d, ("Another", "wins"), ("", "wins")), ), ( "@awards", "award_nominations", lambda d: self._extract_awards(d, ("wins &", "nominations"), ("", "nominations")), ), ( "@metascore", "metacritic_rating", lambda d: d if not self._is_value_none(d) else None, ), ( "@tomatoMeter", "rottentomatoes_rating", lambda d: d if not self._is_value_none(d) else None, ), ( "@tomatoImage", "rottentomatoes_image", lambda d: d if not self._is_value_none(d) else None, ), ( "@tomatoReviews", "rottentomatoes_reviewstotal", lambda d: tools.get_clean_number(d) if not self._is_value_none(d) else None, ), ( "@tomatoFresh", "rottentomatoes_reviewsfresh", lambda d: tools.get_clean_number(d) if not self._is_value_none(d) else None, ), ( "@tomatoRotten", "rottentomatoes_reviewsrotten", lambda d: tools.get_clean_number(d) if not self._is_value_none(d) else None, ), ( "@tomatoConsensus", "rottentomatoes_consensus", lambda d: d if not self._is_value_none(d) else None, ), ( "@tomatoUserMeter", "rottentomatoes_usermeter", lambda d: d if not self._is_value_none(d) else None, ), ( "@tomatoUserReviews", "rottentomatoes_userreviews", lambda d: tools.get_clean_number(d) if not self._is_value_none(d) else None, ), ]