示例#1
0
    def login(self, refresh=False, token_variant=None):
        """Kodi GUI login flow"""
        # If no credentials, ask user for credentials
        if not has_credentials():
            if refresh:
                return open_settings()
            open_settings()
            if not self._credentials_changed():
                return None

        # Check credentials
        login_json = self._get_login_json()

        # Bad credentials
        while login_json.get('errorCode') != 0:
            # Show localized login error messages in Kodi GUI
            message = login_json.get('errorDetails')
            log_error('Login failed: {msg}', msg=message)
            if message == 'invalid loginID or password':
                message = localize(30953)  # Invalid login!
            elif message == 'loginID must be provided':
                message = localize(30955)  # Please fill in username
            elif message == 'Missing required parameter: password':
                message = localize(30956)  # Please fill in password
            ok_dialog(heading=localize(30951), message=message)  # Login failed!
            if refresh:
                return open_settings()
            open_settings()
            if not self._credentials_changed():
                return None
            login_json = self._get_login_json()

        # Get token
        return self._get_new_xvrttoken(login_json, token_variant)
示例#2
0
    def _webscrape_api_data(self, video_url):
        """Scrape api data from VRT NU html page"""
        from webscraper import get_video_attributes
        video_data = get_video_attributes(video_url)

        # Web scraping failed, log error
        if not video_data:
            log_error('Web scraping api data failed, empty video_data')
            return None

        # Store required html data attributes
        client = video_data.get('client') or self._CLIENT
        media_api_url = video_data.get('mediaapiurl')
        video_id = video_data.get('videoid')
        publication_id = video_data.get('publicationid', '')
        # Live stream or on demand
        if video_id is None:
            is_live_stream = True
            video_id = video_data.get('livestream')
        else:
            is_live_stream = False
            publication_id += quote('$')

        if client is None or media_api_url is None or (video_id is None and
                                                       publication_id is None):
            log_error(
                'Web scraping api data failed, required attributes missing')
            return None

        return ApiData(client, media_api_url, video_id, publication_id,
                       is_live_stream)
示例#3
0
 def test_debug_logging():
     xbmc.settings['debug.showloginfo'] = True
     addon.settings['max_log_level'] = '3'
     log(0, 'Logging as quiet')
     log(1, 'Logging as info')
     log(2, 'Logging as verbose')
     log(3, 'Logging as debug')
     log_error('Logging as error')
 def play_latest_episode(self, program):
     """A hidden feature in the VRT NU add-on to play the latest episode of a program"""
     video = self._apihelper.get_latest_episode(program)
     if not video:
         log_error('Play latest episode failed, program {program}', program=program)
         ok_dialog(message=localize(30954))
         end_of_directory()
         return
     self.play(video)
 def play_episode_by_whatson_id(self, whatson_id):
     """Play an episode of a program given the whatson_id"""
     video = self._apihelper.get_single_episode(whatson_id=whatson_id)
     if not video:
         log_error('Play episode by whatson_id failed, whatson_id {whatson_id}', whatson_id=whatson_id)
         ok_dialog(message=localize(30954))
         end_of_directory()
         return
     self.play(video)
 def play_upnext(self, video_id):
     """Play the next episode of a program by video_id"""
     video = self._apihelper.get_single_episode(video_id=video_id)
     if not video:
         log_error('Play Up Next with video_id {video_id} failed', video_id=video_id)
         ok_dialog(message=localize(30954))
         end_of_directory()
         return
     self.play(video)
 def play_whatson(self, whatson_id):
     ''' Play a video by whatson_id '''
     video = self._apihelper.get_single_episode(whatson_id)
     if not video:
         log_error('Play by whatson_id {id} failed', id=whatson_id)
         ok_dialog(message=localize(30954))
         end_of_directory()
         return
     self.play(video)
 def delete_online(self, asset_id):
     """Delete resumepoint online"""
     try:
         result = open_url('https://video-user-data.vrt.be/resume_points/{asset_id}'.format(asset_id=asset_id),
                           headers=self.resumepoint_headers(), method='DELETE', raise_errors='all')
         log(3, "[Resumepoints] '{asset_id}' online deleted: {code}", asset_id=asset_id, code=result.getcode())
     except HTTPError as exc:
         log_error("Failed to remove '{asset_id}' from resumepoints: {error}", asset_id=asset_id, error=exc)
         return False
     return True
 def update_online(self, asset_id, title, url, payload):
     """Update resumepoint online"""
     from json import dumps
     try:
         get_url_json('https://video-user-data.vrt.be/resume_points/{asset_id}'.format(asset_id=asset_id),
                      headers=self.resumepoint_headers(url), data=dumps(payload).encode())
     except HTTPError as exc:
         log_error('Failed to (un)watch episode {title} at VRT NU ({error})', title=title, error=exc)
         notification(message=localize(30977, title=title))
         return False
     return True
 def play_episode_by_air_date(self, channel, start_date, end_date):
     """Play an episode of a program given the channel and the air date in iso format (2019-07-06T19:35:00)"""
     video = self._apihelper.get_episode_by_air_date(channel, start_date, end_date)
     if video and video.get('errorlabel'):
         ok_dialog(message=localize(30986, title=video.get('errorlabel')))
         end_of_directory()
         return
     if not video:
         log_error('Play episode by air date failed, channel {channel}, start_date {start}', channel=channel, start=start_date)
         ok_dialog(message=localize(30954))
         end_of_directory()
         return
     self.play(video)
