Beispiel #1
0
    def _group_method_zero(self):
        sorted_list = []

        for resolution in self.resolution_list:
            sorted_list += self._resolution_sort_helper(
                resolution, self._resolution_lambda,
                tools.extend_array(self.torrent_list, self.cloud_files))
            sorted_list += self._resolution_sort_helper(
                resolution, self._resolution_lambda, self.hoster_list)

            for file in self.hoster_list:
                if ("debrid_provider" not in file and file.get("direct")
                        and file.get("quality") == resolution):
                    sorted_list.append(file)

        for resolution in self.resolution_list:
            sorted_list += self._resolution_sort_helper(
                resolution, self._resolution_lambda, self.hoster_list)

            for file in self.hoster_list:
                if ("debrid_provider" not in file and file.get("direct")
                        and file.get("quality") == resolution):
                    sorted_list.append(file)

        return sorted_list
Beispiel #2
0
    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]
Beispiel #3
0
 def _when_list_extend(possible_array, value):
     result = value
     if isinstance(possible_array, list):
         result = sorted(
             OrderedDict.fromkeys(tools.extend_array(possible_array, value))
         )
     if isinstance(result, list) and len(result) == 0:
         result = None
     return result
Beispiel #4
0
    def _group_method_one(self):
        sorted_list = []
        for resolution in self.resolution_list:
            sorted_list += self._resolution_sort_helper(
                resolution, self._resolution_lambda, self.hoster_list
                )

            for file in self.hoster_list:
                if "debrid_provider" not in file and file["quality"] == resolution and file.get("direct"):
                    sorted_list.append(file)

        sorted_list += self._stacked_for_loops_filter(
            [self.resolution_list, self.debrid_priorities, tools.extend_array(self.torrent_list, self.cloud_files)],
            [self._resolution_lambda],
            )

        return sorted_list
