def search_update(self, context): utils.p('search updater') # if self.search_keywords != '': ui_props = bpy.context.scene.blenderkitUI if ui_props.down_up != 'SEARCH': ui_props.down_up = 'SEARCH' # here we tweak the input if it comes form the clipboard. we need to get rid of asset type and set it to sprops = utils.get_search_props() instr = 'asset_base_id:' atstr = 'asset_type:' kwds = sprops.search_keywords idi = kwds.find(instr) ati = kwds.find(atstr) # if the asset type already isn't there it means this update function # was triggered by it's last iteration and needs to cancel if idi > -1 and ati == -1: return if ati > -1: at = kwds[ati:].lower() # uncertain length of the remaining string - find as better method to check the presence of asset type if at.find('model') > -1: ui_props.asset_type = 'MODEL' elif at.find('material') > -1: ui_props.asset_type = 'MATERIAL' elif at.find('brush') > -1: ui_props.asset_type = 'BRUSH' # now we trim the input copypaste by anything extra that is there, # this is also a way for this function to recognize that it already has parsed the clipboard # the search props can have changed and this needs to transfer the data to the other field # this complex behaviour is here for the case where the user needs to paste manually into blender? sprops = utils.get_search_props() sprops.search_keywords = kwds[:ati].rstrip() search()
def download(asset_data, **kwargs): '''start the download thread''' user_preferences = bpy.context.preferences.addons['blenderkit'].preferences api_key = user_preferences.api_key scene_id = get_scene_id() tcom = ThreadCom() tcom.passargs = kwargs if kwargs.get('retry_counter', 0) > 3: sprops = utils.get_search_props() report = f"Maximum retries exceeded for {asset_data['name']}" sprops.report = report ui.add_report(report, 5, colors.RED) utils.p(sprops.report) return # incoming data can be either directly dict from python, or blender id property # (recovering failed downloads on reload) if type(asset_data) == dict: asset_data = copy.deepcopy(asset_data) else: asset_data = asset_data.to_dict() readthread = Downloader(asset_data, tcom, scene_id, api_key) readthread.start() global download_threads download_threads.append([readthread, asset_data, tcom])
def download(asset_data, **kwargs): '''start the download thread''' user_preferences = bpy.context.preferences.addons['blenderkit'].preferences api_key = user_preferences.api_key scene_id = get_scene_id() if api_key == '': props = utils.get_search_props() props.report = 'Register online to use the free library.' return tcom = ThreadCom() tcom.passargs = kwargs # incoming data can be either directly dict from python, or blender id property # (recovering failed downloads on reload) if type(asset_data) == dict: asset_data = copy.deepcopy(asset_data) else: asset_data = asset_data.to_dict() # main_thread(asset_data, tcom, scene_id, api_key) readthread = threading.Thread(target=main_thread, args=([asset_data, tcom, scene_id, api_key]), daemon=True) readthread.start() global download_threads download_threads.append( [readthread, asset_data, tcom])
def write_tokens(auth_token, refresh_token): utils.p('writing tokens') preferences = bpy.context.preferences.addons['blenderkit'].preferences preferences.api_key_refresh = refresh_token preferences.api_key = auth_token preferences.login_attempt = False props = utils.get_search_props() props.report = 'Login success!' search.get_profile() categories.fetch_categories_thread(auth_token)
def search_by_author(self, asset_index): sr = bpy.context.scene['search results'] asset_data = sr[asset_index] a = asset_data['author']['id'] if a is not None: sprops = utils.get_search_props() sprops.search_keywords = '' sprops.search_verification_status = 'ALL' utils.p('author:', a) search.search(author_id=a) return True
def search_more(self): sro = bpy.context.window_manager.get('search results orig') if sro is None: return; if sro.get('next') is None: return search_props = utils.get_search_props() if search_props.is_searching: return blenderkit.search.search(get_next=True)
def execute(self, context): # TODO ; this should all get transferred to properties of the search operator, so sprops don't have to be fetched here at all. sprops = utils.get_search_props() if self.author_id != '': sprops.search_keywords = '' if self.keywords != '': sprops.search_keywords = self.keywords search(category=self.category, get_next=self.get_next, author_id=self.author_id) # bpy.ops.view3d.blenderkit_asset_bar() return {'FINISHED'}
def check_clipboard(): # clipboard monitoring to search assets from web global last_clipboard if bpy.context.window_manager.clipboard != last_clipboard: last_clipboard = bpy.context.window_manager.clipboard instr = 'asset_base_id:' # first check if contains asset id, then asset type if last_clipboard[:len(instr)] == instr: atstr = 'asset_type:' ati = last_clipboard.find(atstr) # this only checks if the asset_type keyword is there but let's the keywords update function do the parsing. if ati > -1: search_props = utils.get_search_props() search_props.search_keywords = last_clipboard
def write_tokens(auth_token, refresh_token, oauth_response): utils.p('writing tokens') preferences = bpy.context.preferences.addons['blenderkit'].preferences preferences.api_key_refresh = refresh_token preferences.api_key = auth_token preferences.api_key_timeout = time.time() + oauth_response['expires_in'] preferences.api_key_life = oauth_response['expires_in'] preferences.login_attempt = False preferences.refresh_in_progress = False props = utils.get_search_props() if props is not None: props.report = '' ui.add_report('BlenderKit Re-Login success') search.get_profile() categories.fetch_categories_thread(auth_token)
def check_clipboard(): ''' Checks clipboard for an exact string containing asset ID. The string is generated on www.blenderkit.com as for example here: https://www.blenderkit.com/get-blenderkit/54ff5c85-2c73-49e9-ba80-aec18616a408/ ''' # clipboard monitoring to search assets from web if platform.system() != 'Linux': global last_clipboard if bpy.context.window_manager.clipboard != last_clipboard: last_clipboard = bpy.context.window_manager.clipboard instr = 'asset_base_id:' # first check if contains asset id, then asset type if last_clipboard[:len(instr)] == instr: atstr = 'asset_type:' ati = last_clipboard.find(atstr) # this only checks if the asset_type keyword is there but let's the keywords update function do the parsing. if ati > -1: search_props = utils.get_search_props() search_props.search_keywords = last_clipboard
def timer_update( ): # TODO might get moved to handle all blenderkit stuff, not to slow down. '''check for running and finished downloads and react. write progressbars too.''' global download_threads if len(download_threads) == 0: return 1 s = bpy.context.scene for threaddata in download_threads: t = threaddata[0] asset_data = threaddata[1] tcom = threaddata[2] progress_bars = [] downloaders = [] if t.is_alive(): # set downloader size sr = bpy.context.scene.get('search results') if sr is not None: for r in sr: if asset_data['id'] == r['id']: r['downloaded'] = tcom.progress if not t.is_alive(): if tcom.error: sprops = utils.get_search_props() sprops.report = tcom.report download_threads.remove(threaddata) return file_names = paths.get_download_filenames(asset_data) wm = bpy.context.window_manager at = asset_data['asset_type'] if ((bpy.context.mode == 'OBJECT' and (at == 'model' \ or at == 'material'))) \ or ((at == 'brush') \ and wm.get( 'appendable') == True) or at == 'scene': # don't do this stuff in editmode and other modes, just wait... download_threads.remove(threaddata) # duplicate file if the global and subdir are used in prefs if len( file_names ) == 2: # todo this should try to check if both files exist and are ok. shutil.copyfile(file_names[0], file_names[1]) utils.p('appending asset') # progress bars: # we need to check if mouse isn't down, which means an operator can be running. # Especially for sculpt mode, where appending a brush during a sculpt stroke causes crasehes # if tcom.passargs.get('redownload'): # handle lost libraries here: for l in bpy.data.libraries: if l.get('asset_data') is not None and l['asset_data'][ 'id'] == asset_data['id']: l.filepath = file_names[-1] l.reload() else: done = try_finished_append(asset_data, **tcom.passargs) if not done: at = asset_data['asset_type'] tcom.passargs['retry_counter'] = tcom.passargs.get( 'retry_counter', 0) + 1 if at in ('model', 'material'): download(asset_data, **tcom.passargs) elif asset_data['asset_type'] == 'material': download(asset_data, **tcom.passargs) elif asset_data['asset_type'] == 'scene': download(asset_data, **tcom.passargs) elif asset_data['asset_type'] == 'brush' or asset_data[ 'asset_type'] == 'texture': download(asset_data, **tcom.passargs) if bpy.context.scene['search results'] is not None and done: for sres in bpy.context.scene['search results']: if asset_data['id'] == sres['id']: sres['downloaded'] = 100 utils.p('finished download thread') return .5
def timer_update(): # TODO might get moved to handle all blenderkit stuff. # this makes a first search after opening blender. showing latest assets. global first_time preferences = bpy.context.preferences.addons['blenderkit'].preferences if first_time: first_time = False if preferences.show_on_start: search() if preferences.tips_on_start: ui.get_largest_3dview() ui.update_ui_size(ui.active_area, ui.active_region) ui.add_report(text='BlenderKit Tip: ' + random.choice(rtips), timeout=12, color=colors.GREEN) # clipboard monitoring to search assets from web global last_clipboard if bpy.context.window_manager.clipboard != last_clipboard: last_clipboard = bpy.context.window_manager.clipboard instr = 'asset_base_id:' if last_clipboard[:len(instr)] == instr: atstr = 'asset_type:' ati = last_clipboard.find(atstr) if ati > -1: at = last_clipboard[ati:] search_props = utils.get_search_props() search_props.search_keywords = last_clipboard search() global search_threads # don't do anything while dragging - this could switch asset type during drag, and make results list length different, # causing a lot of throuble literally. if len(search_threads) == 0 or bpy.context.scene.blenderkitUI.dragging: return 1 for thread in search_threads: # TODO this doesn't check all processes when one gets removed, # but most of the time only one is running anyway if not thread[0].is_alive(): search_threads.remove(thread) # icons_dir = thread[1] scene = bpy.context.scene # these 2 lines should update the previews enum and set the first result as active. s = bpy.context.scene asset_type = thread[2] if asset_type == 'model': props = scene.blenderkit_models json_filepath = os.path.join(icons_dir, 'model_searchresult.json') search_name = 'bkit model search' if asset_type == 'scene': props = scene.blenderkit_scene json_filepath = os.path.join(icons_dir, 'scene_searchresult.json') search_name = 'bkit scene search' if asset_type == 'material': props = scene.blenderkit_mat json_filepath = os.path.join(icons_dir, 'material_searchresult.json') search_name = 'bkit material search' if asset_type == 'brush': props = scene.blenderkit_brush json_filepath = os.path.join(icons_dir, 'brush_searchresult.json') search_name = 'bkit brush search' s[search_name] = [] global reports if reports != '': props.report = str(reports) return .2 with open(json_filepath, 'r') as data_file: rdata = json.load(data_file) result_field = [] ok, error = check_errors(rdata) if ok: bpy.ops.object.run_assetbar_fix_context() for r in rdata['results']: # TODO remove this fix when filesSize is fixed. # this is a temporary fix for too big numbers from the server. try: r['filesSize'] = int(r['filesSize'] / 1024) except: utils.p('asset with no files-size') if r['assetType'] == asset_type: if len(r['files']) > 0: furl = None tname = None allthumbs = [] durl, tname = None, None for f in r['files']: if f['fileType'] == 'thumbnail': tname = paths.extract_filename_from_url( f['fileThumbnailLarge']) small_tname = paths.extract_filename_from_url( f['fileThumbnail']) allthumbs.append( tname ) # TODO just first thumb is used now. tdict = {} for i, t in enumerate(allthumbs): tdict['thumbnail_%i'] = t if f['fileType'] == 'blend': durl = f['downloadUrl'].split('?')[0] # fname = paths.extract_filename_from_url(f['filePath']) if durl and tname: tooltip = generate_tooltip(r) asset_data = { 'thumbnail': tname, 'thumbnail_small': small_tname, # 'thumbnails':allthumbs, 'download_url': durl, 'id': r['id'], 'asset_base_id': r['assetBaseId'], 'name': r['name'], 'asset_type': r['assetType'], 'tooltip': tooltip, 'tags': r['tags'], 'can_download': r.get('canDownload', True), 'verification_status': r['verificationStatus'], 'author_id': str(r['author']['id']) # 'author': r['author']['firstName'] + ' ' + r['author']['lastName'] # 'description': r['description'], # 'author': r['description'], } asset_data['downloaded'] = 0 # parse extra params needed for blender here params = utils.params_to_dict(r['parameters']) if asset_type == 'model': if params.get('boundBoxMinX') != None: bbox = { 'bbox_min': (float(params['boundBoxMinX']), float(params['boundBoxMinY']), float(params['boundBoxMinZ'])), 'bbox_max': (float(params['boundBoxMaxX']), float(params['boundBoxMaxY']), float(params['boundBoxMaxZ'])) } else: bbox = { 'bbox_min': (-.5, -.5, 0), 'bbox_max': (.5, .5, 1) } asset_data.update(bbox) if asset_type == 'material': asset_data[ 'texture_size_meters'] = params.get( 'textureSizeMeters', 1.0) asset_data.update(tdict) if r['assetBaseId'] in scene.get( 'assets used', {}).keys(): asset_data['downloaded'] = 100 result_field.append(asset_data) # results = rdata['results'] s[search_name] = result_field s['search results'] = result_field s[search_name + ' orig'] = rdata s['search results orig'] = rdata load_previews() ui_props = bpy.context.scene.blenderkitUI if len(result_field) < ui_props.scrolloffset: ui_props.scrolloffset = 0 props.is_searching = False props.search_error = False props.report = 'Found %i results. ' % ( s['search results orig']['count']) if len(s['search results']) == 0: tasks_queue.add_task( (ui.add_report, ('No matching results found.', ))) # (rdata['next']) # if rdata['next'] != None: # search(False, get_next = True) else: print('error', error) props.report = error props.search_error = True # print('finished search thread') mt('preview loading finished') return .3
def on_invoke(self, context, event): if self.do_search: #TODO: move the search behaviour to separate operator, since asset bar can be already woken up from a timer. # we erase search keywords for cateogry search now, since these combinations usually return nothing now. # when the db gets bigger, this can be deleted. if self.category != '': sprops = utils.get_search_props() sprops.search_keywords = '' search.search(category=self.category) ui_props = context.scene.blenderkitUI if ui_props.assetbar_on: #TODO solve this otehrwise to enable more asset bars? # we don't want to run the assetbar many times, that's why it has a switch on/off behaviour, # unless being called with 'keep_running' prop. if not self.keep_running: # this sends message to the originally running operator, so it quits, and then it ends this one too. # If it initiated a search, the search will finish in a thread. The switch off procedure is run # by the 'original' operator, since if we get here, it means # same operator is already running. ui_props.turn_off = True # if there was an error, we need to turn off these props so we can restart after 2 clicks ui_props.assetbar_on = False else: pass return False ui_props.assetbar_on = True self.active_index = -1 widgets_panel = self.widgets_panel widgets_panel.extend(self.buttons) widgets_panel.extend(self.asset_buttons) widgets_panel.extend(self.validation_icons) widgets = [self.panel] widgets += widgets_panel widgets.append(self.tooltip_panel) widgets += self.tooltip_widgets self.init_widgets(context, widgets) self.panel.add_widgets(widgets_panel) self.tooltip_panel.add_widgets(self.tooltip_widgets) # Open the panel at the mouse location # self.panel.set_location(bpy.context.area.width - event.mouse_x, # bpy.context.area.height - event.mouse_y + 20) self.panel.set_location(self.bar_x, self.bar_y) self.context = context args = (self, context) # self._handle_2d_tooltip = bpy.types.SpaceView3D.draw_handler_add(draw_callback_tooltip, args, 'WINDOW', 'POST_PIXEL') return True
def draw(self, context): s = context.scene ui_props = s.blenderkitUI user_preferences = bpy.context.preferences.addons['blenderkit'].preferences wm = bpy.context.window_manager layout = self.layout # layout.prop_tabs_enum(ui_props, "asset_type", icon_only = True) row = layout.row() # row.scale_x = 1.6 # row.scale_y = 1.6 # row.prop(ui_props, 'down_up', expand=True, icon_only=False) # row.label(text='') # row = row.split().row() # layout.alert = True # layout.alignment = 'CENTER' # row = layout.row(align = True) # split = row.split(factor=.5) # row.prop(ui_props, 'asset_type', expand=True, icon_only=True) # row = layout.column(align = False) layout.prop(ui_props, 'asset_type', expand=False, text='') w = context.region.width if user_preferences.login_attempt: draw_login_progress(layout) return if len(user_preferences.api_key) < 20 and user_preferences.asset_counter > 20: if user_preferences.enable_oauth: draw_login_buttons(layout) else: op = layout.operator("wm.url_open", text="Get your API Key", icon='QUESTION') op.url = paths.BLENDERKIT_SIGNUP_URL layout.label(text='Paste your API Key:') layout.prop(user_preferences, 'api_key', text='') layout.separator() # if bpy.data.filepath == '': # layout.alert = True # label_multiline(layout, text="It's better to save your file first.", width=w) # layout.alert = False # layout.separator() if ui_props.down_up == 'SEARCH': if utils.profile_is_validator(): search_props = utils.get_search_props() layout.prop(search_props, 'search_verification_status') if ui_props.asset_type == 'MODEL': # noinspection PyCallByClass draw_panel_model_search(self, context) if ui_props.asset_type == 'SCENE': # noinspection PyCallByClass draw_panel_scene_search(self, context) elif ui_props.asset_type == 'MATERIAL': draw_panel_material_search(self, context) elif ui_props.asset_type == 'BRUSH': if context.sculpt_object or context.image_paint_object: # noinspection PyCallByClass draw_panel_brush_search(self, context) else: label_multiline(layout, text='switch to paint or sculpt mode.', width=context.region.width) return elif ui_props.down_up == 'UPLOAD': if not ui_props.assetbar_on: text = 'Show asset preview - ;' else: text = 'Hide asset preview - ;' op = layout.operator('view3d.blenderkit_asset_bar', text=text, icon='EXPORT') op.keep_running = False op.do_search = False op.tooltip = 'Show/Hide asset preview' e = s.render.engine if e not in ('CYCLES', 'BLENDER_EEVEE'): rtext = 'Only Cycles and EEVEE render engines are currently supported. ' \ 'Please use Cycles for all assets you upload to BlenderKit.' label_multiline(layout, rtext, icon='ERROR', width=w) return; if ui_props.asset_type == 'MODEL': # label_multiline(layout, "Uploaded models won't be available in b2.79", icon='ERROR') if bpy.context.view_layer.objects.active is not None: draw_panel_model_upload(self, context) else: layout.label(text='selet object to upload') elif ui_props.asset_type == 'SCENE': draw_panel_scene_upload(self, context) elif ui_props.asset_type == 'MATERIAL': # label_multiline(layout, "Uploaded materials won't be available in b2.79", icon='ERROR') if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None: draw_panel_material_upload(self, context) else: label_multiline(layout, text='select object with material to upload materials', width=w) elif ui_props.asset_type == 'BRUSH': if context.sculpt_object or context.image_paint_object: draw_panel_brush_upload(self, context) else: layout.label(text='switch to paint or sculpt mode.') elif ui_props.down_up == 'RATING': # the poll functions didn't work here, don't know why. if ui_props.asset_type == 'MODEL': # TODO improve poll here to parenting structures if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.get( 'asset_data') != None: ad = bpy.context.active_object.get('asset_data') layout.label(text=ad['name']) draw_panel_model_rating(self, context) if ui_props.asset_type == 'MATERIAL': if bpy.context.view_layer.objects.active is not None and \ bpy.context.active_object.active_material is not None and \ bpy.context.active_object.active_material.blenderkit.asset_base_id != '': layout.label(text=bpy.context.active_object.active_material.blenderkit.name + ' :') # noinspection PyCallByClass draw_panel_material_ratings(self, context) if ui_props.asset_type == 'BRUSH': if context.sculpt_object or context.image_paint_object: props = utils.get_brush_props(context) if props.asset_base_id != '': layout.label(text=props.name + ' :') # noinspection PyCallByClass draw_panel_brush_ratings(self, context) if ui_props.asset_type == 'TEXTURE': layout.label(text='not yet implemented')
def timer_update2(): preferences = bpy.context.preferences.addons['blenderkit'].preferences if search.first_time: search.first_time = False if preferences.show_on_start: search() if preferences.tips_on_start: ui.get_largest_3dview() ui.update_ui_size(ui.active_area, ui.active_region) ui.add_report(text='BlenderKit Tip: ' + random.choice(search.rtips), timeout=12, color=colors.GREEN) if bpy.context.window_manager.clipboard != search.last_clipboard: last_clipboard = bpy.context.window_manager.clipboard instr = 'asset_base_id:' if last_clipboard[:len(instr)] == instr: atstr = 'asset_type:' ati = last_clipboard.find(atstr) if ati > -1: search_props = utils.get_search_props() search_props.search_keywords = last_clipboard if len(search.search_threads ) == 0 or bpy.context.scene.blenderkitUI.dragging: return 1 for thread in search.search_threads: if not thread[0].is_alive(): search.search_threads.remove(thread) # icons_dir = thread[1] scene = bpy.context.scene s = bpy.context.scene asset_type = thread[2] if asset_type == 'model': props = scene.blenderkit_models json_filepath = os.path.join(icons_dir, 'model_searchresult.json') search_name = 'bkit model search' if asset_type == 'scene': props = scene.blenderkit_scene json_filepath = os.path.join(icons_dir, 'scene_searchresult.json') search_name = 'bkit scene search' if asset_type == 'material': props = scene.blenderkit_mat json_filepath = os.path.join(icons_dir, 'material_searchresult.json') search_name = 'bkit material search' if asset_type == 'brush': props = scene.blenderkit_brush json_filepath = os.path.join(icons_dir, 'brush_searchresult.json') search_name = 'bkit brush search' s[search_name] = [] if search.reports != '': props.report = str(search.reports) return .2 with open(json_filepath, 'r') as data_file: rdata = json.load(data_file) result_field = [] ok, error = search.check_errors(rdata) if ok: bpy.ops.object.run_assetbar_fix_context() for r in rdata['results']: try: r['filesSize'] = int(r['filesSize'] / 1024) except: utils.p('asset with no files-size') if r['assetType'] == asset_type: if len(r['files']) > 0: tname = None allthumbs = [] durl, tname = None, None for f in r['files']: if f['fileType'] == 'thumbnail': tname = paths.extract_filename_from_url( f['fileThumbnailLarge']) small_tname = paths.extract_filename_from_url( f['fileThumbnail']) allthumbs.append(tname) tdict = {} for i, t in enumerate(allthumbs): tdict['thumbnail_%i'] = t if f['fileType'] == 'blend': durl = f['downloadUrl'].split('?')[0] if durl and tname: tooltip = search.generate_tooltip(r) asset_data = { 'thumbnail': tname, 'thumbnail_small': small_tname, 'download_url': durl, 'id': r['id'], 'asset_base_id': r['assetBaseId'], 'name': r['name'], 'asset_type': r['assetType'], 'tooltip': tooltip, 'tags': r['tags'], 'can_download': r.get('canDownload', True), 'verification_status': r['verificationStatus'], 'author_id': str(r['author']['id']) } asset_data['downloaded'] = 0 if 'description' in r: asset_data['description'] = r[ 'description'] if 'metadata' in r: asset_data['metadata'] = r['metadata'] if 'sku' in r: asset_data['sku'] = r['sku'] if 'client' in r: asset_data['client'] = r['client'] params = utils.params_to_dict(r['parameters']) if asset_type == 'model': if params.get('boundBoxMinX') is not None: bbox = { 'bbox_min': (float(params['boundBoxMinX']), float(params['boundBoxMinY']), float(params['boundBoxMinZ'])), 'bbox_max': (float(params['boundBoxMaxX']), float(params['boundBoxMaxY']), float(params['boundBoxMaxZ'])) } else: bbox = { 'bbox_min': (-.5, -.5, 0), 'bbox_max': (.5, .5, 1) } asset_data.update(bbox) if asset_type == 'material': asset_data[ 'texture_size_meters'] = params.get( 'textureSizeMeters', 1.0) asset_data.update(tdict) if r['assetBaseId'] in scene.get( 'assets used', {}).keys(): asset_data['downloaded'] = 100 result_field.append(asset_data) s[search_name] = result_field s['search results'] = result_field s[search_name + ' orig'] = rdata s['search results orig'] = rdata search.load_previews() ui_props = bpy.context.scene.blenderkitUI if len(result_field) < ui_props.scrolloffset: ui_props.scrolloffset = 0 props.is_searching = False props.search_error = False props.report = 'Found %i results. ' % ( s['search results orig']['count']) if len(s['search results']) == 0: tasks_queue.add_task( (ui.add_report, ('No matching results found.', ))) else: print('error', error) props.report = error props.search_error = True search.mt('preview loading finished') return .3