def get_video_attributes(vrtnu_url):
    """Return a dictionary with video attributes by scraping the VRT NU website"""

    # Get cache
    cache_file = 'web_video_attrs_multi.json'
    video_attrs_multi = get_cache(cache_file, ttl=ttl('indirect'))
    if not video_attrs_multi:
        video_attrs_multi = {}
    if vrtnu_url in video_attrs_multi:
        return video_attrs_multi[vrtnu_url]

    # Scrape video attributes
    from bs4 import BeautifulSoup, SoupStrainer
    try:
        response = open_url(vrtnu_url, raise_errors='all')
    except HTTPError as exc:
        log_error('Web scraping video attributes failed: {error}', error=exc)
        return None
    if response is None:
        return None
    html_page = response.read()
    strainer = SoupStrainer(
        ['section', 'div'],
        {'class': ['video-detail__player', 'livestream__inner']})
    soup = BeautifulSoup(html_page, 'html.parser', parse_only=strainer)
    item = None
    epg_channel = None
    if '#epgchannel=' in vrtnu_url:
        epg_channel = vrtnu_url.split('#epgchannel=')[1]
    for item in soup:
        if epg_channel and epg_channel == item.get('data-epgchannel'):
            break
    if not epg_channel and len(soup) > 1:
        return None
    try:
        video_attrs = item.find(name='nui-media').attrs
    except AttributeError as exc:
        log_error('Web scraping video attributes failed: {error}', error=exc)
        return None

    # Update cache
    if vrtnu_url in video_attrs_multi:
        # Update existing
        video_attrs_multi[vrtnu_url] = video_attrs
    else:
        # Create new
        video_attrs_multi.update({vrtnu_url: video_attrs})
    from json import dumps
    update_cache(cache_file, dumps(video_attrs_multi))

    return video_attrs
示例#12
0
 def resumepoints_headers():
     """Generate http headers for VRT NU Resumepoint API"""
     from tokenresolver import TokenResolver
     vrtlogin_at = TokenResolver().get_token('vrtlogin-at')
     headers = {}
     if vrtlogin_at:
         headers = {
             'Authorization': 'Bearer ' + vrtlogin_at,
             'Content-Type': 'application/json',
         }
     else:
         log_error('Failed to get access token from VRT NU')
         notification(message=localize(30975))
     return headers
示例#13
0
    def update(self, program, title, value=True):
        """Set a program as favorite, and update local copy"""

        # Survive any recent updates
        self.refresh(ttl=5)

        if value is self.is_favorite(program):
            # Already followed/unfollowed, nothing to do
            return True

        from tokenresolver import TokenResolver
        xvrttoken = TokenResolver().get_xvrttoken(token_variant='user')
        if xvrttoken is None:
            log_error('Failed to get favorites token from VRT NU')
            notification(message=localize(30975))
            return False

        headers = {
            'authorization': 'Bearer ' + xvrttoken,
            'content-type': 'application/json',
            'Referer': 'https://www.vrt.be/vrtnu',
        }

        from json import dumps
        from utils import program_to_url
        payload = dict(isFavorite=value,
                       programUrl=program_to_url(program, 'short'),
                       title=title)
        data = dumps(payload).encode('utf-8')
        program_id = program_to_id(program)
        try:
            get_url_json(
                'https://video-user-data.vrt.be/favorites/{program_id}'.format(
                    program_id=program_id),
                headers=headers,
                data=data)
        except HTTPError as exc:
            log_error(
                "Failed to (un)follow program '{program}' at VRT NU ({error})",
                program=program,
                error=exc)
            notification(message=localize(30976, program=program))
            return False
        # NOTE: Updates to favorites take a longer time to take effect, so we keep our own cache and use it
        self._data[program_id] = dict(value=payload)
        update_cache('favorites.json', dumps(self._data))
        invalidate_caches('my-offline-*.json', 'my-recent-*.json')
        return True
示例#14
0
 def resumepoint_headers(url=None):
     """Generate http headers for VRT NU Resumepoints API"""
     from tokenresolver import TokenResolver
     xvrttoken = TokenResolver().get_xvrttoken(token_variant='user')
     headers = {}
     if xvrttoken:
         url = 'https://www.vrt.be' + url if url else 'https://www.vrt.be/vrtnu'
         headers = {
             'authorization': 'Bearer ' + xvrttoken,
             'content-type': 'application/json',
             'Referer': url,
         }
     else:
         log_error('Failed to get usertoken from VRT NU')
         notification(message=localize(30975))
     return headers
