コード例 #1
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())))
コード例 #2
0
ファイル: plugin.py プロジェクト: DCL777/plugin.video.vtm.go
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()
コード例 #3
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()
コード例 #4
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())))
コード例 #5
0
ファイル: vtmgo.py プロジェクト: iitians/plugin.video.vtm.go
class VtmGo:
    """ VTM GO API """
    API_ENDPOINT = 'https://api.vtmgo.be'

    CONTENT_TYPE_MOVIE = 'MOVIE'
    CONTENT_TYPE_PROGRAM = 'PROGRAM'
    CONTENT_TYPE_EPISODE = 'EPISODE'

    def __init__(self, kodi):
        """ Initialise object
        :type kodi: resources.lib.kodiwrapper.KodiWrapper
        """
        self._kodi = kodi
        self._auth = VtmGoAuth(kodi)

        self._session = requests.session()
        self._session.proxies = kodi.get_proxies()
        self._authenticate()

    def _authenticate(self):
        """ Apply authentication headers in the session """
        self._session.headers = {
            'x-app-version': '8',
            'x-persgroep-mobile-app': 'true',
            'x-persgroep-os': 'android',
            'x-persgroep-os-version': '23',
        }
        token = self._auth.get_token()
        if token:
            self._session.headers['x-dpp-jwt'] = token

            profile = self._auth.get_profile()
            if profile:
                self._session.headers['x-dpp-profile'] = profile
            else:
                # Select default profile
                default_profile = self.get_profiles()[0]
                self._session.headers['x-dpp-profile'] = default_profile.key

    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:
                    items.append(self._parse_movie_teaser(item))

                elif item.get('target',
                              {}).get('type') == self.CONTENT_TYPE_PROGRAM:
                    items.append(self._parse_program_teaser(item))

            categories.append(
                Category(
                    category_id=cat.get('id'),
                    title=cat.get('title'),
                    content=items,
                ))

        return categories

    def 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:
                items.append(self._parse_movie_teaser(item))

            elif item.get('target',
                          {}).get('type') == self.CONTENT_TYPE_PROGRAM:
                items.append(self._parse_program_teaser(item))

            elif item.get('target',
                          {}).get('type') == self.CONTENT_TYPE_EPISODE:
                items.append(self._parse_episode_teaser(item))

        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:
                items.append(self._parse_movie_teaser(item))

            elif item.get('target',
                          {}).get('type') == self.CONTENT_TYPE_PROGRAM:
                items.append(self._parse_program_teaser(item))

        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/search/?query=%s' %
                                 (self._mode(), quote(search)))
        results = json.loads(response)

        items = []
        for category in results.get('results', []):
            for item in category.get('teasers'):
                if item.get('target',
                            {}).get('type') == self.CONTENT_TYPE_MOVIE:
                    items.append(self._parse_movie_teaser(item))

                elif item.get('target',
                              {}).get('type') == self.CONTENT_TYPE_PROGRAM:
                    items.append(self._parse_program_teaser(item))
        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, AttributeError):
            return None

    def _parse_movie_teaser(self, item):
        """ Parse the movie json and return an Movie instance.
        :type item: dict
        :rtype 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')
            return movie

        return Movie(
            movie_id=item.get('target', {}).get('id'),
            name=item.get('title'),
            cover=item.get('imageUrl'),
            image=item.get('imageUrl'),
            geoblocked=item.get('geoBlocked'),
        )

    def _parse_program_teaser(self, item):
        """ Parse the program json and return an Program instance.
        :type item: dict
        :rtype 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')
            return program

        return Program(
            program_id=item.get('target', {}).get('id'),
            name=item.get('title'),
            cover=item.get('imageUrl'),
            image=item.get('imageUrl'),
            geoblocked=item.get('geoBlocked'),
        )

    def _parse_episode_teaser(self, item):
        """ Parse the episode json and return an Episode instance.
        :type item: dict
        :rtype 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

        return 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'),
        )

    @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
        :type params: dict
        :rtype str
        """
        try:
            return self._request('GET', url, params=params)
        except InvalidLoginException:
            self._auth.clear_token()
            self._authenticate()
            # Retry the same request
            return self._request('GET', url, params=params)

    def _put_url(self, url, params=None):
        """ Makes a PUT request for the specified URL.
        :type url: str
        :type params: dict
        :rtype str
        """
        try:
            return self._request('PUT', url, params=params)
        except InvalidLoginException:
            self._auth.clear_token()
            self._authenticate()
            # Retry the same request
            return self._request('PUT', url, params=params)

    def _post_url(self, url, params=None, data=None):
        """ Makes a POST request for the specified URL.
        :type url: str
        :type params: dict
        :type data: dict
        :rtype str
        """
        try:
            return self._request('POST', url, params=params, data=data)
        except InvalidLoginException:
            self._auth.clear_token()
            self._authenticate()
            # Retry the same request
            return self._request('POST', url, params=params)

    def _delete_url(self, url, params=None):
        """ Makes a DELETE request for the specified URL.
        :type url: str
        :type params: dict
        :rtype str
        """
        try:
            return self._request('DELETE', url, params=params)
        except InvalidLoginException:
            self._auth.clear_token()
            self._authenticate()
            # Retry the same request
            return self._request('DELETE', url, params=params)

    def _request(self, method, url, params=None, data=None):
        """ Makes a request for the specified URL.
        :type url: str
        :type params: dict
        :type data: dict
        :rtype str
        """
        _LOGGER.debug('Sending %s %s...', method, url)
        response = self._session.request(method,
                                         self.API_ENDPOINT + url,
                                         params=params,
                                         json=data)

        # 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 == 401:
            raise InvalidLoginException()

        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