Example #1
0
 def __init__(self):
     Monitor.__init__(self)
     self.kodi = KodiWrapper()
     self.vtm_go = VtmGo(self.kodi)
     self.vtm_go_auth = VtmGoAuth(self.kodi)
     self.update_interval = 24 * 3600  # Every 24 hours
     self.cache_expiry = 30 * 24 * 3600  # One month
Example #2
0
 def __init__(self, kodi):
     """ Initialise object
     :type kodi: resources.lib.kodiwrapper.KodiWrapper
     """
     self._kodi = kodi
     self._proxies = kodi.get_proxies()
     self._auth = VtmGoAuth(kodi)
Example #3
0
 def __init__(self):
     """ Initialise object """
     self._auth = VtmGoAuth(kodiutils.get_setting('username'),
                            kodiutils.get_setting('password'), 'VTM',
                            kodiutils.get_setting('profile'),
                            kodiutils.get_tokens_path())
     self._vtm_go = VtmGo(self._auth)
Example #4
0
class BackgroundService(Monitor):
    """ Background service code """
    def __init__(self):
        Monitor.__init__(self)
        self._kodi = KodiWrapper()
        self._player = PlayerMonitor(kodi=self._kodi)
        self.vtm_go = VtmGo(self._kodi)
        self.vtm_go_auth = VtmGoAuth(self._kodi)
        self.update_interval = 24 * 3600  # Every 24 hours
        self.cache_expiry = 30 * 24 * 3600  # One month

    def run(self):
        """ Background loop for maintenance tasks """
        _LOGGER.debug('Service started')

        while not self.abortRequested():
            # Update every `update_interval` after the last update
            if self._kodi.get_setting_as_bool('metadata_update') and int(
                    self._kodi.get_setting('metadata_last_updated',
                                           0)) + self.update_interval < time():
                self._update_metadata()

            # Stop when abort requested
            if self.waitForAbort(10):
                break

        _LOGGER.debug('Service stopped')

    def onSettingsChanged(self):  # pylint: disable=invalid-name
        """ Callback when a setting has changed """
        # Refresh our VtmGo instance
        kodiwrapper.ADDON = Addon()
        self.vtm_go = VtmGo(self._kodi)

        if self.vtm_go_auth.has_credentials_changed():
            _LOGGER.debug('Clearing auth tokens due to changed credentials')
            self.vtm_go_auth.clear_token()

            # Refresh container
            self._kodi.container_refresh()

    def _update_metadata(self):
        """ Update the metadata for the listings """
        from resources.lib.modules.metadata import Metadata

        # Clear outdated metadata
        self._kodi.invalidate_cache(self.cache_expiry)

        def update_status(_i, _total):
            """ Allow to cancel the background job """
            return self.abortRequested(
            ) or not self._kodi.get_setting_as_bool('metadata_update')

        success = Metadata(self._kodi).fetch_metadata(callback=update_status)

        # Update metadata_last_updated
        if success:
            self._kodi.set_setting('metadata_last_updated', str(int(time())))
Example #5
0
    def __init__(self, kodi):
        """ Initialise object
        :type kodi: resources.lib.kodiwrapper.KodiWrapper
        """
        self._kodi = kodi
        self._auth = VtmGoAuth(kodi)

        self._session = requests.session()
        self._session.proxies = kodi.get_proxies()
        self._authenticate()
Example #6
0
 def __init__(self):
     """ Initialise object """
     self._auth = VtmGoAuth(kodiutils.get_setting('username'),
                            kodiutils.get_setting('password'),
                            kodiutils.get_setting('loginprovider'),
                            kodiutils.get_setting('profile'),
                            kodiutils.get_tokens_path())
     self._api = VtmGo(self._auth)
Example #7
0
 def __init__(self):
     """ Initialise object """
     try:
         self._auth = VtmGoAuth(kodiutils.get_setting('username'),
                                kodiutils.get_setting('password'), 'VTM',
                                kodiutils.get_setting('profile'),
                                kodiutils.get_tokens_path())
     except NoLoginException:
         self._auth = None
     self._vtm_go = VtmGo(self._auth)
Example #8
0
 def __init__(self, port):
     """ Initialise object
     :type port: int
     """
     self._auth = VtmGoAuth(kodiutils.get_setting('username'),
                            kodiutils.get_setting('password'),
                            'VTM',
                            kodiutils.get_setting('profile'),
                            kodiutils.get_tokens_path())
     self._vtm_go = VtmGo(self._auth)
     self._vtm_go_epg = VtmGoEpg()
     self.port = port