示例#15
0
 def watchlater_headers(url=None):
     """Generate http headers for VRT NU watchLater API"""
     from tokenresolver import TokenResolver
     xvrttoken = TokenResolver().get_token('X-VRT-Token', variant='user')
     headers = {}
     if xvrttoken:
         url = 'https://www.vrt.be' + url if url else 'https://www.vrt.be/vrtnu'
         headers = {
             'Authorization': 'Bearer ' + xvrttoken,
             'Content-Type': 'application/json',
             'Referer': url,
         }
     else:
         log_error('Failed to get usertoken from VRT NU')
         notification(message=localize(30975))
     return headers
    def virtualsubclip_seektozero(self):
        """VRT NU already offers some programs (mostly current affairs programs) as video on demand while the program is still being broadcasted live.
           To do so, a start timestamp is added to the livestream url so the Unified Origin streaming platform knows
           it should return a time bounded manifest file that indicates the beginning of the program.
           This is called a Live-to-VOD stream or virtual subclip: https://docs.unified-streaming.com/documentation/vod/player-urls.html#virtual-subclips
           e.g. https://live-cf-vrt.akamaized.net/groupc/live/8edf3bdf-7db3-41c3-a318-72cb7f82de66/live.isml/.mpd?t=2020-07-20T11:07:00

           For some unclear reason the virtual subclip defined by a single start timestamp still behaves as a ordinary livestream
           and starts at the live edge of the stream. It seems this is not a Kodi or Inputstream Adaptive bug, because other players
           like THEOplayer or DASH-IF's reference player treat this kind of manifest files the same way.
           The only difference is that seeking to the beginning of the program is possible. So if the url contains a single start timestamp,
           we can work around this problem by automatically seeking to the beginning of the program.
        """
        playing_file = self.getPlayingFile()
        if any(param in playing_file for param in ('?t=', '&t=')):
            try:  # Python 3
                from urllib.parse import parse_qs, urlsplit
            except ImportError:  # Python 2
                from urlparse import parse_qs, urlsplit
            import re
            # Detect single start timestamp
            timestamp = parse_qs(urlsplit(playing_file).query).get('t')[0]
            rgx = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$')
            is_single_start_timestamp = bool(re.match(rgx, timestamp))
            if is_single_start_timestamp:
                # Check resume status
                resume_info = jsonrpc(method='Player.GetItem',
                                      params=dict(playerid=1,
                                                  properties=['resume'
                                                              ])).get('result')
                if resume_info:
                    resume_position = resume_info.get('item').get(
                        'resume').get('position')
                    is_resumed = abs(resume_position - self.getTime()) < 1
                    # Seek to zero if the user didn't resume the program
                    if not is_resumed:
                        log(3,
                            '[PlayerInfo {id}] Virtual subclip: seeking to the beginning of the program',
                            id=self.thread_id)
                        self.seekTime(0)
                else:
                    log_error(
                        'Failed to start virtual subclip {playing_file} at start timestamp',
                        playing_file=playing_file)
 def _handle_bad_stream_error(protocol, code=None, reason=None):
     """Show a localized error message in Kodi GUI for a failing VRT NU stream based on protocol: hls, hls_aes, mpeg_dash)
         message: VRT NU stream <stream_type> problem, try again with (InputStream Adaptive) (and) (DRM) enabled/disabled:
             30959=and DRM, 30960=disabled, 30961=enabled
    """
     # HLS AES DRM failed
     if protocol == 'hls_aes' and not supports_drm():
         message = localize(30962, protocol=protocol.upper(), version=kodi_version_major())
     elif protocol == 'hls_aes' and not has_inputstream_adaptive() and not get_setting_bool('usedrm', default=True):
         message = localize(30958, protocol=protocol.upper(), component=localize(30959), state=localize(30961))
     elif protocol == 'hls_aes' and has_inputstream_adaptive():
         message = localize(30958, protocol=protocol.upper(), component='Widevine DRM', state=localize(30961))
     elif protocol == 'hls_aes' and get_setting_bool('usedrm', default=True):
         message = localize(30958, protocol=protocol.upper(), component='InputStream Adaptive', state=localize(30961))
     else:
         message = localize(30958, protocol=protocol.upper(), component='InputStream Adaptive', state=localize(30960))
     heading = 'HTTP Error {code}: {reason}'.format(code=code, reason=reason) if code and reason else None
     log_error('Unable to play stream. {error}', error=heading)
     ok_dialog(heading=heading, message=message)
     end_of_directory()
示例#18
0
 def delete_online(self, asset_id):
     """Delete resumepoint online"""
     req = Request(
         'https://video-user-data.vrt.be/resume_points/{asset_id}'.format(
             asset_id=asset_id),
         headers=self.resumepoint_headers())
     req.get_method = lambda: 'DELETE'
     try:
         result = urlopen(req)
         log(3,
             "[Resumepoints] '{asset_id}' online deleted: {code}",
             asset_id=asset_id,
             code=result.getcode())
     except HTTPError as exc:
         log_error(
             "Failed to remove '{asset_id}' from resumepoints: {error}",
             asset_id=asset_id,
             error=exc)
         return False
     return True
