Rigidy: update for internal changes to deferred property registration
[blender-addons.git] / blenderkit / upload.py
blob110994e38880fe17b9d05694e3be142442e1ffb4
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 from blenderkit import asset_inspector, paths, utils, bg_blender, autothumb, version_checker, search, ui_panels, ui, \
21 overrides, colors, rerequests, categories, upload_bg, tasks_queue, image_utils
23 import tempfile, os, subprocess, json, re
25 import bpy
26 import requests
27 import threading
28 import sys
30 BLENDERKIT_EXPORT_DATA_FILE = "data.json"
32 from bpy.props import ( # TODO only keep the ones actually used when cleaning
33 EnumProperty,
34 BoolProperty,
35 StringProperty,
37 from bpy.types import (
38 Operator,
39 Panel,
40 AddonPreferences,
41 PropertyGroup,
42 UIList
45 licenses = (
46 ('royalty_free', 'Royalty Free', 'royalty free commercial license'),
47 ('cc_zero', 'Creative Commons Zero', 'Creative Commons Zero'),
50 def comma2array(text):
51 commasep = text.split(',')
52 ar = []
53 for i, s in enumerate(commasep):
54 s = s.strip()
55 if s != '':
56 ar.append(s)
57 return ar
60 def get_app_version():
61 ver = bpy.app.version
62 return '%i.%i.%i' % (ver[0], ver[1], ver[2])
65 def add_version(data):
66 app_version = get_app_version()
67 addon_version = version_checker.get_addon_version()
68 data["sourceAppName"] = "blender"
69 data["sourceAppVersion"] = app_version
70 data["addonVersion"] = addon_version
73 def write_to_report(props, text):
74 props.report = props.report + text + '\n'
77 def check_missing_data_model(props):
78 props.report = ''
79 autothumb.update_upload_model_preview(None, None)
81 if props.name == '':
82 write_to_report(props, 'Set model name')
83 # if props.tags == '':
84 # write_to_report(props, 'Write at least 3 tags')
85 if not props.has_thumbnail:
86 write_to_report(props, 'Add thumbnail:')
88 props.report += props.thumbnail_generating_state + '\n'
89 if props.engine == 'NONE':
90 write_to_report(props, 'Set at least one rendering/output engine')
91 if not any(props.dimensions):
92 write_to_report(props, 'Run autotags operator or fill in dimensions manually')
95 def check_missing_data_scene(props):
96 props.report = ''
97 autothumb.update_upload_model_preview(None, None)
99 if props.name == '':
100 write_to_report(props, 'Set scene name')
101 # if props.tags == '':
102 # write_to_report(props, 'Write at least 3 tags')
103 if not props.has_thumbnail:
104 write_to_report(props, 'Add thumbnail:')
106 props.report += props.thumbnail_generating_state + '\n'
107 if props.engine == 'NONE':
108 write_to_report(props, 'Set at least one rendering/output engine')
111 def check_missing_data_material(props):
112 props.report = ''
113 autothumb.update_upload_material_preview(None, None)
114 if props.name == '':
115 write_to_report(props, 'Set material name')
116 # if props.tags == '':
117 # write_to_report(props, 'Write at least 3 tags')
118 if not props.has_thumbnail:
119 write_to_report(props, 'Add thumbnail:')
120 props.report += props.thumbnail_generating_state
121 if props.engine == 'NONE':
122 write_to_report(props, 'Set rendering/output engine')
125 def check_missing_data_brush(props):
126 autothumb.update_upload_brush_preview(None, None)
127 props.report = ''
128 if props.name == '':
129 write_to_report(props, 'Set brush name')
130 # if props.tags == '':
131 # write_to_report(props, 'Write at least 3 tags')
132 if not props.has_thumbnail:
133 write_to_report(props, 'Add thumbnail:')
134 props.report += props.thumbnail_generating_state
137 def sub_to_camel(content):
138 replaced = re.sub(r"_.",
139 lambda m: m.group(0)[1].upper(), content)
140 return (replaced)
143 def camel_to_sub(content):
144 replaced = re.sub(r"[A-Z]", lambda m: '_' + m.group(0).lower(), content)
145 return replaced
148 def get_upload_data(caller=None, context=None, asset_type=None):
150 works though metadata from addom props and prepares it for upload to dicts.
151 Parameters
152 ----------
153 caller - upload operator or none
154 context - context
155 asset_type - asset type in capitals (blender enum)
157 Returns
158 -------
159 export_ddta- all extra data that the process needs to upload and communicate with UI from a thread.
160 - eval_path_computing - string path to UI prop that denots if upload is still running
161 - eval_path_state - string path to UI prop that delivers messages about upload to ui
162 - eval_path - path to object holding upload data to be able to access it with various further commands
163 - models - in case of model upload, list of objects
164 - thumbnail_path - path to thumbnail file
166 upload_data - asset_data generated from the ui properties
169 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
170 api_key = user_preferences.api_key
172 export_data = {
173 # "type": asset_type,
175 upload_params = {}
176 if asset_type == 'MODEL':
177 # Prepare to save the file
178 mainmodel = utils.get_active_model()
180 props = mainmodel.blenderkit
182 obs = utils.get_hierarchy(mainmodel)
183 obnames = []
184 for ob in obs:
185 obnames.append(ob.name)
186 export_data["models"] = obnames
187 export_data["thumbnail_path"] = bpy.path.abspath(props.thumbnail)
189 eval_path_computing = "bpy.data.objects['%s'].blenderkit.uploading" % mainmodel.name
190 eval_path_state = "bpy.data.objects['%s'].blenderkit.upload_state" % mainmodel.name
191 eval_path = "bpy.data.objects['%s']" % mainmodel.name
193 engines = [props.engine.lower()]
194 if props.engine1 != 'NONE':
195 engines.append(props.engine1.lower())
196 if props.engine2 != 'NONE':
197 engines.append(props.engine2.lower())
198 if props.engine3 != 'NONE':
199 engines.append(props.engine3.lower())
200 if props.engine == 'OTHER':
201 engines.append(props.engine_other.lower())
203 style = props.style.lower()
204 # if style == 'OTHER':
205 # style = props.style_other.lower()
207 pl_dict = {'FINISHED': 'finished', 'TEMPLATE': 'template'}
209 upload_data = {
210 "assetType": 'model',
213 upload_params = {
214 "productionLevel": props.production_level.lower(),
215 "model_style": style,
216 "engines": engines,
217 "modifiers": comma2array(props.modifiers),
218 "materials": comma2array(props.materials),
219 "shaders": comma2array(props.shaders),
220 "uv": props.uv,
221 "dimensionX": round(props.dimensions[0], 4),
222 "dimensionY": round(props.dimensions[1], 4),
223 "dimensionZ": round(props.dimensions[2], 4),
225 "boundBoxMinX": round(props.bbox_min[0], 4),
226 "boundBoxMinY": round(props.bbox_min[1], 4),
227 "boundBoxMinZ": round(props.bbox_min[2], 4),
229 "boundBoxMaxX": round(props.bbox_max[0], 4),
230 "boundBoxMaxY": round(props.bbox_max[1], 4),
231 "boundBoxMaxZ": round(props.bbox_max[2], 4),
233 "animated": props.animated,
234 "rig": props.rig,
235 "simulation": props.simulation,
236 "purePbr": props.pbr,
237 "faceCount": props.face_count,
238 "faceCountRender": props.face_count_render,
239 "manifold": props.manifold,
240 "objectCount": props.object_count,
242 "procedural": props.is_procedural,
243 "nodeCount": props.node_count,
244 "textureCount": props.texture_count,
245 "megapixels": round(props.total_megapixels / 1000000),
246 # "scene": props.is_scene,
248 if props.use_design_year:
249 upload_params["designYear"] = props.design_year
250 if props.condition != 'UNSPECIFIED':
251 upload_params["condition"] = props.condition.lower()
252 if props.pbr:
253 pt = props.pbr_type
254 pt = pt.lower()
255 upload_params["pbrType"] = pt
257 if props.texture_resolution_max > 0:
258 upload_params["textureResolutionMax"] = props.texture_resolution_max
259 upload_params["textureResolutionMin"] = props.texture_resolution_min
260 if props.mesh_poly_type != 'OTHER':
261 upload_params["meshPolyType"] = props.mesh_poly_type.lower() # .replace('_',' ')
263 optional_params = ['manufacturer', 'designer', 'design_collection', 'design_variant']
264 for p in optional_params:
265 if eval('props.%s' % p) != '':
266 upload_params[sub_to_camel(p)] = eval('props.%s' % p)
268 if asset_type == 'SCENE':
269 # Prepare to save the file
270 s = bpy.context.scene
272 props = s.blenderkit
274 export_data["scene"] = s.name
275 export_data["thumbnail_path"] = bpy.path.abspath(props.thumbnail)
277 eval_path_computing = "bpy.data.scenes['%s'].blenderkit.uploading" % s.name
278 eval_path_state = "bpy.data.scenes['%s'].blenderkit.upload_state" % s.name
279 eval_path = "bpy.data.scenes['%s']" % s.name
281 engines = [props.engine.lower()]
282 if props.engine1 != 'NONE':
283 engines.append(props.engine1.lower())
284 if props.engine2 != 'NONE':
285 engines.append(props.engine2.lower())
286 if props.engine3 != 'NONE':
287 engines.append(props.engine3.lower())
288 if props.engine == 'OTHER':
289 engines.append(props.engine_other.lower())
291 style = props.style.lower()
292 # if style == 'OTHER':
293 # style = props.style_other.lower()
295 pl_dict = {'FINISHED': 'finished', 'TEMPLATE': 'template'}
297 upload_data = {
298 "assetType": 'scene',
301 upload_params = {
302 "productionLevel": props.production_level.lower(),
303 "model_style": style,
304 "engines": engines,
305 "modifiers": comma2array(props.modifiers),
306 "materials": comma2array(props.materials),
307 "shaders": comma2array(props.shaders),
308 "uv": props.uv,
310 "animated": props.animated,
311 # "simulation": props.simulation,
312 "purePbr": props.pbr,
313 "faceCount": 1, # props.face_count,
314 "faceCountRender": 1, # props.face_count_render,
315 "objectCount": 1, # props.object_count,
317 # "scene": props.is_scene,
319 if props.use_design_year:
320 upload_params["designYear"] = props.design_year
321 if props.condition != 'UNSPECIFIED':
322 upload_params["condition"] = props.condition.lower()
323 if props.pbr:
324 pt = props.pbr_type
325 pt = pt.lower()
326 upload_params["pbrType"] = pt
328 if props.texture_resolution_max > 0:
329 upload_params["textureResolutionMax"] = props.texture_resolution_max
330 upload_params["textureResolutionMin"] = props.texture_resolution_min
331 if props.mesh_poly_type != 'OTHER':
332 upload_params["meshPolyType"] = props.mesh_poly_type.lower() # .replace('_',' ')
334 elif asset_type == 'MATERIAL':
335 mat = bpy.context.active_object.active_material
336 props = mat.blenderkit
338 # props.name = mat.name
340 export_data["material"] = str(mat.name)
341 export_data["thumbnail_path"] = bpy.path.abspath(props.thumbnail)
342 # mat analytics happen here, since they don't take up any time...
343 asset_inspector.check_material(props, mat)
345 eval_path_computing = "bpy.data.materials['%s'].blenderkit.uploading" % mat.name
346 eval_path_state = "bpy.data.materials['%s'].blenderkit.upload_state" % mat.name
347 eval_path = "bpy.data.materials['%s']" % mat.name
349 engine = props.engine
350 if engine == 'OTHER':
351 engine = props.engine_other
352 engine = engine.lower()
353 style = props.style.lower()
354 # if style == 'OTHER':
355 # style = props.style_other.lower()
357 upload_data = {
358 "assetType": 'material',
362 upload_params = {
363 "material_style": style,
364 "engine": engine,
365 "shaders": comma2array(props.shaders),
366 "uv": props.uv,
367 "animated": props.animated,
368 "purePbr": props.pbr,
369 "textureSizeMeters": props.texture_size_meters,
370 "procedural": props.is_procedural,
371 "nodeCount": props.node_count,
372 "textureCount": props.texture_count,
373 "megapixels": round(props.total_megapixels / 1000000),
377 if props.pbr:
378 upload_params["pbrType"] = props.pbr_type.lower()
380 if props.texture_resolution_max > 0:
381 upload_params["textureResolutionMax"] = props.texture_resolution_max
382 upload_params["textureResolutionMin"] = props.texture_resolution_min
384 elif asset_type == 'BRUSH':
385 brush = utils.get_active_brush()
387 props = brush.blenderkit
388 # props.name = brush.name
390 export_data["brush"] = str(brush.name)
391 export_data["thumbnail_path"] = bpy.path.abspath(brush.icon_filepath)
393 eval_path_computing = "bpy.data.brushes['%s'].blenderkit.uploading" % brush.name
394 eval_path_state = "bpy.data.brushes['%s'].blenderkit.upload_state" % brush.name
395 eval_path = "bpy.data.brushes['%s']" % brush.name
397 # mat analytics happen here, since they don't take up any time...
399 brush_type = ''
400 if bpy.context.sculpt_object is not None:
401 brush_type = 'sculpt'
403 elif bpy.context.image_paint_object: # could be just else, but for future p
404 brush_type = 'texture_paint'
406 upload_params = {
407 "mode": brush_type,
410 upload_data = {
411 "assetType": 'brush',
414 elif asset_type == 'HDR':
415 ui_props = bpy.context.scene.blenderkitUI
417 # imagename = ui_props.hdr_upload_image
418 image = ui_props.hdr_upload_image # bpy.data.images.get(imagename)
419 if not image:
420 return None, None
422 props = image.blenderkit
423 # props.name = brush.name
424 base, ext = os.path.splitext(image.filepath)
425 thumb_path = base + '.jpg'
426 export_data["thumbnail_path"] = bpy.path.abspath(thumb_path)
428 export_data["hdr"] = str(image.name)
429 export_data["hdr_filepath"] = str(bpy.path.abspath(image.filepath))
430 # export_data["thumbnail_path"] = bpy.path.abspath(brush.icon_filepath)
432 eval_path_computing = "bpy.data.images['%s'].blenderkit.uploading" % image.name
433 eval_path_state = "bpy.data.images['%s'].blenderkit.upload_state" % image.name
434 eval_path = "bpy.data.images['%s']" % image.name
436 # mat analytics happen here, since they don't take up any time...
438 upload_params = {
439 "textureResolutionMax": props.texture_resolution_max
443 upload_data = {
444 "assetType": 'hdr',
447 elif asset_type == 'TEXTURE':
448 style = props.style
449 # if style == 'OTHER':
450 # style = props.style_other
452 upload_data = {
453 "assetType": 'texture',
456 upload_params = {
457 "style": style,
458 "animated": props.animated,
459 "purePbr": props.pbr,
460 "resolution": props.resolution,
462 if props.pbr:
463 pt = props.pbr_type
464 pt = pt.lower()
465 upload_data["pbrType"] = pt
467 add_version(upload_data)
469 # caller can be upload operator, but also asset bar called from tooltip generator
470 if caller and caller.properties.main_file == True:
471 upload_data["name"] = props.name
472 upload_data["displayName"] = props.name
473 else:
474 upload_data["displayName"] = props.name
476 upload_data["description"] = props.description
477 upload_data["tags"] = comma2array(props.tags)
478 # category is always only one value by a slug, that's why we go down to the lowest level and overwrite.
479 if props.category == '':
480 upload_data["category"] = asset_type.lower()
481 else:
482 upload_data["category"] = props.category
483 if props.subcategory != 'NONE':
484 upload_data["category"] = props.subcategory
485 if props.subcategory1 != 'NONE':
486 upload_data["category"] = props.subcategory1
488 upload_data["license"] = props.license
489 upload_data["isFree"] = props.is_free
490 upload_data["isPrivate"] = props.is_private == 'PRIVATE'
491 upload_data["token"] = user_preferences.api_key
493 upload_data['parameters'] = upload_params
495 # if props.asset_base_id != '':
496 export_data['assetBaseId'] = props.asset_base_id
497 export_data['id'] = props.id
498 export_data['eval_path_computing'] = eval_path_computing
499 export_data['eval_path_state'] = eval_path_state
500 export_data['eval_path'] = eval_path
502 return export_data, upload_data
505 def patch_individual_metadata(asset_id, metadata_dict, api_key):
506 upload_data = metadata_dict
507 url = paths.get_api_url() + 'assets/' + str(asset_id) + '/'
508 headers = utils.get_headers(api_key)
509 try:
510 r = rerequests.patch(url, json=upload_data, headers=headers, verify=True) # files = files,
511 except requests.exceptions.RequestException as e:
512 print(e)
513 return {'CANCELLED'}
514 return {'FINISHED'}
519 # class OBJECT_MT_blenderkit_fast_metadata_menu(bpy.types.Menu):
520 # bl_label = "Fast category change"
521 # bl_idname = "OBJECT_MT_blenderkit_fast_metadata_menu"
523 # def draw(self, context):
524 # layout = self.layout
525 # ui_props = context.scene.blenderkitUI
527 # # sr = bpy.context.window_manager['search results']
528 # sr = bpy.context.window_manager['search results']
529 # asset_data = sr[ui_props.active_index]
530 # categories = bpy.context.window_manager['bkit_categories']
531 # wm = bpy.context.win
532 # for c in categories:
533 # if c['name'].lower() == asset_data['assetType']:
534 # for ch in c['children']:
535 # op = layout.operator('wm.blenderkit_fast_metadata', text = ch['name'])
536 # op = layout.operator('wm.blenderkit_fast_metadata', text = ch['name'])
539 def update_free_full(self, context):
540 if self.asset_type == 'material':
541 if self.free_full == 'FULL':
542 self.free_full = 'FREE'
543 ui_panels.ui_message(title = "All BlenderKit materials are free",
544 message = "Any material uploaded to BlenderKit is free." \
545 " However, it can still earn money for the author," \
546 " based on our fair share system. " \
547 "Part of subscription is sent to artists based on usage by paying users.")
549 def can_edit_asset(active_index = -1, asset_data = None):
550 if active_index == -1 and not asset_data:
551 return False
552 profile = bpy.context.window_manager.get('bkit profile')
553 if profile is None:
554 return False
555 if utils.profile_is_validator():
556 return True
557 if not asset_data:
558 sr = bpy.context.window_manager['search results']
559 asset_data = dict(sr[active_index])
560 # print(profile, asset_data)
561 if asset_data['author']['id'] == profile['user']['id']:
562 return True
563 return False
565 class FastMetadata(bpy.types.Operator):
566 """Fast change of the category of object directly in asset bar."""
567 bl_idname = "wm.blenderkit_fast_metadata"
568 bl_label = "Update metadata"
569 bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
571 asset_id: StringProperty(
572 name="Asset Base Id",
573 description="Unique name of the asset (hidden)",
574 default=""
576 name: StringProperty(
577 name="Name",
578 description="Main name of the asset",
579 default="",
581 description: StringProperty(
582 name="Description",
583 description="Description of the asset",
584 default="")
585 tags: StringProperty(
586 name="Tags",
587 description="List of tags, separated by commas (optional)",
588 default="",
590 category: EnumProperty(
591 name="Category",
592 description="main category to put into",
593 items=categories.get_category_enums,
594 update=categories.update_category_enums
596 subcategory: EnumProperty(
597 name="Subcategory",
598 description="main category to put into",
599 items=categories.get_subcategory_enums,
600 update = categories.update_subcategory_enums
602 subcategory1: EnumProperty(
603 name="Subcategory",
604 description="main category to put into",
605 items=categories.get_subcategory1_enums
607 license: EnumProperty(
608 items=licenses,
609 default='royalty_free',
610 description='License. Please read our help for choosing the right licenses',
612 is_private: EnumProperty(
613 name="Thumbnail Style",
614 items=(
615 ('PRIVATE', 'Private', "You asset will be hidden to public. The private assets are limited by a quota."),
616 ('PUBLIC', 'Public', '"Your asset will go into the validation process automatically')
618 description="If not marked private, your asset will go into the validation process automatically\n"
619 "Private assets are limited by quota.",
620 default="PUBLIC",
623 free_full:EnumProperty(
624 name="Free or Full Plan",
625 items=(
626 ('FREE', 'Free', "You consent you want to release this asset as free for everyone"),
627 ('FULL', 'Full', 'Your asset will be in the full plan')
629 description="Choose whether the asset should be free or in the Full Plan",
630 default="FULL",
631 update=update_free_full
634 @classmethod
635 def poll(cls, context):
636 scene = bpy.context.scene
637 ui_props = scene.blenderkitUI
638 return can_edit_asset(active_index=ui_props.active_index)
640 def draw(self, context):
641 layout = self.layout
642 # col = layout.column()
643 layout.label(text=self.message)
644 row = layout.row()
646 layout.prop(self, 'category')
647 if self.category != 'NONE' and self.subcategory != 'NONE':
648 layout.prop(self, 'subcategory')
649 if self.subcategory != 'NONE' and self.subcategory1 != 'NONE':
650 enums = categories.get_subcategory1_enums(self, context)
651 if enums[0][0]!='NONE':
652 layout.prop(self, 'subcategory1')
653 layout.prop(self, 'name')
654 layout.prop(self, 'description')
655 layout.prop(self, 'tags')
656 layout.prop(self, 'is_private', expand=True)
657 layout.prop(self, 'free_full', expand=True)
658 if self.is_private == 'PUBLIC':
659 layout.prop(self, 'license')
663 def execute(self, context):
664 user_preferences = bpy.context.preferences.addons['blenderkit'].preferences
665 props = bpy.context.scene.blenderkitUI
666 if self.subcategory1 != 'NONE':
667 category = self.subcategory1
668 elif self.subcategory != 'NONE':
669 category = self.subcategory
670 else:
671 category = self.category
672 utils.update_tags(self, context)
674 mdict = {
675 'category': category,
676 'displayName': self.name,
677 'description': self.description,
678 'tags': comma2array(self.tags),
679 'isPrivate': self.is_private == 'PRIVATE',
680 'isFree': self.free_full == 'FREE',
681 'license': self.license,
684 thread = threading.Thread(target=patch_individual_metadata,
685 args=(self.asset_id, mdict, user_preferences.api_key))
686 thread.start()
687 tasks_queue.add_task((ui.add_report, (f'Uploading metadata for {self.name}. '
688 f'Refreash search results to see that changes applied correctly.', 8,)))
690 return {'FINISHED'}
692 def invoke(self, context, event):
693 scene = bpy.context.scene
694 ui_props = scene.blenderkitUI
695 if ui_props.active_index > -1:
696 sr = bpy.context.window_manager['search results']
697 asset_data = dict(sr[ui_props.active_index])
698 else:
699 for result in bpy.context.window_manager['search results']:
700 if result['id'] == self.asset_id:
701 asset_data = dict(result)
703 if not can_edit_asset(asset_data=asset_data):
704 return {'CANCELLED'}
705 self.asset_id = asset_data['id']
706 self.asset_type = asset_data['assetType']
707 cat_path = categories.get_category_path(bpy.context.window_manager['bkit_categories'],
708 asset_data['category'])
709 try:
710 if len(cat_path) > 1:
711 self.category = cat_path[1]
712 if len(cat_path) > 2:
713 self.subcategory = cat_path[2]
714 except Exception as e:
715 print(e)
716 self.message = f"Fast edit metadata of {asset_data['name']}"
717 self.name = asset_data['displayName']
718 self.description = asset_data['description']
719 self.tags = ','.join(asset_data['tags'])
720 if asset_data['isPrivate']:
721 self.is_private = 'PRIVATE'
722 else:
723 self.is_private = 'PUBLIC'
725 if asset_data['isFree']:
726 self.free_full = 'FREE'
727 else:
728 self.free_full = 'FULL'
729 self.license = asset_data['license']
731 wm = context.window_manager
733 return wm.invoke_props_dialog(self, width = 600)
736 def verification_status_change_thread(asset_id, state, api_key):
737 upload_data = {
738 "verificationStatus": state
740 url = paths.get_api_url() + 'assets/' + str(asset_id) + '/'
741 headers = utils.get_headers(api_key)
742 try:
743 r = rerequests.patch(url, json=upload_data, headers=headers, verify=True) # files = files,
744 except requests.exceptions.RequestException as e:
745 print(e)
746 return {'CANCELLED'}
747 return {'FINISHED'}
750 def get_upload_location(props):
752 not used by now, gets location of uploaded asset - potentially usefull if we draw a nice upload gizmo in viewport.
753 Parameters
754 ----------
755 props
757 Returns
758 -------
761 scene = bpy.context.scene
762 ui_props = scene.blenderkitUI
763 if ui_props.asset_type == 'MODEL':
764 if bpy.context.view_layer.objects.active is not None:
765 ob = utils.get_active_model()
766 return ob.location
767 if ui_props.asset_type == 'SCENE':
768 return None
769 elif ui_props.asset_type == 'MATERIAL':
770 if bpy.context.view_layer.objects.active is not None and bpy.context.active_object.active_material is not None:
771 return bpy.context.active_object.location
772 elif ui_props.asset_type == 'TEXTURE':
773 return None
774 elif ui_props.asset_type == 'BRUSH':
775 return None
776 return None
779 def check_storage_quota(props):
780 if props.is_private == 'PUBLIC':
781 return True
783 profile = bpy.context.window_manager.get('bkit profile')
784 if profile is None or profile.get('remainingPrivateQuota') is None:
785 preferences = bpy.context.preferences.addons['blenderkit'].preferences
786 adata = search.request_profile(preferences.api_key)
787 if adata is None:
788 props.report = 'Please log-in first.'
789 return False
790 search.write_profile(adata)
791 profile = adata
792 quota = profile['user'].get('remainingPrivateQuota')
793 if quota is None or quota > 0:
794 return True
795 props.report = 'Private storage quota exceeded.'
796 return False
799 def auto_fix(asset_type=''):
800 # this applies various procedures to ensure coherency in the database.
801 asset = utils.get_active_asset()
802 props = utils.get_upload_props()
803 if asset_type == 'MATERIAL':
804 overrides.ensure_eevee_transparency(asset)
805 asset.name = props.name
808 upload_threads = []
811 class Uploader(threading.Thread):
813 Upload thread -
814 - first uploads metadata
815 - blender gets started to process the file if .blend is uploaded
816 - if files need to be uploaded, uploads them
817 - thumbnail goes first
818 - files get uploaded
820 Returns
821 -------
825 def __init__(self, upload_data=None, export_data=None, upload_set=None):
826 super(Uploader, self).__init__()
827 self.upload_data = upload_data
828 self.export_data = export_data
829 self.upload_set = upload_set
830 self._stop_event = threading.Event()
832 def stop(self):
833 self._stop_event.set()
835 def stopped(self):
836 return self._stop_event.is_set()
838 def send_message(self, message):
839 message = str(message)
840 # this adds a UI report but also writes above the upload panel fields.
841 tasks_queue.add_task((ui.add_report, (message,)))
842 estring = f"{self.export_data['eval_path_state']} = '{message}'"
843 tasks_queue.add_task((exec, (estring,)))
845 def end_upload(self, message):
846 estring = self.export_data['eval_path_computing'] + ' = False'
847 tasks_queue.add_task((exec, (estring,)))
848 self.send_message(message)
850 def run(self):
851 # utils.pprint(upload_data)
852 self.upload_data['parameters'] = utils.dict_to_params(
853 self.upload_data['parameters']) # weird array conversion only for upload, not for tooltips.
855 script_path = os.path.dirname(os.path.realpath(__file__))
857 # first upload metadata to server, so it can be saved inside the current file
858 url = paths.get_api_url() + 'assets/'
860 headers = utils.get_headers(self.upload_data['token'])
862 # self.upload_data['license'] = 'ovejajojo'
863 json_metadata = self.upload_data # json.dumps(self.upload_data, ensure_ascii=False).encode('utf8')
865 # tasks_queue.add_task((ui.add_report, ('Posting metadata',)))
866 self.send_message('Posting metadata')
867 if self.export_data['assetBaseId'] == '':
868 try:
869 r = rerequests.post(url, json=json_metadata, headers=headers, verify=True,
870 immediate=True) # files = files,
872 # tasks_queue.add_task((ui.add_report, ('uploaded metadata',)))
873 utils.p(r.text)
874 self.send_message('uploaded metadata')
876 except requests.exceptions.RequestException as e:
877 print(e)
878 self.end_upload(e)
879 return {'CANCELLED'}
881 else:
882 url += self.export_data['id'] + '/'
883 try:
884 if 'MAINFILE' in self.upload_set:
885 json_metadata["verificationStatus"] = "uploading"
886 r = rerequests.patch(url, json=json_metadata, headers=headers, verify=True,
887 immediate=True) # files = files,
888 self.send_message('uploaded metadata')
890 # tasks_queue.add_task((ui.add_report, ('uploaded metadata',)))
891 # parse the request
892 # print('uploaded metadata')
893 print(r.text)
894 except requests.exceptions.RequestException as e:
895 print(e)
896 self.end_upload(e)
897 return {'CANCELLED'}
899 if self.stopped():
900 self.end_upload('Upload cancelled by user')
901 return
902 # props.upload_state = 'step 1'
903 if self.upload_set == ['METADATA']:
904 self.end_upload('Metadata posted successfully')
905 return {'FINISHED'}
906 try:
907 rj = r.json()
908 utils.pprint(rj)
909 # if r.status_code not in (200, 201):
910 # if r.status_code == 401:
911 # ###ui.add_report(r.detail, 5, colors.RED)
912 # return {'CANCELLED'}
913 # if props.asset_base_id == '':
914 # props.asset_base_id = rj['assetBaseId']
915 # props.id = rj['id']
916 if self.export_data['assetBaseId'] == '':
917 self.export_data['assetBaseId'] = rj['assetBaseId']
918 self.export_data['id'] = rj['id']
919 # here we need to send asset ID's back into UI to be written in asset data.
920 estring = f"{self.export_data['eval_path']}.blenderkit.asset_base_id = '{rj['assetBaseId']}'"
921 tasks_queue.add_task((exec, (estring,)))
922 estring = f"{self.export_data['eval_path']}.blenderkit.id = '{rj['id']}'"
923 tasks_queue.add_task((exec, (estring,)))
924 # after that, the user's file needs to be saved to save the
926 self.upload_data['assetBaseId'] = self.export_data['assetBaseId']
927 self.upload_data['id'] = self.export_data['id']
929 # props.uploading = True
931 if 'MAINFILE' in self.upload_set:
932 if self.upload_data['assetType'] == 'hdr':
933 fpath = self.export_data['hdr_filepath']
934 else:
935 fpath = os.path.join(self.export_data['temp_dir'], self.upload_data['assetBaseId'] + '.blend')
937 clean_file_path = paths.get_clean_filepath()
939 data = {
940 'export_data': self.export_data,
941 'upload_data': self.upload_data,
942 'debug_value': self.export_data['debug_value'],
943 'upload_set': self.upload_set,
945 datafile = os.path.join(self.export_data['temp_dir'], BLENDERKIT_EXPORT_DATA_FILE)
947 with open(datafile, 'w', encoding = 'utf-8') as s:
948 json.dump(data, s, ensure_ascii=False, indent=4)
950 # non waiting method - not useful here..
951 # proc = subprocess.Popen([
952 # binary_path,
953 # "--background",
954 # "-noaudio",
955 # clean_file_path,
956 # "--python", os.path.join(script_path, "upload_bg.py"),
957 # "--", datafile # ,filepath, tempdir
958 # ], bufsize=5000, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
959 # tasks_queue.add_task((ui.add_report, ('preparing scene - running blender instance',)))
960 self.send_message('preparing scene - running blender instance')
962 proc = subprocess.run([
963 self.export_data['binary_path'],
964 "--background",
965 "-noaudio",
966 clean_file_path,
967 "--python", os.path.join(script_path, "upload_bg.py"),
968 "--", datafile
969 ], bufsize=1, stdout=sys.stdout, stdin=subprocess.PIPE, creationflags=utils.get_process_flags())
971 if self.stopped():
972 self.end_upload('Upload stopped by user')
973 return
975 files = []
976 if 'THUMBNAIL' in self.upload_set:
977 files.append({
978 "type": "thumbnail",
979 "index": 0,
980 "file_path": self.export_data["thumbnail_path"]
982 if 'MAINFILE' in self.upload_set:
983 files.append({
984 "type": "blend",
985 "index": 0,
986 "file_path": fpath
989 if not os.path.exists(fpath):
990 self.send_message ("File packing failed, please try manual packing first")
991 return {'CANCELLED'}
993 self.send_message('Uploading files')
995 uploaded = upload_bg.upload_files(self.upload_data, files)
997 if uploaded:
998 # mark on server as uploaded
999 if 'MAINFILE' in self.upload_set:
1000 confirm_data = {
1001 "verificationStatus": "uploaded"
1004 url = paths.get_api_url() + 'assets/'
1006 headers = utils.get_headers(self.upload_data['token'])
1008 url += self.upload_data["id"] + '/'
1010 r = rerequests.patch(url, json=confirm_data, headers=headers, verify=True) # files = files,
1012 self.end_upload('Upload finished successfully')
1013 else:
1014 self.end_upload('Upload failed')
1015 except Exception as e:
1016 self.end_upload(e)
1017 print(e)
1018 return {'CANCELLED'}
1021 def check_missing_data(asset_type, props):
1023 checks if user did everything allright for particular assets and notifies him back if not.
1024 Parameters
1025 ----------
1026 asset_type
1027 props
1029 Returns
1030 -------
1033 if asset_type == 'MODEL':
1034 check_missing_data_model(props)
1035 if asset_type == 'SCENE':
1036 check_missing_data_scene(props)
1037 elif asset_type == 'MATERIAL':
1038 check_missing_data_material(props)
1039 elif asset_type == 'BRUSH':
1040 check_missing_data_brush(props)
1043 def start_upload(self, context, asset_type, reupload, upload_set):
1044 '''start upload process, by processing data, then start a thread that cares about the rest of the upload.'''
1046 # fix the name first
1047 props = utils.get_upload_props()
1049 utils.name_update(props)
1051 storage_quota_ok = check_storage_quota(props)
1052 if not storage_quota_ok:
1053 self.report({'ERROR_INVALID_INPUT'}, props.report)
1054 return {'CANCELLED'}
1056 location = get_upload_location(props)
1057 props.upload_state = 'preparing upload'
1059 auto_fix(asset_type=asset_type)
1061 # do this for fixing long tags in some upload cases
1062 props.tags = props.tags[:]
1064 # check for missing metadata
1065 check_missing_data(asset_type, props)
1066 # if previous check did find any problems then
1067 if props.report != '':
1068 self.report({'ERROR_INVALID_INPUT'}, props.report)
1069 return {'CANCELLED'}
1071 if not reupload:
1072 props.asset_base_id = ''
1073 props.id = ''
1075 export_data, upload_data = get_upload_data(caller=self, context=context, asset_type=asset_type)
1077 # check if thumbnail exists, generate for HDR:
1078 if 'THUMBNAIL' in upload_set:
1079 if asset_type == 'HDR':
1080 image_utils.generate_hdr_thumbnail()
1081 elif not os.path.exists(export_data["thumbnail_path"]):
1082 props.upload_state = 'Thumbnail not found'
1083 props.uploading = False
1084 return {'CANCELLED'}
1086 if upload_set == {'METADATA'}:
1087 props.upload_state = "Updating metadata. Please don't close Blender until upload finishes"
1088 else:
1089 props.upload_state = "Starting upload. Please don't close Blender until upload finishes"
1090 props.uploading = True
1092 # save a copy of the file for processing. Only for blend files
1093 basename, ext = os.path.splitext(bpy.data.filepath)
1094 if not ext:
1095 ext = ".blend"
1096 export_data['temp_dir'] = tempfile.mkdtemp()
1097 export_data['source_filepath'] = os.path.join(export_data['temp_dir'], "export_blenderkit" + ext)
1098 if asset_type != 'HDR':
1099 bpy.ops.wm.save_as_mainfile(filepath=export_data['source_filepath'], compress=False, copy=True)
1101 export_data['binary_path'] = bpy.app.binary_path
1102 export_data['debug_value'] = bpy.app.debug_value
1104 upload_thread = Uploader(upload_data=upload_data, export_data=export_data, upload_set=upload_set)
1106 upload_thread.start()
1108 upload_threads.append(upload_thread)
1109 return {'FINISHED'}
1112 asset_types = (
1113 ('MODEL', 'Model', 'Set of objects'),
1114 ('SCENE', 'Scene', 'Scene'),
1115 ('HDR', 'HDR', 'HDR image'),
1116 ('MATERIAL', 'Material', 'Any .blend Material'),
1117 ('TEXTURE', 'Texture', 'A texture, or texture set'),
1118 ('BRUSH', 'Brush', 'Brush, can be any type of blender brush'),
1119 ('ADDON', 'Addon', 'Addnon'),
1123 class UploadOperator(Operator):
1124 """Tooltip"""
1125 bl_idname = "object.blenderkit_upload"
1126 bl_description = "Upload or re-upload asset + thumbnail + metadata"
1128 bl_label = "BlenderKit asset upload"
1129 bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
1131 # type of upload - model, material, textures, e.t.c.
1132 asset_type: EnumProperty(
1133 name="Type",
1134 items=asset_types,
1135 description="Type of upload",
1136 default="MODEL",
1139 reupload: BoolProperty(
1140 name="reupload",
1141 description="reupload but also draw so that it asks what to reupload",
1142 default=False,
1143 options={'SKIP_SAVE'}
1146 metadata: BoolProperty(
1147 name="metadata",
1148 default=True,
1149 options={'SKIP_SAVE'}
1152 thumbnail: BoolProperty(
1153 name="thumbnail",
1154 default=False,
1155 options={'SKIP_SAVE'}
1158 main_file: BoolProperty(
1159 name="main file",
1160 default=False,
1161 options={'SKIP_SAVE'}
1164 @classmethod
1165 def poll(cls, context):
1166 return utils.uploadable_asset_poll()
1168 def execute(self, context):
1169 bpy.ops.object.blenderkit_auto_tags()
1170 props = utils.get_upload_props()
1172 # in case of name change, we have to reupload everything, since the name is stored in blender file,
1173 # and is used for linking to scene
1174 # if props.name_changed:
1175 # # TODO: this needs to be replaced with new double naming scheme (metadata vs blend data)
1176 # # print('has to reupload whole data, name has changed.')
1177 # self.main_file = True
1178 # props.name_changed = False
1180 upload_set = []
1181 if not self.reupload:
1182 upload_set = ['METADATA', 'THUMBNAIL', 'MAINFILE']
1183 else:
1184 if self.metadata:
1185 upload_set.append('METADATA')
1186 if self.thumbnail:
1187 upload_set.append('THUMBNAIL')
1188 if self.main_file:
1189 upload_set.append('MAINFILE')
1191 #this is accessed later in get_upload_data and needs to be written.
1192 # should pass upload_set all the way to it probably
1193 if 'MAINFILE' in upload_set:
1194 self.main_file = True
1196 result = start_upload(self, context, self.asset_type, self.reupload, upload_set=upload_set, )
1198 return {'FINISHED'}
1200 def draw(self, context):
1201 props = utils.get_upload_props()
1202 layout = self.layout
1204 if self.reupload:
1205 # layout.prop(self, 'metadata')
1206 layout.prop(self, 'main_file')
1207 layout.prop(self, 'thumbnail')
1209 if props.asset_base_id != '' and not self.reupload:
1210 layout.label(text="Really upload as new? ")
1211 layout.label(text="Do this only when you create a new asset from an old one.")
1212 layout.label(text="For updates of thumbnail or model use reupload.")
1214 if props.is_private == 'PUBLIC':
1215 utils.label_multiline(layout, text='public assets are validated several hours'
1216 ' or days after upload. Remember always to '
1217 'test download your asset to a clean file'
1218 ' to see if it uploaded correctly.'
1219 , width=300)
1221 def invoke(self, context, event):
1222 props = utils.get_upload_props()
1224 if not utils.user_logged_in():
1225 ui_panels.draw_not_logged_in(self, message='To upload assets you need to login/signup.')
1226 return {'CANCELLED'}
1228 if props.is_private == 'PUBLIC':
1229 return context.window_manager.invoke_props_dialog(self)
1230 else:
1231 return self.execute(context)
1234 class AssetDebugPrint(Operator):
1235 """Change verification status"""
1236 bl_idname = "object.blenderkit_print_asset_debug"
1237 bl_description = "BlenderKit print asset data for debug purposes"
1238 bl_label = "BlenderKit print asset data"
1239 bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
1241 # type of upload - model, material, textures, e.t.c.
1242 asset_id: StringProperty(
1243 name="asset id",
1246 @classmethod
1247 def poll(cls, context):
1248 return True
1250 def execute(self, context):
1251 preferences = bpy.context.preferences.addons['blenderkit'].preferences
1253 if not bpy.context.window_manager['search results']:
1254 print('no search results found')
1255 return {'CANCELLED'};
1256 # update status in search results for validator's clarity
1257 sr = bpy.context.window_manager['search results']
1258 sro = bpy.context.window_manager['search results orig']['results']
1260 result = None
1261 for r in sr:
1262 if r['id'] == self.asset_id:
1263 result = r.to_dict()
1264 if not result:
1265 for r in sro:
1266 if r['id'] == self.asset_id:
1267 result = r.to_dict()
1268 if not result:
1269 ad = bpy.context.active_object.get('asset_data')
1270 if ad:
1271 result = ad.to_dict()
1272 if result:
1273 t = bpy.data.texts.new(result['name'])
1274 t.write(json.dumps(result, indent=4, sort_keys=True))
1275 print(json.dumps(result, indent=4, sort_keys=True))
1276 return {'FINISHED'}
1279 class AssetVerificationStatusChange(Operator):
1280 """Change verification status"""
1281 bl_idname = "object.blenderkit_change_status"
1282 bl_description = "Change asset ststus"
1283 bl_label = "Change verification status"
1284 bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
1286 # type of upload - model, material, textures, e.t.c.
1287 asset_id: StringProperty(
1288 name="asset id",
1291 state: StringProperty(
1292 name="verification_status",
1293 default='uploaded'
1296 @classmethod
1297 def poll(cls, context):
1298 return True
1300 def draw(self, context):
1301 layout = self.layout
1302 # if self.state == 'deleted':
1303 layout.label(text='Really delete asset from BlenderKit online storage?')
1304 # layout.prop(self, 'state')
1306 def execute(self, context):
1307 preferences = bpy.context.preferences.addons['blenderkit'].preferences
1309 if not bpy.context.window_manager['search results']:
1310 return {'CANCELLED'};
1311 # update status in search results for validator's clarity
1312 sr = bpy.context.window_manager['search results']
1313 sro = bpy.context.window_manager['search results orig']['results']
1315 for r in sr:
1316 if r['id'] == self.asset_id:
1317 r['verificationStatus'] = self.state
1318 for r in sro:
1319 if r['id'] == self.asset_id:
1320 r['verificationStatus'] = self.state
1322 thread = threading.Thread(target=verification_status_change_thread,
1323 args=(self.asset_id, self.state, preferences.api_key))
1324 thread.start()
1325 return {'FINISHED'}
1327 def invoke(self, context, event):
1328 # print(self.state)
1329 if self.state == 'deleted':
1330 wm = context.window_manager
1331 return wm.invoke_props_dialog(self)
1332 return {'RUNNING_MODAL'}
1335 def register_upload():
1336 bpy.utils.register_class(UploadOperator)
1337 # bpy.utils.register_class(FastMetadataMenu)
1338 bpy.utils.register_class(FastMetadata)
1339 bpy.utils.register_class(AssetDebugPrint)
1340 bpy.utils.register_class(AssetVerificationStatusChange)
1343 def unregister_upload():
1344 bpy.utils.unregister_class(UploadOperator)
1345 # bpy.utils.unregister_class(FastMetadataMenu)
1346 bpy.utils.unregister_class(FastMetadata)
1347 bpy.utils.unregister_class(AssetDebugPrint)
1348 bpy.utils.unregister_class(AssetVerificationStatusChange)