SickGear/lib/tvinfo_base/base.py
JackDandy fce8878fa9 Add menu Shows/"TMDB Cards".
Add top rated, popular, trending today, trending this week, to TMDB cards.
Change Shows/Add show... Trakt and IMDb card descriptions to be dynamic.
Add network data to trakt cards.
Add fallback for show cast when no suitable items are found in tvdb_api.
Change refactor for trakt lib update.
Change sanitise api response dates for cards (an invalid trakt date caused a UI glitch).
Fix issue where trakt can return no tvdb id causing a card to not display.
Change view-show TVDb genre links to use anon link setting.
Change remove displayShow.tmpl code for '|' sep as it is a fixed list string at data source.
Change add TVINFO_FANSITE placeholder icon.
Change refactor api sg.`CMD_SickGearShowsBrowseTrakt`.
---
Refactor date handling to make it's usage consistent from indexerapi
Split when_past out to to started_past and return_past for UI.
Fix trakt returning dates.
Change bring properly into use, the proper new season returning date.
Change remove PY2 unused import statements.
---
Fix issue with Trakt cards where an incorrect cache image obscures the default card placeholder image.
Simplify airtime for cards.
Remove nonsense TMDB api overview text.
Rearrange cards order on top menu.
---
Remove unused vars and simplify regex use cases.
Fix tv cards issue with Trakt and Tvmaze where show_info does not carry a first episode_info.
Instantiate with TVInfoEpisode() instead of None, so that expected properties exist without a need for Nonetype tests.
Change simplify `overview` and `airtime` use after lib output type is made trustable.
Change cards view to not allow items with no seriesname.
2023-05-03 00:43:59 +01:00

1692 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
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__