示例#19
0
    def update(self, program, title, value=True):
        ''' Set a program as favorite, and update local copy '''

        self.refresh(ttl=60)
        if value is self.is_favorite(program):
            # Already followed/unfollowed, nothing to do
            return True

        from tokenresolver import TokenResolver
        xvrttoken = TokenResolver().get_xvrttoken(token_variant='user')
        if xvrttoken is None:
            log_error('Failed to get favorites token from VRT NU')
            notification(message=localize(30975))
            return False

        headers = {
            'authorization': 'Bearer ' + xvrttoken,
            'content-type': 'application/json',
            'Referer': 'https://www.vrt.be/vrtnu',
        }

        from statichelper import program_to_url
        payload = dict(isFavorite=value, programUrl=program_to_url(program, 'short'), title=title)
        from json import dumps
        data = dumps(payload).encode('utf-8')
        program_uuid = self.program_to_uuid(program)
        log(2, 'URL post: https://video-user-data.vrt.be/favorites/{uuid}', uuid=program_uuid)
        req = Request('https://video-user-data.vrt.be/favorites/%s' % program_uuid, data=data, headers=headers)
        result = urlopen(req)
        if result.getcode() != 200:
            log_error("Failed to (un)follow program '{program}' at VRT NU", program=program)
            notification(message=localize(30976, program=program))
            return False
        # NOTE: Updates to favorites take a longer time to take effect, so we keep our own cache and use it
        self._favorites[program_uuid] = dict(value=payload)
        update_cache('favorites.json', self._favorites)
        invalidate_caches('my-offline-*.json', 'my-recent-*.json')
        return True
示例#20
0
    def _webscrape_api_data(self, video_url):
        ''' Scrape api data from VRT NU html page '''
        from bs4 import BeautifulSoup, SoupStrainer
        log(2, 'URL get: {url}', url=unquote(video_url))
        html_page = urlopen(video_url).read()
        strainer = SoupStrainer(
            ['section', 'div'],
            {'class': ['video-player', 'livestream__player']})
        soup = BeautifulSoup(html_page, 'html.parser', parse_only=strainer)
        try:
            video_data = soup.find(lambda tag: tag.name == 'nui-media').attrs
        except Exception as exc:  # pylint: disable=broad-except
            # Web scraping failed, log error
            log_error('Web scraping api data failed: {error}', error=exc)
            return None

        # Web scraping failed, log error
        if not video_data:
            log_error('Web scraping api data failed, empty video_data')
            return None

        # Store required html data attributes
        client = video_data.get('client') or self._CLIENT
        media_api_url = video_data.get('mediaapiurl')
        video_id = video_data.get('videoid')
        publication_id = video_data.get('publicationid', '')
        # Live stream or on demand
        if video_id is None:
            is_live_stream = True
            video_id = video_data.get('livestream')
        else:
            is_live_stream = False
            publication_id += quote('$')

        if client is None or media_api_url is None or (video_id is None and
                                                       publication_id is None):
            log_error(
                'Web scraping api data failed, required attributes missing')
            return None

        return ApiData(client, media_api_url, video_id, publication_id,
                       is_live_stream)
