Cleanup: trailing space
[blender-addons.git] / blenderkit / search.py
blob836c29b0e695d2d560b30a71183bf8a3a53d90ea
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 from blenderkit import paths, utils, categories, ui, colors, bkit_oauth, version_checker, tasks_queue, rerequests, \
20 resolutions, image_utils
22 import blenderkit
23 from bpy.app.handlers import persistent
25 from bpy.props import ( # TODO only keep the ones actually used when cleaning
26 IntProperty,
27 FloatProperty,
28 FloatVectorProperty,
29 StringProperty,
30 EnumProperty,
31 BoolProperty,
32 PointerProperty,
34 from bpy.types import (
35 Operator,
36 Panel,
37 AddonPreferences,
38 PropertyGroup,
39 UIList
42 import requests, os, random
43 import time
44 import threading
45 import platform
46 import bpy
47 import copy
48 import json
49 import math
51 import logging
53 bk_logger = logging.getLogger('blenderkit')
55 search_start_time = 0
56 prev_time = 0
59 def check_errors(rdata):
60 if rdata.get('statusCode') and int(rdata.get('statusCode')) > 299:
61 utils.p(rdata)
62 if rdata.get('detail') == 'Invalid token.':
63 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
64 if user_preferences.api_key != '':
65 if user_preferences.enable_oauth:
66 bkit_oauth.refresh_token_thread()
67 return False, rdata.get('detail')
68 return False, 'Use login panel to connect your profile.'
69 else:
70 return False, rdata.get('detail')
71 return True, ''
74 search_threads = []
75 thumb_sml_download_threads = {}
76 thumb_full_download_threads = {}
77 reports = ''
79 rtips = ['Click or drag model or material in scene to link/append ',
80 "Please rate responsively and plentifully. This helps us distribute rewards to the authors.",
81 "Click on brushes to link them into scene.",
82 "All materials and brushes are free.",
83 "Storage for public assets is unlimited.",
84 "Locked models are available if you subscribe to Full plan.",
85 "Login to upload your own models, materials or brushes.",
86 "Use 'A' key over asset bar to search assets by same author.",
87 "Use 'W' key over asset bar to open Authors webpage.", ]
90 def refresh_token_timer():
91 ''' this timer gets run every time the token needs refresh. It refreshes tokens and also categories.'''
92 utils.p('refresh timer')
93 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
94 fetch_server_data()
95 categories.load_categories()
97 return max(3600, user_preferences.api_key_life - 3600)
100 def update_ad(ad):
101 if not ad.get('assetBaseId'):
102 try:
103 ad['assetBaseId'] = ad['asset_base_id'] # this should stay ONLY for compatibility with older scenes
104 ad['assetType'] = ad['asset_type'] # this should stay ONLY for compatibility with older scenes
105 ad['verificationStatus'] = ad[
106 'verification_status'] # this should stay ONLY for compatibility with older scenes
107 ad['author'] = {}
108 ad['author']['id'] = ad['author_id'] # this should stay ONLY for compatibility with older scenes
109 ad['canDownload'] = ad['can_download'] # this should stay ONLY for compatibility with older scenes
110 except Exception as e:
111 bk_logger.error('BLenderKit failed to update older asset data')
112 return ad
115 def update_assets_data(): # updates assets data on scene load.
116 '''updates some properties that were changed on scenes with older assets.
117 The properties were mainly changed from snake_case to CamelCase to fit the data that is coming from the server.
119 data = bpy.data
121 datablocks = [
122 bpy.data.objects,
123 bpy.data.materials,
124 bpy.data.brushes,
126 for dtype in datablocks:
127 for block in dtype:
128 if block.get('asset_data') != None:
129 update_ad(block['asset_data'])
131 dicts = [
132 'assets used',
133 # 'assets rated',# assets rated stores only true/false, not asset data.
135 for s in bpy.data.scenes:
136 for bkdict in dicts:
138 d = s.get(bkdict)
139 if not d:
140 continue;
142 for asset_id in d.keys():
143 update_ad(d[asset_id])
144 # bpy.context.scene['assets used'][ad] = ad
147 def purge_search_results():
148 ''' clean up search results on save/load.'''
150 s = bpy.context.scene
152 sr_props = [
153 'search results',
154 'search results orig',
156 asset_types = ['model', 'material', 'scene', 'hdr', 'brush']
157 for at in asset_types:
158 sr_props.append('bkit {at} search')
159 sr_props.append('bkit {at} search orig')
160 for sr_prop in sr_props:
161 if s.get(sr_prop):
162 del (s[sr_prop])
165 @persistent
166 def scene_load(context):
168 Loads categories , checks timers registration, and updates scene asset data.
169 Should (probably)also update asset data from server (after user consent)
171 wm = bpy.context.window_manager
172 purge_search_results()
173 fetch_server_data()
174 categories.load_categories()
175 if not bpy.app.timers.is_registered(refresh_token_timer):
176 bpy.app.timers.register(refresh_token_timer, persistent=True, first_interval=36000)
177 update_assets_data()
180 def fetch_server_data():
181 ''' download categories , profile, and refresh token if needed.'''
182 if not bpy.app.background:
183 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
184 api_key = user_preferences.api_key
185 # Only refresh new type of tokens(by length), and only one hour before the token timeouts.
186 if user_preferences.enable_oauth and \
187 len(user_preferences.api_key) < 38 and len(user_preferences.api_key) > 0 and \
188 user_preferences.api_key_timeout < time.time() + 3600:
189 bkit_oauth.refresh_token_thread()
190 if api_key != '' and bpy.context.window_manager.get('bkit profile') == None:
191 get_profile()
192 if bpy.context.window_manager.get('bkit_categories') is None:
193 categories.fetch_categories_thread(api_key, force = False)
196 first_time = True
197 last_clipboard = ''
200 def check_clipboard():
202 Checks clipboard for an exact string containing asset ID.
203 The string is generated on www.blenderkit.com as for example here:
204 https://www.blenderkit.com/get-blenderkit/54ff5c85-2c73-49e9-ba80-aec18616a408/
207 # clipboard monitoring to search assets from web
208 if platform.system() != 'Linux':
209 global last_clipboard
210 if bpy.context.window_manager.clipboard != last_clipboard:
211 last_clipboard = bpy.context.window_manager.clipboard
212 instr = 'asset_base_id:'
213 # first check if contains asset id, then asset type
214 if last_clipboard[:len(instr)] == instr:
215 atstr = 'asset_type:'
216 ati = last_clipboard.find(atstr)
217 # this only checks if the asset_type keyword is there but let's the keywords update function do the parsing.
218 if ati > -1:
219 search_props = utils.get_search_props()
220 search_props.search_keywords = last_clipboard
221 # don't run search after this - assigning to keywords runs the search_update function.
224 def parse_result(r):
226 needed to generate some extra data in the result(by now)
227 Parameters
228 ----------
229 r - search result, also called asset_data
231 scene = bpy.context.scene
233 # TODO remove this fix when filesSize is fixed.
234 # this is a temporary fix for too big numbers from the server.
235 # try:
236 # r['filesSize'] = int(r['filesSize'] / 1024)
237 # except:
238 # utils.p('asset with no files-size')
239 asset_type = r['assetType']
240 if len(r['files']) > 0:#TODO remove this condition so all assets are parsed.
241 r['available_resolutions'] = []
242 allthumbs = []
243 durl, tname, small_tname = '', '', ''
244 for f in r['files']:
245 if f['fileType'] == 'thumbnail':
246 tname = paths.extract_filename_from_url(f['fileThumbnailLarge'])
247 small_tname = paths.extract_filename_from_url(f['fileThumbnail'])
248 allthumbs.append(tname) # TODO just first thumb is used now.
250 tdict = {}
251 for i, t in enumerate(allthumbs):
252 tdict['thumbnail_%i'] = t
253 if f['fileType'] == 'blend':
254 durl = f['downloadUrl'].split('?')[0]
255 # fname = paths.extract_filename_from_url(f['filePath'])
257 if f['fileType'].find('resolution') > -1:
258 r['available_resolutions'].append(resolutions.resolutions[f['fileType']])
259 r['max_resolution'] = 0
260 if r['available_resolutions']: # should check only for non-empty sequences
261 r['max_resolution'] = max(r['available_resolutions'])
263 tooltip = generate_tooltip(r)
264 # for some reason, the id was still int on some occurances. investigate this.
265 r['author']['id'] = str(r['author']['id'])
267 # some helper props, but generally shouldn't be renaming/duplifiying original properties,
268 # so blender's data is same as on server.
269 asset_data = {'thumbnail': tname,
270 'thumbnail_small': small_tname,
271 # 'thumbnails':allthumbs,
272 # 'download_url': durl, #made obsolete since files are stored in orig form.
273 # 'id': r['id'],
274 # 'asset_base_id': r['assetBaseId'],#this should stay ONLY for compatibility with older scenes
275 # 'name': r['name'],
276 # 'asset_type': r['assetType'], #this should stay ONLY for compatibility with older scenes
277 'tooltip': tooltip,
278 # 'tags': r['tags'],
279 # 'can_download': r.get('canDownload', True),#this should stay ONLY for compatibility with older scenes
280 # 'verification_status': r['verificationStatus'],#this should stay ONLY for compatibility with older scenes
281 # 'author_id': r['author']['id'],#this should stay ONLY for compatibility with older scenes
282 # 'author': r['author']['firstName'] + ' ' + r['author']['lastName']
283 # 'description': r['description'],
285 asset_data['downloaded'] = 0
287 # parse extra params needed for blender here
288 params = utils.params_to_dict(r['parameters'])
290 if asset_type == 'model':
291 if params.get('boundBoxMinX') != None:
292 bbox = {
293 'bbox_min': (
294 float(params['boundBoxMinX']),
295 float(params['boundBoxMinY']),
296 float(params['boundBoxMinZ'])),
297 'bbox_max': (
298 float(params['boundBoxMaxX']),
299 float(params['boundBoxMaxY']),
300 float(params['boundBoxMaxZ']))
303 else:
304 bbox = {
305 'bbox_min': (-.5, -.5, 0),
306 'bbox_max': (.5, .5, 1)
308 asset_data.update(bbox)
309 if asset_type == 'material':
310 asset_data['texture_size_meters'] = params.get('textureSizeMeters', 1.0)
312 asset_data.update(tdict)
314 au = scene.get('assets used', {})
315 if au == {}:
316 scene['assets used'] = au
317 if r['assetBaseId'] in au.keys():
318 asset_data['downloaded'] = 100
319 # transcribe all urls already fetched from the server
320 r_previous = au[r['assetBaseId']]
321 if r_previous.get('files'):
322 for f in r_previous['files']:
323 if f.get('url'):
324 for f1 in r['files']:
325 if f1['fileType'] == f['fileType']:
326 f1['url'] = f['url']
328 # attempt to switch to use original data gradually, since the parsing as itself should become obsolete.
329 asset_data.update(r)
330 return asset_data
333 # @bpy.app.handlers.persistent
334 def timer_update():
335 # this makes a first search after opening blender. showing latest assets.
336 # utils.p('timer search')
338 global first_time
339 preferences = bpy.context.preferences.addons['blenderkit'].preferences
340 if first_time and not bpy.app.background: # first time
342 first_time = False
343 if preferences.show_on_start:
344 # TODO here it should check if there are some results, and only open assetbar if this is the case, not search.
345 # if bpy.context.window_manager.get('search results') is None:
346 search()
347 # preferences.first_run = False
348 if preferences.tips_on_start:
349 utils.get_largest_area()
350 ui.update_ui_size(ui.active_area_pointer, ui.active_region_pointer)
351 ui.add_report(text='BlenderKit Tip: ' + random.choice(rtips), timeout=12, color=colors.GREEN)
352 return 3.0
354 # if preferences.first_run:
355 # search()
356 # preferences.first_run = False
358 # check_clipboard()
360 global search_threads
361 if len(search_threads) == 0:
362 return 1.0
363 # don't do anything while dragging - this could switch asset during drag, and make results list length different,
364 # causing a lot of throuble.
365 if bpy.context.scene.blenderkitUI.dragging:
366 return 0.5
367 for thread in search_threads:
368 # TODO this doesn't check all processes when one gets removed,
369 # but most of the time only one is running anyway
370 if not thread[0].is_alive():
371 search_threads.remove(thread) #
372 icons_dir = thread[1]
373 scene = bpy.context.scene
374 # these 2 lines should update the previews enum and set the first result as active.
375 wm = bpy.context.window_manager
376 asset_type = thread[2]
377 if asset_type == 'model':
378 props = scene.blenderkit_models
379 # json_filepath = os.path.join(icons_dir, 'model_searchresult.json')
380 if asset_type == 'scene':
381 props = scene.blenderkit_scene
382 # json_filepath = os.path.join(icons_dir, 'scene_searchresult.json')
383 if asset_type == 'hdr':
384 props = scene.blenderkit_HDR
385 # json_filepath = os.path.join(icons_dir, 'scene_searchresult.json')
386 if asset_type == 'material':
387 props = scene.blenderkit_mat
388 # json_filepath = os.path.join(icons_dir, 'material_searchresult.json')
389 if asset_type == 'brush':
390 props = scene.blenderkit_brush
391 # json_filepath = os.path.join(icons_dir, 'brush_searchresult.json')
392 search_name = f'bkit {asset_type} search'
394 wm[search_name] = []
396 global reports
397 if reports != '':
398 props.report = str(reports)
399 return .2
401 rdata = thread[0].result
403 result_field = []
404 ok, error = check_errors(rdata)
405 if ok:
406 bpy.ops.object.run_assetbar_fix_context()
407 for r in rdata['results']:
408 asset_data = parse_result(r)
409 if asset_data != None:
410 result_field.append(asset_data)
412 # results = rdata['results']
413 wm[search_name] = result_field
414 wm['search results'] = result_field
415 wm[search_name + ' orig'] = copy.deepcopy(rdata)
416 wm['search results orig'] = wm[search_name + ' orig']
418 load_previews()
419 ui_props = bpy.context.scene.blenderkitUI
420 if len(result_field) < ui_props.scrolloffset:
421 ui_props.scrolloffset = 0
422 props.is_searching = False
423 props.search_error = False
424 props.report = 'Found %i results. ' % (wm['search results orig']['count'])
425 if len(wm['search results']) == 0:
426 tasks_queue.add_task((ui.add_report, ('No matching results found.',)))
427 # undo push
428 bpy.ops.wm.undo_push_context(message='Get BlenderKit search')
430 else:
431 bk_logger.error(error)
432 props.report = error
433 props.search_error = True
435 # print('finished search thread')
436 mt('preview loading finished')
438 return .3
441 def load_previews():
442 scene = bpy.context.scene
443 # FIRST START SEARCH
444 props = scene.blenderkitUI
445 directory = paths.get_temp_dir('%s_search' % props.asset_type.lower())
446 s = bpy.context.scene
447 results = bpy.context.window_manager.get('search results')
449 if results is not None:
450 inames = []
451 tpaths = []
453 i = 0
454 for r in results:
455 tpath = os.path.join(directory, r['thumbnail_small'])
456 if not r['thumbnail_small']:
457 tpath = paths.get_addon_thumbnail_path('thumbnail_not_available.jpg')
459 if not os.path.exists(tpath):
460 continue
461 iname = utils.previmg_name(i)
463 # if os.path.exists(tpath): # sometimes we are unlucky...
464 img = bpy.data.images.get(iname)
466 if img is None:
467 img = bpy.data.images.load(tpath)
468 img.name = iname
469 elif img.filepath != tpath:
470 # had to add this check for autopacking files...
471 if img.packed_file is not None:
472 img.unpack(method='USE_ORIGINAL')
473 img.filepath = tpath
474 img.reload()
475 if r['assetType'] == 'hdr':
476 # to display hdr thumbnails correctly, we use non-color, otherwise looks shifted
477 image_utils.set_colorspace(img, 'Non-Color')
478 else:
479 image_utils.set_colorspace(img, 'sRGB')
481 i += 1
482 # print('previews loaded')
485 # line splitting for longer texts...
486 def split_subs(text, threshold=40):
487 if text == '':
488 return []
489 # temporarily disable this, to be able to do this in drawing code
491 text = text.rstrip()
492 text = text.replace('\r\n', '\n')
494 lines = []
496 while len(text) > threshold:
497 # first handle if there's an \n line ending
498 i_rn = text.find('\n')
499 if 1 < i_rn < threshold:
500 i = i_rn
501 text = text.replace('\n', '', 1)
502 else:
503 i = text.rfind(' ', 0, threshold)
504 i1 = text.rfind(',', 0, threshold)
505 i2 = text.rfind('.', 0, threshold)
506 i = max(i, i1, i2)
507 if i <= 0:
508 i = threshold
509 lines.append(text[:i])
510 text = text[i:]
511 lines.append(text)
512 return lines
515 def list_to_str(input):
516 output = ''
517 for i, text in enumerate(input):
518 output += text
519 if i < len(input) - 1:
520 output += ', '
521 return output
524 def writeblock(t, input, width=40): # for longer texts
525 dlines = split_subs(input, threshold=width)
526 for i, l in enumerate(dlines):
527 t += '%s\n' % l
528 return t
531 def writeblockm(tooltip, mdata, key='', pretext=None, width=40): # for longer texts
532 if mdata.get(key) == None:
533 return tooltip
534 else:
535 intext = mdata[key]
536 if type(intext) == list:
537 intext = list_to_str(intext)
538 if type(intext) == float:
539 intext = round(intext, 3)
540 intext = str(intext)
541 if intext.rstrip() == '':
542 return tooltip
543 if pretext == None:
544 pretext = key
545 if pretext != '':
546 pretext = pretext + ': '
547 text = pretext + intext
548 dlines = split_subs(text, threshold=width)
549 for i, l in enumerate(dlines):
550 tooltip += '%s\n' % l
552 return tooltip
555 def fmt_length(prop):
556 prop = str(round(prop, 2)) + 'm'
557 return prop
560 def has(mdata, prop):
561 if mdata.get(prop) is not None and mdata[prop] is not None and mdata[prop] is not False:
562 return True
563 else:
564 return False
567 def generate_tooltip(mdata):
568 col_w = 40
569 if type(mdata['parameters']) == list:
570 mparams = utils.params_to_dict(mdata['parameters'])
571 else:
572 mparams = mdata['parameters']
573 t = ''
574 t = writeblock(t, mdata['displayName'], width=col_w)
575 t += '\n'
577 t = writeblockm(t, mdata, key='description', pretext='', width=col_w)
578 if mdata['description'] != '':
579 t += '\n'
581 bools = (('rig', None), ('animated', None), ('manifold', 'non-manifold'), ('scene', None), ('simulation', None),
582 ('uv', None))
583 for b in bools:
584 if mparams.get(b[0]):
585 mdata['tags'].append(b[0])
586 elif b[1] != None:
587 mdata['tags'].append(b[1])
589 bools_data = ('adult',)
590 for b in bools_data:
591 if mdata.get(b) and mdata[b]:
592 mdata['tags'].append(b)
593 t = writeblockm(t, mparams, key='designer', pretext='designer', width=col_w)
594 t = writeblockm(t, mparams, key='manufacturer', pretext='manufacturer', width=col_w)
595 t = writeblockm(t, mparams, key='designCollection', pretext='design collection', width=col_w)
597 # t = writeblockm(t, mparams, key='engines', pretext='engine', width = col_w)
598 # t = writeblockm(t, mparams, key='model_style', pretext='style', width = col_w)
599 # t = writeblockm(t, mparams, key='material_style', pretext='style', width = col_w)
600 # t = writeblockm(t, mdata, key='tags', width = col_w)
601 # t = writeblockm(t, mparams, key='condition', pretext='condition', width = col_w)
602 # t = writeblockm(t, mparams, key='productionLevel', pretext='production level', width = col_w)
603 if has(mdata, 'purePbr'):
604 t = writeblockm(t, mparams, key='pbrType', pretext='pbr', width=col_w)
606 t = writeblockm(t, mparams, key='designYear', pretext='design year', width=col_w)
608 if has(mparams, 'dimensionX'):
609 t += 'size: %s, %s, %s\n' % (fmt_length(mparams['dimensionX']),
610 fmt_length(mparams['dimensionY']),
611 fmt_length(mparams['dimensionZ']))
612 if has(mparams, 'faceCount'):
613 t += 'face count: %s, render: %s\n' % (mparams['faceCount'], mparams['faceCountRender'])
615 # write files size - this doesn't reflect true file size, since files size is computed from all asset files, including resolutions.
616 if mdata.get('filesSize'):
617 fs = utils.files_size_to_text(mdata['filesSize'])
618 t += f'files size: {fs}\n'
620 # t = writeblockm(t, mparams, key='meshPolyType', pretext='mesh type', width = col_w)
621 # t = writeblockm(t, mparams, key='objectCount', pretext='nubmber of objects', width = col_w)
623 # t = writeblockm(t, mparams, key='materials', width = col_w)
624 # t = writeblockm(t, mparams, key='modifiers', width = col_w)
625 # t = writeblockm(t, mparams, key='shaders', width = col_w)
627 if has(mparams, 'textureSizeMeters'):
628 t += 'texture size: %s\n' % fmt_length(mparams['textureSizeMeters'])
630 if has(mparams, 'textureResolutionMax') and mparams['textureResolutionMax'] > 0:
631 if not mparams.get('textureResolutionMin'): # for HDR's
632 t = writeblockm(t, mparams, key='textureResolutionMax', pretext='Resolution', width=col_w)
633 elif mparams.get('textureResolutionMin') == mparams['textureResolutionMax']:
634 t = writeblockm(t, mparams, key='textureResolutionMin', pretext='texture resolution', width=col_w)
635 else:
636 t += 'tex resolution: %i - %i\n' % (mparams.get('textureResolutionMin'), mparams['textureResolutionMax'])
638 if has(mparams, 'thumbnailScale'):
639 t = writeblockm(t, mparams, key='thumbnailScale', pretext='preview scale', width=col_w)
641 # t += 'uv: %s\n' % mdata['uv']
642 # t += '\n'
643 t = writeblockm(t, mdata, key='license', width=col_w)
645 fs = mdata.get('files')
647 if utils.profile_is_validator():
648 if fs:
649 resolutions = 'resolutions:'
650 for f in fs:
651 if f['fileType'].find('resolution') > -1:
652 resolutions += f['fileType'][11:] + ' '
653 resolutions += '\n'
654 t += resolutions
656 t = writeblockm(t, mdata, key='isFree', width=col_w)
657 else:
658 if fs:
659 for f in fs:
660 if f['fileType'].find('resolution') > -1:
661 t += 'Asset has lower resolutions available\n'
662 break;
663 # generator is for both upload preview and search, this is only after search
664 # if mdata.get('versionNumber'):
665 # # t = writeblockm(t, mdata, key='versionNumber', pretext='version', width = col_w)
666 # a_id = mdata['author'].get('id')
667 # if a_id != None:
668 # adata = bpy.context.window_manager['bkit authors'].get(str(a_id))
669 # if adata != None:
670 # t += generate_author_textblock(adata)
672 # t += '\n'
673 rc = mdata.get('ratingsCount')
674 if utils.profile_is_validator() and rc:
676 if rc:
677 rcount = min(rc['quality'], rc['workingHours'])
678 else:
679 rcount = 0
680 if rcount < 10:
681 t += f"Please rate this asset, \nit doesn't have enough ratings.\n"
682 else:
683 t += f"Quality rating: {int(mdata['ratingsAverage']['quality']) * '*'}\n"
684 t += f"Hours saved rating: {int(mdata['ratingsAverage']['workingHours'])}\n"
685 if utils.profile_is_validator():
686 t += f"Score: {int(mdata['score'])}\n"
688 t += f"Ratings count {rc['quality']}*/{rc['workingHours']}wh value " \
689 f"{mdata['ratingsAverage']['quality']}*/{mdata['ratingsAverage']['workingHours']}wh\n"
690 if len(t.split('\n')) < 11:
691 t += '\n'
692 t += get_random_tip(mdata)
693 t += '\n'
694 return t
697 def get_random_tip(mdata):
698 t = ''
700 tip = 'Tip: ' + random.choice(rtips)
701 t = writeblock(t, tip)
702 return t
703 # at = mdata['assetType']
704 # if at == 'brush' or at == 'texture':
705 # t += 'click to link %s' % mdata['assetType']
706 # if at == 'model' or at == 'material':
707 # tips = ['Click or drag in scene to link/append %s' % mdata['assetType'],
708 # "'A' key to search assets by same author",
709 # "'W' key to open Authors webpage",
711 # tip = 'Tip: ' + random.choice(tips)
712 # t = writeblock(t, tip)
713 return t
716 def generate_author_textblock(adata):
717 t = '\n\n\n'
719 if adata not in (None, ''):
720 col_w = 40
721 if len(adata['firstName'] + adata['lastName']) > 0:
722 t = 'Author:\n'
723 t += '%s %s\n' % (adata['firstName'], adata['lastName'])
724 t += '\n'
725 if adata.get('aboutMeUrl') is not None:
726 t = writeblockm(t, adata, key='aboutMeUrl', pretext='', width=col_w)
727 t += '\n'
728 if adata.get('aboutMe') is not None:
729 t = writeblockm(t, adata, key='aboutMe', pretext='', width=col_w)
730 t += '\n'
731 return t
734 def get_items_models(self, context):
735 global search_items_models
736 return search_items_models
739 def get_items_brushes(self, context):
740 global search_items_brushes
741 return search_items_brushes
744 def get_items_materials(self, context):
745 global search_items_materials
746 return search_items_materials
749 def get_items_textures(self, context):
750 global search_items_textures
751 return search_items_textures
754 class ThumbDownloader(threading.Thread):
755 query = None
757 def __init__(self, url, path):
758 super(ThumbDownloader, self).__init__()
759 self.url = url
760 self.path = path
761 self._stop_event = threading.Event()
763 def stop(self):
764 self._stop_event.set()
766 def stopped(self):
767 return self._stop_event.is_set()
769 def run(self):
770 r = rerequests.get(self.url, stream=False)
771 if r.status_code == 200:
772 with open(self.path, 'wb') as f:
773 f.write(r.content)
774 # ORIGINALLY WE DOWNLOADED THUMBNAILS AS STREAM, BUT THIS WAS TOO SLOW.
775 # with open(path, 'wb') as f:
776 # for chunk in r.iter_content(1048576*4):
777 # f.write(chunk)
780 def write_gravatar(a_id, gravatar_path):
782 Write down gravatar path, as a result of thread-based gravatar image download.
783 This should happen on timer in queue.
785 # print('write author', a_id, type(a_id))
786 authors = bpy.context.window_manager['bkit authors']
787 if authors.get(a_id) is not None:
788 adata = authors.get(a_id)
789 adata['gravatarImg'] = gravatar_path
792 def fetch_gravatar(adata):
793 # utils.p('fetch gravatar')
794 if adata.get('gravatarHash') is not None:
795 gravatar_path = paths.get_temp_dir(subdir='bkit_g/') + adata['gravatarHash'] + '.jpg'
797 if os.path.exists(gravatar_path):
798 tasks_queue.add_task((write_gravatar, (adata['id'], gravatar_path)))
799 return;
801 url = "https://www.gravatar.com/avatar/" + adata['gravatarHash'] + '?d=404'
802 r = rerequests.get(url, stream=False)
803 if r.status_code == 200:
804 with open(gravatar_path, 'wb') as f:
805 f.write(r.content)
806 tasks_queue.add_task((write_gravatar, (adata['id'], gravatar_path)))
807 elif r.status_code == '404':
808 adata['gravatarHash'] = None
809 utils.p('gravatar for author not available.')
812 fetching_gravatars = {}
815 def get_author(r):
816 ''' Writes author info (now from search results) and fetches gravatar if needed.'''
817 global fetching_gravatars
819 a_id = str(r['author']['id'])
820 preferences = bpy.context.preferences.addons['blenderkit'].preferences
821 authors = bpy.context.window_manager.get('bkit authors', {})
822 if authors == {}:
823 bpy.context.window_manager['bkit authors'] = authors
824 a = authors.get(a_id)
825 if a is None: # or a is '' or (a.get('gravatarHash') is not None and a.get('gravatarImg') is None):
826 a = r['author']
827 a['id'] = a_id
828 a['tooltip'] = generate_author_textblock(a)
830 authors[a_id] = a
831 if fetching_gravatars.get(a['id']) is None:
832 fetching_gravatars[a['id']] = True
834 thread = threading.Thread(target=fetch_gravatar, args=(a.copy(),), daemon=True)
835 thread.start()
836 return a
839 def write_profile(adata):
840 utils.p('writing profile information')
841 user = adata['user']
842 # we have to convert to MiB here, numbers too big for python int type
843 if user.get('sumAssetFilesSize') is not None:
844 user['sumAssetFilesSize'] /= (1024 * 1024)
845 if user.get('sumPrivateAssetFilesSize') is not None:
846 user['sumPrivateAssetFilesSize'] /= (1024 * 1024)
847 if user.get('remainingPrivateQuota') is not None:
848 user['remainingPrivateQuota'] /= (1024 * 1024)
850 if adata.get('canEditAllAssets') is True:
851 user['exmenu'] = True
852 else:
853 user['exmenu'] = False
855 bpy.context.window_manager['bkit profile'] = adata
858 def request_profile(api_key):
859 a_url = paths.get_api_url() + 'me/'
860 headers = utils.get_headers(api_key)
861 r = rerequests.get(a_url, headers=headers)
862 adata = r.json()
863 if adata.get('user') is None:
864 utils.p(adata)
865 utils.p('getting profile failed')
866 return None
867 return adata
870 def fetch_profile(api_key):
871 utils.p('fetch profile')
872 try:
873 adata = request_profile(api_key)
874 if adata is not None:
875 tasks_queue.add_task((write_profile, (adata,)))
876 except Exception as e:
877 bk_logger.error(e)
880 def get_profile():
881 preferences = bpy.context.preferences.addons['blenderkit'].preferences
882 a = bpy.context.window_manager.get('bkit profile')
883 thread = threading.Thread(target=fetch_profile, args=(preferences.api_key,), daemon=True)
884 thread.start()
885 return a
888 class Searcher(threading.Thread):
889 query = None
891 def __init__(self, query, params, orig_result):
892 super(Searcher, self).__init__()
893 self.query = query
894 self.params = params
895 self._stop_event = threading.Event()
896 self.result = orig_result
898 def stop(self):
899 self._stop_event.set()
901 def stopped(self):
902 return self._stop_event.is_set()
904 def query_to_url(self):
905 query = self.query
906 params = self.params
907 # build a new request
908 url = paths.get_api_url() + 'search/'
910 # build request manually
911 # TODO use real queries
912 requeststring = '?query='
914 if query.get('query') not in ('', None):
915 requeststring += query['query'].lower()
916 for i, q in enumerate(query):
917 if q != 'query':
918 requeststring += '+'
919 requeststring += q + ':' + str(query[q]).lower()
921 # result ordering: _score - relevance, score - BlenderKit score
922 order = []
923 if params['free_first']:
924 order = ['-is_free', ]
925 if query.get('query') is None and query.get('category_subtree') == None:
926 # assumes no keywords and no category, thus an empty search that is triggered on start.
927 # orders by last core file upload
928 if query.get('verification_status') == 'uploaded':
929 # for validators, sort uploaded from oldest
930 order.append('created')
931 else:
932 order.append('-last_upload')
933 elif query.get('author_id') is not None and utils.profile_is_validator():
935 order.append('-created')
936 else:
937 if query.get('category_subtree') is not None:
938 order.append('-score,_score')
939 else:
940 order.append('_score')
941 requeststring += '+order:' + ','.join(order)
943 requeststring += '&addon_version=%s' % params['addon_version']
944 if params.get('scene_uuid') is not None:
945 requeststring += '&scene_uuid=%s' % params['scene_uuid']
946 # print('params', params)
947 urlquery = url + requeststring
948 return urlquery
950 def run(self):
951 maxthreads = 50
952 query = self.query
953 params = self.params
954 global reports
956 t = time.time()
957 mt('search thread started')
958 tempdir = paths.get_temp_dir('%s_search' % query['asset_type'])
959 # json_filepath = os.path.join(tempdir, '%s_searchresult.json' % query['asset_type'])
961 headers = utils.get_headers(params['api_key'])
963 rdata = {}
964 rdata['results'] = []
966 if params['get_next']:
967 urlquery = self.result['next']
968 if not params['get_next']:
969 urlquery = self.query_to_url()
971 try:
972 utils.p(urlquery)
973 r = rerequests.get(urlquery, headers=headers) # , params = rparameters)
974 # print(r.url)
975 reports = ''
976 # utils.p(r.text)
977 except requests.exceptions.RequestException as e:
978 bk_logger.error(e)
979 reports = e
980 # props.report = e
981 return
982 mt('search response is back ')
983 try:
984 rdata = r.json()
985 except Exception as e:
986 reports = r.text
987 bk_logger.error(e)
989 mt('data parsed ')
990 if not rdata.get('results'):
991 utils.pprint(rdata)
992 # if the result was converted to json and didn't return results,
993 # it means it's a server error that has a clear message.
994 # That's why it gets processed in the update timer, where it can be passed in messages to user.
995 self.result = rdata
996 return
997 # print('number of results: ', len(rdata.get('results', [])))
998 if self.stopped():
999 utils.p('stopping search : ' + str(query))
1000 return
1002 mt('search finished')
1003 i = 0
1005 thumb_small_urls = []
1006 thumb_small_filepaths = []
1007 thumb_full_urls = []
1008 thumb_full_filepaths = []
1009 # END OF PARSING
1010 for d in rdata.get('results', []):
1012 get_author(d)
1014 for f in d['files']:
1015 # TODO move validation of published assets to server, too manmy checks here.
1016 if f['fileType'] == 'thumbnail' and f['fileThumbnail'] != None and f['fileThumbnailLarge'] != None:
1017 if f['fileThumbnail'] == None:
1018 f['fileThumbnail'] = 'NONE'
1019 if f['fileThumbnailLarge'] == None:
1020 f['fileThumbnailLarge'] = 'NONE'
1022 thumb_small_urls.append(f['fileThumbnail'])
1023 thumb_full_urls.append(f['fileThumbnailLarge'])
1025 imgname = paths.extract_filename_from_url(f['fileThumbnail'])
1026 imgpath = os.path.join(tempdir, imgname)
1027 thumb_small_filepaths.append(imgpath)
1029 imgname = paths.extract_filename_from_url(f['fileThumbnailLarge'])
1030 imgpath = os.path.join(tempdir, imgname)
1031 thumb_full_filepaths.append(imgpath)
1033 sml_thbs = zip(thumb_small_filepaths, thumb_small_urls)
1034 full_thbs = zip(thumb_full_filepaths, thumb_full_urls)
1036 # we save here because a missing thumbnail check is in the previous loop
1037 # we can also prepend previous results. These have downloaded thumbnails already...
1038 if params['get_next']:
1039 rdata['results'][0:0] = self.result['results']
1040 self.result = rdata
1041 # with open(json_filepath, 'w', encoding = 'utf-8') as outfile:
1042 # json.dump(rdata, outfile, ensure_ascii=False, indent=4)
1044 killthreads_sml = []
1045 for k in thumb_sml_download_threads.keys():
1046 if k not in thumb_small_filepaths:
1047 killthreads_sml.append(k) # do actual killing here?
1049 killthreads_full = []
1050 for k in thumb_full_download_threads.keys():
1051 if k not in thumb_full_filepaths:
1052 killthreads_full.append(k) # do actual killing here?
1053 # TODO do the killing/ stopping here! remember threads might have finished inbetween!
1055 if self.stopped():
1056 utils.p('stopping search : ' + str(query))
1057 return
1059 # this loop handles downloading of small thumbnails
1060 for imgpath, url in sml_thbs:
1061 if imgpath not in thumb_sml_download_threads and not os.path.exists(imgpath):
1062 thread = ThumbDownloader(url, imgpath)
1063 # thread = threading.Thread(target=download_thumbnail, args=([url, imgpath]),
1064 # daemon=True)
1065 thread.start()
1066 thumb_sml_download_threads[imgpath] = thread
1067 # threads.append(thread)
1069 if len(thumb_sml_download_threads) > maxthreads:
1070 while len(thumb_sml_download_threads) > maxthreads:
1071 threads_copy = thumb_sml_download_threads.copy() # because for loop can erase some of the items.
1072 for tk, thread in threads_copy.items():
1073 if not thread.is_alive():
1074 thread.join()
1075 # utils.p(x)
1076 del (thumb_sml_download_threads[tk])
1077 # utils.p('fetched thumbnail ', i)
1078 i += 1
1079 if self.stopped():
1080 utils.p('stopping search : ' + str(query))
1081 return
1082 idx = 0
1083 while len(thumb_sml_download_threads) > 0:
1084 threads_copy = thumb_sml_download_threads.copy() # because for loop can erase some of the items.
1085 for tk, thread in threads_copy.items():
1086 if not thread.is_alive():
1087 thread.join()
1088 del (thumb_sml_download_threads[tk])
1089 i += 1
1091 if self.stopped():
1092 utils.p('stopping search : ' + str(query))
1093 return
1095 # start downloading full thumbs in the end
1096 for imgpath, url in full_thbs:
1097 if imgpath not in thumb_full_download_threads and not os.path.exists(imgpath):
1098 thread = ThumbDownloader(url, imgpath)
1099 # thread = threading.Thread(target=download_thumbnail, args=([url, imgpath]),
1100 # daemon=True)
1101 thread.start()
1102 thumb_full_download_threads[imgpath] = thread
1103 mt('thumbnails finished')
1106 def build_query_common(query, props):
1107 '''add shared parameters to query'''
1108 query_common = {}
1109 if props.search_keywords != '':
1110 query_common["query"] = props.search_keywords
1112 if props.search_verification_status != 'ALL':
1113 query_common['verification_status'] = props.search_verification_status.lower()
1115 if props.unrated_only:
1116 query["quality_count"] = 0
1118 if props.search_file_size:
1119 query_common["files_size_gte"] = props.search_file_size_min * 1024 * 1024
1120 query_common["files_size_lte"] = props.search_file_size_max * 1024 * 1024
1122 query.update(query_common)
1125 def build_query_model():
1126 '''use all search input to request results from server'''
1128 props = bpy.context.scene.blenderkit_models
1129 query = {
1130 "asset_type": 'model',
1131 # "engine": props.search_engine,
1132 # "adult": props.search_adult,
1134 if props.search_style != 'ANY':
1135 if props.search_style != 'OTHER':
1136 query["model_style"] = props.search_style
1137 else:
1138 query["model_style"] = props.search_style_other
1140 # the 'free_only' parametr gets moved to the search command and is used for ordering the assets as free first
1141 # if props.free_only:
1142 # query["is_free"] = True
1144 # if props.search_advanced:
1145 if props.search_condition != 'UNSPECIFIED':
1146 query["condition"] = props.search_condition
1147 if props.search_design_year:
1148 query["designYear_gte"] = props.search_design_year_min
1149 query["designYear_lte"] = props.search_design_year_max
1150 if props.search_polycount:
1151 query["faceCount_gte"] = props.search_polycount_min
1152 query["faceCount_lte"] = props.search_polycount_max
1153 if props.search_texture_resolution:
1154 query["textureResolutionMax_gte"] = props.search_texture_resolution_min
1155 query["textureResolutionMax_lte"] = props.search_texture_resolution_max
1157 build_query_common(query, props)
1159 return query
1162 def build_query_scene():
1163 '''use all search input to request results from server'''
1165 props = bpy.context.scene.blenderkit_scene
1166 query = {
1167 "asset_type": 'scene',
1168 # "engine": props.search_engine,
1169 # "adult": props.search_adult,
1171 build_query_common(query, props)
1172 return query
1175 def build_query_HDR():
1176 '''use all search input to request results from server'''
1178 props = bpy.context.scene.blenderkit_HDR
1179 query = {
1180 "asset_type": 'hdr',
1181 # "engine": props.search_engine,
1182 # "adult": props.search_adult,
1184 build_query_common(query, props)
1185 return query
1188 def build_query_material():
1189 props = bpy.context.scene.blenderkit_mat
1190 query = {
1191 "asset_type": 'material',
1194 # if props.search_engine == 'NONE':
1195 # query["engine"] = ''
1196 # if props.search_engine != 'OTHER':
1197 # query["engine"] = props.search_engine
1198 # else:
1199 # query["engine"] = props.search_engine_other
1200 if props.search_style != 'ANY':
1201 if props.search_style != 'OTHER':
1202 query["style"] = props.search_style
1203 else:
1204 query["style"] = props.search_style_other
1205 if props.search_procedural == 'TEXTURE_BASED':
1206 # todo this procedural hack should be replaced with the parameter
1207 query["textureResolutionMax_gte"] = 0
1208 # query["procedural"] = False
1209 if props.search_texture_resolution:
1210 query["textureResolutionMax_gte"] = props.search_texture_resolution_min
1211 query["textureResolutionMax_lte"] = props.search_texture_resolution_max
1215 elif props.search_procedural == "PROCEDURAL":
1216 # todo this procedural hack should be replaced with the parameter
1217 query["files_size_lte"] = 1024 * 1024
1218 # query["procedural"] = True
1220 build_query_common(query, props)
1222 return query
1225 def build_query_texture():
1226 props = bpy.context.scene.blenderkit_tex
1227 query = {
1228 "asset_type": 'texture',
1232 if props.search_style != 'ANY':
1233 if props.search_style != 'OTHER':
1234 query["search_style"] = props.search_style
1235 else:
1236 query["search_style"] = props.search_style_other
1238 build_query_common(query, props)
1240 return query
1243 def build_query_brush():
1244 props = bpy.context.scene.blenderkit_brush
1246 brush_type = ''
1247 if bpy.context.sculpt_object is not None:
1248 brush_type = 'sculpt'
1250 elif bpy.context.image_paint_object: # could be just else, but for future p
1251 brush_type = 'texture_paint'
1253 query = {
1254 "asset_type": 'brush',
1256 "mode": brush_type
1259 build_query_common(query, props)
1261 return query
1264 def mt(text):
1265 global search_start_time, prev_time
1266 alltime = time.time() - search_start_time
1267 since_last = time.time() - prev_time
1268 prev_time = time.time()
1269 utils.p(text, alltime, since_last)
1272 def add_search_process(query, params, orig_result):
1273 global search_threads
1275 while (len(search_threads) > 0):
1276 old_thread = search_threads.pop(0)
1277 old_thread[0].stop()
1278 # TODO CARE HERE FOR ALSO KILLING THE Thumbnail THREADS.?
1279 # AT LEAST NOW SEARCH DONE FIRST WON'T REWRITE AN OLDER ONE
1280 tempdir = paths.get_temp_dir('%s_search' % query['asset_type'])
1281 thread = Searcher(query, params, orig_result)
1282 thread.start()
1284 search_threads.append([thread, tempdir, query['asset_type'], {}]) # 4th field is for results
1286 mt('search thread started')
1289 def get_search_simple(parameters, filepath=None, page_size=100, max_results=100000000, api_key=''):
1291 Searches and returns the
1294 Parameters
1295 ----------
1296 parameters - dict of blenderkit elastic parameters
1297 filepath - a file to save the results. If None, results are returned
1298 page_size - page size for retrieved results
1299 max_results - max results of the search
1300 api_key - BlenderKit api key
1302 Returns
1303 -------
1304 Returns search results as a list, and optionally saves to filepath
1307 headers = utils.get_headers(api_key)
1308 url = paths.get_api_url() + 'search/'
1309 requeststring = url + '?query='
1310 for p in parameters.keys():
1311 requeststring += f'+{p}:{parameters[p]}'
1313 requeststring += '&page_size=' + str(page_size)
1314 bk_logger.debug(requeststring)
1315 response = rerequests.get(requeststring, headers=headers) # , params = rparameters)
1316 # print(r.json())
1317 search_results = response.json()
1319 results = []
1320 results.extend(search_results['results'])
1321 page_index = 2
1322 page_count = math.ceil(search_results['count'] / page_size)
1323 while search_results.get('next') and len(results) < max_results:
1324 bk_logger.info(f'getting page {page_index} , total pages {page_count}')
1325 response = rerequests.get(search_results['next'], headers=headers) # , params = rparameters)
1326 search_results = response.json()
1327 # print(search_results)
1328 results.extend(search_results['results'])
1329 page_index += 1
1331 if not filepath:
1332 return results
1334 with open(filepath, 'w', encoding='utf-8') as s:
1335 json.dump(results, s, ensure_ascii=False, indent=4)
1336 bk_logger.info(f'retrieved {len(results)} assets from elastic search')
1337 return results
1340 def search(category='', get_next=False, author_id=''):
1341 ''' initialize searching'''
1342 global search_start_time
1343 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
1344 search_start_time = time.time()
1345 # mt('start')
1346 scene = bpy.context.scene
1347 ui_props = scene.blenderkitUI
1349 if ui_props.asset_type == 'MODEL':
1350 if not hasattr(scene, 'blenderkit'):
1351 return;
1352 props = scene.blenderkit_models
1353 query = build_query_model()
1355 if ui_props.asset_type == 'SCENE':
1356 if not hasattr(scene, 'blenderkit_scene'):
1357 return;
1358 props = scene.blenderkit_scene
1359 query = build_query_scene()
1361 if ui_props.asset_type == 'HDR':
1362 if not hasattr(scene, 'blenderkit_HDR'):
1363 return;
1364 props = scene.blenderkit_HDR
1365 query = build_query_HDR()
1367 if ui_props.asset_type == 'MATERIAL':
1368 if not hasattr(scene, 'blenderkit_mat'):
1369 return;
1371 props = scene.blenderkit_mat
1372 query = build_query_material()
1374 if ui_props.asset_type == 'TEXTURE':
1375 if not hasattr(scene, 'blenderkit_tex'):
1376 return;
1377 # props = scene.blenderkit_tex
1378 # query = build_query_texture()
1380 if ui_props.asset_type == 'BRUSH':
1381 if not hasattr(scene, 'blenderkit_brush'):
1382 return;
1383 props = scene.blenderkit_brush
1384 query = build_query_brush()
1386 # it's possible get_net was requested more than once.
1387 if props.is_searching and get_next == True:
1388 return;
1390 if category != '':
1391 if utils.profile_is_validator():
1392 query['category'] = category
1393 else:
1394 query['category_subtree'] = category
1396 if author_id != '':
1397 query['author_id'] = author_id
1399 elif props.own_only:
1400 # if user searches for [another] author, 'only my assets' is invalid. that's why in elif.
1401 profile = bpy.context.window_manager.get('bkit profile')
1402 if profile is not None:
1403 query['author_id'] = str(profile['user']['id'])
1405 # utils.p('searching')
1406 props.is_searching = True
1408 params = {
1409 'scene_uuid': bpy.context.scene.get('uuid', None),
1410 'addon_version': version_checker.get_addon_version(),
1411 'api_key': user_preferences.api_key,
1412 'get_next': get_next,
1413 'free_first': props.free_only
1416 # if free_only:
1417 # query['keywords'] += '+is_free:true'
1418 orig_results = bpy.context.window_manager.get(f'bkit {ui_props.asset_type.lower()} search orig', {})
1419 if orig_results != {}:
1420 # ensure it's a copy in dict for what we are passing to thread:
1421 orig_results = orig_results.to_dict()
1422 add_search_process(query, params, orig_results)
1423 tasks_queue.add_task((ui.add_report, ('BlenderKit searching....', 2)))
1425 props.report = 'BlenderKit searching....'
1428 def search_update(self, context):
1429 utils.p('search updater')
1430 # if self.search_keywords != '':
1431 ui_props = bpy.context.scene.blenderkitUI
1432 if ui_props.down_up != 'SEARCH':
1433 ui_props.down_up = 'SEARCH'
1435 # here we tweak the input if it comes form the clipboard. we need to get rid of asset type and set it in UI
1436 sprops = utils.get_search_props()
1437 instr = 'asset_base_id:'
1438 atstr = 'asset_type:'
1439 kwds = sprops.search_keywords
1440 idi = kwds.find(instr)
1441 ati = kwds.find(atstr)
1442 # if the asset type already isn't there it means this update function
1443 # was triggered by it's last iteration and needs to cancel
1444 if ati > -1:
1445 at = kwds[ati:].lower()
1446 # uncertain length of the remaining string - find as better method to check the presence of asset type
1447 if at.find('model') > -1:
1448 ui_props.asset_type = 'MODEL'
1449 elif at.find('material') > -1:
1450 ui_props.asset_type = 'MATERIAL'
1451 elif at.find('brush') > -1:
1452 ui_props.asset_type = 'BRUSH'
1453 elif at.find('scene') > -1:
1454 ui_props.asset_type = 'SCENE'
1455 elif at.find('hdr') > -1:
1456 ui_props.asset_type = 'HDR'
1457 # now we trim the input copypaste by anything extra that is there,
1458 # this is also a way for this function to recognize that it already has parsed the clipboard
1459 # the search props can have changed and this needs to transfer the data to the other field
1460 # this complex behaviour is here for the case where the user needs to paste manually into blender?
1461 sprops = utils.get_search_props()
1462 sprops.search_keywords = kwds[:ati].rstrip()
1463 # return here since writing into search keywords triggers this update function once more.
1464 return
1466 search()
1469 class SearchOperator(Operator):
1470 """Tooltip"""
1471 bl_idname = "view3d.blenderkit_search"
1472 bl_label = "BlenderKit asset search"
1473 bl_description = "Search online for assets"
1474 bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
1475 own: BoolProperty(name="own assets only",
1476 description="Find all own assets",
1477 default=False)
1479 category: StringProperty(
1480 name="category",
1481 description="search only subtree of this category",
1482 default="",
1483 options={'SKIP_SAVE'}
1486 author_id: StringProperty(
1487 name="Author ID",
1488 description="Author ID - search only assets by this author",
1489 default="",
1490 options={'SKIP_SAVE'}
1493 get_next: BoolProperty(name="next page",
1494 description="get next page from previous search",
1495 default=False,
1496 options={'SKIP_SAVE'}
1499 keywords: StringProperty(
1500 name="Keywords",
1501 description="Keywords",
1502 default="",
1503 options={'SKIP_SAVE'}
1506 @classmethod
1507 def poll(cls, context):
1508 return True
1510 def execute(self, context):
1511 # TODO ; this should all get transferred to properties of the search operator, so sprops don't have to be fetched here at all.
1512 sprops = utils.get_search_props()
1513 if self.author_id != '':
1514 sprops.search_keywords = ''
1515 if self.keywords != '':
1516 sprops.search_keywords = self.keywords
1518 search(category=self.category, get_next=self.get_next, author_id=self.author_id)
1519 # bpy.ops.view3d.blenderkit_asset_bar()
1521 return {'FINISHED'}
1524 classes = [
1525 SearchOperator
1529 def register_search():
1530 bpy.app.handlers.load_post.append(scene_load)
1532 for c in classes:
1533 bpy.utils.register_class(c)
1535 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
1536 if user_preferences.use_timers:
1537 bpy.app.timers.register(timer_update)
1539 categories.load_categories()
1542 def unregister_search():
1543 bpy.app.handlers.load_post.remove(scene_load)
1545 for c in classes:
1546 bpy.utils.unregister_class(c)
1548 if bpy.app.timers.is_registered(timer_update):
1549 bpy.app.timers.unregister(timer_update)