def update(self, program_name, title, program_id, is_favorite=True): """Set a program as favorite, and update local copy""" # Survive any recent updates self.refresh(ttl=5) if is_favorite is self.is_favorite(program_name): # Already followed/unfollowed, nothing to do return True # Lookup program_id if program_id == 'None' or program_id is None: program_id = self.get_program_id_graphql(program_name) # Update local favorites cache if is_favorite is True: self._favorites[program_name] = dict(program_id=program_id, title=title) else: del self._favorites[program_name] # Update cache dict from json import dumps update_cache(self.FAVORITES_CACHE_FILE, dumps(self._favorites)) invalidate_caches('my-offline-*.json', 'my-recent-*.json') # Update online self.set_favorite_graphql(program_id, title, is_favorite) return True
def update_local(self, asset_id, resumepoint_json, menu_caches=None): """Update resumepoint locally and update cache""" self._data.update({asset_id: resumepoint_json}) from json import dumps update_cache('resume_points.json', dumps(self._data)) if menu_caches: invalidate_caches(*menu_caches)
def update_watchlater(self, episode_id, title, watch_later=None): """Set program watchLater status and update local copy""" self.refresh_watchlater(ttl=5) # Update log(3, "[watchLater] Update {episode_id} watchLater status", episode_id=episode_id) # watchLater status is not changed, nothing to do if watch_later is not None and watch_later is self.is_watchlater( episode_id): return True # Update local watch_later cache if watch_later is True: self._watchlater[episode_id] = dict(title=title) else: del self._watchlater[episode_id] # Update cache from json import dumps update_cache(self.WATCHLATER_CACHE_FILE, dumps(self._watchlater)) invalidate_caches('watchlater-*.json') # Update online self.set_watchlater_graphql(episode_id, title, watch_later) return True
def delete_local(self, asset_id, menu_caches=None): """Delete resumepoint locally and update cache""" if asset_id in self._data: del self._data[asset_id] from json import dumps update_cache('resume_points.json', dumps(self._data)) if menu_caches: invalidate_caches(*menu_caches)
def cleanup_userdata(self): """Cleanup userdata""" # Delete token cache self.delete_tokens() # Delete user-related caches invalidate_caches('continue-*.json', 'favorites.json', 'my-offline-*.json', 'my-recent-*.json', 'resume_points.json', 'watchlater-*.json')
def onSettingsChanged(self): # pylint: disable=invalid-name """Handler for changes to settings""" log(1, 'Settings changed') TokenResolver().refresh_login() invalidate_caches('continue-*.json', 'favorites.json', 'my-offline-*.json', 'my-recent-*.json', 'resume_points.json', 'watchlater-*.json') # Init watching activity again when settings change self.init_watching_activity() # Refresh container when settings change container_refresh()
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
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
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_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)
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