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 """ 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 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 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