def _web_login(self): """ Executes a login and returns the JSON Web Token. :rtype str """ # Yes, we have accepted the cookies util.SESSION.cookies.clear() util.SESSION.cookies.set('authId', str(uuid4())) # Start login flow util.http_get('https://vtm.be/vtmgo/aanmelden?redirectUrl=https://vtm.be/vtmgo') # Send login credentials try: response = util.http_post('https://login2.vtm.be/login?client_id=vtm-go-web', form={ 'userName': self._username, 'password': self._password, 'jsEnabled': 'true', }) except HTTPError as exc: if exc.response.status_code == 400: raise InvalidLoginException() raise if 'errorBlock-OIDC-004' in response.text: # E-mailadres is niet gekend. raise InvalidLoginException() if 'errorBlock-OIDC-003' in response.text: # Wachtwoord is niet correct. raise InvalidLoginException() if 'OIDC-999' in response.text: # Ongeldige login. raise InvalidLoginException() # Follow login response = util.http_get('https://login2.vtm.be/authorize/continue?client_id=vtm-go-web') # Extract state and code matches_state = re.search(r'name="state" value="([^"]+)', response.text) if matches_state: state = matches_state.group(1) else: raise LoginErrorException(code=101) # Could not extract authentication code matches_code = re.search(r'name="code" value="([^"]+)', response.text) if matches_code: code = matches_code.group(1) else: raise LoginErrorException(code=101) # Could not extract authentication code # Okay, final stage. We now need to POST our state and code to get a valid JWT. util.http_post('https://vtm.be/vtmgo/login-callback', form={ 'state': state, 'code': code, }) # Get JWT from cookies self._account.jwt_token = util.SESSION.cookies.get('lfvp_auth') self._save_cache() return self._account
def _anvato_get_anvacks(self, access_key): """ Get the anvacks from anvato. (not needed) :type access_key: string :rtype dict """ url = 'https://access-prod.apis.anvato.net/anvacks/{key}'.format( key=access_key) _LOGGER.debug('Getting anvacks from %s', url) response = util.http_get(url, params={ 'apikey': self._ANVATO_API_KEY, }, headers={ 'X-Anvato-User-Agent': self._ANVATO_USER_AGENT, }) _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) if response.status_code != 200: raise Exception('Error %s in _anvato_get_anvacks.' % response.status_code) info = json.loads(response.text) return info
def get_epgs(self, date=None): """ Load EPG information for the specified date. :type date: str :rtype: EpgChannel[] """ date = self._parse_date(date) response = util.http_get(self.EPG_URL.format(date=date)) epg = json.loads(response.text) # We get an EPG for all channels return [ EpgChannel( name=epg_channel.get('name'), key=epg_channel.get('seoKey'), logo=epg_channel.get('channelLogoUrl'), uuid=epg_channel.get('uuid'), broadcasts=[ self._parse_broadcast(broadcast) for broadcast in epg_channel.get('broadcasts', []) if broadcast.get('title', '') != self.EPG_NO_BROADCAST ] ) for epg_channel in epg.get('channels', []) ]
def _anvato_get_server_time(self, access_key): """ Get the server time from anvato. (not needed) :type access_key: string :rtype dict """ url = 'https://tkx.apis.anvato.net/rest/v2/server_time' _LOGGER.debug('Getting servertime from %s with access_key %s', url, access_key) response = util.http_get(url, params={ 'anvack': access_key, 'anvtrid': self._generate_random_id(), }, headers={ 'X-Anvato-User-Agent': self._ANVATO_USER_AGENT, 'User-Agent': self._ANVATO_USER_AGENT, }) _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) if response.status_code != 200: raise Exception('Error %s.' % response.status_code) info = json.loads(response.text) return info
def get_episode(self, episode_id): """ Get some details of the specified episode. :type episode_id: str :rtype Episode """ response = util.http_get(API_ENDPOINT + '/%s/play/episode/%s' % (self._mode(), episode_id), token=self._tokens.jwt_token, profile=self._tokens.profile) episode = json.loads(response.text) # 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 get_items(self, category=None, content_filter=None, cache=CACHE_ONLY): """ Get a list of all the items in a category. :type category: str :type content_filter: class :type cache: int :rtype list[resources.lib.vtmgo.Movie | resources.lib.vtmgo.Program] """ # Fetch from API response = util.http_get(API_ENDPOINT + '/%s/catalog' % self._mode(), params={'pageSize': 2000, 'filter': quote(category) if category else None}, token=self._tokens.jwt_token, profile=self._tokens.profile) info = json.loads(response.text) content = info.get('pagedTeasers', {}).get('content', []) items = [] for item in content: if item.get('target', {}).get('type') == CONTENT_TYPE_MOVIE and content_filter in [None, Movie]: items.append(self._parse_movie_teaser(item, cache=cache)) elif item.get('target', {}).get('type') == CONTENT_TYPE_PROGRAM and content_filter in [None, Program]: items.append(self._parse_program_teaser(item, cache=cache)) return items
def get_catalog_ids(self): """ Returns the IDs of the contents of the Catalog """ # Try to fetch from cache items = kodiutils.get_cache(['catalog_id'], 300) # 5 minutes ttl if items: return items # Fetch from API response = util.http_get( API_ENDPOINT + '/%s/catalog' % self._mode(), params={ 'pageSize': 2000, 'filter': None }, token=self._tokens.jwt_token if self._tokens else None, profile=self._tokens.profile if self._tokens else None) info = json.loads(response.text) items = [ item.get('target', {}).get('id') for item in info.get('pagedTeasers', {}).get('content', []) ] kodiutils.set_cache(['catalog_id'], items) return items
def get_storefront_category(self, storefront, category): """ Returns a storefront. :param str storefront: The ID of the storefront. :param str category: The ID of the category. :rtype: Category """ response = util.http_get( API_ENDPOINT + '/%s/storefronts/%s/detail/%s' % (self._mode(), storefront, category), token=self._tokens.jwt_token if self._tokens else None, profile=self._tokens.profile if self._tokens else None) result = json.loads(response.text) items = [] for item in result.get('row', {}).get('teasers'): if item.get('target', {}).get('type') == CONTENT_TYPE_MOVIE: items.append(self._parse_movie_teaser(item)) elif item.get('target', {}).get('type') == CONTENT_TYPE_PROGRAM: items.append(self._parse_program_teaser(item)) return Category(category_id=category, title=result.get('row', {}).get('title'), content=items)
def get_epg(self, channel, date=None): """ Load EPG information for the specified channel and date. :type channel: str :type date: str :rtype: EpgChannel """ date = self._parse_date(date) response = util.http_get(self.EPG_URL.format(date=date)) epg = json.loads(response.text) # We get an EPG for all channels, but we only return the requested channel. for epg_channel in epg.get('channels', []): if epg_channel.get('seoKey') == channel: return EpgChannel( name=epg_channel.get('name'), key=epg_channel.get('seoKey'), logo=epg_channel.get('channelLogoUrl'), uuid=epg_channel.get('uuid'), broadcasts=[ self._parse_broadcast(broadcast) for broadcast in epg_channel.get('broadcasts', []) if broadcast.get('title', '') != self.EPG_NO_BROADCAST ]) raise Exception('Channel %s not found in the EPG' % channel)
def get_live_channels(self): """ Get a list of all the live tv channels. :rtype list[LiveChannel] """ import dateutil.parser response = util.http_get(API_ENDPOINT + '/%s/live' % self._mode(), token=self._tokens.jwt_token, profile=self._tokens.profile) info = json.loads(response.text) 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_config(self): """ Returns the config for the app """ # This is currently not used response = util.http_get(API_ENDPOINT + '/config', token=self._tokens.jwt_token) info = json.loads(response.text) # This contains a player.updateIntervalSeconds that could be used to notify VTM GO about the playing progress return info
def _download_text(url): """ Download a file as text. :type url: str :rtype str """ _LOGGER.debug('Downloading text from %s', url) response = util.http_get(url) if response.status_code != 200: raise Exception('Error %s.' % response.status_code) return response.text
def get_storefront(self, storefront): """ Returns a storefront. :param str storefront: The ID of the storefront. :rtype: list[Category|Program|Movie] """ response = util.http_get( API_ENDPOINT + '/%s/storefronts/%s' % (self._mode(), storefront), token=self._tokens.jwt_token if self._tokens else None, profile=self._tokens.profile if self._tokens else None) result = json.loads(response.text) items = [] for row in result.get('rows', []): if row.get('rowType') in [ 'SWIMLANE_DEFAULT', 'SWIMLANE_PORTRAIT', 'SWIMLANE_LANDSCAPE' ]: items.append( Category( category_id=row.get('id'), title=row.get('title'), )) continue if row.get('rowType') == 'CAROUSEL': for item in row.get('teasers'): if item.get('target', {}).get('type') == CONTENT_TYPE_MOVIE: items.append(self._parse_movie_teaser(item)) elif item.get('target', {}).get('type') == CONTENT_TYPE_PROGRAM: items.append(self._parse_program_teaser(item)) continue if row.get('rowType') in ['TOP_BANNER', 'MARKETING_BLOCK']: item = row.get('teaser') if item.get('target', {}).get('type') == CONTENT_TYPE_MOVIE: items.append(self._parse_movie_teaser(item)) elif item.get('target', {}).get('type') == CONTENT_TYPE_PROGRAM: items.append(self._parse_program_teaser(item)) continue _LOGGER.debug('Skipping recommendation %s with type %s', row.get('title'), row.get('rowType')) return items
def _download_text(self, url): """ Download a file as text. :type url: str :rtype str """ _LOGGER.debug('Downloading text from %s', url) response = util.http_get(url, headers={ 'X-Anvato-User-Agent': self._ANVATO_USER_AGENT, 'User-Agent': self._ANVATO_USER_AGENT, }) if response.status_code != 200: raise Exception('Error %s.' % response.status_code) return response.text
def get_categories(self): """ Get a list of all the categories. :rtype list[Category] """ response = util.http_get(API_ENDPOINT + '/%s/catalog/filters' % self._mode(), token=self._tokens.jwt_token, profile=self._tokens.profile) info = json.loads(response.text) categories = [] for item in info.get('catalogFilters', []): categories.append(Category( category_id=item.get('id'), title=item.get('title'), )) return categories
def _delay_subtitles(self, subtitles, json_manifest): """ Modify the subtitles timings to account for ad breaks. :type subtitles: list[string] :type json_manifest: dict :rtype list[str] """ # Clean up old subtitles temp_dir = os.path.join(kodiutils.addon_profile(), 'temp', '') _, files = kodiutils.listdir(temp_dir) if files: for item in files: if kodiutils.to_unicode(item).endswith('.vtt'): kodiutils.delete(temp_dir + kodiutils.to_unicode(item)) # Return if there are no subtitles available if not subtitles: return None import re if not kodiutils.exists(temp_dir): kodiutils.mkdirs(temp_dir) ad_breaks = list() delayed_subtitles = list() webvtt_timing_regex = re.compile( r'\n(\d{2}:\d{2}:\d{2}\.\d{3}) --> (\d{2}:\d{2}:\d{2}\.\d{3})\s') # Get advertising breaks info from json manifest cues = json_manifest.get('interstitials').get('cues') for cue in cues: ad_breaks.append( dict(start=cue.get('start'), duration=cue.get('break_duration'))) for subtitle in subtitles: output_file = temp_dir + subtitle.get('name') + '.' + subtitle.get( 'url').split('.')[-1] webvtt_content = util.http_get(subtitle.get('url')).text webvtt_content = webvtt_timing_regex.sub( lambda match: self._delay_webvtt_timing(match, ad_breaks), webvtt_content) with kodiutils.open_file(output_file, 'w') as webvtt_output: webvtt_output.write(kodiutils.from_unicode(webvtt_content)) delayed_subtitles.append(output_file) return delayed_subtitles
def get_mylist_ids(self): """ Returns the IDs of the contents of My List """ # Try to fetch from cache items = kodiutils.get_cache(['mylist_id'], 300) # 5 minutes ttl if items: return items # Fetch from API response = util.http_get(API_ENDPOINT + '/%s/main/swimlane/%s' % (self._mode(), 'my-list'), token=self._tokens.jwt_token, profile=self._tokens.profile) # Result can be empty result = json.loads(response.text) if response.text else [] items = [item.get('target', {}).get('id') for item in result.get('teasers', [])] kodiutils.set_cache(['mylist_id'], items) return items
def _get_stream_info(self, strtype, stream_id): """ Get the stream info for the specified stream. :type strtype: str :type stream_id: str :rtype: dict """ url = 'https://videoplayer-service.api.persgroep.cloud/config/%s/%s' % ( strtype, stream_id) _LOGGER.debug('Getting stream info from %s', url) response = util.http_get( url, params={ 'startPosition': '0.0', 'autoPlay': 'true', }, headers={ 'x-api-key': self._VTM_API_KEY, 'Popcorn-SDK-Version': '2', 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 6.0.1; Nexus 5 Build/M4B30Z)', }, proxies=kodiutils.get_proxies()) _LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text) if response.status_code == 403: error = json.loads(response.text) if error['type'] == 'videoPlaybackGeoblocked': raise StreamGeoblockedException() if error['type'] == 'serviceError': raise StreamUnavailableException() if response.status_code == 404: raise StreamUnavailableException() if response.status_code != 200: raise StreamUnavailableException() info = json.loads(response.text) return info
def get_profiles(self, products='VTM_GO,VTM_GO_KIDS'): """ Returns the available profiles """ response = util.http_get(API_ENDPOINT + '/profiles', {'products': products}, token=self._account.jwt_token) result = json.loads(response.text) 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 do_search(self, search): """ Do a search in the full catalog. :type search: str :rtype list[Union[Movie, Program]] """ response = util.http_get(API_ENDPOINT + '/%s/search/?query=%s' % (self._mode(), kodiutils.to_unicode(quote(kodiutils.from_unicode(search)))), token=self._tokens.jwt_token, profile=self._tokens.profile) results = json.loads(response.text) items = [] for category in results.get('results', []): for item in category.get('teasers'): if item.get('target', {}).get('type') == CONTENT_TYPE_MOVIE: items.append(self._parse_movie_teaser(item)) elif item.get('target', {}).get('type') == CONTENT_TYPE_PROGRAM: items.append(self._parse_program_teaser(item)) return items
def _get_video_info(self, strtype, stream_id): """ Get the stream info for the specified stream. :param str strtype: :param str stream_id: :rtype: dict """ url = 'https://videoplayer-service.api.persgroep.cloud/config/%s/%s' % (strtype, stream_id) _LOGGER.debug('Getting video info from %s', url) response = util.http_get(url, params={ 'startPosition': '0.0', 'autoPlay': 'true', }, headers={ 'Accept': 'application/json', 'x-api-key': self._API_KEY, 'Popcorn-SDK-Version': '4', }) info = json.loads(response.text) return info
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 = kodiutils.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 = util.http_get( API_ENDPOINT + '/%s/movies/%s' % (self._mode(), movie_id), token=self._tokens.jwt_token if self._tokens else None, profile=self._tokens.profile if self._tokens else None) info = json.loads(response.text) movie = info.get('movie', {}) kodiutils.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'), thumb=movie.get('teaserImageUrl'), fanart=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 _download_subtitles(subtitles): # Clean up old subtitles temp_dir = os.path.join(kodiutils.addon_profile(), 'temp', '') _, files = kodiutils.listdir(temp_dir) if files: for item in files: kodiutils.delete(temp_dir + kodiutils.to_unicode(item)) # Return if there are no subtitles available if not subtitles: return None if not kodiutils.exists(temp_dir): kodiutils.mkdirs(temp_dir) downloaded_subtitles = list() for subtitle in subtitles: output_file = temp_dir + subtitle.get('name') webvtt_content = util.http_get(subtitle.get('url')).text with kodiutils.open_file(output_file, 'w') as webvtt_output: webvtt_output.write(kodiutils.from_unicode(webvtt_content)) downloaded_subtitles.append(output_file) return downloaded_subtitles
def get_mylist(self, content_filter=None, cache=CACHE_ONLY): """ Returns the contents of My List """ response = util.http_get( API_ENDPOINT + '/%s/my-list' % (self._mode()), token=self._tokens.jwt_token if self._tokens else None, profile=self._tokens.profile if self._tokens else None) # Result can be empty if not response.text: return [] result = json.loads(response.text) items = [] for item in result.get('teasers'): if item.get( 'target', {}).get('type') == CONTENT_TYPE_MOVIE and content_filter in [ None, Movie ]: items.append(self._parse_movie_teaser(item, cache=cache)) elif item.get( 'target', {}).get('type') == CONTENT_TYPE_PROGRAM and content_filter in [ None, Program ]: items.append(self._parse_program_teaser(item, cache=cache)) elif item.get( 'target', {}).get('type') == CONTENT_TYPE_EPISODE and content_filter in [ None, Episode ]: items.append(self._parse_episode_teaser(item, cache=cache)) return items
def get_recommendations(self, storefront): """ Returns the config for the dashboard. :param str storefront: The ID of the listing. :rtype: list[Category] """ response = util.http_get(API_ENDPOINT + '/%s/storefronts/%s' % (self._mode(), storefront), token=self._tokens.jwt_token, profile=self._tokens.profile) recommendations = json.loads(response.text) 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') == CONTENT_TYPE_MOVIE: items.append(self._parse_movie_teaser(item)) elif item.get('target', {}).get('type') == CONTENT_TYPE_PROGRAM: items.append(self._parse_program_teaser(item)) categories.append( Category( category_id=cat.get('id'), title=cat.get('title'), content=items, )) return categories
def _android_login(self): """ Executes an android login and returns the JSON Web Token. :rtype str """ # We should start fresh util.SESSION.cookies.clear() # Start login flow util.http_get('https://login2.vtm.be/authorize', params={ 'client_id': 'vtm-go-android', 'response_type': 'id_token', 'scope': 'openid email profile address phone', 'nonce': 1550073732654, 'sdkVersion': '0.13.1', 'state': 'dnRtLWdvLWFuZHJvaWQ=', # vtm-go-android 'redirect_uri': 'https://login2.vtm.be/continue', }) # Send login credentials try: response = util.http_post('https://login2.vtm.be/login', params={ 'client_id': 'vtm-go-android', }, form={ 'userName': self._username, 'password': self._password, }) except HTTPError as exc: if exc.response.status_code == 400: raise InvalidLoginException() raise if 'errorBlock-OIDC-004' in response.text: # E-mailadres is niet gekend. raise InvalidLoginException() if 'errorBlock-OIDC-003' in response.text: # Wachtwoord is niet correct. raise InvalidLoginException() if 'OIDC-999' in response.text: # Ongeldige login. raise InvalidLoginException() # Extract redirect match = re.search(r"window.location.href = '([^']+)'", response.text) if not match: raise LoginErrorException(code=103) redirect_url = match.group(1) # Follow login response = util.http_get(redirect_url) # We are redirected and our id_token is in the fragment of the redirected url params = parse_qs(urlparse(response.url).fragment) id_token = params['id_token'][0] # Okay, final stage. We now need to authorize our id_token so we get a valid JWT. response = util.http_post('https://lfvp-api.dpgmedia.net/vtmgo/tokens', data={ 'idToken': id_token, }) # Get JWT from reply self._account.jwt_token = json.loads(response.text).get('lfvpToken') self._save_cache() return self._account
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 = kodiutils.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 = util.http_get(API_ENDPOINT + '/%s/programs/%s' % (self._mode(), program_id), token=self._tokens.jwt_token, profile=self._tokens.profile) info = json.loads(response.text) program = info.get('program', {}) kodiutils.set_cache(['program', program_id], program) channel = self._parse_channel(program.get('channelLogoUrl')) # Calculate a hash value of the ids of all episodes program_hash = hashlib.md5() program_hash.update(program.get('id').encode()) 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), ) program_hash.update(item_episode.get('id').encode()) 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'), content_hash=program_hash.hexdigest().upper(), # my_list=program.get('addedToMyList'), # Don't use addedToMyList, since we might have cached this info )