示例#21
0
    def update(self,
               uuid,
               title,
               url,
               watch_later=None,
               position=None,
               total=None):
        ''' Set program resumepoint or watchLater status and update local copy '''

        # The video has no assetPath, so we cannot update resumepoints
        if uuid is None:
            return True

        if position is not None and position >= total - 30:
            watch_later = False

        self.refresh(ttl=0)
        if watch_later is not None and position is None and total is None and watch_later is self.is_watchlater(
                uuid):
            # watchLater status is not changed, nothing to do
            return True

        if watch_later is None and position == self.get_position(
                uuid) and total == self.get_total(uuid):
            # resumepoint is not changed, nothing to do
            return True

        # Collect header info for POST Request
        from tokenresolver import TokenResolver
        xvrttoken = TokenResolver().get_xvrttoken(token_variant='user')
        if xvrttoken is None:
            log_error('Failed to get usertoken from VRT NU')
            notification(message=localize(30975) + title)
            return False

        headers = {
            'authorization': 'Bearer ' + xvrttoken,
            'content-type': 'application/json',
            'Referer': 'https://www.vrt.be' + url,
        }

        if uuid in self._resumepoints:
            # Update existing resumepoint values
            payload = self._resumepoints[uuid]['value']
            payload['url'] = url
        else:
            # Create new resumepoint values
            payload = dict(position=0, total=100, url=url, watchLater=False)

        if position is not None:
            payload['position'] = position

        if total is not None:
            payload['total'] = total

        removes = []
        if position is not None or total is not None:
            removes.append('continue-*.json')

        if watch_later is not None:
            # Add watchLater status to payload
            payload['watchLater'] = watch_later
            removes.append('watchlater-*.json')

        from json import dumps
        data = dumps(payload).encode()
        log(2,
            'URL post: https://video-user-data.vrt.be/resume_points/{uuid}',
            uuid=uuid)
        log(2, 'URL post data:: {data}', data=data)
        try:
            req = Request('https://video-user-data.vrt.be/resume_points/%s' %
                          uuid,
                          data=data,
                          headers=headers)
            urlopen(req)
        except HTTPError as exc:
            log_error('Failed to (un)watch episode at VRT NU ({error})',
                      error=exc)
            notification(message=localize(30977))
            return False

        # NOTE: Updates to resumepoints take a longer time to take effect, so we keep our own cache and use it
        self._resumepoints[uuid] = dict(value=payload)
        update_cache('resume_points.json', self._resumepoints)
        invalidate_caches(*removes)
        return True
    def get_episodes(self, program=None, season=None, episodes=None, category=None, feature=None, programtype=None, keywords=None,
                     whatson_id=None, video_id=None, video_url=None, page=None, use_favorites=False, variety=None, cache_file=None):
        ''' Get episodes or season data from VRT NU Search API '''

        # Contruct params
        if page:
            page = statichelper.realpage(page)
            all_items = False
            params = {
                'from': ((page - 1) * 50) + 1,
                'i': 'video',
                'size': 50,
            }
        elif variety == 'single':
            all_items = False
            params = {
                'i': 'video',
                'size': '1',
            }
        else:
            all_items = True
            params = {
                'i': 'video',
                'size': '300',
            }

        if variety:
            season = 'allseasons'

            if variety == 'offline':
                from datetime import datetime
                import dateutil.tz
                params['facets[assetOffTime]'] = datetime.now(dateutil.tz.gettz('Europe/Brussels')).strftime('%Y-%m-%d')

            if variety == 'oneoff':
                params['facets[programType]'] = 'oneoff'

            if variety == 'watchlater':
                self._resumepoints.refresh(ttl=5 * 60)
                episode_urls = self._resumepoints.watchlater_urls()
                params['facets[url]'] = '[%s]' % (','.join(episode_urls))

            if variety == 'continue':
                self._resumepoints.refresh(ttl=5 * 60)
                episode_urls = self._resumepoints.resumepoints_urls()
                params['facets[url]'] = '[%s]' % (','.join(episode_urls))

            if use_favorites:
                program_urls = [statichelper.program_to_url(p, 'medium') for p in self._favorites.programs()]
                params['facets[programUrl]'] = '[%s]' % (','.join(program_urls))
            elif variety in ('offline', 'recent'):
                channel_filter = [channel.get('name') for channel in CHANNELS if get_setting(channel.get('name'), 'true') == 'true']
                params['facets[programBrands]'] = '[%s]' % (','.join(channel_filter))

        if program:
            params['facets[programUrl]'] = statichelper.program_to_url(program, 'medium')

        if season and season != 'allseasons':
            params['facets[seasonTitle]'] = season

        if episodes:
            params['facets[episodeNumber]'] = '[%s]' % (','.join(str(episode) for episode in episodes))

        if category:
            params['facets[categories]'] = category

        if feature:
            params['facets[programTags.title]'] = feature

        if programtype:
            params['facets[programType]'] = programtype

        if keywords:
            if not season:
                season = 'allseasons'
            params['q'] = quote_plus(statichelper.from_unicode(keywords))
            params['highlight'] = 'true'

        if whatson_id:
            params['facets[whatsonId]'] = whatson_id

        if video_id:
            params['facets[videoId]'] = video_id

        if video_url:
            params['facets[url]'] = video_url

        # Construct VRT NU Search API Url and get api data
        querystring = '&'.join('{}={}'.format(key, value) for key, value in list(params.items()))
        search_url = self._VRTNU_SEARCH_URL + '?' + querystring.replace(' ', '%20')  # Only encode spaces to minimize url length

        from json import load
        if cache_file:
            # Get api data from cache if it is fresh
            search_json = get_cache(cache_file, ttl=60 * 60)
            if not search_json:
                log(2, 'URL get: {url}', url=unquote(search_url))
                req = Request(search_url)
                try:
                    search_json = load(urlopen(req))
                except (TypeError, ValueError):  # No JSON object could be decoded
                    return []
                except HTTPError as exc:
                    url_length = len(req.get_selector())
                    if exc.code == 413 and url_length > 8192:
                        ok_dialog(heading='HTTP Error 413', message=localize(30967))
                        log_error('HTTP Error 413: Exceeded maximum url length: '
                                  'VRT Search API url has a length of {length} characters.', length=url_length)
                        return []
                    if exc.code == 400 and 7600 <= url_length <= 8192:
                        ok_dialog(heading='HTTP Error 400', message=localize(30967))
                        log_error('HTTP Error 400: Probably exceeded maximum url length: '
                                  'VRT Search API url has a length of {length} characters.', length=url_length)
                        return []
                    raise
                update_cache(cache_file, search_json)
        else:
            log(2, 'URL get: {url}', url=unquote(search_url))
            search_json = load(urlopen(search_url))

        # Check for multiple seasons
        seasons = None
        if 'facets[seasonTitle]' not in unquote(search_url):
            facets = search_json.get('facets', dict()).get('facets')
            seasons = next((f.get('buckets', []) for f in facets if f.get('name') == 'seasons' and len(f.get('buckets', [])) > 1), None)

        episodes = search_json.get('results', [{}])
        show_seasons = bool(season != 'allseasons')

        # Return seasons
        if show_seasons and seasons:
            return (seasons, episodes)

        api_pages = search_json.get('meta').get('pages').get('total')
        api_page_size = search_json.get('meta').get('pages').get('size')
        total_results = search_json.get('meta').get('total_results')

        if all_items and total_results > api_page_size:
            for api_page in range(1, api_pages):
                api_page_url = search_url + '&from=' + str(api_page * api_page_size + 1)
                api_page_json = load(urlopen(api_page_url))
                episodes += api_page_json.get('results', [{}])

        # Return episodes
        return episodes
    def get_upnext(self, info):
        ''' Get up next data from VRT Search API '''
        program = info.get('program')
        path = info.get('path')
        season = None
        current_ep_no = None

        # Get current episode unique identifier
        ep_id = statichelper.play_url_to_id(path)

        # Get all episodes from current program and sort by program, seasonTitle and episodeNumber
        episodes = sorted(self.get_episodes(keywords=program), key=lambda k: (k.get('program'), k.get('seasonTitle'), k.get('episodeNumber')))
        upnext = dict()
        for episode in episodes:
            if ep_id.get('whatson_id') == episode.get('whatsonId') or \
               ep_id.get('video_id') == episode.get('videoId') or \
               ep_id.get('video_url') == episode.get('url'):
                season = episode.get('seasonTitle')
                current_ep_no = episode.get('episodeNumber')
                program = episode.get('program')
                try:
                    upnext['current'] = episode
                    next_episode = episodes[episodes.index(episode) + 1]
                    upnext['next'] = next_episode if next_episode.get('program') == program else None
                except IndexError:
                    pass

        if upnext.get('next'):
            current_ep = upnext.get('current')
            next_ep = upnext.get('next')

            art = self._metadata.get_art(current_ep)
            current_episode = dict(
                episodeid=current_ep.get('whatsonId'),
                tvshowid=current_ep.get('programWhatsonId'),
                title=self._metadata.get_plotoutline(current_ep),
                art={
                    'tvshow.poster': art.get('thumb'),
                    'thumb': art.get('thumb'),
                    'tvshow.fanart': art.get('fanart'),
                    'tvshow.landscape': art.get('thumb'),
                    'tvshow.clearart': None,
                    'tvshow.clearlogo': None,
                },
                plot=self._metadata.get_plot(current_ep),
                showtitle=self._metadata.get_tvshowtitle(current_ep),
                playcount=info.get('playcount'),
                season=self._metadata.get_season(current_ep),
                episode=self._metadata.get_episode(current_ep),
                rating=info.get('rating'),
                firstaired=self._metadata.get_aired(current_ep),
                runtime=info.get('runtime'),
            )

            art = self._metadata.get_art(next_ep)
            next_episode = dict(
                episodeid=next_ep.get('whatsonId'),
                tvshowid=next_ep.get('programWhatsonId'),
                title=self._metadata.get_plotoutline(next_ep),
                art={
                    'tvshow.poster': art.get('thumb'),
                    'thumb': art.get('thumb'),
                    'tvshow.fanart': art.get('fanart'),
                    'tvshow.landscape': art.get('thumb'),
                    'tvshow.clearart': None,
                    'tvshow.clearlogo': None,
                },
                plot=self._metadata.get_plot(next_ep),
                showtitle=self._metadata.get_tvshowtitle(next_ep),
                playcount=None,
                season=self._metadata.get_season(next_ep),
                episode=self._metadata.get_episode(next_ep),
                rating=None,
                firstaired=self._metadata.get_aired(next_ep),
                runtime=self._metadata.get_duration(next_ep),
            )

            play_info = dict(
                whatson_id=next_ep.get('whatsonId'),
            )

            next_info = dict(
                current_episode=current_episode,
                next_episode=next_episode,
                play_info=play_info,
                notification_time=SECONDS_MARGIN,
            )
            return next_info

        if upnext.get('current'):
            if upnext.get('current').get('episodeNumber') == upnext.get('current').get('seasonNbOfEpisodes'):
                log_error(message='[Up Next] Last episode of season, next season not implemented for "{program} S{season}E{episode}"',
                          program=program, season=season, episode=current_ep_no)
            return None
        log_error(message='[Up Next] No api data found for "{program}s S{season}E{episode}"',
                  program=program, season=season, episode=current_ep_no)
        return None
