mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-01 00:43:37 +00:00
4e52d2da08
Fix person image scaler Change make a unified generator for the p_chars var in persons search Change add plays_self to TMDB, TVmaze, Trakt Fix TMDb person search page for shows without first aired date (future shows for example) Add also display guest appearances in TVmaze person search Add parsing of vote_average / rating for main cast Add genres to person show data Fix, auto close qTip if mouse is over nav menu Change add rating, vote_average, vote_count, popularity to TMDB person characters shows Change TVmaze character name TBA|D to '' Change add person gender to Trakt
1693 lines
65 KiB
Python
1693 lines
65 KiB
Python
import copy
|
|
import datetime
|
|
import diskcache
|
|
import itertools
|
|
import logging
|
|
import threading
|
|
import shutil
|
|
import time
|
|
from collections import deque
|
|
from exceptions_helper import ex
|
|
|
|
from six import integer_types, iteritems, iterkeys, string_types, text_type
|
|
from typing import Callable
|
|
|
|
|
|
from lib.tvinfo_base.exceptions import *
|
|
from sg_helpers import calc_age, make_path
|
|
|
|
# noinspection PyUnreachableCode
|
|
if False:
|
|
from typing import Any, AnyStr, Dict, List, Optional, Set, Tuple, Union
|
|
str_int = Union[AnyStr, integer_types]
|
|
|
|
TVINFO_TVDB = 1
|
|
TVINFO_TVRAGE = 2
|
|
TVINFO_TVMAZE = 3
|
|
TVINFO_TMDB = 4
|
|
|
|
# old tvdb api - version 1
|
|
# TVINFO_TVDB_V1 = 10001
|
|
|
|
# mapped only source
|
|
TVINFO_IMDB = 100
|
|
TVINFO_TRAKT = 101
|
|
# old tmdb id
|
|
TVINFO_TMDB_OLD = 102
|
|
# end mapped only source
|
|
TVINFO_TVDB_SLUG = 1001
|
|
TVINFO_TRAKT_SLUG = 1101
|
|
|
|
# generic stuff
|
|
TVINFO_SLUG = 100000
|
|
|
|
# social media sources
|
|
TVINFO_TWITTER = 250000
|
|
TVINFO_FACEBOOK = 250001
|
|
TVINFO_INSTAGRAM = 250002
|
|
TVINFO_WIKIPEDIA = 250003
|
|
TVINFO_REDDIT = 250004
|
|
TVINFO_YOUTUBE = 250005
|
|
TVINFO_WIKIDATA = 250006
|
|
TVINFO_TIKTOK = 250007
|
|
TVINFO_LINKEDIN = 25008
|
|
TVINFO_OFFICIALSITE = 250009
|
|
TVINFO_FANSITE = 250010
|
|
|
|
tv_src_names = {
|
|
TVINFO_TVDB: 'tvdb',
|
|
TVINFO_TVRAGE: 'tvrage',
|
|
TVINFO_TVMAZE: 'tvmaze',
|
|
|
|
10001: 'tvdb v1',
|
|
TVINFO_IMDB: 'imdb',
|
|
TVINFO_TRAKT: 'trakt',
|
|
TVINFO_TMDB: 'tmdb',
|
|
TVINFO_TVDB_SLUG: 'tvdb slug',
|
|
TVINFO_TRAKT_SLUG: 'trakt slug',
|
|
|
|
TVINFO_SLUG: 'generic slug',
|
|
|
|
TVINFO_TWITTER: 'twitter',
|
|
TVINFO_FACEBOOK: 'facebook',
|
|
TVINFO_INSTAGRAM: 'instagram',
|
|
TVINFO_WIKIPEDIA: 'wikipedia',
|
|
TVINFO_REDDIT: 'reddit',
|
|
TVINFO_YOUTUBE: 'youtube',
|
|
TVINFO_WIKIDATA: 'wikidata',
|
|
TVINFO_TIKTOK: 'tiktok',
|
|
TVINFO_LINKEDIN: 'linkedin',
|
|
TVINFO_OFFICIALSITE: 'officialsite',
|
|
TVINFO_FANSITE: 'fansite'
|
|
|
|
}
|
|
|
|
TVINFO_MID_SEASON_FINALE = 1
|
|
TVINFO_SEASON_FINALE = 2
|
|
TVINFO_SERIES_FINALE = 3
|
|
|
|
final_types = {
|
|
TVINFO_MID_SEASON_FINALE: 'mid-season',
|
|
TVINFO_SEASON_FINALE: 'season',
|
|
TVINFO_SERIES_FINALE: 'series'
|
|
}
|
|
|
|
log = logging.getLogger('TVInfo')
|
|
log.addHandler(logging.NullHandler())
|
|
TVInfoShowContainer = {} # type: Union[ShowContainer, Dict]
|
|
|
|
|
|
class ShowContainer(dict):
|
|
"""Simple dict that holds a series of Show instances
|
|
"""
|
|
|
|
def __init__(self, **kwargs):
|
|
super(ShowContainer, self).__init__(**kwargs)
|
|
# limit caching of TVInfoShow objects to 15 minutes
|
|
self.max_age = 900 # type: integer_types
|
|
self.lock = threading.RLock()
|
|
|
|
def __setitem__(self, k, v):
|
|
super(ShowContainer, self).__setitem__(k, (v, time.time()))
|
|
|
|
def __getitem__(self, k):
|
|
return super(ShowContainer, self).__getitem__(k)[0]
|
|
|
|
def cleanup_old(self):
|
|
"""
|
|
remove entries that are older than max_age
|
|
"""
|
|
acquired_lock = self.lock.acquire(False)
|
|
if acquired_lock:
|
|
try:
|
|
current_time = time.time()
|
|
for k, v in list(self.items()):
|
|
if self.max_age < current_time - v[1]:
|
|
lock_acquired = self[k].lock.acquire(False)
|
|
if lock_acquired:
|
|
try:
|
|
del self[k]
|
|
except (BaseException, Exception):
|
|
try:
|
|
self[k].lock.release()
|
|
except RuntimeError:
|
|
pass
|
|
finally:
|
|
self.lock.release()
|
|
|
|
def __str__(self):
|
|
nr_shows = len(self)
|
|
return '<ShowContainer (containing %s Show%s)>' % (nr_shows, ('s', '')[1 == nr_shows])
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class TVInfoIDs(object):
|
|
def __init__(
|
|
self,
|
|
tvdb=None, # type: integer_types
|
|
tmdb=None, # type: integer_types
|
|
tvmaze=None, # type: integer_types
|
|
imdb=None, # type: integer_types
|
|
trakt=None, # type: integer_types
|
|
rage=None, # type: integer_types
|
|
ids=None # type: Dict[int, integer_types]
|
|
):
|
|
ids = ids or {}
|
|
self.tvdb = tvdb or ids.get(TVINFO_TVDB)
|
|
self.tmdb = tmdb or ids.get(TVINFO_TMDB)
|
|
self.tvmaze = tvmaze or ids.get(TVINFO_TVMAZE)
|
|
self.imdb = imdb or ids.get(TVINFO_IMDB)
|
|
self.trakt = trakt or ids.get(TVINFO_TRAKT)
|
|
self.rage = rage or ids.get(TVINFO_TVRAGE)
|
|
|
|
def __getitem__(self, key):
|
|
return {TVINFO_TVDB: self.tvdb, TVINFO_TMDB: self.tmdb, TVINFO_TVMAZE: self.tvmaze,
|
|
TVINFO_IMDB: self.imdb, TVINFO_TRAKT: self.trakt, TVINFO_TVRAGE: self.rage}.get(key)
|
|
|
|
def __setitem__(self, key, value):
|
|
self.__dict__[{
|
|
TVINFO_TVDB: 'tvdb', TVINFO_TMDB: 'tmdb', TVINFO_TVMAZE: 'tvmaze',
|
|
TVINFO_IMDB: 'imdb', TVINFO_TRAKT: 'trakt', TVINFO_TVRAGE: 'rage'
|
|
}[key]] = value
|
|
|
|
def get(self, key):
|
|
return self.__getitem__(key)
|
|
|
|
def keys(self):
|
|
for k, v in iter(((TVINFO_TVDB, self.tvdb), (TVINFO_TMDB, self.tmdb), (TVINFO_TVMAZE, self.tvmaze),
|
|
(TVINFO_IMDB, self.imdb), (TVINFO_TRAKT, self.trakt), (TVINFO_TVRAGE, self.rage))):
|
|
if None is not v:
|
|
yield k
|
|
|
|
def __iter__(self):
|
|
for s, v in iter(((TVINFO_TVDB, self.tvdb), (TVINFO_TMDB, self.tmdb), (TVINFO_TVMAZE, self.tvmaze),
|
|
(TVINFO_IMDB, self.imdb), (TVINFO_TRAKT, self.trakt), (TVINFO_TVRAGE, self.rage))):
|
|
if None is not v:
|
|
yield s, v
|
|
|
|
def __len__(self):
|
|
counter = itertools.count()
|
|
deque(zip(self.__iter__(), counter), maxlen=0) # (consume at C speed)
|
|
return next(counter)
|
|
|
|
def __str__(self):
|
|
return ', '.join('%s: %s' % (tv_src_names.get(k, k), v) for k, v in self.__iter__())
|
|
|
|
def __eq__(self, other):
|
|
return self.__dict__ == other.__dict__
|
|
|
|
__repr__ = __str__
|
|
iteritems = __iter__
|
|
items = __iter__
|
|
iterkeys = keys
|
|
|
|
|
|
class TVInfoSocialIDs(object):
|
|
def __init__(
|
|
self,
|
|
twitter=None, # type: str_int
|
|
instagram=None, # type: str_int
|
|
facebook=None, # type: str_int
|
|
wikipedia=None, # type: str_int
|
|
ids=None, # type: Dict[int, str_int]
|
|
reddit=None, # type: str_int
|
|
youtube=None, # type: AnyStr
|
|
wikidata=None, # type: AnyStr
|
|
tiktok=None, # type: AnyStr
|
|
linkedin=None, # type: AnyStr
|
|
fansite=None # type: AnyStr
|
|
):
|
|
ids = ids or {}
|
|
self.twitter = twitter or ids.get(TVINFO_TWITTER)
|
|
self.instagram = instagram or ids.get(TVINFO_INSTAGRAM)
|
|
self.facebook = facebook or ids.get(TVINFO_FACEBOOK)
|
|
self.wikipedia = wikipedia or ids.get(TVINFO_WIKIPEDIA)
|
|
self.reddit = reddit or ids.get(TVINFO_REDDIT)
|
|
self.youtube = youtube or ids.get(TVINFO_YOUTUBE)
|
|
self.wikidata = wikidata or ids.get(TVINFO_WIKIDATA)
|
|
self.tiktok = tiktok or ids.get(TVINFO_TIKTOK)
|
|
self.linkedin = linkedin or ids.get(TVINFO_LINKEDIN)
|
|
self.fansite = fansite or ids.get(TVINFO_FANSITE)
|
|
|
|
def __getitem__(self, key):
|
|
return {TVINFO_TWITTER: self.twitter, TVINFO_INSTAGRAM: self.instagram, TVINFO_FACEBOOK: self.facebook,
|
|
TVINFO_WIKIDATA: self.wikidata, TVINFO_WIKIPEDIA: self.wikipedia, TVINFO_REDDIT: self.reddit,
|
|
TVINFO_TIKTOK: self.tiktok, TVINFO_LINKEDIN: self.linkedin, TVINFO_FANSITE: self.fansite,
|
|
TVINFO_YOUTUBE: self.youtube}.get(key)
|
|
|
|
def __setitem__(self, key, value):
|
|
self.__dict__[{
|
|
TVINFO_TWITTER: 'twitter', TVINFO_INSTAGRAM: 'instagram', TVINFO_FACEBOOK: 'facebook',
|
|
TVINFO_WIKIPEDIA: 'wikipedia', TVINFO_REDDIT: 'reddit', TVINFO_YOUTUBE: 'youtube',
|
|
TVINFO_WIKIDATA: 'wikidata', TVINFO_TIKTOK: 'tiktok', TVINFO_LINKEDIN: 'linkedin', TVINFO_FANSITE: 'fansite'
|
|
}[key]] = value
|
|
|
|
def get(self, key):
|
|
return self.__getitem__(key)
|
|
|
|
def keys(self):
|
|
for k, v in iter(((TVINFO_TWITTER, self.twitter), (TVINFO_INSTAGRAM, self.instagram),
|
|
(TVINFO_FACEBOOK, self.facebook), (TVINFO_TIKTOK, self.tiktok),
|
|
(TVINFO_WIKIPEDIA, self.wikipedia), (TVINFO_WIKIDATA, self.wikidata),
|
|
(TVINFO_REDDIT, self.reddit), (TVINFO_YOUTUBE, self.youtube),
|
|
(TVINFO_LINKEDIN, self.linkedin), (TVINFO_FANSITE, self.fansite))):
|
|
if None is not v:
|
|
yield k
|
|
|
|
def __iter__(self):
|
|
for s, v in iter(((TVINFO_TWITTER, self.twitter), (TVINFO_INSTAGRAM, self.instagram),
|
|
(TVINFO_FACEBOOK, self.facebook), (TVINFO_TIKTOK, self.tiktok),
|
|
(TVINFO_WIKIPEDIA, self.wikipedia), (TVINFO_WIKIDATA, self.wikidata),
|
|
(TVINFO_REDDIT, self.reddit), (TVINFO_YOUTUBE, self.youtube),
|
|
(TVINFO_LINKEDIN, self.linkedin), (TVINFO_FANSITE, self.fansite))):
|
|
if None is not v:
|
|
yield s, v
|
|
|
|
def __len__(self):
|
|
counter = itertools.count()
|
|
deque(zip(self.__iter__(), counter), maxlen=0) # (consume at C speed)
|
|
return next(counter)
|
|
|
|
def __str__(self):
|
|
return ', '.join('%s: %s' % (tv_src_names.get(k, k), v) for k, v in self.__iter__())
|
|
|
|
def __eq__(self, other):
|
|
return self.__dict__ == other.__dict__
|
|
|
|
__repr__ = __str__
|
|
iteritems = __iter__
|
|
items = __iter__
|
|
iterkeys = keys
|
|
|
|
|
|
class TVInfoImageType(object):
|
|
poster = 1
|
|
banner = 2
|
|
# fanart/background
|
|
fanart = 3
|
|
typography = 4
|
|
other = 10
|
|
# person
|
|
person_poster = 50
|
|
# season
|
|
season_poster = 100
|
|
season_banner = 101
|
|
season_fanart = 103
|
|
# stills
|
|
still = 200
|
|
|
|
reverse_str = {
|
|
poster: 'poster',
|
|
banner: 'banner',
|
|
# fanart/background
|
|
fanart: 'fanart',
|
|
typography: 'typography',
|
|
other: 'other',
|
|
# person
|
|
person_poster: 'person poster',
|
|
# season
|
|
season_poster: 'season poster',
|
|
season_banner: 'season banner',
|
|
season_fanart: 'season fanart',
|
|
# stills
|
|
still: 'still'
|
|
}
|
|
|
|
|
|
class TVInfoImageSize(object):
|
|
original = 1
|
|
medium = 2
|
|
small = 3
|
|
|
|
reverse_str = {
|
|
1: 'original',
|
|
2: 'medium',
|
|
3: 'small'
|
|
}
|
|
|
|
|
|
class TVInfoImage(object):
|
|
def __init__(self, image_type, sizes, img_id=None, main_image=False, type_str='', rating=None, votes=None,
|
|
lang=None, height=None, width=None, aspect_ratio=None, updated_at=None):
|
|
self.img_id = img_id # type: Optional[integer_types]
|
|
self.image_type = image_type # type: integer_types
|
|
self.sizes = sizes # type: Union[TVInfoImageSize, Dict]
|
|
self.type_str = type_str # type: AnyStr
|
|
self.main_image = main_image # type: bool
|
|
self.rating = rating # type: Optional[Union[float, integer_types]]
|
|
self.votes = votes # type: Optional[integer_types]
|
|
self.lang = lang # type: Optional[AnyStr]
|
|
self.height = height # type: Optional[integer_types]
|
|
self.width = width # type: Optional[integer_types]
|
|
self.aspect_ratio = aspect_ratio # type: Optional[Union[float, integer_types]]
|
|
self.updated_at = updated_at # type: Optional[integer_types]
|
|
|
|
def __eq__(self, other):
|
|
return self.__dict__ == other.__dict__
|
|
|
|
def __str__(self):
|
|
return '<TVInfoImage %s [%s]>' % (TVInfoImageType.reverse_str.get(self.image_type, 'unknown'),
|
|
', '.join(TVInfoImageSize.reverse_str.get(s, 'unknown') for s in self.sizes))
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class TVInfoNetwork(object):
|
|
def __init__(self, name, n_id=None, country=None, country_code=None, timezone=None, stream=None, active_date=None,
|
|
inactive_date=None):
|
|
# type: (AnyStr, integer_types, AnyStr, AnyStr, AnyStr, bool, AnyStr, AnyStr) -> None
|
|
self.name = name # type: AnyStr
|
|
self.id = n_id # type: Optional[integer_types]
|
|
self.country = country # type: Optional[AnyStr]
|
|
self.country_code = country_code # type: Optional[AnyStr]
|
|
self.timezone = timezone # type: Optional[AnyStr]
|
|
self.stream = stream # type: Optional[bool]
|
|
self.active_date = active_date # type: Optional[AnyStr]
|
|
self.inactive_date = inactive_date # type: Optional[AnyStr]
|
|
|
|
def __eq__(self, other):
|
|
return self.__dict__ == other.__dict__
|
|
|
|
def __str__(self):
|
|
return '<Network (%s)>' % ', '.join('%s' % s for s in [self.name, self.id, self.country, self.country_code,
|
|
self.timezone] if s)
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class TVInfoShow(dict):
|
|
"""Holds a dict of seasons, and show data.
|
|
"""
|
|
|
|
def __init__(self, show_loaded=True):
|
|
dict.__init__(self)
|
|
self.lock = threading.RLock()
|
|
self.data = {} # type: Dict
|
|
self.ep_loaded = False # type: bool
|
|
self.poster_loaded = False # type: bool
|
|
self.banner_loaded = False # type: bool
|
|
self.fanart_loaded = False # type: bool
|
|
self.season_images_loaded = False # type: bool
|
|
self.seasonwide_images_loaded = False # type: bool
|
|
self.actors_loaded = False # type: bool
|
|
self.show_not_found = False # type: bool
|
|
self.id = None # type: integer_types
|
|
self.ids = TVInfoIDs() # type: TVInfoIDs
|
|
self.social_ids = TVInfoSocialIDs() # type: TVInfoSocialIDs
|
|
self.slug = None # type: Optional[AnyStr]
|
|
self.seriesname = None # type: Optional[AnyStr]
|
|
self.aliases = [] # type: List[AnyStr]
|
|
self.season = None # type: integer_types
|
|
self.classification = None # type: Optional[AnyStr]
|
|
self.genre = None # type: Optional[AnyStr]
|
|
self.genre_list = [] # type: List[AnyStr]
|
|
self.actors = [] # type: List[Dict]
|
|
self.cast = CastList() # type: CastList
|
|
self.crew = CrewList() # type: CrewList
|
|
self.show_type = [] # type: List[AnyStr]
|
|
self.networks = [] # type: List[TVInfoNetwork]
|
|
self.network = None # type: Optional[AnyStr]
|
|
self.network_id = None # type: integer_types
|
|
self.network_timezone = None # type: Optional[AnyStr]
|
|
self.network_country = None # type: Optional[AnyStr]
|
|
self.network_country_code = None # type: Optional[AnyStr]
|
|
self.network_is_stream = None # type: Optional[bool]
|
|
self.runtime = None # type: integer_types
|
|
self.language = None # type: Optional[AnyStr]
|
|
self.spoken_languages = [] # type: List[string_types]
|
|
self.official_site = None # type: Optional[AnyStr]
|
|
self.imdb_id = None # type: Optional[AnyStr]
|
|
self.zap2itid = None # type: Optional[AnyStr]
|
|
self.airs_dayofweek = None # type: Optional[AnyStr]
|
|
self.airs_time = None # type: Optional[AnyStr]
|
|
self.time = None # type: Optional[datetime.time]
|
|
self.firstaired = None # type: Optional[AnyStr]
|
|
self.added = None # type: Optional[AnyStr]
|
|
self.addedby = None # type: Union[integer_types, AnyStr]
|
|
self.siteratingcount = None # type: integer_types
|
|
self.lastupdated = None # type: integer_types
|
|
self.contentrating = None # type: Optional[AnyStr]
|
|
self.rating = None # type: Union[integer_types, float]
|
|
self.status = None # type: Optional[AnyStr]
|
|
self.overview = '' # type: AnyStr
|
|
self.poster = None # type: Optional[AnyStr]
|
|
self.poster_thumb = None # type: Optional[AnyStr]
|
|
self.banner = None # type: Optional[AnyStr]
|
|
self.banner_thumb = None # type: Optional[AnyStr]
|
|
self.fanart = None # type: Optional[AnyStr]
|
|
self.banners = {} # type: Dict
|
|
self.images = {} # type: Dict[TVInfoImageType, List[TVInfoImage]]
|
|
self.updated_timestamp = None # type: Optional[integer_types]
|
|
# special properties for trending, popular, ...
|
|
self.popularity = None # type: Optional[Union[integer_types, float]]
|
|
self.vote_count = None # type: Optional[integer_types]
|
|
self.vote_average = None # type: Optional[Union[integer_types, float]]
|
|
self.origin_countries = [] # type: List[AnyStr]
|
|
self.requested_language = '' # type: AnyStr
|
|
self.alt_ep_numbering = {} # type: Dict[Any, Dict[integer_types, Dict[integer_types, TVInfoEpisode]]]
|
|
self.watcher_count = None # type: integer_types
|
|
self.play_count = None # type: integer_types
|
|
self.collected_count = None # type: integer_types
|
|
self.collector_count = None # type: integer_types
|
|
self.next_season_airdate = None # type: Optional[string_types]
|
|
# trailers dict containing: {language: trailer url} , 'any' for unknown langauge
|
|
self.trailers = {} # type: Dict[string_types, string_types]
|
|
self.show_loaded = show_loaded # type: bool
|
|
self.load_method = None # type: Optional[Callable]
|
|
|
|
def load_data(self):
|
|
if not self.show_loaded and self.id and isinstance(self.load_method, Callable):
|
|
_new_show_data = self.load_method(self.id, load_actors=False)
|
|
if isinstance(_new_show_data, TVInfoShow):
|
|
self.__dict__.update(_new_show_data.__dict__)
|
|
self.show_loaded = True
|
|
|
|
@property
|
|
def seriesid(self):
|
|
# type: (...) -> integer_types
|
|
return self.id
|
|
|
|
@seriesid.setter
|
|
def seriesid(self, val):
|
|
# type: (integer_types) -> None
|
|
self.id = val
|
|
|
|
def __str__(self):
|
|
nr_seasons = len(self)
|
|
return '<Show %r (containing %s season%s)>' % (self.seriesname, nr_seasons, ('s', '')[1 == nr_seasons])
|
|
|
|
def __getattr__(self, key):
|
|
if key in self:
|
|
# Key is an episode, return it
|
|
return self[key]
|
|
|
|
if key in self.data:
|
|
# Non-numeric request is for show-data
|
|
return self.data[key]
|
|
|
|
raise AttributeError
|
|
|
|
def __getitem__(self, key):
|
|
if isinstance(key, string_types) and key in self.__dict__:
|
|
return self.__dict__[key]
|
|
|
|
if key in self:
|
|
# Key is an episode, return it
|
|
return dict.__getitem__(self, key)
|
|
|
|
if key in self.data:
|
|
# Non-numeric request is for show-data
|
|
return dict.__getitem__(self.data, key)
|
|
|
|
# Data wasn't found, raise appropriate error
|
|
if isinstance(key, integer_types) or isinstance(key, string_types) and key.isdigit():
|
|
# Episode number x was not found
|
|
raise BaseTVinfoSeasonnotfound('Could not find season %s' % (repr(key)))
|
|
else:
|
|
# If it's not numeric, it must be an attribute name, which
|
|
# doesn't exist, so attribute error.
|
|
raise BaseTVinfoAttributenotfound('Cannot find attribute %s' % (repr(key)))
|
|
|
|
def get(self, __key, *args):
|
|
try:
|
|
return self.__getitem__(__key)
|
|
except (BaseException, Exception):
|
|
if 0 != len(args):
|
|
return args[0]
|
|
|
|
def __deepcopy__(self, memo):
|
|
cls = self.__class__
|
|
result = cls.__new__(cls)
|
|
memo[id(self)] = result
|
|
for k, v in self.__dict__.items():
|
|
if 'lock' == k:
|
|
setattr(result, k, threading.RLock())
|
|
elif 'load_method' == k:
|
|
setattr(result, k, None)
|
|
else:
|
|
setattr(result, k, copy.deepcopy(v, memo))
|
|
for k, v in self.items():
|
|
result[k] = copy.deepcopy(v, memo)
|
|
if isinstance(k, integer_types):
|
|
setattr(result[k], 'show', result)
|
|
return result
|
|
|
|
def __bool__(self):
|
|
# type: (...) -> bool
|
|
return bool(self.id) or any(iterkeys(self.data))
|
|
|
|
def to_dict(self):
|
|
return self.__dict__.copy()
|
|
|
|
def aired_on(self, date):
|
|
ret = self.search(str(date), 'firstaired')
|
|
if 0 == len(ret):
|
|
raise BaseTVinfoEpisodenotfound('Could not find any episodes that aired on %s' % date)
|
|
return ret
|
|
|
|
def search(self, term=None, key=None):
|
|
"""
|
|
Search all episodes in show. Can search all data, or a specific key (for
|
|
example, episodename)
|
|
|
|
Always returns an array (can be empty). First index contains the first
|
|
match, and so on.
|
|
"""
|
|
results = []
|
|
for cur_season in self.values():
|
|
searchresult = cur_season.search(term=term, key=key)
|
|
if 0 != len(searchresult):
|
|
results.extend(searchresult)
|
|
|
|
return results
|
|
|
|
def __getstate__(self):
|
|
d = dict(self.__dict__)
|
|
for d_a in ('lock', 'load_method'):
|
|
try:
|
|
del d[d_a]
|
|
except (BaseException, Exception):
|
|
pass
|
|
return d
|
|
|
|
def __setstate__(self, d):
|
|
self.__dict__ = d
|
|
self.lock = threading.RLock()
|
|
self.load_method = None
|
|
|
|
__repr__ = __str__
|
|
__nonzero__ = __bool__
|
|
|
|
|
|
class TVInfoSeason(dict):
|
|
def __init__(self, show=None, number=None, **kwargs):
|
|
"""The show attribute points to the parent show
|
|
"""
|
|
super(TVInfoSeason, self).__init__(**kwargs)
|
|
self.show = show # type: TVInfoShow
|
|
self.id = None # type: integer_types
|
|
self.number = number # type: integer_types
|
|
self.name = None # type: Optional[AnyStr]
|
|
self.actors = [] # type: List[Dict]
|
|
self.cast = CastList() # type: Dict[integer_types, TVInfoCharacter]
|
|
self.network = None # type: Optional[AnyStr]
|
|
self.network_id = None # type: Optional[integer_types]
|
|
self.network_timezone = None # type: Optional[AnyStr]
|
|
self.network_country = None # type: Optional[AnyStr]
|
|
self.network_country_code = None # type: Optional[AnyStr]
|
|
self.network_is_stream = None # type: Optional[bool]
|
|
self.ordered = None # type: Optional[integer_types]
|
|
self.start_date = None # type: Optional[AnyStr]
|
|
self.end_date = None # type: Optional[AnyStr]
|
|
self.poster = None # type: Optional[AnyStr]
|
|
self.summery = None # type: Optional[AnyStr]
|
|
self.episode_order = None # type: Optional[integer_types]
|
|
|
|
def __str__(self):
|
|
nr_episodes = len(self)
|
|
return '<Season %s instance (containing %s episode%s)>' % \
|
|
(self.number, nr_episodes, ('s', '')[1 == nr_episodes])
|
|
|
|
def __getattr__(self, episode_number):
|
|
if episode_number in self:
|
|
return self[episode_number]
|
|
raise AttributeError
|
|
|
|
def __getitem__(self, episode_number):
|
|
if episode_number not in self:
|
|
raise BaseTVinfoEpisodenotfound('Could not find episode %s' % (repr(episode_number)))
|
|
else:
|
|
return dict.__getitem__(self, episode_number)
|
|
|
|
def __deepcopy__(self, memo):
|
|
cls = self.__class__
|
|
result = cls.__new__(cls)
|
|
memo[id(self)] = result
|
|
for k, v in self.__dict__.items():
|
|
# noinspection PyArgumentList
|
|
setattr(result, k, copy.deepcopy(v, memo))
|
|
for k, v in self.items():
|
|
result[k] = copy.deepcopy(v, memo)
|
|
if isinstance(k, integer_types):
|
|
setattr(result[k], 'season', result)
|
|
return result
|
|
|
|
def search(self, term=None, key=None):
|
|
"""Search all episodes in season, returns a list of matching Episode
|
|
instances.
|
|
"""
|
|
results = []
|
|
for ep in self.values():
|
|
searchresult = ep.search(term=term, key=key)
|
|
if None is not searchresult:
|
|
results.append(searchresult)
|
|
return results
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class TVInfoEpisode(dict):
|
|
def __init__(self, season=None, show=None, **kwargs):
|
|
"""The season attribute points to the parent season
|
|
"""
|
|
super(TVInfoEpisode, self).__init__(**kwargs)
|
|
self.id = None # type: integer_types
|
|
self.seriesid = None # type: integer_types
|
|
self.season = season # type: TVInfoSeason
|
|
self.seasonnumber = None # type: integer_types
|
|
self.episodenumber = None # type: integer_types
|
|
self.absolute_number = None # type: integer_types
|
|
self.is_special = None # type: Optional[bool]
|
|
self.actors = [] # type: List[Dict]
|
|
self.gueststars = None # type: Optional[AnyStr]
|
|
self.gueststars_list = [] # type: List[AnyStr]
|
|
self.cast = CastList() # type: Dict[integer_types, TVInfoCharacter]
|
|
self.directors = [] # type: List[AnyStr]
|
|
self.writer = None # type: Optional[AnyStr]
|
|
self.writers = [] # type: List[AnyStr]
|
|
self.crew = CrewList() # type: CrewList
|
|
self.episodename = None # type: Optional[AnyStr]
|
|
self.overview = '' # type: AnyStr
|
|
self.language = {'episodeName': None, 'overview': None} # type: Dict[AnyStr, Optional[AnyStr]]
|
|
self.productioncode = None # type: Optional[AnyStr]
|
|
self.showurl = None # type: Optional[AnyStr]
|
|
self.lastupdated = None # type: integer_types
|
|
self.dvddiscid = None # type: Optional[AnyStr]
|
|
self.dvd_season = None # type: integer_types
|
|
self.dvd_episodenumber = None # type: integer_types
|
|
self.dvdchapter = None # type: integer_types
|
|
self.firstaired = None # type: Optional[AnyStr]
|
|
self.airtime = None # type: Optional[datetime.time]
|
|
self.runtime = 0 # type: integer_types
|
|
self.timestamp = None # type: Optional[integer_types]
|
|
self.network = None # type: Optional[AnyStr]
|
|
self.network_id = None # type: integer_types
|
|
self.network_timezone = None # type: Optional[AnyStr]
|
|
self.network_country = None # type: Optional[AnyStr]
|
|
self.network_country_code = None # type: Optional[AnyStr]
|
|
self.network_is_stream = None # type: Optional[bool]
|
|
self.filename = None # type: Optional[AnyStr]
|
|
self.lastupdatedby = None # type: Union[integer_types, AnyStr]
|
|
self.airsafterseason = None # type: integer_types
|
|
self.airsbeforeseason = None # type: integer_types
|
|
self.airsbeforeepisode = None # type: integer_types
|
|
self.imdb_id = None # type: Optional[AnyStr]
|
|
self.contentrating = None # type: Optional[AnyStr]
|
|
self.thumbadded = None # type: Optional[AnyStr]
|
|
self.rating = None # type: Union[integer_types, float]
|
|
self.vote_count = None # type: integer_types
|
|
self.siteratingcount = None # type: integer_types
|
|
self.show = show # type: Optional[TVInfoShow]
|
|
self.alt_nums = {} # type: Dict[AnyStr, Dict[integer_types, integer_types]]
|
|
self.finale_type = None # type: Optional[integer_types]
|
|
|
|
def __str__(self):
|
|
show_name = (self.show and self.show.seriesname and '<Show %s> - ' % self.show.seriesname) or ''
|
|
seasno, epno = int(getattr(self, 'seasonnumber', 0) or 0), int(getattr(self, 'episodenumber', 0) or 0)
|
|
epname = getattr(self, 'episodename', '')
|
|
finale_str = (self.finale_type and ' (%s finale)' % final_types.get(self.finale_type).capitalize()) or ''
|
|
if None is not epname:
|
|
return '%s<Episode %02dx%02d - %r%s>' % (show_name, seasno, epno, epname, finale_str)
|
|
else:
|
|
return '%s<Episode %02dx%02d%s>' % (show_name, seasno, epno, finale_str)
|
|
|
|
def __getattr__(self, key):
|
|
if key in self:
|
|
return self[key]
|
|
raise AttributeError
|
|
|
|
def __getitem__(self, key):
|
|
try:
|
|
return dict.__getitem__(self, key)
|
|
except KeyError:
|
|
raise BaseTVinfoAttributenotfound('Cannot find attribute %s' % (repr(key)))
|
|
|
|
def __deepcopy__(self, memo):
|
|
cls = self.__class__
|
|
result = cls.__new__(cls)
|
|
memo[id(self)] = result
|
|
for k, v in self.__dict__.items():
|
|
# noinspection PyArgumentList
|
|
setattr(result, k, copy.deepcopy(v, memo))
|
|
for k, v in self.items():
|
|
result[k] = copy.deepcopy(v, memo)
|
|
return result
|
|
|
|
def __bool__(self):
|
|
# type: (...) -> bool
|
|
return bool(self.id) or bool(self.episodename)
|
|
|
|
def search(self, term=None, key=None):
|
|
"""Search episode data for term, if it matches, return the Episode (self).
|
|
The key parameter can be used to limit the search to a specific element,
|
|
for example, episodename.
|
|
"""
|
|
if None is term:
|
|
raise TypeError('must supply string to search for (contents)')
|
|
|
|
term = text_type(term).lower()
|
|
for cur_key, cur_value in iteritems(self):
|
|
cur_key, cur_value = text_type(cur_key).lower(), text_type(cur_value).lower()
|
|
if None is not key and cur_key != key:
|
|
# Do not search this key
|
|
continue
|
|
if cur_value.find(text_type(term).lower()) > -1:
|
|
return self
|
|
|
|
__unicode__ = __str__
|
|
__repr__ = __str__
|
|
__nonzero__ = __bool__
|
|
|
|
|
|
class Persons(dict):
|
|
"""Holds all Persons instances for a show
|
|
"""
|
|
def __str__(self):
|
|
persons_count = len(self)
|
|
return '<Persons (containing %s Person%s)>' % (persons_count, ('', 's')[1 != persons_count])
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class CastList(Persons):
|
|
def __init__(self, **kwargs):
|
|
super(CastList, self).__init__(**kwargs)
|
|
for t in iterkeys(RoleTypes.reverse):
|
|
if t < RoleTypes.crew_limit:
|
|
self[t] = [] # type: List[TVInfoCharacter]
|
|
|
|
def __str__(self):
|
|
persons_count = []
|
|
for t in iterkeys(RoleTypes.reverse):
|
|
if t < RoleTypes.crew_limit:
|
|
if len(self.get(t, [])):
|
|
persons_count.append('%s: %s' % (RoleTypes.reverse[t], len(self.get(t, []))))
|
|
persons_text = ', '.join(persons_count)
|
|
persons_text = ('0', '(%s)' % persons_text)['' != persons_text]
|
|
return '<Cast (containing %s Person%s)>' % (persons_text, ('', 's')['' != persons_text])
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class CrewList(Persons):
|
|
def __init__(self, **kwargs):
|
|
super(CrewList, self).__init__(**kwargs)
|
|
for t in iterkeys(RoleTypes.reverse):
|
|
if t >= RoleTypes.crew_limit:
|
|
self[t] = [] # type: List[Crew]
|
|
|
|
def __str__(self):
|
|
persons_count = []
|
|
for t in iterkeys(RoleTypes.reverse):
|
|
if t >= RoleTypes.crew_limit:
|
|
if len(self.get(t, [])):
|
|
persons_count.append('%s: %s' % (RoleTypes.reverse[t], len(self.get(t, []))))
|
|
persons_text = ', '.join(persons_count)
|
|
persons_text = ('0', '(%s)' % persons_text)['' != persons_text]
|
|
return '<Crew (containing %s Person%s)>' % (persons_text, ('', 's')['' != persons_text])
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class PersonBase(dict):
|
|
"""Represents a single person. Should contain..
|
|
|
|
id,
|
|
image,
|
|
name,
|
|
role,
|
|
sortorder
|
|
"""
|
|
def __init__(
|
|
self,
|
|
p_id=None, # type: integer_types
|
|
name=None, # type: AnyStr
|
|
image=None, # type: AnyStr
|
|
images=None, # type: List[TVInfoImage]
|
|
gender=None, # type: integer_types
|
|
bio=None, # type: AnyStr
|
|
birthdate=None, # type: datetime.date
|
|
deathdate=None, # type: datetime.date
|
|
country=None, # type: AnyStr
|
|
country_code=None, # type: AnyStr
|
|
country_timezone=None, # type: AnyStr
|
|
ids=None, # type: TVInfoIDs
|
|
thumb_url=None, # type: AnyStr
|
|
**kwargs # type: Dict
|
|
):
|
|
super(PersonBase, self).__init__(**kwargs)
|
|
self.id = p_id # type: Optional[integer_types]
|
|
self.name = name # type: Optional[AnyStr]
|
|
self.image = image # type: Optional[AnyStr]
|
|
self.images = images or [] # type: List[TVInfoImage]
|
|
self.thumb_url = thumb_url # type: Optional[AnyStr]
|
|
self.gender = gender # type: Optional[int]
|
|
self.bio = bio # type: Optional[AnyStr]
|
|
self.birthdate = birthdate # type: Optional[datetime.date]
|
|
self.deathdate = deathdate # type: Optional[datetime.date]
|
|
self.country = country # type: Optional[AnyStr]
|
|
self.country_code = country_code # type: Optional[AnyStr]
|
|
self.country_timezone = country_timezone # type: Optional[AnyStr]
|
|
self.ids = ids or TVInfoIDs() # type: TVInfoIDs
|
|
|
|
def calc_age(self, date=None):
|
|
# type: (Optional[datetime.date]) -> Optional[int]
|
|
return calc_age(self.birthdate, self.deathdate, date)
|
|
|
|
@property
|
|
def age(self):
|
|
# type: (...) -> Optional[int]
|
|
"""
|
|
:return: age of person if birthdate is known, in case of deathdate is known return age of death
|
|
"""
|
|
return self.calc_age()
|
|
|
|
def __bool__(self):
|
|
# type: (...) -> bool
|
|
return bool(self.name)
|
|
|
|
def __str__(self):
|
|
return '<Person "%s">' % self.name
|
|
|
|
__repr__ = __str__
|
|
__nonzero__ = __bool__
|
|
|
|
|
|
class PersonGenders(object):
|
|
unknown = 0
|
|
male = 1
|
|
female = 2
|
|
|
|
named = {'unknown': 0, 'male': 1, 'female': 2}
|
|
reverse = {v: k for k, v in iteritems(named)}
|
|
tmdb_map = {0: unknown, 1: female, 2: male}
|
|
imdb_map = {'female': female, 'male': male}
|
|
tvdb_map = {0: unknown, 1: male, 2: female, 3: unknown} # 3 is technically: other
|
|
trakt_map = {'female': female, 'male': male}
|
|
|
|
|
|
class Crew(PersonBase):
|
|
|
|
def __init__(self, crew_type_name=None, **kwargs):
|
|
super(Crew, self).__init__(**kwargs)
|
|
self.crew_type_name = crew_type_name
|
|
|
|
def __str__(self):
|
|
return '<Crew%s "%s)">' % (('', ('/%s' % self.crew_type_name))[isinstance(self.crew_type_name, string_types)],
|
|
self.name)
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class TVInfoPerson(PersonBase):
|
|
def __init__(
|
|
self,
|
|
p_id=None, # type: integer_types
|
|
name=None, # type: AnyStr
|
|
image=None, # type: Optional[AnyStr]
|
|
images=None, # type: List[TVInfoImage]
|
|
thumb_url=None, # type: AnyStr
|
|
gender=None, # type: integer_types
|
|
bio=None, # type: AnyStr
|
|
birthdate=None, # type: datetime.date
|
|
deathdate=None, # type: datetime.date
|
|
country=None, # type: AnyStr
|
|
country_code=None, # type: AnyStr
|
|
country_timezone=None, # type: AnyStr
|
|
ids=None, # type: TVInfoIDs
|
|
homepage=None, # type: Optional[AnyStr]
|
|
social_ids=None, # type: TVInfoSocialIDs
|
|
birthplace=None, # type: AnyStr
|
|
deathplace=None, # type: AnyStr
|
|
url=None, # type: AnyStr
|
|
characters=None, # type: List[TVInfoCharacter]
|
|
height=None, # type: Union[integer_types, float]
|
|
nicknames=None, # type: Set[AnyStr]
|
|
real_name=None, # type: AnyStr
|
|
akas=None, # type: Set[AnyStr]
|
|
**kwargs # type: Dict
|
|
):
|
|
super(TVInfoPerson, self).__init__(
|
|
p_id=p_id, name=name, image=image, thumb_url=thumb_url, bio=bio, gender=gender,
|
|
birthdate=birthdate, deathdate=deathdate, country=country, images=images,
|
|
country_code=country_code, country_timezone=country_timezone, ids=ids, **kwargs)
|
|
self.credits = [] # type: List
|
|
self.homepage = homepage # type: Optional[AnyStr]
|
|
self.social_ids = social_ids or TVInfoSocialIDs() # type: TVInfoSocialIDs
|
|
self.birthplace = birthplace # type: Optional[AnyStr]
|
|
self.deathplace = deathplace # type: Optional[AnyStr]
|
|
self.nicknames = nicknames or set() # type: Set[AnyStr]
|
|
self.real_name = real_name # type: AnyStr
|
|
self.url = url # type: Optional[AnyStr]
|
|
self.height = height # type: Optional[Union[integer_types, float]]
|
|
self.akas = akas or set() # type: Set[AnyStr]
|
|
self.characters = characters or [] # type: List[TVInfoCharacter]
|
|
|
|
def __str__(self):
|
|
return '<Person "%s">' % self.name
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class TVInfoCharacter(PersonBase):
|
|
def __init__(self, person=None, voice=None, plays_self=None, regular=None, ti_show=None, start_year=None,
|
|
end_year=None, ids=None, name=None, episode_count=None, guest_episodes_numbers=None, **kwargs):
|
|
# type: (List[TVInfoPerson], bool, bool, bool, TVInfoShow, int, int, TVInfoIDs, AnyStr, int, Dict[int, List[int]], ...) -> None
|
|
super(TVInfoCharacter, self).__init__(ids=ids, **kwargs)
|
|
self.person = person # type: List[TVInfoPerson]
|
|
self.voice = voice # type: Optional[bool]
|
|
self.plays_self = plays_self # type: Optional[bool]
|
|
self.regular = regular # type: Optional[bool]
|
|
self.ti_show = ti_show # type: Optional[TVInfoShow]
|
|
self.start_year = start_year # type: Optional[integer_types]
|
|
self.end_year = end_year # type: Optional[integer_types]
|
|
self.name = name # type: Optional[AnyStr]
|
|
self.episode_count = episode_count # type: Optional[int]
|
|
self.guest_episodes_numbers = guest_episodes_numbers or {} # type: Dict[int, List[int]]
|
|
|
|
def __str__(self):
|
|
pn = []
|
|
char_type = ('', ' [Guest]')[False is self.regular]
|
|
char_show = None is not self.ti_show and ' [%s]' % self.ti_show.seriesname
|
|
if None is not self.person:
|
|
for p in self.person:
|
|
if getattr(p, 'name', None):
|
|
pn.append(p.name)
|
|
return '<Character%s "%s%s%s">' % (char_type, self.name, ('', ' - (%s)' % ', '.join(pn))[bool(pn)], char_show)
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class RoleTypes(object):
|
|
# Actor types
|
|
ActorMain = 1
|
|
ActorRecurring = 2
|
|
ActorGuest = 3
|
|
ActorSpecialGuest = 4
|
|
Host = 10
|
|
HostGuest = 11
|
|
Presenter = 12
|
|
PresenterGuest = 13
|
|
Interviewer = 14
|
|
InterviewerGuest = 15
|
|
MusicalGuest = 16
|
|
# Crew types (int's >= crew_limit)
|
|
CrewDirector = 50
|
|
CrewWriter = 51
|
|
CrewProducer = 52
|
|
CrewExecutiveProducer = 53
|
|
CrewCreator = 60
|
|
CrewEditor = 61
|
|
CrewCamera = 62
|
|
CrewMusic = 63
|
|
CrewStylist = 64
|
|
CrewMakeup = 65
|
|
CrewPhotography = 66
|
|
CrewSound = 67
|
|
CrewDesigner = 68
|
|
CrewDeveloper = 69
|
|
CrewAnimation = 70
|
|
CrewVisualEffects = 71
|
|
CrewShowrunner = 72
|
|
CrewOther = 100
|
|
|
|
reverse = {1: 'Main', 2: 'Recurring', 3: 'Guest', 4: 'Special Guest', 10: 'Host', 11: 'Host Guest',
|
|
12: 'Presenter', 13: 'Presenter Guest', 14: 'Interviewer', 15: 'Interviewer Guest',
|
|
16: 'Musical Guest', 50: 'Director', 51: 'Writer', 52: 'Producer', 53: 'Executive Producer',
|
|
60: 'Creator', 61: 'Editor', 62: 'Camera', 63: 'Music', 64: 'Stylist', 65: 'Makeup',
|
|
66: 'Photography', 67: 'Sound', 68: 'Designer', 69: 'Developer', 70: 'Animation',
|
|
71: 'Visual Effects', 72: 'Showrunner', 100: 'Other'}
|
|
crew_limit = 50
|
|
|
|
# just a helper to generate the reverse data
|
|
# def __init__(self):
|
|
# import re
|
|
# {value: re.sub(r'([a-z])([A-Z])', r'\1 \2', name.replace('Actor', '').replace('Crew', ''))
|
|
# for name, value in iteritems(vars(RoleTypes)) if not name.startswith('_')
|
|
# and name not in ('reverse', 'crew_limit')}
|
|
|
|
|
|
crew_type_names = {c.lower(): v for v, c in iteritems(RoleTypes.reverse) if v >= RoleTypes.crew_limit}
|
|
|
|
|
|
class TVInfoSeasonTypes(object):
|
|
default = 'default'
|
|
official = 'official'
|
|
dvd = 'dvd'
|
|
|
|
|
|
class TVInfoBase(object):
|
|
supported_id_searches = []
|
|
supported_person_id_searches = []
|
|
_supported_languages = None
|
|
map_languages = {'cs': 'ces', 'da': 'dan', 'de': 'deu', 'en': 'eng', 'es': 'spa', 'fi': 'fin', 'fr': 'fra',
|
|
'he': 'heb', 'hr': 'hrv', 'hu': 'hun', 'it': 'ita', 'ja': 'jpn', 'ko': 'kor', 'nb': 'nor',
|
|
'nl': 'nld', 'no': 'nor',
|
|
'pl': 'pol', 'pt': 'pot', 'ru': 'rus', 'sk': 'slv', 'sv': 'swe', 'zh': 'zho', '_1': 'srp'}
|
|
reverse_map_languages = {v: k for k, v in iteritems(map_languages)}
|
|
|
|
def __init__(self, banners=False, posters=False, seasons=False, seasonwides=False, fanart=False, actors=False,
|
|
dvdorder=False, *args, **kwargs):
|
|
global TVInfoShowContainer
|
|
if self.__class__.__name__ not in TVInfoShowContainer:
|
|
TVInfoShowContainer[self.__class__.__name__] = ShowContainer()
|
|
self.ti_shows = TVInfoShowContainer[self.__class__.__name__] # type: ShowContainer[integer_types, TVInfoShow]
|
|
self.ti_shows.cleanup_old()
|
|
self.lang = None # type: Optional[AnyStr]
|
|
self.corrections = {} # type: Dict
|
|
self.show_not_found = False # type: bool
|
|
self.not_found = False # type: bool
|
|
self._old_config = None
|
|
self._cachedir = kwargs.get('diskcache_dir') # type: AnyStr
|
|
self.diskcache = diskcache.Cache(directory=self._cachedir, disk_pickle_protocol=2) # type: diskcache.Cache
|
|
self.cache_expire = 60 * 60 * 18 # type: integer_types
|
|
self.search_cache_expire = 60 * 15 # type: integer_types
|
|
self.schedule_cache_expire = 60 * 30 # type: integer_types
|
|
self.config = {
|
|
'apikey': '',
|
|
'debug_enabled': False,
|
|
'custom_ui': None,
|
|
'proxy': None,
|
|
'cache_enabled': False,
|
|
'cache_location': '',
|
|
'valid_languages': [],
|
|
'langabbv_to_id': {},
|
|
'language': 'en',
|
|
'base_url': '',
|
|
'banners_enabled': banners,
|
|
'posters_enabled': posters,
|
|
'seasons_enabled': seasons,
|
|
'seasonwides_enabled': seasonwides,
|
|
'fanart_enabled': fanart,
|
|
'actors_enabled': actors,
|
|
'cache_search': kwargs.get('cache_search'),
|
|
'dvdorder': dvdorder,
|
|
} # type: Dict[AnyStr, Any]
|
|
|
|
def _must_load_data(self, sid, load_episodes, banners, posters, seasons, seasonwides, fanart, actors, lang):
|
|
# type: (integer_types, bool, bool, bool, bool, bool, bool, bool, str) -> bool
|
|
"""
|
|
returns if show data has to be fetched for (extra) data (episodes, images, ...)
|
|
or can taken from self.shows cache
|
|
:param sid: show id
|
|
:param load_episodes: should episodes be loaded
|
|
:param banners: should load banners
|
|
:param posters: should load posters
|
|
:param seasons: should load season images
|
|
:param seasonwides: should load season wide images
|
|
:param fanart: should load fanart
|
|
:param actors: should load actors
|
|
:param lang: requested language
|
|
"""
|
|
if sid not in self.ti_shows or None is self.ti_shows[sid].id or \
|
|
(load_episodes and not getattr(self.ti_shows[sid], 'ep_loaded', False)):
|
|
return True
|
|
_show = self.ti_shows[sid] # type: TVInfoShow
|
|
if _show.requested_language != lang:
|
|
_show.ep_loaded = _show.poster_loaded = _show.banner_loaded = _show.actors_loaded = _show.fanart_loaded = \
|
|
_show.seasonwide_images_loaded = _show.season_images_loaded = False
|
|
return True
|
|
for data_type, en_type, p_type in [(u'poster', 'posters_enabled', posters),
|
|
(u'banner', 'banners_enabled', banners),
|
|
(u'fanart', 'fanart_enabled', fanart),
|
|
(u'season', 'seasons_enabled', seasons),
|
|
(u'seasonwide', 'seasonwides_enabled', seasonwides),
|
|
(u'actors', 'actors_enabled', actors)]:
|
|
if (p_type or self.config.get(en_type, False)) and \
|
|
not getattr(_show, '%s_loaded' % data_type, False):
|
|
return True
|
|
return False
|
|
|
|
def clear_cache(self):
|
|
"""
|
|
Clear cache.
|
|
"""
|
|
try:
|
|
with self.diskcache as dc:
|
|
dc.clear()
|
|
except (BaseException, Exception):
|
|
pass
|
|
|
|
def clean_cache(self):
|
|
"""
|
|
Remove expired items from cache.
|
|
"""
|
|
try:
|
|
with self.diskcache as dc:
|
|
dc.expire()
|
|
except (BaseException, Exception):
|
|
pass
|
|
|
|
def check_cache(self):
|
|
"""
|
|
checks cache
|
|
"""
|
|
try:
|
|
with self.diskcache as dc:
|
|
dc.check()
|
|
except (BaseException, Exception):
|
|
pass
|
|
|
|
def _get_cache_entry(self, key, retry=False):
|
|
# type: (Any, bool) -> Tuple[bool, Any]
|
|
"""
|
|
returns tuple of is_None and value
|
|
:param key:
|
|
:param retry:
|
|
"""
|
|
with self.diskcache as dc:
|
|
try:
|
|
v = dc.get(key)
|
|
return 'None' == v, (v, None)['None' == v]
|
|
except ValueError as e:
|
|
if not retry:
|
|
dc.close()
|
|
try:
|
|
shutil.rmtree(self._cachedir)
|
|
except (BaseException, Exception) as e:
|
|
log.error(ex(e))
|
|
pass
|
|
try:
|
|
make_path(self._cachedir)
|
|
except (BaseException, Exception):
|
|
pass
|
|
return self._get_cache_entry(key, retry=True)
|
|
else:
|
|
log.error('Error getting %s from cache: %s' % (key, ex(e)))
|
|
except (BaseException, Exception) as e:
|
|
log.error('Error getting %s from cache: %s' % (key, ex(e)))
|
|
return False, None
|
|
|
|
def _set_cache_entry(self, key, value, tag=None, expire=None):
|
|
# type: (Any, Any, AnyStr, int) -> None
|
|
try:
|
|
with self.diskcache as dc:
|
|
dc.set(key, (value, 'None')[None is value], expire=expire or self.cache_expire, tag=tag)
|
|
except (BaseException, Exception) as e:
|
|
log.error('Error setting %s to cache: %s' % (key, ex(e)))
|
|
|
|
def get_person(self, p_id, get_show_credits=False, get_images=False, **kwargs):
|
|
# type: (integer_types, bool, bool, Any) -> Optional[TVInfoPerson]
|
|
"""
|
|
get person's data for id or list of matching persons for name
|
|
|
|
:param p_id: persons id
|
|
:param get_show_credits: get show credits
|
|
:param get_images: get images for person
|
|
:return: person object
|
|
"""
|
|
pass
|
|
|
|
def _search_person(self, name=None, ids=None):
|
|
# type: (AnyStr, Dict[integer_types, integer_types]) -> List[TVInfoPerson]
|
|
"""
|
|
search for person by name
|
|
:param name: name to search for
|
|
:param ids: dict of ids to search
|
|
:return: list of found person's
|
|
"""
|
|
return []
|
|
|
|
def search_person(self, name=None, ids=None):
|
|
# type: (AnyStr, Dict[integer_types, integer_types]) -> List[TVInfoPerson]
|
|
"""
|
|
search for person by name
|
|
:param name: name to search for
|
|
:param ids: dict of ids to search
|
|
:return: list of found person's
|
|
"""
|
|
if not name and not ids:
|
|
log.debug('Nothing to search')
|
|
raise BaseTVinfoPersonNotFound('Nothing to search')
|
|
found_persons = []
|
|
if ids:
|
|
if not any(1 for i in ids if i in self.supported_person_id_searches) and not name:
|
|
log.debug('Id type not supported')
|
|
raise BaseTVinfoPersonNotFound('Id type not supported')
|
|
found_persons = self._search_person(name=name, ids=ids)
|
|
elif name:
|
|
found_persons = self._search_person(name=name, ids=ids)
|
|
return found_persons
|
|
|
|
def _get_show_data(self, sid, language, get_ep_info=False, banners=False, posters=False, seasons=False,
|
|
seasonwides=False, fanart=False, actors=False, **kwargs):
|
|
# type: (integer_types, AnyStr, bool, bool, bool, bool, bool, bool, bool, Optional[Any]) -> bool
|
|
"""
|
|
internal function that should be overwritten in class to get data for given show id
|
|
:param sid: show id
|
|
:param language: language
|
|
:param get_ep_info: get episodes
|
|
:param banners: load banners
|
|
:param posters: load posters
|
|
:param seasons: load seasons
|
|
:param seasonwides: load seasonwides
|
|
:param fanart: load fanard
|
|
:param actors: load actors
|
|
"""
|
|
pass
|
|
|
|
def get_show(
|
|
self,
|
|
show_id, # type: integer_types
|
|
load_episodes=True, # type: bool
|
|
banners=False, # type: bool
|
|
posters=False, # type: bool
|
|
seasons=False, # type: bool
|
|
seasonwides=False, # type: bool
|
|
fanart=False, # type: bool
|
|
actors=False, # type: bool
|
|
old_call=False, # type: bool
|
|
language=None, # type: AnyStr
|
|
# **kwargs # type: dict
|
|
):
|
|
# type: (...) -> Optional[TVInfoShow]
|
|
"""
|
|
get data for show id
|
|
:param show_id: id of show
|
|
:param load_episodes: load episodes
|
|
:param banners: load banners
|
|
:param posters: load posters
|
|
:param seasons: load season images
|
|
:param seasonwides: load season wide images
|
|
:param fanart: load fanart
|
|
:param actors: load actors
|
|
:param old_call: load legacy call
|
|
:param language: set the request language
|
|
:return: show object
|
|
"""
|
|
if not old_call and None is self._old_config:
|
|
self._old_config = self.config.copy()
|
|
self.config.update({'banners_enabled': banners, 'posters_enabled': posters, 'seasons_enabled': seasons,
|
|
'seasonwides_enabled': seasonwides, 'fanart_enabled': fanart, 'actors_enabled': actors,
|
|
'language': language or 'en'})
|
|
self.ti_shows.lock.acquire()
|
|
try:
|
|
if show_id not in self.ti_shows:
|
|
self.ti_shows[show_id] = TVInfoShow() # type: TVInfoShow
|
|
with self.ti_shows[show_id].lock:
|
|
self.ti_shows.lock.release()
|
|
try:
|
|
if self._must_load_data(show_id, load_episodes, banners, posters, seasons, seasonwides, fanart,
|
|
actors, self.config['language']):
|
|
self.ti_shows[show_id].requested_language = self.config['language']
|
|
self._get_show_data(show_id, self.map_languages.get(self.config['language'],
|
|
self.config['language']),
|
|
load_episodes, banners, posters, seasons, seasonwides, fanart, actors)
|
|
if None is self.ti_shows[show_id].id:
|
|
with self.ti_shows.lock:
|
|
del self.ti_shows[show_id]
|
|
if show_id not in self.ti_shows:
|
|
return None
|
|
else:
|
|
show_copy = copy.deepcopy(self.ti_shows[show_id]) # type: TVInfoShow
|
|
# provide old call compatibility for dvd order
|
|
if self.config.get('dvdorder') and TVInfoSeasonTypes.dvd in show_copy.alt_ep_numbering:
|
|
org_seasons, dvd_seasons = list(show_copy), \
|
|
list(show_copy.alt_ep_numbering[TVInfoSeasonTypes.dvd])
|
|
for r_season in set(org_seasons) - set(dvd_seasons):
|
|
try:
|
|
del show_copy[r_season]
|
|
except (BaseException, Exception):
|
|
continue
|
|
for ti_season in dvd_seasons:
|
|
show_copy[ti_season] = show_copy.alt_ep_numbering[TVInfoSeasonTypes.dvd][ti_season]
|
|
return show_copy
|
|
finally:
|
|
try:
|
|
if None is self.ti_shows[show_id].id:
|
|
with self.ti_shows.lock:
|
|
del self.ti_shows[show_id]
|
|
except (BaseException, Exception):
|
|
pass
|
|
finally:
|
|
try:
|
|
self.ti_shows.lock.release()
|
|
except RuntimeError:
|
|
pass
|
|
if not old_call and None is not self._old_config:
|
|
self.config = self._old_config
|
|
self._old_config = None
|
|
|
|
# noinspection PyMethodMayBeStatic
|
|
def _search_show(self, name=None, ids=None, lang=None, **kwargs):
|
|
# type: (Union[AnyStr, List[AnyStr]], Dict[integer_types, integer_types], Optional[string_types], Optional[Any]) -> List[Dict]
|
|
"""
|
|
internal search function to find shows, should be overwritten in class
|
|
:param name: name to search for
|
|
:param ids: dict of ids {tvid: prodid} to search for
|
|
:param lang: language code
|
|
"""
|
|
return []
|
|
|
|
@staticmethod
|
|
def _convert_search_names(name):
|
|
if name:
|
|
names = ([name], name)[isinstance(name, list)]
|
|
for i, n in enumerate(names):
|
|
if not isinstance(n, string_types):
|
|
names[i] = text_type(n)
|
|
names[i] = names[i].lower()
|
|
return names
|
|
return name
|
|
|
|
def search_show(
|
|
self,
|
|
name=None, # type: Union[AnyStr, List[AnyStr]]
|
|
ids=None, # type: Dict[integer_types, integer_types]
|
|
lang=None, # type: Optional[string_types]
|
|
# **kwargs # type: Optional[Any]
|
|
):
|
|
# type: (...) -> List[Dict]
|
|
"""
|
|
search for series with name(s) or ids
|
|
|
|
:param name: series name or list of names to search for
|
|
:param ids: dict of ids {tvid: prodid} to search for
|
|
:param lang: language code
|
|
:return: combined list of series results
|
|
"""
|
|
if None is lang:
|
|
if self.config.get('language'):
|
|
lang = self.config['language']
|
|
lang = self.map_languages.get(lang, lang)
|
|
if not name and not ids:
|
|
log.debug('Nothing to search')
|
|
raise BaseTVinfoShownotfound('Nothing to search')
|
|
name, selected_series = self._convert_search_names(name), []
|
|
if ids:
|
|
if not name and not any(1 for i in ids if i in self.supported_id_searches):
|
|
log.debug('Id type not supported')
|
|
raise BaseTVinfoShownotfound('Id type not supported')
|
|
selected_series = self._search_show(name=name, ids=ids, lang=lang)
|
|
elif name:
|
|
selected_series = self._search_show(name, lang=lang)
|
|
if isinstance(selected_series, dict):
|
|
selected_series = [selected_series]
|
|
if not isinstance(selected_series, list) or 0 == len(selected_series):
|
|
log.debug('Series result returned zero')
|
|
raise BaseTVinfoShownotfound('Show-name search returned zero results (cannot find show on %s)' %
|
|
self.__class__.__name__)
|
|
return selected_series
|
|
|
|
def _set_item(self, sid, seas, ep, attrib, value):
|
|
# type: (integer_types, integer_types, integer_types, integer_types, Any, Any) -> None
|
|
"""Creates a new episode, creating Show(), Season() and
|
|
Episode()s as required. Called by _get_show_data to populate show
|
|
|
|
Since the nice-to-use tvinfo[1][24]['name] interface
|
|
makes it impossible to do tvinfo[1][24]['name] = "name"
|
|
and still be capable of checking if an episode exists
|
|
so we can raise tvinfo_shownotfound, we have a slightly
|
|
less pretty method of setting items.. but since the API
|
|
is supposed to be read-only, this is the best way to
|
|
do it!
|
|
The problem is that calling tvinfo[1][24]['episodename'] = "name"
|
|
calls __getitem__ on tvinfo[1], there is no way to check if
|
|
tvinfo.__dict__ should have a key "1" before we auto-create it
|
|
"""
|
|
# if sid not in self.ti_shows:
|
|
# self.ti_shows[sid] = TVInfoShow()
|
|
if seas not in self.ti_shows[sid]:
|
|
self.ti_shows[sid][seas] = TVInfoSeason(show=self.ti_shows[sid])
|
|
self.ti_shows[sid][seas].number = seas
|
|
if ep not in self.ti_shows[sid][seas]:
|
|
self.ti_shows[sid][seas][ep] = TVInfoEpisode(season=self.ti_shows[sid][seas], show=self.ti_shows[sid])
|
|
if attrib not in ('cast', 'crew'):
|
|
self.ti_shows[sid][seas][ep][attrib] = value
|
|
self.ti_shows[sid][seas][ep].__dict__[attrib] = value
|
|
|
|
def _set_show_data(self, sid, key, value, add=False):
|
|
# type: (integer_types, Any, Any, bool) -> None
|
|
"""Sets self.ti_shows[sid] to a new Show instance, or sets the data
|
|
"""
|
|
# if sid not in self.ti_shows:
|
|
# self.ti_shows[sid] = TVInfoShow()
|
|
if key not in ('cast', 'crew'):
|
|
if add and isinstance(self.ti_shows[sid].data, dict) and key in self.ti_shows[sid].data:
|
|
self.ti_shows[sid].data[key].update(value)
|
|
else:
|
|
self.ti_shows[sid].data[key] = value
|
|
if '_banners' == key:
|
|
p_key = 'banners'
|
|
else:
|
|
p_key = key
|
|
if add and key in self.ti_shows[sid].__dict__ and isinstance(self.ti_shows[sid].__dict__[p_key], dict):
|
|
self.ti_shows[sid].__dict__[p_key].update(self.ti_shows[sid].data[key])
|
|
else:
|
|
self.ti_shows[sid].__dict__[p_key] = self.ti_shows[sid].data[key]
|
|
else:
|
|
if add and key in self.ti_shows[sid].__dict__ and isinstance(self.ti_shows[sid].__dict__[key], dict):
|
|
self.ti_shows[sid].__dict__[key].update(value)
|
|
else:
|
|
self.ti_shows[sid].__dict__[key] = value
|
|
|
|
def get_updated_shows(self):
|
|
# type: (...) -> Dict[integer_types, integer_types]
|
|
"""
|
|
gets all ids and timestamp of updated shows
|
|
returns dict of id: timestamp
|
|
"""
|
|
return {}
|
|
|
|
def get_similar(self, tvid, result_count=100, **kwargs):
|
|
# type: (integer_types, int, Any) -> List[TVInfoShow]
|
|
"""
|
|
return list of similar shows to given id
|
|
:param tvid: id to give similar shows for
|
|
:param result_count: count of results requested
|
|
"""
|
|
return []
|
|
|
|
def get_recommended_for_show(self, tvid, result_count=100, **kwargs):
|
|
# type: (integer_types, int, Any) -> List[TVInfoShow]
|
|
"""
|
|
list of recommended shows to the provided tv id
|
|
:param tvid: id to find recommended shows for
|
|
:param result_count: result count to returned
|
|
"""
|
|
return []
|
|
|
|
def get_trending(self, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get trending shows
|
|
:param result_count:
|
|
"""
|
|
return []
|
|
|
|
def get_popular(self, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get all popular shows
|
|
"""
|
|
return []
|
|
|
|
def get_top_rated(self, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get top rated shows
|
|
"""
|
|
return []
|
|
|
|
def get_new_shows(self, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get new shows
|
|
"""
|
|
return []
|
|
|
|
def get_new_seasons(self, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get new seasons
|
|
"""
|
|
return []
|
|
|
|
def discover(self, result_count=100, get_extra_images=False, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
return []
|
|
|
|
def get_premieres(self, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get all premiering shows
|
|
"""
|
|
return []
|
|
|
|
def get_returning(self, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get all returning shows
|
|
"""
|
|
return []
|
|
|
|
def get_most_played(self, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get most played shows
|
|
:param result_count: how many results are suppose to be returned
|
|
"""
|
|
return []
|
|
|
|
def get_most_watched(self, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get most watched shows
|
|
:param result_count: how many results are suppose to be returned
|
|
"""
|
|
return []
|
|
|
|
def get_most_collected(self, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get most collected shows
|
|
:param result_count: how many results are suppose to be returned
|
|
"""
|
|
return []
|
|
|
|
def get_recommended(self, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get most recommended shows
|
|
:param result_count: how many results are suppose to be returned
|
|
"""
|
|
return []
|
|
|
|
def get_recommended_for_account(self, account, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get recommended shows for account
|
|
:param account: account to get recommendations for
|
|
:param result_count: how many results are suppose to be returned
|
|
"""
|
|
return []
|
|
|
|
def hide_recommended_for_account(self, account, show_ids, **kwargs):
|
|
# type: (integer_types, List[integer_types], Any) -> List[integer_types]
|
|
"""
|
|
hide recommended show for account
|
|
:param account: account to get recommendations for
|
|
:param show_ids: list of show_ids to no longer recommend for account
|
|
:return: list of added ids
|
|
"""
|
|
return []
|
|
|
|
def unhide_recommended_for_account(self, account, show_ids, **kwargs):
|
|
# type: (integer_types, List[integer_types], Any) -> List[integer_types]
|
|
"""
|
|
unhide recommended show for account
|
|
:param account: account to get recommendations for
|
|
:param show_ids: list of show_ids to be included in possible recommend for account
|
|
:return: list of removed ids
|
|
"""
|
|
return []
|
|
|
|
def list_hidden_recommended_for_account(self, account, **kwargs):
|
|
# type: (integer_types, Any) -> List[TVInfoShow]
|
|
"""
|
|
list hidden recommended show for account
|
|
:param account: account to get recommendations for
|
|
:return: list of hidden shows
|
|
"""
|
|
return []
|
|
|
|
def get_watchlisted_for_account(self, account, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get most watchlisted shows for account
|
|
:param account: account to get recommendations for
|
|
:param result_count: how many results are suppose to be returned
|
|
"""
|
|
return []
|
|
|
|
def get_anticipated(self, result_count=100, **kwargs):
|
|
# type: (...) -> List[TVInfoShow]
|
|
"""
|
|
get anticipated shows
|
|
:param result_count: how many results are suppose to be returned
|
|
"""
|
|
return []
|
|
|
|
def __getitem__(self, item):
|
|
# type: (Union[AnyStr, integer_types, Tuple[integer_types, bool]]) -> Union[TVInfoShow, List[Dict], None]
|
|
"""Legacy handler (use get_show or search_show instead)
|
|
Handles class_instance['seriesname'] calls.
|
|
The dict index should be the show id
|
|
"""
|
|
arg = None
|
|
if isinstance(item, tuple) and 2 == len(item):
|
|
item, arg = item
|
|
if not isinstance(arg, bool):
|
|
arg = None
|
|
|
|
if isinstance(item, integer_types):
|
|
# Item is integer, treat as show id
|
|
return self.get_show(item, (True, arg)[None is not arg], old_call=True)
|
|
|
|
# maybe adding this to make callee use showname so that i can bring in the new endpoint
|
|
if isinstance(arg, string_types) and 'Tvdb' == self.__class__.__name__:
|
|
return self.search_show(item)
|
|
|
|
return self.search_show(item)
|
|
|
|
# noinspection PyMethodMayBeStatic
|
|
def search(self, series):
|
|
# type: (AnyStr) -> List
|
|
"""This searches for the series name
|
|
and returns the result list
|
|
"""
|
|
return []
|
|
|
|
@staticmethod
|
|
def _which_type(img_width, img_ratio):
|
|
# type: (integer_types, Union[integer_types, float]) -> Optional[int]
|
|
"""
|
|
|
|
:param img_width:
|
|
:param img_ratio:
|
|
"""
|
|
|
|
msg_success = 'Treating image as %s with extracted aspect ratio'
|
|
# most posters are around 0.68 width/height ratio (eg. 680/1000)
|
|
# noinspection DuplicatedCode
|
|
if 0.55 <= img_ratio <= 0.8:
|
|
log.debug(msg_success % 'poster')
|
|
return TVInfoImageType.poster
|
|
|
|
# most banners are around 5.4 width/height ratio (eg. 758/140)
|
|
if 5 <= img_ratio <= 6:
|
|
log.debug(msg_success % 'banner')
|
|
return TVInfoImageType.banner
|
|
|
|
# most fan art are around 1.7 width/height ratio (eg. 1280/720 or 1920/1080)
|
|
if 1.7 <= img_ratio <= 1.8:
|
|
if 500 < img_width:
|
|
log.debug(msg_success % 'fanart')
|
|
return TVInfoImageType.fanart
|
|
|
|
log.warning(u'Skipped image with fanart aspect ratio but less than 500 pixels wide')
|
|
else:
|
|
log.warning(u'Skipped image with useless ratio %s' % img_ratio)
|
|
|
|
def _get_languages(self):
|
|
# type: (...) -> None
|
|
"""
|
|
overwrite in class to create the language lists
|
|
"""
|
|
pass
|
|
|
|
def get_languages(self):
|
|
# type: (...) -> List[Dict]
|
|
"""
|
|
get all supported languages as list of dicts
|
|
[{'id': 'lang code', 'name': 'english name', 'nativeName': 'native name', 'sg_lang': 'sg lang code'}]
|
|
"""
|
|
if not self._supported_languages:
|
|
self._get_languages()
|
|
return self._supported_languages or []
|
|
|
|
def __str__(self):
|
|
return '<TVInfo(%s) (containing: %s)>' % (self.__class__.__name__, text_type(self.ti_shows))
|
|
|
|
__repr__ = __str__
|