def __init__(self): Monitor.__init__(self) self.kodi = KodiWrapper() self.vtm_go = VtmGo(self.kodi) self.vtm_go_auth = VtmGoAuth(self.kodi) self.update_interval = 24 * 3600 # Every 24 hours self.cache_expiry = 30 * 24 * 3600 # One month
def __init__(self, kodi): """ Initialise object :type kodi: resources.lib.kodiwrapper.KodiWrapper """ self._kodi = kodi self._proxies = kodi.get_proxies() self._auth = VtmGoAuth(kodi)
def __init__(self): """ Initialise object """ self._auth = VtmGoAuth(kodiutils.get_setting('username'), kodiutils.get_setting('password'), 'VTM', kodiutils.get_setting('profile'), kodiutils.get_tokens_path()) self._vtm_go = VtmGo(self._auth)
class BackgroundService(Monitor): """ Background service code """ def __init__(self): Monitor.__init__(self) self._kodi = KodiWrapper() self._player = PlayerMonitor(kodi=self._kodi) self.vtm_go = VtmGo(self._kodi) self.vtm_go_auth = VtmGoAuth(self._kodi) self.update_interval = 24 * 3600 # Every 24 hours self.cache_expiry = 30 * 24 * 3600 # One month def run(self): """ Background loop for maintenance tasks """ _LOGGER.debug('Service started') while not self.abortRequested(): # Update every `update_interval` after the last update if self._kodi.get_setting_as_bool('metadata_update') and int( self._kodi.get_setting('metadata_last_updated', 0)) + self.update_interval < time(): self._update_metadata() # Stop when abort requested if self.waitForAbort(10): break _LOGGER.debug('Service stopped') def onSettingsChanged(self): # pylint: disable=invalid-name """ Callback when a setting has changed """ # Refresh our VtmGo instance kodiwrapper.ADDON = Addon() self.vtm_go = VtmGo(self._kodi) if self.vtm_go_auth.has_credentials_changed(): _LOGGER.debug('Clearing auth tokens due to changed credentials') self.vtm_go_auth.clear_token() # Refresh container self._kodi.container_refresh() def _update_metadata(self): """ Update the metadata for the listings """ from resources.lib.modules.metadata import Metadata # Clear outdated metadata self._kodi.invalidate_cache(self.cache_expiry) def update_status(_i, _total): """ Allow to cancel the background job """ return self.abortRequested( ) or not self._kodi.get_setting_as_bool('metadata_update') success = Metadata(self._kodi).fetch_metadata(callback=update_status) # Update metadata_last_updated if success: self._kodi.set_setting('metadata_last_updated', str(int(time())))
def __init__(self, kodi): """ Initialise object :type kodi: resources.lib.kodiwrapper.KodiWrapper """ self._kodi = kodi self._auth = VtmGoAuth(kodi) self._session = requests.session() self._session.proxies = kodi.get_proxies() self._authenticate()
def __init__(self): """ Initialise object """ self._auth = VtmGoAuth(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_setting('loginprovider'), kodiutils.get_setting('profile'), kodiutils.get_tokens_path()) self._api = VtmGo(self._auth)
def __init__(self): """ Initialise object """ try: self._auth = VtmGoAuth(kodiutils.get_setting('username'), kodiutils.get_setting('password'), 'VTM', kodiutils.get_setting('profile'), kodiutils.get_tokens_path()) except NoLoginException: self._auth = None self._vtm_go = VtmGo(self._auth)
def __init__(self, port): """ Initialise object :type port: int """ self._auth = VtmGoAuth(kodiutils.get_setting('username'), kodiutils.get_setting('password'), 'VTM', kodiutils.get_setting('profile'), kodiutils.get_tokens_path()) self._vtm_go = VtmGo(self._auth) self._vtm_go_epg = VtmGoEpg() self.port = port
def check_credentials(): """ Check credentials (called from settings) """ try: auth = VtmGoAuth(kodi) auth.clear_token() auth.get_token() kodi.show_ok_dialog( message=kodi.localize(30202)) # Credentials are correct! except InvalidLoginException: kodi.show_ok_dialog( message=kodi.localize(30203)) # Your credentials are not valid! kodi.open_settings()
def check_credentials(self): """ Check credentials (called from settings) """ try: from resources.lib.vtmgo.vtmgoauth import VtmGoAuth auth = VtmGoAuth(self._kodi) auth.clear_token() auth.get_token() self._kodi.show_ok_dialog( message=self._kodi.localize(30202)) # Credentials are correct! except InvalidLoginException: self._kodi.show_ok_dialog(message=self._kodi.localize( 30203)) # Your credentials are not valid! except LoginErrorException as e: self._kodi.show_ok_dialog(message=self._kodi.localize( 30702, code=e.code)) # Unknown error while logging in: {code} self._kodi.open_settings()
class VtmGo: """ VTM GO API """ def __init__(self, kodi): self._kodi = kodi # type: KodiWrapper self._proxies = kodi.get_proxies() self._auth = VtmGoAuth(kodi) def _mode(self): """ Return the mode that should be used for API calls """ return 'vtmgo-kids' if self._kodi.kids_mode() else 'vtmgo' def get_config(self): """ Returns the config for the app. """ # This is currently not used response = self._get_url('/config') info = json.loads(response) # This contains a player.updateIntervalSeconds that could be used to notify VTM GO about the playing progress return info def get_recommendations(self): """ Returns the config for the dashboard. """ response = self._get_url('/%s/main' % self._mode()) recommendations = json.loads(response) categories = [] for cat in recommendations.get('rows', []): if cat.get('rowType') in ['SWIMLANE_DEFAULT']: items = [] for item in cat.get('teasers'): items.append( Content( content_id=item.get('target', {}).get('id'), video_type=item.get('target', {}).get('type'), title=item.get('title'), geoblocked=item.get('geoBlocked'), cover=item.get('imageUrl'), )) categories.append( Category( category_id=cat.get('id'), title=cat.get('title'), content=items, )) return categories def get_mylist(self): """ Returns the contents of My List """ response = self._get_url('/%s/main/swimlane/my-list' % self._mode()) # My list can be empty if not response: return [] result = json.loads(response) items = [] for item in result.get('teasers'): items.append( Content( content_id=item.get('target', {}).get('id'), video_type=item.get('target', {}).get('type'), title=item.get('title'), geoblocked=item.get('geoBlocked'), cover=item.get('imageUrl'), )) return items def add_mylist(self, video_type, content_id): """ Add an item to My List """ self._put_url('/%s/userData/myList/%s/%s' % (self._mode(), video_type, content_id)) def del_mylist(self, video_type, content_id): """ Delete an item from My List """ self._delete_url('/%s/userData/myList/%s/%s' % (self._mode(), video_type, content_id)) def get_live_channels(self): """ Get a list of all the live tv channels. :rtype list[LiveChannel] """ import dateutil.parser response = self._get_url('/%s/live' % self._mode()) info = json.loads(response) channels = [] for item in info.get('channels'): epg = [] for item_epg in item.get('broadcasts', []): epg.append( LiveChannelEpg( title=item_epg.get('name'), start=dateutil.parser.parse(item_epg.get('startsAt')), end=dateutil.parser.parse(item_epg.get('endsAt')), )) channels.append( LiveChannel( channel_id=item.get('channelId'), logo=item.get('channelLogoUrl'), name=item.get('name'), epg=epg, )) return channels def get_categories(self): """ Get a list of all the categories. :rtype list[Category] """ response = self._get_url('/%s/catalog/filters' % self._mode()) info = json.loads(response) categories = [] for item in info.get('catalogFilters', []): categories.append( Category( category_id=item.get('id'), title=item.get('title'), )) return categories def get_items(self, category=None): """ Get a list of all the items in a category. :type category: str :rtype list[Content] """ if category and category != 'all': response = self._get_url('/%s/catalog?pageSize=%d&filter=%s' % (self._mode(), 1000, quote(category))) else: response = self._get_url('/%s/catalog?pageSize=%d' % (self._mode(), 1000)) info = json.loads(response) items = [] for item in info.get('pagedTeasers', {}).get('content', []): items.append( Content( content_id=item.get('target', {}).get('id'), title=item.get('title'), cover=item.get('imageUrl'), video_type=item.get('target', {}).get('type'), geoblocked=item.get('geoBlocked'), )) return items def get_movie(self, movie_id, only_cache=False): """ Get the details of the specified movie. :type movie_id: str :type only_cache: bool :rtype Movie """ if only_cache: # Fetch from cache if asked movie = self._kodi.get_cache(['movie', movie_id]) if not movie: return None else: # Fetch from API response = self._get_url('/%s/movies/%s' % (self._mode(), movie_id)) info = json.loads(response) movie = info.get('movie', {}) self._kodi.set_cache(['movie', movie_id], movie) channel_url = movie.get('channelLogoUrl') if channel_url: import os.path channel = os.path.basename(channel_url).split('-')[0].upper() else: channel = 'VTM GO' return Movie( movie_id=movie.get('id'), name=movie.get('name'), description=movie.get('description'), duration=movie.get('durationSeconds'), cover=movie.get('bigPhotoUrl'), year=movie.get('productionYear'), geoblocked=movie.get('geoBlocked'), remaining=movie.get('remainingDaysAvailable'), legal=movie.get('legalIcons'), aired=movie.get('broadcastTimestamp'), channel=channel, ) def get_program(self, program_id, only_cache=False): """ Get the details of the specified program. :type program_id: str :type only_cache: bool :rtype Program """ if only_cache: # Fetch from cache if asked program = self._kodi.get_cache(['program', program_id]) if not program: return None else: # Fetch from API response = self._get_url('/%s/programs/%s' % (self._mode(), program_id)) info = json.loads(response) program = info.get('program', {}) self._kodi.set_cache(['program', program_id], program) channel_url = program.get('channelLogoUrl') if channel_url: import os.path channel = os.path.basename(channel_url).split('-')[0].upper() else: channel = 'VTM GO' seasons = {} for item_season in program.get('seasons', []): episodes = {} for item_episode in item_season.get('episodes', []): episodes[item_episode.get('index')] = Episode( episode_id=item_episode.get('id'), number=item_episode.get('index'), season=item_season.get('index'), name=item_episode.get('name'), description=item_episode.get('description'), duration=item_episode.get('durationSeconds'), cover=item_episode.get('bigPhotoUrl'), geoblocked=program.get('geoBlocked'), remaining=item_episode.get('remainingDaysAvailable'), channel=channel, legal=program.get('legalIcons'), aired=item_episode.get('broadcastTimestamp'), ) seasons[item_season.get('index')] = Season( number=item_season.get('index'), episodes=episodes, cover=item_season.get('episodes', [{}])[0].get('bigPhotoUrl') if episodes else program.get('bigPhotoUrl'), geoblocked=program.get('geoBlocked'), channel=channel, legal=program.get('legalIcons'), ) return Program( program_id=program.get('id'), name=program.get('name'), description=program.get('description'), cover=program.get('bigPhotoUrl'), geoblocked=program.get('geoBlocked'), seasons=seasons, channel=channel, legal=program.get('legalIcons'), ) def get_episode(self, episode_id): """ Get the details of the specified episode. :type episode_id: str :rtype Episode """ response = self._get_url('/%s/episodes/%s' % (self._mode(), episode_id)) info = json.loads(response) episode = info.get('episode', {}) return Episode( episode_id=episode.get('id'), number=episode.get('index'), season=episode.get('seasonIndex'), name=episode.get('name'), description=episode.get('description'), cover=episode.get('bigPhotoUrl'), ) def do_search(self, search): """ Do a search in the full catalogue. :type search: str :rtype list[Content] """ response = self._get_url('/%s/autocomplete/?maxItems=%d&keywords=%s' % (self._mode(), 50, quote(search))) results = json.loads(response) items = [] for item in results.get('suggestions', []): items.append( Content( content_id=item.get('id'), title=item.get('name'), video_type=item.get('type'), )) return items def _get_url(self, url): """ Makes a GET request for the specified URL. :type url: str :rtype str """ headers = { 'x-app-version': '5', 'x-persgroep-mobile-app': 'true', 'x-persgroep-os': 'android', 'x-persgroep-os-version': '23', 'User-Agent': 'VTMGO/6.5.0 (be.vmma.vtm.zenderapp; build:11019; Android 23) okhttp/3.12.1' } token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token self._kodi.log('Sending GET {url}...', LOG_INFO, url=url) response = requests.session().get('https://api.vtmgo.be' + url, headers=headers, verify=False, proxies=self._proxies) self._kodi.log('Got response: {response}', LOG_DEBUG, response=response.text) if response.status_code == 404: raise UnavailableException() if response.status_code not in [200, 204]: raise Exception('Error %s.' % response.status_code) return response.text def _put_url(self, url): """ Makes a PUT request for the specified URL. :type url: str :rtype str """ headers = { 'x-app-version': '5', 'x-persgroep-mobile-app': 'true', 'x-persgroep-os': 'android', 'x-persgroep-os-version': '23', 'User-Agent': 'VTMGO/6.5.0 (be.vmma.vtm.zenderapp; build:11019; Android 23) okhttp/3.12.1' } token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token self._kodi.log('Sending PUT {url}...', LOG_INFO, url=url) response = requests.session().put('https://api.vtmgo.be' + url, headers=headers, verify=False, proxies=self._proxies) self._kodi.log('Got response: {response}', LOG_DEBUG, response=response.text) if response.status_code == 404: raise UnavailableException() if response.status_code not in [200, 204]: raise Exception('Error %s.' % response.status_code) return response.text def _delete_url(self, url): """ Makes a DELETE request for the specified URL. :type url: str :rtype str """ headers = { 'x-app-version': '5', 'x-persgroep-mobile-app': 'true', 'x-persgroep-os': 'android', 'x-persgroep-os-version': '23', 'User-Agent': 'VTMGO/6.5.0 (be.vmma.vtm.zenderapp; build:11019; Android 23) okhttp/3.12.1' } token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token self._kodi.log('Sending DELETE {url}...', LOG_INFO, url=url) response = requests.session().delete('https://api.vtmgo.be' + url, headers=headers, verify=False, proxies=self._proxies) self._kodi.log('Got response: {response}', LOG_DEBUG, response=response.text) if response.status_code == 404: raise UnavailableException() if response.status_code not in [200, 204]: raise Exception('Error %s.' % response.status_code) return response.text
class VtmGo: """ VTM GO API """ CONTENT_TYPE_MOVIE = 'MOVIE' CONTENT_TYPE_PROGRAM = 'PROGRAM' CONTENT_TYPE_EPISODE = 'EPISODE' _HEADERS = { 'x-app-version': '8', 'x-persgroep-mobile-app': 'true', 'x-persgroep-os': 'android', 'x-persgroep-os-version': '23', } def __init__(self, kodi): """ Initialise object :type kodi: resources.lib.kodiwrapper.KodiWrapper """ self._kodi = kodi self._proxies = kodi.get_proxies() self._auth = VtmGoAuth(kodi) def _mode(self): """ Return the mode that should be used for API calls """ return 'vtmgo-kids' if self.get_product() == 'VTM_GO_KIDS' else 'vtmgo' def get_config(self): """ Returns the config for the app """ # This is currently not used response = self._get_url('/config') info = json.loads(response) # This contains a player.updateIntervalSeconds that could be used to notify VTM GO about the playing progress return info def get_profiles(self, products='VTM_GO,VTM_GO_KIDS'): """ Returns the available profiles """ response = self._get_url('/profiles', {'products': products}) result = json.loads(response) profiles = [ Profile( key=profile.get('id'), product=profile.get('product'), name=profile.get('name'), gender=profile.get('gender'), birthdate=profile.get('birthDate'), color=profile.get('color', {}).get('start'), color2=profile.get('color', {}).get('end'), ) for profile in result ] return profiles def get_recommendations(self): """ Returns the config for the dashboard """ response = self._get_url('/%s/main' % self._mode()) recommendations = json.loads(response) categories = [] for cat in recommendations.get('rows', []): if cat.get('rowType') not in ['SWIMLANE_DEFAULT']: _LOGGER.debug('Skipping recommendation %s with type %s', cat.get('title'), cat.get('rowType')) continue items = [] for item in cat.get('teasers'): if item.get('target', {}).get('type') == self.CONTENT_TYPE_MOVIE: movie = self.get_movie(item.get('target', {}).get('id'), cache=CACHE_ONLY) if movie: # We have a cover from the overview that we don't have in the details movie.cover = item.get('imageUrl') items.append(movie) else: items.append( Movie( movie_id=item.get('target', {}).get('id'), name=item.get('title'), cover=item.get('imageUrl'), image=item.get('imageUrl'), geoblocked=item.get('geoBlocked'), )) elif item.get('target', {}).get('type') == self.CONTENT_TYPE_PROGRAM: program = self.get_program(item.get('target', {}).get('id'), cache=CACHE_ONLY) if program: # We have a cover from the overview that we don't have in the details program.cover = item.get('imageUrl') items.append(program) else: items.append( Program( program_id=item.get('target', {}).get('id'), name=item.get('title'), cover=item.get('imageUrl'), image=item.get('imageUrl'), geoblocked=item.get('geoBlocked'), )) categories.append( Category( category_id=cat.get('id'), title=cat.get('title'), content=items, )) return categories def get_swimlane(self, swimlane=None): """ Returns the contents of My List """ response = self._get_url('/%s/main/swimlane/%s' % (self._mode(), swimlane)) # Result can be empty if not response: return [] result = json.loads(response) items = [] for item in result.get('teasers'): if item.get('target', {}).get('type') == self.CONTENT_TYPE_MOVIE: movie = self.get_movie(item.get('target', {}).get('id'), cache=CACHE_ONLY) if movie: # We have a cover from the overview that we don't have in the details movie.cover = item.get('imageUrl') items.append(movie) else: items.append( Movie( movie_id=item.get('target', {}).get('id'), name=item.get('title'), geoblocked=item.get('geoBlocked'), cover=item.get('imageUrl'), image=item.get('imageUrl'), )) elif item.get('target', {}).get('type') == self.CONTENT_TYPE_PROGRAM: program = self.get_program(item.get('target', {}).get('id'), cache=CACHE_ONLY) if program: # We have a cover from the overview that we don't have in the details program.cover = item.get('imageUrl') items.append(program) else: items.append( Program( program_id=item.get('target', {}).get('id'), name=item.get('title'), geoblocked=item.get('geoBlocked'), cover=item.get('imageUrl'), image=item.get('imageUrl'), )) elif item.get('target', {}).get('type') == self.CONTENT_TYPE_EPISODE: program = self.get_program(item.get('target', {}).get('programId'), cache=CACHE_ONLY) episode = self.get_episode_from_program( program, item.get('target', {}).get('id')) if program else None items.append( Episode( episode_id=item.get('target', {}).get('id'), program_id=item.get('target', {}).get('programId'), program_name=item.get('title'), name=item.get('label'), description=episode.description if episode else None, geoblocked=item.get('geoBlocked'), cover=item.get('imageUrl'), progress=item.get('playerPositionSeconds'), watched=False, remaining=item.get('remainingDaysAvailable'), )) return items def add_mylist(self, video_type, content_id): """ Add an item to My List """ self._put_url('/%s/userData/myList/%s/%s' % (self._mode(), video_type, content_id)) def del_mylist(self, video_type, content_id): """ Delete an item from My List """ self._delete_url('/%s/userData/myList/%s/%s' % (self._mode(), video_type, content_id)) def get_live_channels(self): """ Get a list of all the live tv channels. :rtype list[LiveChannel] """ import dateutil.parser response = self._get_url('/%s/live' % self._mode()) info = json.loads(response) channels = [] for item in info.get('channels'): epg = [] for item_epg in item.get('broadcasts', []): epg.append( LiveChannelEpg( title=item_epg.get('name'), start=dateutil.parser.parse(item_epg.get('startsAt')), end=dateutil.parser.parse(item_epg.get('endsAt')), )) channels.append( LiveChannel( key=item.get('seoKey'), channel_id=item.get('channelId'), logo=item.get('channelLogoUrl'), background=item.get('channelPosterUrl'), name=item.get('name'), epg=epg, )) return channels def get_live_channel(self, key): """ Get a the specified live tv channel. :rtype LiveChannel """ channels = self.get_live_channels() return next(c for c in channels if c.key == key) def get_categories(self): """ Get a list of all the categories. :rtype list[Category] """ response = self._get_url('/%s/catalog/filters' % self._mode()) info = json.loads(response) categories = [] for item in info.get('catalogFilters', []): categories.append( Category( category_id=item.get('id'), title=item.get('title'), )) return categories def get_items(self, category=None): """ Get a list of all the items in a category. :type category: str :rtype list[Union[Movie, Program]] """ # Fetch from API if category is None: response = self._get_url('/%s/catalog' % self._mode(), {'pageSize': 1000}) else: response = self._get_url('/%s/catalog' % self._mode(), { 'pageSize': 1000, 'filter': quote(category) }) info = json.loads(response) content = info.get('pagedTeasers', {}).get('content', []) items = [] for item in content: if item.get('target', {}).get('type') == self.CONTENT_TYPE_MOVIE: movie = self.get_movie(item.get('target', {}).get('id'), cache=CACHE_ONLY) if movie: # We have a cover from the overview that we don't have in the details movie.cover = item.get('imageUrl') items.append(movie) else: items.append( Movie( movie_id=item.get('target', {}).get('id'), name=item.get('title'), cover=item.get('imageUrl'), geoblocked=item.get('geoBlocked'), )) elif item.get('target', {}).get('type') == self.CONTENT_TYPE_PROGRAM: program = self.get_program(item.get('target', {}).get('id'), cache=CACHE_ONLY) if program: # We have a cover from the overview that we don't have in the details program.cover = item.get('imageUrl') items.append(program) else: items.append( Program( program_id=item.get('target', {}).get('id'), name=item.get('title'), cover=item.get('imageUrl'), geoblocked=item.get('geoBlocked'), )) return items def get_movie(self, movie_id, cache=CACHE_AUTO): """ Get the details of the specified movie. :type movie_id: str :type cache: int :rtype Movie """ if cache in [CACHE_AUTO, CACHE_ONLY]: # Try to fetch from cache movie = self._kodi.get_cache(['movie', movie_id]) if movie is None and cache == CACHE_ONLY: return None else: movie = None if movie is None: # Fetch from API response = self._get_url('/%s/movies/%s' % (self._mode(), movie_id)) info = json.loads(response) movie = info.get('movie', {}) self._kodi.set_cache(['movie', movie_id], movie) return Movie( movie_id=movie.get('id'), name=movie.get('name'), description=movie.get('description'), duration=movie.get('durationSeconds'), cover=movie.get('bigPhotoUrl'), image=movie.get('bigPhotoUrl'), year=movie.get('productionYear'), geoblocked=movie.get('geoBlocked'), remaining=movie.get('remainingDaysAvailable'), legal=movie.get('legalIcons'), # aired=movie.get('broadcastTimestamp'), channel=self._parse_channel(movie.get('channelLogoUrl')), ) def get_program(self, program_id, cache=CACHE_AUTO): """ Get the details of the specified program. :type program_id: str :type cache: int :rtype Program """ if cache in [CACHE_AUTO, CACHE_ONLY]: # Try to fetch from cache program = self._kodi.get_cache(['program', program_id]) if program is None and cache == CACHE_ONLY: return None else: program = None if program is None: # Fetch from API response = self._get_url('/%s/programs/%s' % (self._mode(), program_id)) info = json.loads(response) program = info.get('program', {}) self._kodi.set_cache(['program', program_id], program) channel = self._parse_channel(program.get('channelLogoUrl')) seasons = {} for item_season in program.get('seasons', []): episodes = {} for item_episode in item_season.get('episodes', []): episodes[item_episode.get('index')] = Episode( episode_id=item_episode.get('id'), program_id=program_id, program_name=program.get('name'), number=item_episode.get('index'), season=item_season.get('index'), name=item_episode.get('name'), description=item_episode.get('description'), duration=item_episode.get('durationSeconds'), cover=item_episode.get('bigPhotoUrl'), geoblocked=program.get('geoBlocked'), remaining=item_episode.get('remainingDaysAvailable'), channel=channel, legal=program.get('legalIcons'), aired=item_episode.get('broadcastTimestamp'), progress=item_episode.get('playerPositionSeconds', 0), watched=item_episode.get('doneWatching', False), ) seasons[item_season.get('index')] = Season( number=item_season.get('index'), episodes=episodes, cover=item_season.get('episodes', [{}])[0].get('bigPhotoUrl') if episodes else program.get('bigPhotoUrl'), geoblocked=program.get('geoBlocked'), channel=channel, legal=program.get('legalIcons'), ) return Program( program_id=program.get('id'), name=program.get('name'), description=program.get('description'), cover=program.get('bigPhotoUrl'), image=program.get('bigPhotoUrl'), geoblocked=program.get('geoBlocked'), seasons=seasons, channel=channel, legal=program.get('legalIcons'), ) @staticmethod def get_episode_from_program(program, episode_id): """ Extract the specified episode from the program data. :type program: Program :type episode_id: str :rtype Episode """ for season in list(program.seasons.values()): for episode in list(season.episodes.values()): if episode.episode_id == episode_id: return episode return None @staticmethod def get_next_episode_from_program(program, season, number): """ Search for the next episode in the program data. :type program: Program :type season: int :type number: int :rtype Episode """ next_season_episode = None # First, try to find a match in the current season for episode in [ e for s in list(program.seasons.values()) for e in list(s.episodes.values()) ]: if episode.season == season and episode.number == number + 1: return episode if episode.season == season + 1 and episode.number == 1: next_season_episode = episode # No match, use the first episode of next season if next_season_episode: return next_season_episode # We are playing the last episode return None def get_episode(self, episode_id): """ Get some details of the specified episode. :type episode_id: str :rtype Episode """ response = self._get_url('/%s/play/episode/%s' % (self._mode(), episode_id)) episode = json.loads(response) # Extract next episode info if available next_playable = episode.get('nextPlayable') if next_playable: next_episode = Episode( episode_id=next_playable['id'], program_name=next_playable['title'], name=next_playable['subtitle'], description=next_playable['description'], cover=next_playable['imageUrl'], ) else: next_episode = None return Episode( episode_id=episode.get('id'), name=episode.get('title'), cover=episode.get('posterImageUrl'), progress=episode.get('playerPositionSeconds'), next_episode=next_episode, ) def do_search(self, search): """ Do a search in the full catalog. :type search: str :rtype list[Union[Movie, Program]] """ response = self._get_url('/%s/autocomplete/?maxItems=%d&keywords=%s' % (self._mode(), 50, quote(search))) results = json.loads(response) items = [] for item in results.get('suggestions', []): if item.get('type') == self.CONTENT_TYPE_MOVIE: movie = self.get_movie(item.get('id'), cache=CACHE_ONLY) if movie: items.append(movie) else: items.append( Movie( movie_id=item.get('id'), name=item.get('name'), )) elif item.get('type') == self.CONTENT_TYPE_PROGRAM: program = self.get_program(item.get('id'), cache=CACHE_ONLY) if program: items.append(program) else: items.append( Program( program_id=item.get('id'), name=item.get('name'), )) return items def get_product(self): """ Return the product that is currently selected. """ profile = self._kodi.get_setting('profile') try: return profile.split(':')[1] except IndexError: return None @staticmethod def _parse_channel(url): """ Parse the channel logo url, and return an icon that matches resource.images.studios.white :type url: str :rtype str """ if not url: return None import os.path # The channels id's we use in resources.lib.modules.CHANNELS neatly matches this part in the url. return str(os.path.basename(url).split('-')[0]) def _get_url(self, url, params=None): """ Makes a GET request for the specified URL. :type url: str :rtype str """ headers = self._HEADERS token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token profile = self._auth.get_profile() if profile: headers['x-dpp-profile'] = profile _LOGGER.debug('Sending GET %s...', url) response = requests.session().get('https://lfvp-api.dpgmedia.net' + url, params=params, headers=headers, proxies=self._proxies) # Set encoding to UTF-8 if no charset is indicated in http headers (https://github.com/psf/requests/issues/1604) if not response.encoding: response.encoding = 'utf-8' _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) if response.status_code == 404: raise UnavailableException() if response.status_code == 426: raise ApiUpdateRequired() if response.status_code not in [200, 204]: raise Exception('Error %s.' % response.status_code) return response.text def _put_url(self, url): """ Makes a PUT request for the specified URL. :type url: str :rtype str """ headers = self._HEADERS token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token profile = self._auth.get_profile() if profile: headers['x-dpp-profile'] = profile _LOGGER.debug('Sending PUT %s...', url) response = requests.session().put('https://api.vtmgo.be' + url, headers=headers, proxies=self._proxies) _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) if response.status_code == 404: raise UnavailableException() if response.status_code == 426: raise ApiUpdateRequired() if response.status_code not in [200, 204]: raise Exception('Error %s.' % response.status_code) return response.text def _post_url(self, url): """ Makes a POST request for the specified URL. :type url: str :rtype str """ headers = self._HEADERS token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token profile = self._auth.get_profile() if profile: headers['x-dpp-profile'] = profile _LOGGER.debug('Sending POST %s...', url) response = requests.session().post('https://api.vtmgo.be' + url, headers=headers, proxies=self._proxies) _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) if response.status_code == 404: raise UnavailableException() if response.status_code == 426: raise ApiUpdateRequired() if response.status_code not in [200, 204]: raise Exception('Error %s.' % response.status_code) return response.text def _delete_url(self, url): """ Makes a DELETE request for the specified URL. :type url: str :rtype str """ headers = self._HEADERS token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token profile = self._auth.get_profile() if profile: headers['x-dpp-profile'] = profile _LOGGER.debug('Sending DELETE %s...', url) response = requests.session().delete('https://api.vtmgo.be' + url, headers=headers, proxies=self._proxies) _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) if response.status_code == 404: raise UnavailableException() if response.status_code == 426: raise ApiUpdateRequired() if response.status_code not in [200, 204]: raise Exception('Error %s.' % response.status_code) return response.text
class Authentication: """ Code responsible for the Authentication """ def __init__(self): """ Initialise object """ self._auth = VtmGoAuth(kodiutils.get_setting('username'), kodiutils.get_setting('password'), 'VTM', kodiutils.get_setting('profile'), kodiutils.get_tokens_path()) self._vtm_go = VtmGo(self._auth) def select_profile(self, key=None): # pylint: disable=too-many-return-statements """ Show your profiles :type key: str """ try: profiles = self._auth.get_profiles() except InvalidLoginException: kodiutils.ok_dialog(message=kodiutils.localize( 30203)) # Your credentials are not valid! kodiutils.open_settings() return except InvalidTokenException: self._auth.logout() kodiutils.redirect(kodiutils.url_for('select_profile')) return except LoginErrorException as exc: kodiutils.ok_dialog(message=kodiutils.localize( 30702, code=exc.code)) # Unknown error while logging in: {code} kodiutils.open_settings() return except ApiUpdateRequired: kodiutils.ok_dialog(message=kodiutils.localize( 30705)) # The VTM GO Service has been updated... return except Exception as exc: # pylint: disable=broad-except kodiutils.ok_dialog(message="%s" % exc) return # Show warning when you have no profiles if not profiles: # Your account has no profiles defined. Please login on vtm.be/vtmgo and create a Profile. kodiutils.ok_dialog(message=kodiutils.localize(30703)) kodiutils.end_of_directory() return # Select the first profile when you only have one if len(profiles) == 1: key = profiles[0].key # Save the selected profile if key: profile = [x for x in profiles if x.key == key][0] _LOGGER.debug('Setting profile to %s', profile) kodiutils.set_setting('profile', '%s:%s' % (profile.key, profile.product)) kodiutils.set_setting('profile_name', profile.name) kodiutils.redirect(kodiutils.url_for('show_main_menu')) return # Show profile selection when you have multiple profiles listing = [ kodiutils.TitleItem( title=self._get_profile_name(p), path=kodiutils.url_for('select_profile', key=p.key), art_dict=dict(icon='DefaultUser.png'), info_dict=dict(plot=p.name, ), ) for p in profiles ] kodiutils.show_listing(listing, sort=['unsorted'], category=30057) # Select Profile def clear_tokens(self): """ Clear the authentication tokens """ self._auth.logout() kodiutils.notification(message=kodiutils.localize(30706)) @staticmethod def _get_profile_name(profile): """ Get a descriptive string of the profile :type profile: resources.lib.vtmgo.vtmgo.Profile """ title = profile.name # Convert the VTM GO Profile color to a matching Kodi color color_map = { '#64D8E3': 'skyblue', '#4DFF76': 'mediumspringgreen', '#0243FF': 'blue', '#831CFA': 'blueviolet', '#FFB24D': 'khaki', '#FF4DD5': 'violet', '#FFB002': 'gold', '#FF0257': 'crimson', } if color_map.get(profile.color.upper()): title = '[COLOR %s]%s[/COLOR]' % (color_map.get( profile.color), kodiutils.to_unicode(title)) # Append (Kids) if profile.product == 'VTM_GO_KIDS': title = "%s (Kids)" % title return title
def __init__(self, kodi): self._kodi = kodi # type: KodiWrapper self._proxies = kodi.get_proxies() self._auth = VtmGoAuth(kodi)
class VtmGo: """ VTM GO API """ CONTENT_TYPE_MOVIE = 'MOVIE' CONTENT_TYPE_PROGRAM = 'PROGRAM' CONTENT_TYPE_EPISODE = 'EPISODE' _HEADERS = { 'x-app-version': '5', 'x-persgroep-mobile-app': 'true', 'x-persgroep-os': 'android', 'x-persgroep-os-version': '23', 'User-Agent': 'VTMGO/6.8.2 (be.vmma.vtm.zenderapp; build:11215; Android 23) okhttp/3.14.2' } def __init__(self, kodi): self._kodi = kodi # type: KodiWrapper self._proxies = kodi.get_proxies() self._auth = VtmGoAuth(kodi) def _mode(self): """ Return the mode that should be used for API calls """ return 'vtmgo-kids' if self._kodi.kids_mode() else 'vtmgo' def get_config(self): """ Returns the config for the app """ # This is currently not used response = self._get_url('/config') info = json.loads(response) # This contains a player.updateIntervalSeconds that could be used to notify VTM GO about the playing progress return info def get_recommendations(self): """ Returns the config for the dashboard """ response = self._get_url('/%s/main' % self._mode()) recommendations = json.loads(response) categories = [] for cat in recommendations.get('rows', []): if cat.get('rowType') not in ['SWIMLANE_DEFAULT']: self._kodi.log( 'Skipping recommendation {name} with type={type}', name=cat.get('title'), type=cat.get('rowType')) continue items = [] for item in cat.get('teasers'): if item.get('target', {}).get('type') == self.CONTENT_TYPE_MOVIE: movie = self.get_movie(item.get('target', {}).get('id'), cache=True) if movie: items.append(movie) else: items.append( Movie( movie_id=item.get('target', {}).get('id'), name=item.get('title'), cover=item.get('imageUrl'), geoblocked=item.get('geoBlocked'), )) elif item.get('target', {}).get('type') == self.CONTENT_TYPE_PROGRAM: program = self.get_program(item.get('target', {}).get('id'), cache=True) if program: items.append(program) else: items.append( Program( program_id=item.get('target', {}).get('id'), name=item.get('title'), cover=item.get('imageUrl'), geoblocked=item.get('geoBlocked'), )) categories.append( Category( category_id=cat.get('id'), title=cat.get('title'), content=items, )) return categories def get_swimlane(self, swimlane=None): """ Returns the contents of My List """ response = self._get_url('/%s/main/swimlane/%s' % (self._mode(), swimlane)) # Result can be empty if not response: return [] result = json.loads(response) items = [] for item in result.get('teasers'): if item.get('target', {}).get('type') == self.CONTENT_TYPE_MOVIE: movie = self.get_movie(item.get('target', {}).get('id'), cache=True) if movie: items.append(movie) else: items.append( Movie( movie_id=item.get('target', {}).get('id'), name=item.get('title'), geoblocked=item.get('geoBlocked'), cover=item.get('imageUrl'), )) elif item.get('target', {}).get('type') == self.CONTENT_TYPE_PROGRAM: program = self.get_program(item.get('target', {}).get('id'), cache=True) if program: items.append(program) else: items.append( Program( program_id=item.get('target', {}).get('id'), name=item.get('title'), geoblocked=item.get('geoBlocked'), cover=item.get('imageUrl'), )) elif item.get('target', {}).get('type') == self.CONTENT_TYPE_EPISODE: if swimlane == 'continue-watching': title = '%dx%02d' % (item.get( 'target', {}).get('seasonIndex'), item.get( 'target', {}).get('episodeIndex')) else: title = item.get('title') items.append( Episode( episode_id=item.get('target', {}).get('id'), program_id=item.get('target', {}).get('programId'), program_name=item.get('target', {}).get('programName'), number=item.get('target', {}).get('episodeIndex'), season=item.get('target', {}).get('seasonIndex'), name=title, geoblocked=item.get('geoBlocked'), cover=item.get('imageUrl'), progress=item.get('playerPositionSeconds'), watched=False, )) return items def add_mylist(self, video_type, content_id): """ Add an item to My List """ self._put_url('/%s/userData/myList/%s/%s' % (self._mode(), video_type, content_id)) def del_mylist(self, video_type, content_id): """ Delete an item from My List """ self._delete_url('/%s/userData/myList/%s/%s' % (self._mode(), video_type, content_id)) def get_live_channels(self): """ Get a list of all the live tv channels. :rtype list[LiveChannel] """ import dateutil.parser response = self._get_url('/%s/live' % self._mode()) info = json.loads(response) channels = [] for item in info.get('channels'): epg = [] for item_epg in item.get('broadcasts', []): epg.append( LiveChannelEpg( title=item_epg.get('name'), start=dateutil.parser.parse(item_epg.get('startsAt')), end=dateutil.parser.parse(item_epg.get('endsAt')), )) channels.append( LiveChannel( key=item.get('seoKey'), channel_id=item.get('channelId'), logo=self._parse_channel(item.get('channelLogoUrl')), name=item.get('name'), epg=epg, )) return channels def get_live_channel(self, key): """ Get a the specified live tv channel. :rtype LiveChannel """ channels = self.get_live_channels() return next(c for c in channels if c.key == key) def get_categories(self): """ Get a list of all the categories. :rtype list[Category] """ response = self._get_url('/%s/catalog/filters' % self._mode()) info = json.loads(response) categories = [] for item in info.get('catalogFilters', []): categories.append( Category( category_id=item.get('id'), title=item.get('title'), )) return categories def get_items(self, category=None, cache=False): """ Get a list of all the items in a category. :type category: str :type cache: bool :rtype list[Union[Movie, Program]] """ if cache and category is None: # Fetch from cache if asked content = self._kodi.get_cache(['catalog']) if not content: return None else: # Fetch from API if category is None: response = self._get_url('/%s/catalog?pageSize=%d' % (self._mode(), 1000)) info = json.loads(response) content = info.get('pagedTeasers', {}).get('content', []) self._kodi.set_cache(['catalog'], content) else: response = self._get_url('/%s/catalog?pageSize=%d&filter=%s' % (self._mode(), 1000, quote(category))) info = json.loads(response) content = info.get('pagedTeasers', {}).get('content', []) items = [] for item in content: if item.get('target', {}).get('type') == self.CONTENT_TYPE_MOVIE: movie = self.get_movie(item.get('target', {}).get('id'), cache=True) if movie: items.append(movie) else: items.append( Movie( movie_id=item.get('target', {}).get('id'), name=item.get('title'), cover=item.get('imageUrl'), geoblocked=item.get('geoBlocked'), )) elif item.get('target', {}).get('type') == self.CONTENT_TYPE_PROGRAM: program = self.get_program(item.get('target', {}).get('id'), cache=True) if program: items.append(program) else: items.append( Program( program_id=item.get('target', {}).get('id'), name=item.get('title'), cover=item.get('imageUrl'), geoblocked=item.get('geoBlocked'), )) return items def get_movie(self, movie_id, cache=False): """ Get the details of the specified movie. :type movie_id: str :type cache: bool :rtype Movie """ if cache: # Fetch from cache if asked movie = self._kodi.get_cache(['movie', movie_id]) if not movie: return None else: # Fetch from API response = self._get_url('/%s/movies/%s' % (self._mode(), movie_id)) info = json.loads(response) movie = info.get('movie', {}) self._kodi.set_cache(['movie', movie_id], movie) return Movie( movie_id=movie.get('id'), name=movie.get('name'), description=movie.get('description'), duration=movie.get('durationSeconds'), cover=movie.get('bigPhotoUrl'), year=movie.get('productionYear'), geoblocked=movie.get('geoBlocked'), remaining=movie.get('remainingDaysAvailable'), legal=movie.get('legalIcons'), aired=movie.get('broadcastTimestamp'), channel=self._parse_channel(movie.get('channelLogoUrl')), ) def get_program(self, program_id, cache=False): """ Get the details of the specified program. :type program_id: str :type cache: bool :rtype Program """ if cache: # Fetch from cache if asked program = self._kodi.get_cache(['program', program_id]) if not program: return None else: # Fetch from API response = self._get_url('/%s/programs/%s' % (self._mode(), program_id)) info = json.loads(response) program = info.get('program', {}) self._kodi.set_cache(['program', program_id], program) channel = self._parse_channel(program.get('channelLogoUrl')) seasons = {} for item_season in program.get('seasons', []): episodes = {} for item_episode in item_season.get('episodes', []): episodes[item_episode.get('index')] = Episode( episode_id=item_episode.get('id'), program_id=program_id, program_name=program.get('name'), number=item_episode.get('index'), season=item_season.get('index'), name=item_episode.get('name'), description=item_episode.get('description'), duration=item_episode.get('durationSeconds'), cover=item_episode.get('bigPhotoUrl'), geoblocked=program.get('geoBlocked'), remaining=item_episode.get('remainingDaysAvailable'), channel=channel, legal=program.get('legalIcons'), aired=item_episode.get('broadcastTimestamp'), progress=item_episode.get('playerPositionSeconds', 0), watched=item_episode.get('doneWatching', False), ) seasons[item_season.get('index')] = Season( number=item_season.get('index'), episodes=episodes, cover=item_season.get('episodes', [{}])[0].get('bigPhotoUrl') if episodes else program.get('bigPhotoUrl'), geoblocked=program.get('geoBlocked'), channel=channel, legal=program.get('legalIcons'), ) return Program( program_id=program.get('id'), name=program.get('name'), description=program.get('description'), cover=program.get('bigPhotoUrl'), geoblocked=program.get('geoBlocked'), seasons=seasons, channel=channel, legal=program.get('legalIcons'), ) @staticmethod def get_episode_from_program(program, episode_id): """ Extract the specified episode from the program data. :type program: Program :type episode_id: str :rtype Episode """ for season in program.seasons.values(): for episode in season.episodes.values(): if episode.episode_id == episode_id: return episode return None def get_episode(self, episode_id): """ Get the details of the specified episode. :type episode_id: str :rtype Episode """ # The following API doesn't seem to be available in API version 6 anymore. response = self._get_url('/%s/episodes/%s' % (self._mode(), episode_id)) info = json.loads(response) episode = info.get('episode', {}) return Episode( episode_id=episode.get('id'), program_id=episode.get('programId'), program_name=episode.get('programName'), number=episode.get('index'), season=episode.get('seasonIndex'), name=episode.get('name'), description=episode.get('description'), cover=episode.get('bigPhotoUrl'), progress=episode.get('playerPositionSeconds'), ) def do_search(self, search): """ Do a search in the full catalog. :type search: str :rtype list[Union[Movie, Program]] """ response = self._get_url('/%s/autocomplete/?maxItems=%d&keywords=%s' % (self._mode(), 50, quote(search))) results = json.loads(response) items = [] for item in results.get('suggestions', []): if item.get('type') == self.CONTENT_TYPE_MOVIE: movie = self.get_movie(item.get('id'), cache=True) if movie: items.append(movie) else: items.append( Movie( movie_id=item.get('id'), name=item.get('name'), )) elif item.get('type') == self.CONTENT_TYPE_PROGRAM: program = self.get_program(item.get('id'), cache=True) if program: items.append(program) else: items.append( Program( program_id=item.get('id'), name=item.get('name'), )) return items @staticmethod def _parse_channel(url): """ Parse the channel logo url, and return an icon that matches resource.images.studios.white :type url: str :rtype str """ if not url: return None import os.path # The channels id's we use in resources.lib.modules.CHANNELS neatly matches this part in the url. return str(os.path.basename(url).split('-')[0]) def _get_url(self, url): """ Makes a GET request for the specified URL. :type url: str :rtype str """ headers = self._HEADERS token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token self._kodi.log('Sending GET {url}...', LOG_INFO, url=url) response = requests.session().get('https://api.vtmgo.be' + url, headers=headers, verify=False, proxies=self._proxies) self._kodi.log('Got response (status={code}): {response}', LOG_DEBUG, code=response.status_code, response=response.text) if response.status_code == 404: raise UnavailableException() if response.status_code not in [200, 204]: raise Exception('Error %s.' % response.status_code) return response.text def _put_url(self, url): """ Makes a PUT request for the specified URL. :type url: str :rtype str """ headers = self._HEADERS token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token self._kodi.log('Sending PUT {url}...', LOG_INFO, url=url) response = requests.session().put('https://api.vtmgo.be' + url, headers=headers, verify=False, proxies=self._proxies) self._kodi.log('Got response: {response}', LOG_DEBUG, response=response.text) if response.status_code == 404: raise UnavailableException() if response.status_code not in [200, 204]: raise Exception('Error %s.' % response.status_code) return response.text def _delete_url(self, url): """ Makes a DELETE request for the specified URL. :type url: str :rtype str """ headers = self._HEADERS token = self._auth.get_token() if token: headers['x-dpp-jwt'] = token self._kodi.log('Sending DELETE {url}...', LOG_INFO, url=url) response = requests.session().delete('https://api.vtmgo.be' + url, headers=headers, verify=False, proxies=self._proxies) self._kodi.log('Got response: {response}', LOG_DEBUG, response=response.text) if response.status_code == 404: raise UnavailableException() if response.status_code not in [200, 204]: raise Exception('Error %s.' % response.status_code) return response.text
class BackgroundService(Monitor): """ Background service code """ def __init__(self): Monitor.__init__(self) self.kodi = KodiWrapper() self.vtm_go = VtmGo(self.kodi) self.vtm_go_auth = VtmGoAuth(self.kodi) self.update_interval = 24 * 3600 # Every 24 hours self.cache_expiry = 30 * 24 * 3600 # One month def run(self): """ Background loop for maintenance tasks """ self.kodi.log('Service started', LOG_INFO) while not self.abortRequested(): # Update every `update_interval` after the last update if self.kodi.get_setting_as_bool('metadata_update') and int( self.kodi.get_setting('metadata_last_updated', 0)) + self.update_interval < time(): self._update_metadata() # Stop when abort requested if self.waitForAbort(10): break self.kodi.log('Service stopped', LOG_INFO) def onSettingsChanged(self): """ Callback when a setting has changed """ # Refresh our VtmGo instance self.vtm_go = VtmGo(self.kodi) if self.vtm_go_auth.has_credentials_changed(): self.kodi.log('Clearing auth tokens due to changed credentials', LOG_INFO) self.vtm_go_auth.clear_token() # Refresh container self.kodi.container_refresh() def _update_metadata(self): """ Update the metadata for the listings. """ from resources.lib.modules.metadata import Metadata # Clear outdated metadata self.kodi.invalidate_cache(self.cache_expiry) # Create progress indicator progress = self.kodi.show_progress_background( message=self.kodi.localize(30715)) self.kodi.log('Updating metadata in the background') def update_status(i, total): """ Update the progress indicator """ progress.update(int(((i + 1) / total) * 100)) return self.abortRequested( ) or not self.kodi.get_setting_as_bool('metadata_update') success = Metadata(self.kodi).fetch_metadata(callback=update_status) # Close progress indicator progress.close() # Update metadata_last_updated if success: self.kodi.set_setting('metadata_last_updated', str(int(time())))
class Menu: """ Menu code """ def __init__(self): """ Initialise object """ self._auth = VtmGoAuth(kodiutils.get_setting('username'), kodiutils.get_setting('password'), 'VTM', kodiutils.get_setting('profile'), kodiutils.get_tokens_path()) def show_mainmenu(self): """ Show the main menu """ listing = [] account = self._auth.login() listing.append( kodiutils.TitleItem( title=kodiutils.localize(30007), # TV Channels path=kodiutils.url_for('show_channels'), art_dict=dict( icon='DefaultAddonPVRClient.png', fanart=kodiutils.get_addon_info('fanart'), ), info_dict=dict(plot=kodiutils.localize(30008), ), )) if account.product == 'VTM_GO': listing.append( kodiutils.TitleItem( title=kodiutils.localize(30015), # Recommendations path=kodiutils.url_for('show_recommendations', storefront=STOREFRONT_MAIN), art_dict=dict( icon='DefaultFavourites.png', fanart=kodiutils.get_addon_info('fanart'), ), info_dict=dict(plot=kodiutils.localize(30016), ), )) listing.append( kodiutils.TitleItem( title=kodiutils.localize(30003), # Movies path=kodiutils.url_for('show_recommendations', storefront=STOREFRONT_MOVIES), art_dict=dict( icon='DefaultMovies.png', fanart=kodiutils.get_addon_info('fanart'), ), info_dict=dict(plot=kodiutils.localize(30004), ), )) listing.append( kodiutils.TitleItem( title=kodiutils.localize(30005), # Series path=kodiutils.url_for('show_recommendations', storefront=STOREFRONT_SERIES), art_dict=dict( icon='DefaultTVShows.png', fanart=kodiutils.get_addon_info('fanart'), ), info_dict=dict(plot=kodiutils.localize(30006), ), )) listing.append( kodiutils.TitleItem( title=kodiutils.localize(30021), # Kids path=kodiutils.url_for('show_recommendations', storefront=STOREFRONT_KIDS), art_dict=dict( icon='DefaultFavourites.png', fanart=kodiutils.get_addon_info('fanart'), ), info_dict=dict(plot=kodiutils.localize(30022), ), )) elif account.product == 'VTM_GO_KIDS': listing.append( kodiutils.TitleItem( title=kodiutils.localize(30015), # Recommendations path=kodiutils.url_for('show_recommendations', storefront=STOREFRONT_KIDS_MAIN), art_dict=dict( icon='DefaultFavourites.png', fanart=kodiutils.get_addon_info('fanart'), ), info_dict=dict(plot=kodiutils.localize(30016), ), )) listing.append( kodiutils.TitleItem( title=kodiutils.localize(30001), # A-Z path=kodiutils.url_for('show_catalog_all'), art_dict=dict( icon='DefaultMovieTitle.png', fanart=kodiutils.get_addon_info('fanart'), ), info_dict=dict(plot=kodiutils.localize(30002), ), )) # listing.append(kodiutils.TitleItem( # title=kodiutils.localize(30003), # Catalogue # path=kodiutils.url_for('show_catalog'), # art_dict=dict( # icon='DefaultGenre.png', # fanart=kodiutils.get_addon_info('fanart'), # ), # info_dict=dict( # plot=kodiutils.localize(30004), # ), # )) if kodiutils.get_setting_bool( 'interface_show_mylist') and kodiutils.has_credentials(): listing.append( kodiutils.TitleItem( title=kodiutils.localize(30017), # My List path=kodiutils.url_for('show_mylist'), art_dict=dict( icon='DefaultPlaylist.png', fanart=kodiutils.get_addon_info('fanart'), ), info_dict=dict(plot=kodiutils.localize(30018), ), )) if kodiutils.get_setting_bool('interface_show_continuewatching' ) and kodiutils.has_credentials(): listing.append( kodiutils.TitleItem( title=kodiutils.localize(30019), # Continue watching path=kodiutils.url_for('show_continuewatching'), art_dict=dict( icon='DefaultInProgressShows.png', fanart=kodiutils.get_addon_info('fanart'), ), info_dict=dict(plot=kodiutils.localize(30020), ), )) listing.append( kodiutils.TitleItem( title=kodiutils.localize(30009), # Search path=kodiutils.url_for('show_search'), art_dict=dict( icon='DefaultAddonsSearch.png', fanart=kodiutils.get_addon_info('fanart'), ), info_dict=dict(plot=kodiutils.localize(30010), ), )) kodiutils.show_listing(listing, sort=['unsorted']) @staticmethod def format_plot(obj): """ Format the plot for a item :type obj: object :rtype str """ plot = '' if hasattr(obj, 'epg'): if obj.epg: plot += kodiutils.localize( 30213, # Now start=obj.epg[0].start.strftime('%H:%M'), end=obj.epg[0].end.strftime('%H:%M'), title=obj.epg[0].title) + "\n" if len(obj.epg) > 1: plot += kodiutils.localize( 30214, # Next start=obj.epg[1].start.strftime('%H:%M'), end=obj.epg[1].end.strftime('%H:%M'), title=obj.epg[1].title) + "\n" plot += '\n' # Add remaining if hasattr(obj, 'remaining') and obj.remaining is not None: if obj.remaining == 0: plot += '» ' + kodiutils.localize( 30208) + "\n" # Available until midnight elif obj.remaining == 1: plot += '» ' + kodiutils.localize( 30209) + "\n" # One more day remaining elif obj.remaining / 365 > 5: pass # If it is available for more than 5 years, do not show elif obj.remaining / 365 > 2: plot += '» ' + kodiutils.localize( 30210, years=int( obj.remaining / 365)) + "\n" # X years remaining elif obj.remaining / 30.5 > 3: plot += '» ' + kodiutils.localize( 30211, months=int( obj.remaining / 30.5)) + "\n" # X months remaining else: plot += '» ' + kodiutils.localize( 30212, days=obj.remaining) + "\n" # X days remaining plot += '\n' # Add geo-blocked message if hasattr(obj, 'geoblocked') and obj.geoblocked: plot += kodiutils.localize(30207) # Geo-blocked plot += '\n' if hasattr(obj, 'description'): plot += obj.description plot += '\n\n' return plot.rstrip() @classmethod def generate_titleitem(cls, item, progress=False): """ Generate a TitleItem based on a Movie, Program or Episode. :type item: Union[Movie, Program, Episode] :type progress: bool :rtype TitleItem """ art_dict = { 'thumb': item.cover, 'cover': item.cover, } info_dict = { 'title': item.name, 'plot': cls.format_plot(item), 'studio': CHANNELS.get(item.channel, {}).get('studio_icon'), 'mpaa': ', '.join(item.legal) if hasattr(item, 'legal') and item.legal else kodiutils.localize(30216), # All ages } prop_dict = {} # # Movie # if isinstance(item, Movie): if item.my_list: context_menu = [( kodiutils.localize(30101), # Remove from My List 'Container.Update(%s)' % kodiutils.url_for('mylist_del', video_type=CONTENT_TYPE_MOVIE, content_id=item.movie_id))] else: context_menu = [( kodiutils.localize(30100), # Add to My List 'Container.Update(%s)' % kodiutils.url_for('mylist_add', video_type=CONTENT_TYPE_MOVIE, content_id=item.movie_id))] art_dict.update({ 'fanart': item.image, }) info_dict.update({ 'mediatype': 'movie', 'duration': item.duration, 'year': item.year, 'aired': item.aired, }) stream_dict = { 'codec': 'h264', 'duration': item.duration, 'height': 1080, 'width': 1920, } return kodiutils.TitleItem( title=item.name, path=kodiutils.url_for('play', category='movies', item=item.movie_id), art_dict=art_dict, info_dict=info_dict, stream_dict=stream_dict, prop_dict=prop_dict, context_menu=context_menu, is_playable=True, ) # # Program # if isinstance(item, Program): if item.my_list: context_menu = [( kodiutils.localize(30101), # Remove from My List 'Container.Update(%s)' % kodiutils.url_for('mylist_del', video_type=CONTENT_TYPE_PROGRAM, content_id=item.program_id))] else: context_menu = [( kodiutils.localize(30100), # Add to My List 'Container.Update(%s)' % kodiutils.url_for('mylist_add', video_type=CONTENT_TYPE_PROGRAM, content_id=item.program_id))] art_dict.update({ 'fanart': item.image, }) info_dict.update({ 'mediatype': 'tvshow', 'season': len(item.seasons), }) prop_dict.update({ 'hash': item.content_hash, }) return kodiutils.TitleItem( title=item.name, path=kodiutils.url_for('show_catalog_program', program=item.program_id), art_dict=art_dict, info_dict=info_dict, prop_dict=prop_dict, context_menu=context_menu, ) # # Episode # if isinstance(item, Episode): context_menu = [] if item.program_id: context_menu = [( kodiutils.localize(30102), # Go to Program 'Container.Update(%s)' % kodiutils.url_for( 'show_catalog_program', program=item.program_id))] art_dict.update({ 'fanart': item.cover, }) info_dict.update({ 'mediatype': 'episode', 'tvshowtitle': item.program_name, 'duration': item.duration, 'season': item.season, 'episode': item.number, 'set': item.program_name, 'aired': item.aired, }) if progress and item.watched: info_dict.update({ 'playcount': 1, }) stream_dict = { 'codec': 'h264', 'duration': item.duration, 'height': 1080, 'width': 1920, } # Add progress info if progress and not item.watched and item.progress: prop_dict.update({ 'ResumeTime': item.progress, 'TotalTime': item.progress + 1, }) return kodiutils.TitleItem( title=info_dict['title'], path=kodiutils.url_for('play', category='episodes', item=item.episode_id), art_dict=art_dict, info_dict=info_dict, stream_dict=stream_dict, prop_dict=prop_dict, context_menu=context_menu, is_playable=True, ) raise Exception('Unknown video_type')