Example #9
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 #10
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 #11
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
Example #12
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 #13
0
class Authentication:
    """ Code responsible for the Authentication """
    def __init__(self):
        """ Initialise object """
        self._auth = VtmGoAuth(kodiutils.get_setting('username'),
                               kodiutils.get_setting('password'), 'VTM',
                               kodiutils.get_setting('profile'),
                               kodiutils.get_tokens_path())
        self._vtm_go = VtmGo(self._auth)

    def select_profile(self, key=None):  # pylint: disable=too-many-return-statements
        """ Show your profiles
        :type key: str
        """
        try:
            profiles = self._auth.get_profiles()
        except InvalidLoginException:
            kodiutils.ok_dialog(message=kodiutils.localize(
                30203))  # Your credentials are not valid!
            kodiutils.open_settings()
            return

        except InvalidTokenException:
            self._auth.logout()
            kodiutils.redirect(kodiutils.url_for('select_profile'))
            return

        except LoginErrorException as exc:
            kodiutils.ok_dialog(message=kodiutils.localize(
                30702,
                code=exc.code))  # Unknown error while logging in: {code}
            kodiutils.open_settings()
            return

        except ApiUpdateRequired:
            kodiutils.ok_dialog(message=kodiutils.localize(
                30705))  # The VTM GO Service has been updated...
            return

        except Exception as exc:  # pylint: disable=broad-except
            kodiutils.ok_dialog(message="%s" % exc)
            return

        # Show warning when you have no profiles
        if not profiles:
            # Your account has no profiles defined. Please login on vtm.be/vtmgo and create a Profile.
            kodiutils.ok_dialog(message=kodiutils.localize(30703))
            kodiutils.end_of_directory()
            return

        # Select the first profile when you only have one
        if len(profiles) == 1:
            key = profiles[0].key

        # Save the selected profile
        if key:
            profile = [x for x in profiles if x.key == key][0]
            _LOGGER.debug('Setting profile to %s', profile)
            kodiutils.set_setting('profile',
                                  '%s:%s' % (profile.key, profile.product))
            kodiutils.set_setting('profile_name', profile.name)

            kodiutils.redirect(kodiutils.url_for('show_main_menu'))
            return

        # Show profile selection when you have multiple profiles
        listing = [
            kodiutils.TitleItem(
                title=self._get_profile_name(p),
                path=kodiutils.url_for('select_profile', key=p.key),
                art_dict=dict(icon='DefaultUser.png'),
                info_dict=dict(plot=p.name, ),
            ) for p in profiles
        ]

        kodiutils.show_listing(listing, sort=['unsorted'],
                               category=30057)  # Select Profile

    def clear_tokens(self):
        """ Clear the authentication tokens """
        self._auth.logout()
        kodiutils.notification(message=kodiutils.localize(30706))

    @staticmethod
    def _get_profile_name(profile):
        """ Get a descriptive string of the profile
        :type profile: resources.lib.vtmgo.vtmgo.Profile
        """
        title = profile.name

        # Convert the VTM GO Profile color to a matching Kodi color
        color_map = {
            '#64D8E3': 'skyblue',
            '#4DFF76': 'mediumspringgreen',
            '#0243FF': 'blue',
            '#831CFA': 'blueviolet',
            '#FFB24D': 'khaki',
            '#FF4DD5': 'violet',
            '#FFB002': 'gold',
            '#FF0257': 'crimson',
        }
        if color_map.get(profile.color.upper()):
            title = '[COLOR %s]%s[/COLOR]' % (color_map.get(
                profile.color), kodiutils.to_unicode(title))

        # Append (Kids)
        if profile.product == 'VTM_GO_KIDS':
            title = "%s (Kids)" % title

        return title
Example #14
0
 def __init__(self, kodi):
     self._kodi = kodi  # type: KodiWrapper
     self._proxies = kodi.get_proxies()
     self._auth = VtmGoAuth(kodi)
