Example #1
0
 def insert_trakt_episodes(self, episodes):
     g.log("Inserting episodes into sync database: {}".format(
         [i.get("trakt_id") for i in episodes]))
     get = MetadataHandler.get_trakt_info
     self.execute_sql(
         self.upsert_episode_query,
         ((
             i.get("trakt_id"),
             i.get("trakt_show_id"),
             i.get("trakt_season_id"),
             get(i, "playcount"),
             get(i, "collected"),
             tools.validate_date(get(i, "aired")),
             tools.validate_date(get(i, "dateadded")),
             get(i, "season"),
             get(i, "episode"),
             get(i, "tmdb_id"),
             get(i, "tvdb_id"),
             get(i, "imdb_id"),
             None,
             None,
             None,
             self._create_args(i),
             tools.validate_date(get(i, "last_watched_at")),
             tools.validate_date(get(i, "collected_at")),
             None,
             i.get("trakt_id"),
         ) for i in episodes),
     )
     self.save_to_meta_table(episodes, "episodes", "trakt", "trakt_id")
Example #2
0
 def insert_trakt_shows(self, shows):
     g.log("Inserting Shows into sync database: {}".format(
         [i.get("trakt_id") for i in shows]))
     get = MetadataHandler.get_trakt_info
     self.execute_sql(
         self.upsert_show_query,
         ((
             i.get("trakt_id"),
             None,
             None,
             None,
             tools.validate_date(get(i, "aired")),
             tools.validate_date(get(i, "dateadded")),
             get(i, "tmdb_id"),
             get(i, "tvdb_id"),
             get(i, "imdb_id"),
             None,
             get(i, "season_count"),
             get(i, "episode_count"),
             self._create_args(i),
             get(i, "is_airing"),
             i.get("trakt_id"),
         ) for i in shows),
     )
     self.save_to_meta_table(shows, "shows", "trakt", "trakt_id")
Example #3
0
 def insert_trakt_movies(self, movies):
     g.log("Inserting Movies into sync database: {}".format(
         [i.get("trakt_id") for i in movies]))
     get = MetadataHandler.get_trakt_info
     self.execute_sql(
         self.upsert_movie_query,
         ((
             i.get("trakt_id"),
             None,
             None,
             None,
             get(i, "collected"),
             get(i, "watched"),
             tools.validate_date(get(i, "aired")),
             tools.validate_date(get(i, "dateadded")),
             get(i, "tmdb_id"),
             get(i, "imdb_id"),
             None,
             self._create_args(i),
             tools.validate_date(get(i, "collected_at")),
             tools.validate_date(get(i, "last_watched_at")),
             i.get("trakt_id"),
         ) for i in movies),
     )
     self.save_to_meta_table(movies, "movies", "trakt", "trakt_id")
Example #4
0
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("/")]
        )
Example #5
0
    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: tools.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) 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))
Example #6
0
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: tools.validate_date(t)),
        ("overview", ("plot", "plotoutline"), None),
        ("mediatype", "mediatype", None),
    ]

    show_normalization = tools.extend_array(
        [
            (
                "firstAired",
                "year",
                lambda t: tools.validate_date(t)[:4]
                if tools.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),
            ("added", "dateadded", 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",
    }

    session = requests.Session()
    retries = Retry(total=5,
                    backoff_factor=0.1,
                    status_forcelist=[500, 502, 503, 504])
    session.mount("https://", HTTPAdapter(max_retries=retries))

    imageBaseUrl = "https://www.thetvdb.com/banners/"
    _threading_lock = threading.Lock()

    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"
        },
    ]

    def __init__(self):
        self.apiKey = g.get_setting("tvdb.apikey", "43VPI0R8323FB7TI")
        self.jwToken = g.get_setting("tvdb.jw")
        self.tokenExpires = g.get_float_setting("tvdb.expiry")
        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 _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
        with GlobalLock(self.__class__.__name__, self._threading_lock, True,
                        self.jwToken) as lock:
            if lock.runned_once():
                return
            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:
                g.log("Failed to refresh Tvdb Access Token", "error")
                return

    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):
        with GlobalLock(self.__class__.__name__, self._threading_lock,
                        True) as lock:
            if lock.runned_once():
                return
            response = self.session.post(
                self.baseUrl + "login",
                json={
                    "apikey": self.apiKey
                },
                headers=self._get_headers(),
            ).json()
            self._save_settings(response)

    @tvdb_guard_response
    def get(self, url, **params):
        if "language" in params:
            language = params.pop("language")
        else:
            language = None
        return self.session.get(
            self.baseUrl + url,
            params=params,
            headers=self._get_headers(language),
            timeout=3,
        )

    @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(
            "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("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(
                    "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("series/{}".format(tvdb_id), language=language)

    def _get_series_cast(self, tvdb_id):
        return {
            "cast":
            self._handle_cast(self.get_json(
                "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(
            "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("/")])