Rigify: store advanced options in armature instead of window manager.
[blender-addons.git] / blenderkit / search.py
blob3f381b929d684817bad2dc5893f253f447d9c675
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 #####
20 if "bpy" in locals():
21 from importlib import reload
23 paths = reload(paths)
24 utils = reload(utils)
25 categories = reload(categories)
26 ui = reload(ui)
27 bkit_oauth = reload(bkit_oauth)
28 version_checker = reload(version_checker)
29 tasks_queue = reload(tasks_queue)
30 rerequests = reload(rerequests)
31 else:
32 from blenderkit import paths, utils, categories, ui, bkit_oauth, version_checker, tasks_queue, rerequests
34 import blenderkit
35 from bpy.app.handlers import persistent
37 from bpy.props import ( # TODO only keep the ones actually used when cleaning
38 IntProperty,
39 FloatProperty,
40 FloatVectorProperty,
41 StringProperty,
42 EnumProperty,
43 BoolProperty,
44 PointerProperty,
46 from bpy.types import (
47 Operator,
48 Panel,
49 AddonPreferences,
50 PropertyGroup,
51 UIList
54 import requests, os, random
55 import time
56 import threading
57 import tempfile
58 import json
59 import bpy
61 search_start_time = 0
62 prev_time = 0
65 def check_errors(rdata):
66 if rdata.get('statusCode') == 401:
67 utils.p(rdata)
68 if rdata.get('detail') == 'Invalid token.':
69 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
70 if user_preferences.api_key != '':
71 if user_preferences.enable_oauth:
72 bkit_oauth.refresh_token_thread()
73 return False, rdata.get('detail')
74 return False, 'Missing or wrong api_key in addon preferences'
75 return True, ''
78 search_threads = []
79 thumb_sml_download_threads = {}
80 thumb_full_download_threads = {}
81 reports = ''
84 def refresh_token_timer():
85 ''' this timer gets run every time the token needs refresh. It refreshes tokens and also categories.'''
86 utils.p('refresh timer')
87 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
88 fetch_server_data()
89 categories.load_categories()
91 return max(3600, user_preferences.api_key_life - 3600)
94 @persistent
95 def scene_load(context):
96 wm = bpy.context.window_manager
97 fetch_server_data()
98 # following doesn't necessarily happen if version isn't checked yet or similar, first run.
99 # wm['bkit_update'] = version_checker.compare_versions(blenderkit)
100 categories.load_categories()
101 if not bpy.app.timers.is_registered(refresh_token_timer):
102 bpy.app.timers.register(refresh_token_timer, persistent=True, first_interval=36000)
105 def fetch_server_data():
106 ''' download categories and addon version'''
107 if not bpy.app.background:
108 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
109 url = paths.BLENDERKIT_ADDON_URL
110 api_key = user_preferences.api_key
111 # Only refresh new type of tokens(by length), and only one hour before the token timeouts.
112 if user_preferences.enable_oauth and \
113 len(user_preferences.api_key)<38 and \
114 user_preferences.api_key_timeout<time.time()+ 3600:
115 bkit_oauth.refresh_token_thread()
116 if api_key != '':
117 get_profile()
118 categories.fetch_categories_thread(api_key)
121 @bpy.app.handlers.persistent
122 def timer_update(): # TODO might get moved to handle all blenderkit stuff.
124 global search_threads
125 # don't do anything while dragging - this could switch asset type during drag, and make results list length different,
126 # causing a lot of throuble literally.
127 if len(search_threads) == 0 or bpy.context.scene.blenderkitUI.dragging:
128 return 1
129 for thread in search_threads: # TODO this doesn't check all processes when one gets removed,
130 # but most of the time only one is running anyway
131 if not thread[0].is_alive():
132 search_threads.remove(thread) #
133 icons_dir = thread[1]
134 scene = bpy.context.scene
135 # these 2 lines should update the previews enum and set the first result as active.
136 s = bpy.context.scene
137 asset_type = thread[2]
138 if asset_type == 'model':
139 props = scene.blenderkit_models
140 json_filepath = os.path.join(icons_dir, 'model_searchresult.json')
141 search_name = 'bkit model search'
142 if asset_type == 'scene':
143 props = scene.blenderkit_scene
144 json_filepath = os.path.join(icons_dir, 'scene_searchresult.json')
145 search_name = 'bkit scene search'
146 if asset_type == 'material':
147 props = scene.blenderkit_mat
148 json_filepath = os.path.join(icons_dir, 'material_searchresult.json')
149 search_name = 'bkit material search'
150 if asset_type == 'brush':
151 props = scene.blenderkit_brush
152 json_filepath = os.path.join(icons_dir, 'brush_searchresult.json')
153 search_name = 'bkit brush search'
155 s[search_name] = []
157 global reports
158 if reports != '':
159 props.report = str(reports)
160 return .2
161 with open(json_filepath, 'r') as data_file:
162 rdata = json.load(data_file)
164 result_field = []
165 ok, error = check_errors(rdata)
166 if ok:
168 for r in rdata['results']:
169 # TODO remove this fix when filesSize is fixed.
170 # this is a temporary fix for too big numbers from the server.
171 try:
172 r['filesSize'] = int(r['filesSize'] / 1024)
173 except:
174 utils.p('asset with no files-size')
175 if r['assetType'] == asset_type:
176 if len(r['files']) > 0:
177 furl = None
178 tname = None
179 allthumbs = []
180 durl, tname = None, None
181 for f in r['files']:
182 if f['fileType'] == 'thumbnail':
183 tname = paths.extract_filename_from_url(f['fileThumbnailLarge'])
184 small_tname = paths.extract_filename_from_url(f['fileThumbnail'])
185 allthumbs.append(tname) # TODO just first thumb is used now.
187 tdict = {}
188 for i, t in enumerate(allthumbs):
189 tdict['thumbnail_%i'] = t
190 if f['fileType'] == 'blend':
191 durl = f['downloadUrl'].split('?')[0]
192 # fname = paths.extract_filename_from_url(f['filePath'])
193 if durl and tname:
195 tooltip = generate_tooltip(r)
196 asset_data = {'thumbnail': tname,
197 'thumbnail_small': small_tname,
198 # 'thumbnails':allthumbs,
199 'download_url': durl,
200 'id': r['id'],
201 'asset_base_id': r['assetBaseId'],
202 'name': r['name'],
203 'asset_type': r['assetType'],
204 'tooltip': tooltip,
205 'tags': r['tags'],
206 'can_download': r.get('canDownload', True),
207 'verification_status': r['verificationStatus'],
208 'author_id': str(r['author']['id'])
209 # 'author': r['author']['firstName'] + ' ' + r['author']['lastName']
210 # 'description': r['description'],
211 # 'author': r['description'],
213 asset_data['downloaded'] = 0
215 # parse extra params needed for blender here
216 params = params_to_dict(r['parameters'])
218 if asset_type == 'model':
219 if params.get('boundBoxMinX') != None:
220 bbox = {
221 'bbox_min': (
222 float(params['boundBoxMinX']),
223 float(params['boundBoxMinY']),
224 float(params['boundBoxMinZ'])),
225 'bbox_max': (
226 float(params['boundBoxMaxX']),
227 float(params['boundBoxMaxY']),
228 float(params['boundBoxMaxZ']))
231 else:
232 bbox = {
233 'bbox_min': (-.5, -.5, 0),
234 'bbox_max': (.5, .5, 1)
236 asset_data.update(bbox)
237 if asset_type == 'material':
238 asset_data['texture_size_meters'] = params.get('textureSizeMeters', 1.0)
240 asset_data.update(tdict)
241 if r['assetBaseId'] in scene.get('assets used', {}).keys():
242 asset_data['downloaded'] = 100
244 result_field.append(asset_data)
246 # results = rdata['results']
247 s[search_name] = result_field
248 s['search results'] = result_field
249 s[search_name + ' orig'] = rdata
250 s['search results orig'] = rdata
251 load_previews()
252 ui_props = bpy.context.scene.blenderkitUI
253 if len(result_field) < ui_props.scrolloffset:
254 ui_props.scrolloffset = 0
255 props.is_searching = False
256 props.search_error = False
257 props.report = 'Open assetbar to see %i results. ' % len(s['search results'])
258 if len(s['search results']) == 0:
259 tasks_queue.add_task((ui.add_report, ('No matching results found.',)))
261 # (rdata['next'])
262 # if rdata['next'] != None:
263 # search(False, get_next = True)
264 else:
265 print('error', error)
266 props.report = error
267 props.search_error = True
269 # print('finished search thread')
270 mt('preview loading finished')
271 return .3
274 def load_previews():
275 mappingdict = {
276 'MODEL': 'model',
277 'SCENE': 'scene',
278 'MATERIAL': 'material',
279 'TEXTURE': 'texture',
280 'BRUSH': 'brush'
282 scene = bpy.context.scene
283 # FIRST START SEARCH
284 props = scene.blenderkitUI
286 directory = paths.get_temp_dir('%s_search' % mappingdict[props.asset_type])
287 s = bpy.context.scene
288 results = s.get('search results')
290 if results is not None:
291 inames = []
292 tpaths = []
294 i = 0
295 for r in results:
297 tpath = os.path.join(directory, r['thumbnail_small'])
299 iname = utils.previmg_name(i)
301 if os.path.exists(tpath): # sometimes we are unlucky...
302 img = bpy.data.images.get(iname)
303 if img is None:
304 img = bpy.data.images.load(tpath)
305 img.name = iname
306 elif img.filepath != tpath:
307 # had to add this check for autopacking files...
308 if img.packed_file is not None:
309 img.unpack(method='USE_ORIGINAL')
310 img.filepath = tpath
311 img.reload()
312 img.colorspace_settings.name = 'Linear'
313 i += 1
314 # print('previews loaded')
317 # line splitting for longer texts...
318 def split_subs(text, threshold=40):
319 if text == '':
320 return []
321 # temporarily disable this, to be able to do this in drawing code
323 text = text.rstrip()
324 lines = []
326 while len(text) > threshold:
327 i = text.rfind(' ', 0, threshold)
328 i1 = text.rfind(',', 0, threshold)
329 i2 = text.rfind('.', 0, threshold)
330 i = max(i, i1, i2)
331 if i <= 0:
332 i = threshold
333 lines.append(text[:i])
334 text = text[i:]
335 lines.append(text)
336 return lines
339 def list_to_str(input):
340 output = ''
341 for i, text in enumerate(input):
342 output += text
343 if i < len(input) - 1:
344 output += ', '
345 return output
348 def writeblock(t, input, width=40): # for longer texts
349 dlines = split_subs(input, threshold=width)
350 for i, l in enumerate(dlines):
351 t += '%s\n' % l
352 return t
355 def writeblockm(tooltip, mdata, key='', pretext=None, width=40): # for longer texts
356 if mdata.get(key) == None:
357 return tooltip
358 else:
359 intext = mdata[key]
360 if type(intext) == list:
361 intext = list_to_str(intext)
362 if type(intext) == float:
363 intext = round(intext, 3)
364 intext = str(intext)
365 if intext.rstrip() == '':
366 return tooltip
367 if pretext == None:
368 pretext = key
369 if pretext != '':
370 pretext = pretext + ': '
371 text = pretext + intext
372 dlines = split_subs(text, threshold=width)
373 for i, l in enumerate(dlines):
374 tooltip += '%s\n' % l
376 return tooltip
379 def fmt_length(prop):
380 prop = str(round(prop, 2)) + 'm'
381 return prop
384 def has(mdata, prop):
385 if mdata.get(prop) is not None and mdata[prop] is not None and mdata[prop] is not False:
386 return True
387 else:
388 return False
391 def params_to_dict(params):
392 params_dict = {}
393 for p in params:
394 params_dict[p['parameterType']] = p['value']
395 return params_dict
398 def generate_tooltip(mdata):
399 col_w = 40
400 if type(mdata['parameters']) == list:
401 mparams = params_to_dict(mdata['parameters'])
402 else:
403 mparams = mdata['parameters']
404 t = ''
405 t = writeblock(t, mdata['name'], width=col_w)
406 t += '\n'
408 t = writeblockm(t, mdata, key='description', pretext='', width=col_w)
409 if mdata['description'] != '':
410 t += '\n'
412 bools = (('rig', None), ('animated', None), ('manifold', 'non-manifold'), ('scene', None), ('simulation', None),
413 ('uv', None))
414 for b in bools:
415 if mparams.get(b[0]):
416 mdata['tags'].append(b[0])
417 elif b[1] != None:
418 mdata['tags'].append(b[1])
420 bools_data = ('adult',)
421 for b in bools_data:
422 if mdata.get(b) and mdata[b]:
423 mdata['tags'].append(b)
424 t = writeblockm(t, mparams, key='designer', pretext='designer', width=col_w)
425 t = writeblockm(t, mparams, key='manufacturer', pretext='manufacturer', width=col_w)
426 t = writeblockm(t, mparams, key='designCollection', pretext='design collection', width=col_w)
428 # t = writeblockm(t, mparams, key='engines', pretext='engine', width = col_w)
429 # t = writeblockm(t, mparams, key='model_style', pretext='style', width = col_w)
430 # t = writeblockm(t, mparams, key='material_style', pretext='style', width = col_w)
431 # t = writeblockm(t, mdata, key='tags', width = col_w)
432 # t = writeblockm(t, mparams, key='condition', pretext='condition', width = col_w)
433 # t = writeblockm(t, mparams, key='productionLevel', pretext='production level', width = col_w)
434 if has(mdata, 'purePbr'):
435 t = writeblockm(t, mparams, key='pbrType', pretext='pbr', width=col_w)
437 t = writeblockm(t, mparams, key='designYear', pretext='design year', width=col_w)
439 if has(mparams, 'dimensionX'):
440 t += 'size: %s, %s, %s\n' % (fmt_length(mparams['dimensionX']),
441 fmt_length(mparams['dimensionY']),
442 fmt_length(mparams['dimensionZ']))
443 if has(mparams, 'faceCount'):
444 t += 'face count: %s, render: %s\n' % (mparams['faceCount'], mparams['faceCountRender'])
446 # t = writeblockm(t, mparams, key='meshPolyType', pretext='mesh type', width = col_w)
447 # t = writeblockm(t, mparams, key='objectCount', pretext='nubmber of objects', width = col_w)
449 # t = writeblockm(t, mparams, key='materials', width = col_w)
450 # t = writeblockm(t, mparams, key='modifiers', width = col_w)
451 # t = writeblockm(t, mparams, key='shaders', width = col_w)
453 if has(mparams, 'textureSizeMeters'):
454 t += 'texture size: %s\n' % fmt_length(mparams['textureSizeMeters'])
456 if has(mparams, 'textureResolutionMax') and mparams['textureResolutionMax'] > 0:
457 if mparams['textureResolutionMin'] == mparams['textureResolutionMax']:
458 t = writeblockm(t, mparams, key='textureResolutionMin', pretext='texture resolution', width=col_w)
459 else:
460 t += 'tex resolution: %i - %i\n' % (mparams['textureResolutionMin'], mparams['textureResolutionMax'])
462 if has(mparams, 'thumbnailScale'):
463 t = writeblockm(t, mparams, key='thumbnailScale', pretext='preview scale', width=col_w)
465 # t += 'uv: %s\n' % mdata['uv']
466 # t += '\n'
467 # t = writeblockm(t, mdata, key='license', width = col_w)
469 # generator is for both upload preview and search, this is only after search
470 # if mdata.get('versionNumber'):
471 # # t = writeblockm(t, mdata, key='versionNumber', pretext='version', width = col_w)
472 # a_id = mdata['author'].get('id')
473 # if a_id != None:
474 # adata = bpy.context.window_manager['bkit authors'].get(str(a_id))
475 # if adata != None:
476 # t += generate_author_textblock(adata)
478 # t += '\n'
479 if len(t.split('\n')) < 6:
480 t += '\n'
481 t += get_random_tip(mdata)
482 t += '\n'
483 return t
486 def get_random_tip(mdata):
487 t = ''
488 rtips = ['Click or drag model or material in scene to link/append ',
489 "Click on brushes to link them into scene.",
490 "All materials are free.",
491 "All brushes are free.",
492 "Locked models are available if you subscribe to Full plan.",
493 "Login to upload your own models, materials or brushes.",
494 "Use 'A' key to search assets by same author.",
495 "Use 'W' key to open Authors webpage.", ]
496 tip = 'Tip: ' + random.choice(rtips)
497 t = writeblock(t, tip)
498 return t
499 # at = mdata['assetType']
500 # if at == 'brush' or at == 'texture':
501 # t += 'click to link %s' % mdata['assetType']
502 # if at == 'model' or at == 'material':
503 # tips = ['Click or drag in scene to link/append %s' % mdata['assetType'],
504 # "'A' key to search assets by same author",
505 # "'W' key to open Authors webpage",
507 # tip = 'Tip: ' + random.choice(tips)
508 # t = writeblock(t, tip)
509 return t
512 def generate_author_textblock(adata):
513 t = '\n\n\n'
515 if adata not in (None, ''):
516 col_w = 40
517 if len(adata['firstName'] + adata['lastName']) > 0:
518 t = 'Author:\n'
519 t += '%s %s\n' % (adata['firstName'], adata['lastName'])
520 t += '\n'
521 if adata.get('aboutMeUrl') is not None:
522 t = writeblockm(t, adata, key='aboutMeUrl', pretext='', width=col_w)
523 t += '\n'
524 if adata.get('aboutMe') is not None:
525 t = writeblockm(t, adata, key='aboutMe', pretext='', width=col_w)
526 t += '\n'
527 return t
530 def get_items_models(self, context):
531 global search_items_models
532 return search_items_models
535 def get_items_brushes(self, context):
536 global search_items_brushes
537 return search_items_brushes
540 def get_items_materials(self, context):
541 global search_items_materials
542 return search_items_materials
545 def get_items_textures(self, context):
546 global search_items_textures
547 return search_items_textures
550 class ThumbDownloader(threading.Thread):
551 query = None
553 def __init__(self, url, path):
554 super(ThumbDownloader, self).__init__()
555 self.url = url
556 self.path = path
557 self._stop_event = threading.Event()
559 def stop(self):
560 self._stop_event.set()
562 def stopped(self):
563 return self._stop_event.is_set()
565 def run(self):
566 r = rerequests.get(self.url, stream=False)
567 if r.status_code == 200:
568 with open(self.path, 'wb') as f:
569 f.write(r.content)
570 # ORIGINALLY WE DOWNLOADED THUMBNAILS AS STREAM, BUT THIS WAS TOO SLOW.
571 # with open(path, 'wb') as f:
572 # for chunk in r.iter_content(1048576*4):
573 # f.write(chunk)
576 def write_author(a_id, adata):
577 # utils.p('writing author back')
578 authors = bpy.context.window_manager['bkit authors']
579 if authors.get(a_id) in (None, ''):
580 adata['tooltip'] = generate_author_textblock(adata)
581 authors[a_id] = adata
584 def fetch_author(a_id, api_key):
585 utils.p('fetch author')
586 try:
587 a_url = paths.get_api_url() + 'accounts/' + a_id + '/'
588 headers = utils.get_headers(api_key)
589 r = rerequests.get(a_url, headers=headers)
590 if r.status_code == 200:
591 adata = r.json()
592 if not hasattr(adata, 'id'):
593 utils.p(adata)
594 # utils.p(adata)
595 tasks_queue.add_task((write_author, (a_id, adata)))
596 if adata.get('gravatarHash') is not None:
597 gravatar_path = paths.get_temp_dir(subdir='g/') + adata['gravatarHash'] + '.jpg'
598 url = "https://www.gravatar.com/avatar/" + adata['gravatarHash'] + '?d=404'
599 r = rerequests.get(url, stream=False)
600 if r.status_code == 200:
601 with open(gravatar_path, 'wb') as f:
602 f.write(r.content)
603 adata['gravatarImg'] = gravatar_path
604 elif r.status_code == '404':
605 adata['gravatarHash'] = None
606 utils.p('gravatar for author not available.')
607 except Exception as e:
608 utils.p(e)
609 utils.p('finish fetch')
612 def get_author(r):
613 a_id = str(r['author']['id'])
614 preferences = bpy.context.preferences.addons['blenderkit'].preferences
615 authors = bpy.context.window_manager.get('bkit authors', {})
616 if authors == {}:
617 bpy.context.window_manager['bkit authors'] = authors
618 a = authors.get(a_id)
619 if a is None or a is '' or \
620 (a.get('gravatarHash') is not None and a.get('gravatarImg') is None):
621 authors[a_id] = None
622 thread = threading.Thread(target=fetch_author, args=(a_id, preferences.api_key), daemon=True)
623 thread.start()
624 return a
627 def write_profile(adata):
628 utils.p('writing profile')
629 user = adata['user']
630 # we have to convert to MiB here, numbers too big for python int type
631 if user.get('sumAssetFilesSize') is not None:
632 user['sumAssetFilesSize'] /= (1024 * 1024)
633 if user.get('sumPrivateAssetFilesSize') is not None:
634 user['sumPrivateAssetFilesSize'] /= (1024 * 1024)
635 if user.get('remainingPrivateQuota') is not None:
636 user['remainingPrivateQuota'] /= (1024 * 1024)
638 bpy.context.window_manager['bkit profile'] = adata
641 def request_profile(api_key):
642 a_url = paths.get_api_url() + 'me/'
643 headers = utils.get_headers(api_key)
644 r = rerequests.get(a_url, headers=headers)
645 adata = r.json()
646 if adata.get('user') is None:
647 utils.p(adata)
648 utils.p('getting profile failed')
649 return None
650 return adata
653 def fetch_profile(api_key):
654 utils.p('fetch profile')
655 try:
656 adata = request_profile(api_key)
657 if adata is not None:
658 tasks_queue.add_task((write_profile, (adata,)))
659 except Exception as e:
660 utils.p(e)
663 def get_profile():
664 preferences = bpy.context.preferences.addons['blenderkit'].preferences
665 a = bpy.context.window_manager.get('bkit profile')
666 thread = threading.Thread(target=fetch_profile, args=(preferences.api_key,), daemon=True)
667 thread.start()
668 return a
671 class Searcher(threading.Thread):
672 query = None
674 def __init__(self, query, params):
675 super(Searcher, self).__init__()
676 self.query = query
677 self.params = params
678 self._stop_event = threading.Event()
680 def stop(self):
681 self._stop_event.set()
683 def stopped(self):
684 return self._stop_event.is_set()
686 def run(self):
687 maxthreads = 50
688 query = self.query
689 params = self.params
690 global reports
692 t = time.time()
693 mt('search thread started')
694 tempdir = paths.get_temp_dir('%s_search' % query['asset_type'])
695 json_filepath = os.path.join(tempdir, '%s_searchresult.json' % query['asset_type'])
697 headers = utils.get_headers(params['api_key'])
699 rdata = {}
700 rdata['results'] = []
702 if params['get_next']:
703 with open(json_filepath, 'r') as infile:
704 try:
705 origdata = json.load(infile)
706 urlquery = origdata['next']
707 if urlquery == None:
708 return;
709 except:
710 # in case no search results found on drive we don't do next page loading.
711 params['get_next'] = False
712 if not params['get_next']:
713 # build a new request
714 url = paths.get_api_url() + 'search/'
716 # build request manually
717 # TODO use real queries
718 requeststring = '?query=' + query['keywords'].lower() + '+'
720 for i, q in enumerate(query):
721 requeststring += q + ':' + str(query[q]).lower()
722 if i < len(query) - 1:
723 requeststring += '+'
725 # result ordering: _score - relevance, score - BlenderKit score
726 if query.get('category_subtree') is not None:
727 requeststring += '+order:-score,_score'
728 else:
729 requeststring += '+order:_score'
731 requeststring += '&addon_version=%s' % params['addon_version']
732 if params.get('scene_uuid') is not None:
733 requeststring += '&scene_uuid=%s' % params['scene_uuid']
735 urlquery = url + requeststring
737 try:
738 utils.p(urlquery)
739 r = rerequests.get(urlquery, headers=headers)
740 reports = ''
741 # utils.p(r.text)
742 except requests.exceptions.RequestException as e:
743 print(e)
744 reports = e
745 # props.report = e
746 return
747 mt('response is back ')
748 try:
749 rdata = r.json()
750 except Exception as inst:
751 reports = r.text
752 print(inst)
754 mt('data parsed ')
756 # filter results here:
757 # todo remove this in future
758 nresults = []
759 for d in rdata.get('results', []):
760 # TODO this code is for filtering brush types, should vanish after we implement filter in Elastic
761 mode = None
762 if query['asset_type'] == 'brush':
763 for p in d['parameters']:
764 if p['parameterType'] == 'mode':
765 mode = p['value']
766 if query['asset_type'] != 'brush' or (
767 query.get('brushType') != None and query['brushType']) == mode:
768 nresults.append(d)
769 rdata['results'] = nresults
771 # print('number of results: ', len(rdata.get('results', [])))
772 if self.stopped():
773 utils.p('stopping search : ' + query['keywords'])
774 return
776 mt('search finished')
777 i = 0
779 thumb_small_urls = []
780 thumb_small_filepaths = []
781 thumb_full_urls = []
782 thumb_full_filepaths = []
783 # END OF PARSING
784 getting_authors = {}
785 for d in rdata.get('results', []):
786 if getting_authors.get(d['author']['id']) is None:
787 get_author(d)
788 getting_authors[d['author']['id']] = True
790 for f in d['files']:
791 # TODO move validation of published assets to server, too manmy checks here.
792 if f['fileType'] == 'thumbnail' and f['fileThumbnail'] != None and f['fileThumbnailLarge'] != None:
793 if f['fileThumbnail'] == None:
794 f['fileThumbnail'] = 'NONE'
795 if f['fileThumbnailLarge'] == None:
796 f['fileThumbnailLarge'] = 'NONE'
798 thumb_small_urls.append(f['fileThumbnail'])
799 thumb_full_urls.append(f['fileThumbnailLarge'])
801 imgname = paths.extract_filename_from_url(f['fileThumbnail'])
802 imgpath = os.path.join(tempdir, imgname)
803 thumb_small_filepaths.append(imgpath)
805 imgname = paths.extract_filename_from_url(f['fileThumbnailLarge'])
806 imgpath = os.path.join(tempdir, imgname)
807 thumb_full_filepaths.append(imgpath)
809 sml_thbs = zip(thumb_small_filepaths, thumb_small_urls)
810 full_thbs = zip(thumb_full_filepaths, thumb_full_urls)
812 # we save here because a missing thumbnail check is in the previous loop
813 # we can also prepend previous results. These have downloaded thumbnails already...
814 if params['get_next']:
815 rdata['results'][0:0] = origdata['results']
817 with open(json_filepath, 'w') as outfile:
818 json.dump(rdata, outfile)
820 killthreads_sml = []
821 for k in thumb_sml_download_threads.keys():
822 if k not in thumb_small_filepaths:
823 killthreads_sml.append(k) # do actual killing here?
825 killthreads_full = []
826 for k in thumb_full_download_threads.keys():
827 if k not in thumb_full_filepaths:
828 killthreads_full.append(k) # do actual killing here?
829 # TODO do the killing/ stopping here! remember threads might have finished inbetween!
831 if self.stopped():
832 utils.p('stopping search : ' + query['keywords'])
833 return
835 # this loop handles downloading of small thumbnails
836 for imgpath, url in sml_thbs:
837 if imgpath not in thumb_sml_download_threads and not os.path.exists(imgpath):
838 thread = ThumbDownloader(url, imgpath)
839 # thread = threading.Thread(target=download_thumbnail, args=([url, imgpath]),
840 # daemon=True)
841 thread.start()
842 thumb_sml_download_threads[imgpath] = thread
843 # threads.append(thread)
845 if len(thumb_sml_download_threads) > maxthreads:
846 while len(thumb_sml_download_threads) > maxthreads:
847 threads_copy = thumb_sml_download_threads.copy() # because for loop can erase some of the items.
848 for tk, thread in threads_copy.items():
849 if not thread.is_alive():
850 thread.join()
851 # utils.p(x)
852 del (thumb_sml_download_threads[tk])
853 # utils.p('fetched thumbnail ', i)
854 i += 1
855 if self.stopped():
856 utils.p('stopping search : ' + query['keywords'])
857 return
858 idx = 0
859 while len(thumb_sml_download_threads) > 0:
860 threads_copy = thumb_sml_download_threads.copy() # because for loop can erase some of the items.
861 for tk, thread in threads_copy.items():
862 if not thread.is_alive():
863 thread.join()
864 del (thumb_sml_download_threads[tk])
865 i += 1
867 if self.stopped():
868 utils.p('stopping search : ' + query['keywords'])
869 return
871 # start downloading full thumbs in the end
872 for imgpath, url in full_thbs:
873 if imgpath not in thumb_full_download_threads and not os.path.exists(imgpath):
874 thread = ThumbDownloader(url, imgpath)
875 # thread = threading.Thread(target=download_thumbnail, args=([url, imgpath]),
876 # daemon=True)
877 thread.start()
878 thumb_full_download_threads[imgpath] = thread
879 mt('thumbnails finished')
882 def build_query_common(query, props):
883 query_common = {
884 "keywords": props.search_keywords
886 query.update(query_common)
889 # def query_add_range(query, name, rmin, rmax):
891 def build_query_model():
892 '''use all search input to request results from server'''
894 props = bpy.context.scene.blenderkit_models
895 query = {
896 "asset_type": 'model',
897 # "engine": props.search_engine,
898 # "adult": props.search_adult,
900 if props.search_style != 'ANY':
901 if props.search_style != 'OTHER':
902 query["model_style"] = props.search_style
903 else:
904 query["model_style"] = props.search_style_other
906 if props.free_only:
907 query["is_free"] = True
909 if props.search_advanced:
910 if props.search_condition != 'UNSPECIFIED':
911 query["condition"] = props.search_condition
912 if props.search_design_year:
913 query["designYearMin"] = props.search_design_year_min
914 query["designYearMax"] = props.search_design_year_max
915 if props.search_polycount:
916 query["polyCountMin"] = props.search_polycount_min
917 query["polyCountMax"] = props.search_polycount_max
918 if props.search_texture_resolution:
919 query["textureResolutionMin"] = props.search_texture_resolution_min
920 query["textureResolutionMax"] = props.search_texture_resolution_max
922 build_query_common(query, props)
924 return query
927 def build_query_scene():
928 '''use all search input to request results from server'''
930 props = bpy.context.scene.blenderkit_scene
931 query = {
932 "asset_type": 'scene',
933 # "engine": props.search_engine,
934 # "adult": props.search_adult,
936 build_query_common(query, props)
937 return query
940 def build_query_material():
941 props = bpy.context.scene.blenderkit_mat
942 query = {
943 "asset_type": 'material',
946 # if props.search_engine == 'NONE':
947 # query["engine"] = ''
948 # if props.search_engine != 'OTHER':
949 # query["engine"] = props.search_engine
950 # else:
951 # query["engine"] = props.search_engine_other
952 if props.search_style != 'ANY':
953 if props.search_style != 'OTHER':
954 query["style"] = props.search_style
955 else:
956 query["style"] = props.search_style_other
957 build_query_common(query, props)
959 return query
962 def build_query_texture():
963 props = bpy.context.scene.blenderkit_tex
964 query = {
965 "asset_type": 'texture',
969 if props.search_style != 'ANY':
970 if props.search_style != 'OTHER':
971 query["search_style"] = props.search_style
972 else:
973 query["search_style"] = props.search_style_other
975 build_query_common(query, props)
977 return query
980 def build_query_brush():
981 props = bpy.context.scene.blenderkit_brush
983 brush_type = ''
984 if bpy.context.sculpt_object is not None:
985 brush_type = 'sculpt'
987 elif bpy.context.image_paint_object: # could be just else, but for future p
988 brush_type = 'texture_paint'
990 query = {
991 "asset_type": 'brush',
993 "brushType": brush_type
996 build_query_common(query, props)
998 return query
1001 def mt(text):
1002 global search_start_time, prev_time
1003 alltime = time.time() - search_start_time
1004 since_last = time.time() - prev_time
1005 prev_time = time.time()
1006 utils.p(text, alltime, since_last)
1009 def add_search_process(query, params):
1010 global search_threads
1012 while (len(search_threads) > 0):
1013 old_thread = search_threads.pop(0)
1014 old_thread[0].stop()
1015 # TODO CARE HERE FOR ALSO KILLING THE THREADS...AT LEAST NOW SEARCH DONE FIRST WON'T REWRITE AN OLDER ONE
1017 tempdir = paths.get_temp_dir('%s_search' % query['asset_type'])
1018 thread = Searcher(query, params)
1019 thread.start()
1021 search_threads.append([thread, tempdir, query['asset_type']])
1023 mt('thread started')
1026 def search(category='', get_next=False, author_id=''):
1027 ''' initialize searching'''
1028 global search_start_time
1029 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
1031 search_start_time = time.time()
1032 mt('start')
1033 scene = bpy.context.scene
1034 uiprops = scene.blenderkitUI
1036 if uiprops.asset_type == 'MODEL':
1037 if not hasattr(scene, 'blenderkit'):
1038 return;
1039 props = scene.blenderkit_models
1040 query = build_query_model()
1042 if uiprops.asset_type == 'SCENE':
1043 if not hasattr(scene, 'blenderkit_scene'):
1044 return;
1045 props = scene.blenderkit_scene
1046 query = build_query_scene()
1048 if uiprops.asset_type == 'MATERIAL':
1049 if not hasattr(scene, 'blenderkit_mat'):
1050 return;
1051 props = scene.blenderkit_mat
1052 query = build_query_material()
1054 if uiprops.asset_type == 'TEXTURE':
1055 if not hasattr(scene, 'blenderkit_tex'):
1056 return;
1057 # props = scene.blenderkit_tex
1058 # query = build_query_texture()
1060 if uiprops.asset_type == 'BRUSH':
1061 if not hasattr(scene, 'blenderkit_brush'):
1062 return;
1063 props = scene.blenderkit_brush
1064 query = build_query_brush()
1066 if props.is_searching and get_next == True:
1067 return;
1069 if category != '':
1070 query['category_subtree'] = category
1072 if author_id != '':
1073 query['author_id'] = author_id
1075 # utils.p('searching')
1076 props.is_searching = True
1078 params = {
1079 'scene_uuid': bpy.context.scene.get('uuid', None),
1080 'addon_version': version_checker.get_addon_version(),
1081 'api_key': user_preferences.api_key,
1082 'get_next': get_next
1085 # if free_only:
1086 # query['keywords'] += '+is_free:true'
1088 add_search_process(query, params)
1089 tasks_queue.add_task((ui.add_report, ('BlenderKit searching....', 2)))
1091 props.report = 'BlenderKit searching....'
1094 def search_update(self, context):
1095 utils.p('search updater')
1096 if self.search_keywords != '':
1097 search()
1100 class SearchOperator(Operator):
1101 """Tooltip"""
1102 bl_idname = "view3d.blenderkit_search"
1103 bl_label = "BlenderKit asset search"
1104 bl_description = "Search online for assets"
1105 bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
1106 own: BoolProperty(name="own assets only",
1107 description="Find all own assets",
1108 default=False)
1110 category: StringProperty(
1111 name="category",
1112 description="search only subtree of this category",
1113 default="",
1114 options = {'SKIP_SAVE'}
1117 author_id: StringProperty(
1118 name="Author ID",
1119 description="Author ID - search only assets by this author",
1120 default="",
1121 options = {'SKIP_SAVE'}
1124 get_next: BoolProperty(name="next page",
1125 description="get next page from previous search",
1126 default=False,
1127 options = {'SKIP_SAVE'}
1130 keywords: StringProperty(
1131 name="Keywords",
1132 description="Keywords",
1133 default="",
1134 options = {'SKIP_SAVE'}
1137 @classmethod
1138 def poll(cls, context):
1139 return True
1141 def execute(self, context):
1142 # TODO ; this should all get transferred to properties of the search operator, so sprops don't have to be fetched here at all.
1143 sprops = utils.get_search_props()
1144 if self.author_id != '':
1145 sprops.search_keywords = ''
1146 if self.keywords != '':
1147 sprops.search_keywords = self.keywords
1149 search(category=self.category, get_next=self.get_next, author_id=self.author_id)
1150 # bpy.ops.view3d.blenderkit_asset_bar()
1152 return {'FINISHED'}
1155 classes = [
1156 SearchOperator
1160 def register_search():
1161 bpy.app.handlers.load_post.append(scene_load)
1163 for c in classes:
1164 bpy.utils.register_class(c)
1166 bpy.app.timers.register(timer_update, persistent = True)
1168 categories.load_categories()
1171 def unregister_search():
1172 bpy.app.handlers.load_post.remove(scene_load)
1174 for c in classes:
1175 bpy.utils.unregister_class(c)
1177 bpy.app.timers.unregister(timer_update)