Example #15
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 #16
0
class BackgroundService(Monitor):
    """ Background service code """
    def __init__(self):
        Monitor.__init__(self)
        self.kodi = KodiWrapper()
        self.vtm_go = VtmGo(self.kodi)
        self.vtm_go_auth = VtmGoAuth(self.kodi)
        self.update_interval = 24 * 3600  # Every 24 hours
        self.cache_expiry = 30 * 24 * 3600  # One month

    def run(self):
        """ Background loop for maintenance tasks """
        self.kodi.log('Service started', LOG_INFO)

        while not self.abortRequested():
            # Update every `update_interval` after the last update
            if self.kodi.get_setting_as_bool('metadata_update') and int(
                    self.kodi.get_setting('metadata_last_updated',
                                          0)) + self.update_interval < time():
                self._update_metadata()

            # Stop when abort requested
            if self.waitForAbort(10):
                break

        self.kodi.log('Service stopped', LOG_INFO)

    def onSettingsChanged(self):
        """ Callback when a setting has changed """
        # Refresh our VtmGo instance
        self.vtm_go = VtmGo(self.kodi)

        if self.vtm_go_auth.has_credentials_changed():
            self.kodi.log('Clearing auth tokens due to changed credentials',
                          LOG_INFO)
            self.vtm_go_auth.clear_token()

            # Refresh container
            self.kodi.container_refresh()

    def _update_metadata(self):
        """ Update the metadata for the listings. """
        from resources.lib.modules.metadata import Metadata

        # Clear outdated metadata
        self.kodi.invalidate_cache(self.cache_expiry)

        # Create progress indicator
        progress = self.kodi.show_progress_background(
            message=self.kodi.localize(30715))
        self.kodi.log('Updating metadata in the background')

        def update_status(i, total):
            """ Update the progress indicator """
            progress.update(int(((i + 1) / total) * 100))
            return self.abortRequested(
            ) or not self.kodi.get_setting_as_bool('metadata_update')

        success = Metadata(self.kodi).fetch_metadata(callback=update_status)

        # Close progress indicator
        progress.close()

        # Update metadata_last_updated
        if success:
            self.kodi.set_setting('metadata_last_updated', str(int(time())))