Beispiel #5
0
class FanartTv(ApiBase):
    base_url = "http://webservice.fanart.tv/v3/"
    api_key = "dfe6380e34f49f9b2b9518184922b49c"
    session = requests.Session()
    retries = Retry(total=5,
                    backoff_factor=0.1,
                    status_forcelist=[500, 502, 503, 504])
    session.mount('http://', HTTPAdapter(max_retries=retries))

    http_codes = {200: 'Success', 404: 'Not Found'}
    normalization = [('name', ('title', 'sorttitle'), None),
                     ('tmdb_id', 'tmdb_id', None),
                     ('imdb_id', 'imdb_id', None), ('art', 'art', None)]

    show_normalization = tools.extend_array([
        ('thetvdb_id', 'tvdb_id', None),
    ], normalization)
    meta_objects = {
        'movie': normalization,
        'season': show_normalization,
        'tvshow': show_normalization
    }

    def __init__(self):
        self.language = g.get_language_code()
        self.client_key = g.get_setting('fanart.apikey')
        self.fanart_support = False if not self.client_key else True
        self.headers = {'client-key': self.client_key, 'api-key': self.api_key}

        self.meta_hash = tools.md5_hash([
            self.language, self.fanart_support, self.normalization,
            self.show_normalization, self.meta_objects, self.base_url
        ])

    @staticmethod
    def build_image(url, art, image):
        result = {
            'url': url,
            'rating': 5.25 + int(image['likes']) / float(5.0),
            'size': FanartTv._get_image_size(art),
            'language': FanartTv._get_image_language(art, image)
        }
        return result

    @staticmethod
    def _get_image_size(art):
        if art in ('hdtvlogo', 'hdclearart', 'hdmovielogo', 'hdmovieclearart'):
            return 800
        elif art in ('clearlogo', 'clearart', 'movielogo', 'movieart',
                     'musiclogo'):
            return 400
        elif art in ('tvbanner', 'seasonbanner', 'moviebanner'):
            return 1000
        elif art in ('showbackground', 'moviebackground'):
            return 1920
        elif art in ('tvposter', 'seasonposter', 'movieposter'):
            return 1426
        elif art in ('tvthumb', 'seasonthumb'):
            return 500
        elif art == 'characterart':
            return 512
        elif art == 'moviethumb':
            return 1000
        return 0

    @staticmethod
    def _get_image_language(art, image):
        if 'lang' not in image:
            return None
        return image['lang'] if image['lang'] not in ('', '00') else None

    @fanart_guard_response
    def get(self, url, **params):
        if not self.fanart_support:
            return None
        return self.session.get(tools.urljoin(self.base_url, url),
                                params=params,
                                headers=self.headers,
                                timeout=3)

    def get_json(self, url, **params):
        if not self.fanart_support:
            return None
        response = self.get(url)
        if response is None:
            return None
        try:
            return self._handle_response(response.json(), params.pop('type'),
                                         params.pop('season', None))
        except (ValueError, AttributeError):
            g.log(
                'Failed to receive JSON from FanartTv response - response: {}'.
                format(response), 'error')
            return None

    @wrap_fanart_object
    def get_movie(self, tmdb_id):
        if not self.fanart_support:
            return None
        return self.get_json('movies/{}'.format(tmdb_id), type='movie')

    @wrap_fanart_object
    def get_show(self, tvdb_id):
        if not self.fanart_support:
            return None
        return self.get_json('tv/{}'.format(tvdb_id), type='tvshow')

    @use_cache()
    @wrap_fanart_object
    def get_season(self, tvdb_id, season):
        return self.get_json('tv/{}'.format(tvdb_id),
                             type='season',
                             season=season)

    @handle_single_item_or_list
    def _handle_response(self, response, type, season):
        result = {}
        result.update({'art': self._handle_art(response, type, season)})
        result.update(
            {'info': self._normalize_info(self.meta_objects[type], response)})
        return result

    def _handle_art(self, item, type, season=None):
        meta = {}
        if type == 'movie':
            meta.update(
                self.create_meta_data(item, 'clearlogo',
                                      ['movielogo', 'hdmovielogo']))
            meta.update(self.create_meta_data(item, 'discart', ['moviedisc']))
            meta.update(
                self.create_meta_data(item, 'clearart',
                                      ['movieart', 'hdmovieclearart']))
            meta.update(
                self.create_meta_data(item, 'characterart', ['characterart']))
            meta.update(
                self.create_meta_data(item,
                                      'keyart', ['movieposter'],
                                      selector=lambda n, i: self.
                                      _get_image_language(n, i) is None))
            meta.update(
                self.create_meta_data(item,
                                      'poster', ['movieposter'],
                                      selector=lambda n, i: self.
                                      _get_image_language(n, i) is not None))
            meta.update(
                self.create_meta_data(item, 'fanart', ['moviebackground']))
            meta.update(self.create_meta_data(item, 'banner', ['moviebanner']))
            meta.update(
                self.create_meta_data(item, 'landscape', ['moviethumb']))
        elif type == 'tvshow':
            meta.update(
                self.create_meta_data(item, 'clearlogo',
                                      ['hdtvlogo', 'clearlogo']))
            meta.update(
                self.create_meta_data(item, 'clearart',
                                      ['hdclearart', 'clearart']))
            meta.update(
                self.create_meta_data(item, 'characterart', ['characterart']))
            meta.update(
                self.create_meta_data(item,
                                      'keyart', ['tvposter'],
                                      selector=lambda n, i: self.
                                      _get_image_language(n, i) is None))
            meta.update(
                self.create_meta_data(item,
                                      'poster', ['tvposter'],
                                      selector=lambda n, i: self.
                                      _get_image_language(n, i) is not None))
            meta.update(
                self.create_meta_data(item, 'fanart', ['showbackground']))
            meta.update(self.create_meta_data(item, 'banner', ['tvbanner']))
            meta.update(self.create_meta_data(item, 'landscape', ['tvthumb']))
        elif type == 'season':
            meta.update(
                self.create_meta_data(item, 'clearlogo',
                                      ['hdtvlogo', 'clearlogo'], season))
            meta.update(
                self.create_meta_data(item, 'clearart',
                                      ['hdclearart', 'clearart'], season))
            meta.update(
                self.create_meta_data(item, 'characterart', ['characterart'],
                                      season))
            meta.update(
                self.create_meta_data(item, 'landscape', ['seasonthumb'],
                                      season))
            meta.update(
                self.create_meta_data(item, 'banner', ['seasonbanner'],
                                      season))
            meta.update(
                self.create_meta_data(
                    item, 'poster', ['seasonposter'], season,
                    lambda n, i: self._get_image_language(n, i) is not None))
            meta.update(
                self.create_meta_data(
                    item, 'keyart', ['seasonposter'], season,
                    lambda n, i: self._get_image_language(n, i) is None))
            meta.update(
                self.create_meta_data(item, 'fanart',
                                      ['showbackground-season'], season))
        return meta

    def create_meta_data(self,
                         art,
                         dict_name,
                         art_names,
                         season=None,
                         selector=None):
        art_list = []
        for art_item, name in [(art.get(name), name) for name in art_names]:
            if art_item is None:
                continue
            art_list.extend(
                self.build_image(item['url'], name, item) for item in art_item
                if (selector is None or selector(name, item)) and (
                    season is None or item.get('season', 'all') == 'all'
                    or int(item.get('season', 0)) == season))
        if len(art_list) > 0:
            return {dict_name: art_list}
        else:
            return {}
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("/")]
        )
Beispiel #7
0
    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")
Beispiel #8
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: 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("/")])