示例#24
0
 def _handle_stream_api_error(message, video_json=None):
     ''' Show localized stream api error messages in Kodi GUI '''
     if video_json:
         log_error(video_json.get('message'))
     ok_dialog(message=message)
     end_of_directory()
示例#25
0
    def update_resumepoint(self,
                           video_id,
                           asset_str,
                           title,
                           position=None,
                           total=None,
                           path=None,
                           episode_id=None,
                           episode_title=None):
        """Set episode resumepoint and update local copy"""

        if video_id is None:
            return True

        menu_caches = []
        self.refresh_resumepoints(ttl=5)

        # Add existing position and total if None
        if video_id in self._resumepoints and position is None and total is None:
            position = self.get_position(video_id)
            total = self.get_total(video_id)

        # Update
        if (self.still_watching(position, total) or
            (path
             and path.startswith('plugin://plugin.video.vrt.nu/play/upnext'))):
            # Normally, VRT NU resumepoints are deleted when an episode is (un)watched and Kodi GUI automatically sets
            # the (un)watched status when Kodi Player exits. This mechanism doesn't work with "Up Next" episodes because
            # these episodes are not initiated from a ListItem in Kodi GUI.
            # For "Up Next" episodes, we should never delete the VRT NU resumepoints to make sure the watched status
            # can be forced in Kodi GUI using the playcount infolabel.
            log(3,
                "[Resumepoints] Update resumepoint '{video_id}' {position}/{total}",
                video_id=video_id,
                position=position,
                total=total)

            if position == self.get_position(
                    video_id) and total == self.get_total(video_id):
                # Resumepoint is not changed, nothing to do
                return True

            menu_caches.append('continue-*.json')

            # Update online
            gdpr = '{asset_str} gekeken tot {at} seconden.'.format(
                asset_str=asset_str, at=position)
            payload = dict(
                at=position,
                total=total,
                gdpr=gdpr,
            )
            from json import dumps
            try:
                resumepoint_json = get_url_json(
                    '{api}/{video_id}'.format(api=self.RESUMEPOINTS_URL,
                                              video_id=video_id),
                    headers=self.resumepoints_headers(),
                    data=dumps(payload).encode())
            except HTTPError as exc:
                log_error(
                    'Failed to update resumepoint of {title} at VRT NU ({error})',
                    title=title,
                    error=exc)
                notification(message=localize(30977, title=title))
                return False

            # Update local
            for idx, item in enumerate(self._resumepoints.get('items')):
                if item.get('mediaId') == video_id:
                    self._resumepoints.get('items')[idx] = resumepoint_json
                    break
            update_cache(self.RESUMEPOINTS_CACHE_FILE,
                         dumps(self._resumepoints))
            if menu_caches:
                invalidate_caches(*menu_caches)
        else:

            # Delete
            log(3,
                "[Resumepoints] Delete resumepoint '{asset_str}' {position}/{total}",
                asset_str=asset_str,
                position=position,
                total=total)

            # Delete watchlater
            self.update_watchlater(episode_id,
                                   episode_title,
                                   watch_later=False)

            # Do nothing if there is no resumepoint for this video_id
            from json import dumps
            if video_id not in dumps(self._resumepoints):
                log(3,
                    "[Resumepoints] '{video_id}' not present, nothing to delete",
                    video_id=video_id)
                return True

            # Add menu caches
            menu_caches.append('continue-*.json')

            # Delete online
            try:
                result = open_url('{api}/{video_id}'.format(
                    api=self.RESUMEPOINTS_URL, video_id=video_id),
                                  headers=self.resumepoints_headers(),
                                  method='DELETE',
                                  raise_errors='all')
                log(3,
                    "[Resumepoints] '{video_id}' online deleted: {code}",
                    video_id=video_id,
                    code=result.getcode())
            except HTTPError as exc:
                log_error(
                    "Failed to remove resumepoint of '{video_id}': {error}",
                    video_id=video_id,
                    error=exc)
                return False

            # Delete local representation and cache
            for item in self._resumepoints.get('items'):
                if item.get('mediaId') == video_id:
                    self._resumepoints.get('items').remove(item)
                    break

            update_cache(self.RESUMEPOINTS_CACHE_FILE,
                         dumps(self._resumepoints))
            if menu_caches:
                invalidate_caches(*menu_caches)
        return True