Example #17
0
class Menu:
    """ Menu code """
    def __init__(self):
        """ Initialise object """
        self._auth = VtmGoAuth(kodiutils.get_setting('username'),
                               kodiutils.get_setting('password'), 'VTM',
                               kodiutils.get_setting('profile'),
                               kodiutils.get_tokens_path())

    def show_mainmenu(self):
        """ Show the main menu """
        listing = []

        account = self._auth.login()

        listing.append(
            kodiutils.TitleItem(
                title=kodiutils.localize(30007),  # TV Channels
                path=kodiutils.url_for('show_channels'),
                art_dict=dict(
                    icon='DefaultAddonPVRClient.png',
                    fanart=kodiutils.get_addon_info('fanart'),
                ),
                info_dict=dict(plot=kodiutils.localize(30008), ),
            ))

        if account.product == 'VTM_GO':
            listing.append(
                kodiutils.TitleItem(
                    title=kodiutils.localize(30015),  # Recommendations
                    path=kodiutils.url_for('show_recommendations',
                                           storefront=STOREFRONT_MAIN),
                    art_dict=dict(
                        icon='DefaultFavourites.png',
                        fanart=kodiutils.get_addon_info('fanart'),
                    ),
                    info_dict=dict(plot=kodiutils.localize(30016), ),
                ))

            listing.append(
                kodiutils.TitleItem(
                    title=kodiutils.localize(30003),  # Movies
                    path=kodiutils.url_for('show_recommendations',
                                           storefront=STOREFRONT_MOVIES),
                    art_dict=dict(
                        icon='DefaultMovies.png',
                        fanart=kodiutils.get_addon_info('fanart'),
                    ),
                    info_dict=dict(plot=kodiutils.localize(30004), ),
                ))

            listing.append(
                kodiutils.TitleItem(
                    title=kodiutils.localize(30005),  # Series
                    path=kodiutils.url_for('show_recommendations',
                                           storefront=STOREFRONT_SERIES),
                    art_dict=dict(
                        icon='DefaultTVShows.png',
                        fanart=kodiutils.get_addon_info('fanart'),
                    ),
                    info_dict=dict(plot=kodiutils.localize(30006), ),
                ))

            listing.append(
                kodiutils.TitleItem(
                    title=kodiutils.localize(30021),  # Kids
                    path=kodiutils.url_for('show_recommendations',
                                           storefront=STOREFRONT_KIDS),
                    art_dict=dict(
                        icon='DefaultFavourites.png',
                        fanart=kodiutils.get_addon_info('fanart'),
                    ),
                    info_dict=dict(plot=kodiutils.localize(30022), ),
                ))

        elif account.product == 'VTM_GO_KIDS':
            listing.append(
                kodiutils.TitleItem(
                    title=kodiutils.localize(30015),  # Recommendations
                    path=kodiutils.url_for('show_recommendations',
                                           storefront=STOREFRONT_KIDS_MAIN),
                    art_dict=dict(
                        icon='DefaultFavourites.png',
                        fanart=kodiutils.get_addon_info('fanart'),
                    ),
                    info_dict=dict(plot=kodiutils.localize(30016), ),
                ))

        listing.append(
            kodiutils.TitleItem(
                title=kodiutils.localize(30001),  # A-Z
                path=kodiutils.url_for('show_catalog_all'),
                art_dict=dict(
                    icon='DefaultMovieTitle.png',
                    fanart=kodiutils.get_addon_info('fanart'),
                ),
                info_dict=dict(plot=kodiutils.localize(30002), ),
            ))

        # listing.append(kodiutils.TitleItem(
        #     title=kodiutils.localize(30003),  # Catalogue
        #     path=kodiutils.url_for('show_catalog'),
        #     art_dict=dict(
        #         icon='DefaultGenre.png',
        #         fanart=kodiutils.get_addon_info('fanart'),
        #     ),
        #     info_dict=dict(
        #         plot=kodiutils.localize(30004),
        #     ),
        # ))

        if kodiutils.get_setting_bool(
                'interface_show_mylist') and kodiutils.has_credentials():
            listing.append(
                kodiutils.TitleItem(
                    title=kodiutils.localize(30017),  # My List
                    path=kodiutils.url_for('show_mylist'),
                    art_dict=dict(
                        icon='DefaultPlaylist.png',
                        fanart=kodiutils.get_addon_info('fanart'),
                    ),
                    info_dict=dict(plot=kodiutils.localize(30018), ),
                ))

        if kodiutils.get_setting_bool('interface_show_continuewatching'
                                      ) and kodiutils.has_credentials():
            listing.append(
                kodiutils.TitleItem(
                    title=kodiutils.localize(30019),  # Continue watching
                    path=kodiutils.url_for('show_continuewatching'),
                    art_dict=dict(
                        icon='DefaultInProgressShows.png',
                        fanart=kodiutils.get_addon_info('fanart'),
                    ),
                    info_dict=dict(plot=kodiutils.localize(30020), ),
                ))

        listing.append(
            kodiutils.TitleItem(
                title=kodiutils.localize(30009),  # Search
                path=kodiutils.url_for('show_search'),
                art_dict=dict(
                    icon='DefaultAddonsSearch.png',
                    fanart=kodiutils.get_addon_info('fanart'),
                ),
                info_dict=dict(plot=kodiutils.localize(30010), ),
            ))

        kodiutils.show_listing(listing, sort=['unsorted'])

    @staticmethod
    def format_plot(obj):
        """ Format the plot for a item
        :type obj: object
        :rtype str
        """
        plot = ''

        if hasattr(obj, 'epg'):
            if obj.epg:
                plot += kodiutils.localize(
                    30213,  # Now
                    start=obj.epg[0].start.strftime('%H:%M'),
                    end=obj.epg[0].end.strftime('%H:%M'),
                    title=obj.epg[0].title) + "\n"

            if len(obj.epg) > 1:
                plot += kodiutils.localize(
                    30214,  # Next
                    start=obj.epg[1].start.strftime('%H:%M'),
                    end=obj.epg[1].end.strftime('%H:%M'),
                    title=obj.epg[1].title) + "\n"
            plot += '\n'

        # Add remaining
        if hasattr(obj, 'remaining') and obj.remaining is not None:
            if obj.remaining == 0:
                plot += '» ' + kodiutils.localize(
                    30208) + "\n"  # Available until midnight
            elif obj.remaining == 1:
                plot += '» ' + kodiutils.localize(
                    30209) + "\n"  # One more day remaining
            elif obj.remaining / 365 > 5:
                pass  # If it is available for more than 5 years, do not show
            elif obj.remaining / 365 > 2:
                plot += '» ' + kodiutils.localize(
                    30210, years=int(
                        obj.remaining / 365)) + "\n"  # X years remaining
            elif obj.remaining / 30.5 > 3:
                plot += '» ' + kodiutils.localize(
                    30211, months=int(
                        obj.remaining / 30.5)) + "\n"  # X months remaining
            else:
                plot += '» ' + kodiutils.localize(
                    30212, days=obj.remaining) + "\n"  # X days remaining
            plot += '\n'

        # Add geo-blocked message
        if hasattr(obj, 'geoblocked') and obj.geoblocked:
            plot += kodiutils.localize(30207)  # Geo-blocked
            plot += '\n'

        if hasattr(obj, 'description'):
            plot += obj.description
            plot += '\n\n'

        return plot.rstrip()

    @classmethod
    def generate_titleitem(cls, item, progress=False):
        """ Generate a TitleItem based on a Movie, Program or Episode.
        :type item: Union[Movie, Program, Episode]
        :type progress: bool
        :rtype TitleItem
        """
        art_dict = {
            'thumb': item.cover,
            'cover': item.cover,
        }
        info_dict = {
            'title':
            item.name,
            'plot':
            cls.format_plot(item),
            'studio':
            CHANNELS.get(item.channel, {}).get('studio_icon'),
            'mpaa':
            ', '.join(item.legal) if hasattr(item, 'legal') and item.legal else
            kodiutils.localize(30216),  # All ages
        }
        prop_dict = {}

        #
        # Movie
        #
        if isinstance(item, Movie):
            if item.my_list:
                context_menu = [(
                    kodiutils.localize(30101),  # Remove from My List
                    'Container.Update(%s)' %
                    kodiutils.url_for('mylist_del',
                                      video_type=CONTENT_TYPE_MOVIE,
                                      content_id=item.movie_id))]
            else:
                context_menu = [(
                    kodiutils.localize(30100),  # Add to My List
                    'Container.Update(%s)' %
                    kodiutils.url_for('mylist_add',
                                      video_type=CONTENT_TYPE_MOVIE,
                                      content_id=item.movie_id))]

            art_dict.update({
                'fanart': item.image,
            })
            info_dict.update({
                'mediatype': 'movie',
                'duration': item.duration,
                'year': item.year,
                'aired': item.aired,
            })
            stream_dict = {
                'codec': 'h264',
                'duration': item.duration,
                'height': 1080,
                'width': 1920,
            }

            return kodiutils.TitleItem(
                title=item.name,
                path=kodiutils.url_for('play',
                                       category='movies',
                                       item=item.movie_id),
                art_dict=art_dict,
                info_dict=info_dict,
                stream_dict=stream_dict,
                prop_dict=prop_dict,
                context_menu=context_menu,
                is_playable=True,
            )

        #
        # Program
        #
        if isinstance(item, Program):
            if item.my_list:
                context_menu = [(
                    kodiutils.localize(30101),  # Remove from My List
                    'Container.Update(%s)' %
                    kodiutils.url_for('mylist_del',
                                      video_type=CONTENT_TYPE_PROGRAM,
                                      content_id=item.program_id))]
            else:
                context_menu = [(
                    kodiutils.localize(30100),  # Add to My List
                    'Container.Update(%s)' %
                    kodiutils.url_for('mylist_add',
                                      video_type=CONTENT_TYPE_PROGRAM,
                                      content_id=item.program_id))]

            art_dict.update({
                'fanart': item.image,
            })
            info_dict.update({
                'mediatype': 'tvshow',
                'season': len(item.seasons),
            })
            prop_dict.update({
                'hash': item.content_hash,
            })

            return kodiutils.TitleItem(
                title=item.name,
                path=kodiutils.url_for('show_catalog_program',
                                       program=item.program_id),
                art_dict=art_dict,
                info_dict=info_dict,
                prop_dict=prop_dict,
                context_menu=context_menu,
            )

        #
        # Episode
        #
        if isinstance(item, Episode):
            context_menu = []
            if item.program_id:
                context_menu = [(
                    kodiutils.localize(30102),  # Go to Program
                    'Container.Update(%s)' % kodiutils.url_for(
                        'show_catalog_program', program=item.program_id))]

            art_dict.update({
                'fanart': item.cover,
            })
            info_dict.update({
                'mediatype': 'episode',
                'tvshowtitle': item.program_name,
                'duration': item.duration,
                'season': item.season,
                'episode': item.number,
                'set': item.program_name,
                'aired': item.aired,
            })
            if progress and item.watched:
                info_dict.update({
                    'playcount': 1,
                })

            stream_dict = {
                'codec': 'h264',
                'duration': item.duration,
                'height': 1080,
                'width': 1920,
            }

            # Add progress info
            if progress and not item.watched and item.progress:
                prop_dict.update({
                    'ResumeTime': item.progress,
                    'TotalTime': item.progress + 1,
                })

            return kodiutils.TitleItem(
                title=info_dict['title'],
                path=kodiutils.url_for('play',
                                       category='episodes',
                                       item=item.episode_id),
                art_dict=art_dict,
                info_dict=info_dict,
                stream_dict=stream_dict,
                prop_dict=prop_dict,
                context_menu=context_menu,
                is_playable=True,
            )

        raise Exception('Unknown video_type')