Example #1
0
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()
Example #2
0
    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()
Example #3
0
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
Example #4
0
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
Example #5
0
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