示例#26
0
    def get_stream(self, video, roaming=False, api_data=None):
        """Main streamservice function"""
        if not api_data:
            api_data = self._get_api_data(video)

        stream_json = self._get_stream_json(api_data, roaming)

        if not stream_json:

            # Roaming token failed
            if roaming:
                message = localize(
                    30964
                )  # Geoblock error: Cannot be played, need Belgian phone number validation
                return self._handle_stream_api_error(message)
            # X-VRT-Token failed
            message = localize(
                30963)  # You need a VRT NU account to play this stream.
            return self._handle_stream_api_error(message)

        if 'targetUrls' in stream_json:

            # DRM support for ketnet junior/uplynk streaming service
            uplynk = 'uplynk.com' in stream_json.get('targetUrls')[0].get(
                'url')

            vudrm_token = stream_json.get('drm')
            vualto_license_url = stream_json.get(
                'vualto_license_url') or self._get_vualto_license_url()
            drm_stream = (vudrm_token or uplynk)

            # Select streaming protocol
            if not drm_stream and has_inputstream_adaptive():
                protocol = 'mpeg_dash'
            elif drm_stream and self._can_play_drm:
                protocol = 'mpeg_dash'
            elif vudrm_token:
                protocol = 'hls_aes'
            else:
                protocol = 'hls'

            # Get stream manifest url
            manifest_url = next((stream.get('url')
                                 for stream in stream_json.get('targetUrls')
                                 if stream.get('type') == protocol), None)

            # Failed to get compatible manifest url, ask user to toggle "Use Widevine DRM"
            if manifest_url is None:
                available_protocols = [
                    stream.get('type')
                    for stream in stream_json.get('targetUrls')
                ]
                if protocol not in available_protocols:
                    error_json = {
                        'message':
                        '%s is not available for this stream, please try toggling the "Use Widevine DRM" setting'
                        % protocol
                    }
                    message = localize(
                        30989)  # Failed to load a compatible stream
                    return self._handle_stream_api_error(message, error_json)
            else:

                # External virtual subclip, live-to-VOD from past 24 hours archived livestream (airdate feature)
                if video.get('start_date') and video.get('end_date'):
                    manifest_url += '?t=' + video.get(
                        'start_date') + '-' + video.get('end_date')

                # Fix virtual subclip
                from datetime import timedelta
                duration = timedelta(
                    milliseconds=stream_json.get('duration', 0))
                manifest_url = self._fix_virtualsubclip(manifest_url, duration)

                # Prepare stream for Kodi player
                if protocol == 'mpeg_dash' and drm_stream:
                    log(2, 'Protocol: mpeg_dash drm')
                    if vudrm_token:
                        encryption_json = '{{"token":"{0}","drm_info":[D{{SSM}}],"kid":"{{KID}}"}}'.format(
                            vudrm_token)
                        license_key = self._get_license_key(
                            key_url=vualto_license_url,
                            key_type='D',
                            key_value=encryption_json,
                            key_headers={
                                'Content-Type': 'text/plain;charset=UTF-8'
                            })
                    else:
                        license_key = self._get_license_key(
                            key_url=self._UPLYNK_LICENSE_URL, key_type='R')

                    stream = StreamURLS(manifest_url,
                                        license_key=license_key,
                                        use_inputstream_adaptive=True)
                elif protocol == 'mpeg_dash':
                    log(2, 'Protocol: mpeg_dash')
                    stream = StreamURLS(manifest_url,
                                        use_inputstream_adaptive=True)
                else:
                    log(2, 'Protocol: {protocol}', protocol=protocol)
                    # Fix 720p quality for HLS livestreams
                    manifest_url = manifest_url.replace(
                        '.m3u8?', '.m3u8?hd&'
                    ) if '.m3u8?' in manifest_url else manifest_url + '?hd'
                    # Play HLS directly in Kodi Player on Kodi 17
                    if kodi_version_major(
                    ) < 18 or not has_inputstream_adaptive():
                        stream = self._select_hls_substreams(
                            manifest_url, protocol)
                    else:
                        stream = StreamURLS(manifest_url,
                                            use_inputstream_adaptive=True)
                return stream

        # VRT Geoblock: failed to get stream, now try again with roaming enabled
        if stream_json.get('code') in self._GEOBLOCK_ERROR_CODES:
            log_error('VRT Geoblock: {msg}', msg=stream_json.get('message'))
            if not roaming:
                return self.get_stream(video, roaming=True, api_data=api_data)

            if stream_json.get('code') == self._INVALID_LOCATION:
                message = localize(
                    30965
                )  # Geoblock error: Blocked on your geographical location based on your IP address
                return self._handle_stream_api_error(message, stream_json)

            if stream_json.get('code') == self._BELGIUM_ONLY:
                message = localize(
                    30973
                )  # Geoblock error: This program can only be played from EU
                return self._handle_stream_api_error(message, stream_json)

            message = localize(
                30964
            )  # Geoblock error: Cannot be played, need Belgian phone number validation
            return self._handle_stream_api_error(message, stream_json)
        if stream_json.get('code') == 'VIDEO_NOT_FOUND':
            # Refresh listing
            invalidate_caches('*.json')
            container_reload()
            message = localize(30987)  # No stream found
            return self._handle_stream_api_error(message, stream_json)
        if stream_json.get('code') == 'ERROR_AGE_RESTRICTED':
            message = localize(
                30990
            )  # Cannot be played, VRT NU account not allowed to access 12+ content
            return self._handle_stream_api_error(message, stream_json)

        # Failed to get stream, handle error
        message = localize(30954)  # Whoops something went wrong
        return self._handle_stream_api_error(message, stream_json)